Primeira vez aqui? Seja bem vindo e cheque o FAQ!
x

Exercício de Machine Learning usando base de dados do Kaggle: "Rossman Store Sales"

+2 votos
128 visitas
perguntada Dez 13, 2020 em Aprendizagem de Máquinas por Gustavo Medeiros (46 pontos)  
editado Dez 17, 2020 por Gustavo Medeiros

Rossman Store é uma rede de farmácias distribuída pela Europa e baseada na Alemanha. Existem mais de 4000 lojas e emprega cerca de 55 mil trabalhadores.
Os dados disponíveis no site kaggle são uma serie temporal de vendas e informações referentes à especificidades das lojas. O objetivo e fazer um modelo capaz de prever vendas futuras usando esses dados.

Link bases de dados:\(\href {https://www.kaggle.com/c/rossmann-store-sales/data}{RossmanStoreSales}\)

Compartilhe

1 Resposta

0 votos
respondida Dez 13, 2020 por Gustavo Medeiros (46 pontos)  
editado Dez 18, 2020 por Gustavo Medeiros

Os dados disponibilizados são referentes a 1115 lojas Rossman, onde temos 3 bases de dados importantes: base de treino, base de teste e informações sobre as 1115 lojas. As bases de treino e teste são fornecidos por meio de dados em painel, de 01/2013 até 07/2015, totalizando mais de 1 milhão de observações.
Objetivo do modelo é prever o numero de vendas usando as variaveis contidas na base de treino e na base de características individuais de cada loja.
A base de teste é incompleta, no sentido de que as informações sobre vendas e clientes não são fornecidas. Desta forma a utilização desta base para teste fica sem sentido pois não teremos os valores reais para comparar o poder de previsão do modelo. Utilizaremos então as observações da base de treino para o treino e o teste do modelo, sem prejuízos maiores, pois essa base sozinha já tem mais de 1 milhão de observações.

As variáveis da base de treino são:
-Store: ID da loja, variável importante para fazer o \(\it merge\) com as informações disponíveis na base de dados sobre as lojas
-DayofWeek: O dia da semana (segunda-feira, terça-feira etc) da observação, mas em número.
-Date: Ano-Mês-Dia referentes a observação.
-Sales: Volume de vendas, nossa variável dependente.
-Customers: Clientes no determinado dia.
-Open: variavel binária, sendo 1 caso a loja esteja aberta no dia e 0 caso contrário.
-Promo: outra variável binária, sendo 1 para o caso de que a loja esteja com promoção nesse dia e 0 caso não tenha promoção.
-StateHolyday: Variavel categórica para informar sobre feriados. a = Feriado público, b = Pascóa, c = Natal e 0 = nenhum feriado. Geralmente as lojas estão fechadas em dia de feriado.
-SchoolHoliday: variavel binária que indica se houve feriado escolar (1) ou não (0) no dia.

As colunas na base de dados sobre as lojas são:
-Store: ID da loja.
-StoreType: Variável categórica sobre o tipo da loja, sendo 4 opções possíveis (a, b, c, d).
-Assortment: Outra variável categórica, agora informando a variedade da loja, podendo variar entre três tipos, a = básico, b = extra, c = extendido.
-CompetitionDistance: Distância da loja concorrente mais próxima, em metros.
-CompetitionOpenSinceMonth: Mês em que a loja concorrente foi inaugurada.
-CompetitionOpenSinceYear: Ano em que a loja concorrente foi inaugurada.
-Promo2: variavel binária que indica se a loja está participando de promoção continua e consecutiva. Tem valor 1 caso esteja participando e zero caso contrário. Aqui se trata de uma promoção diferente da tratada na variável Promo da base de treino.
-Promo2SinceWeek: Semana do ano em que a a loja começou a participar da Promo2. Aqui teremos vários dados Not Avaible (NA) pois existem lojas que não aderiram a Promo2.
-Promo2SinceYear: Ano em que a loja começou a participar da Promo2. Novamente teremos muios NA aqui.
-PromoInterval: Variável categórica que descreve o intervalo em que a promo2 começou.

Logo, no primeiro momento iremos importar as bibliotecas para verificar e organizar os dados. Para tal, usaremos pandas, numpy, datetime e seaborn.

import pandas as pd
import numpy as np
import datetime
import seaborn as sns

from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_log_error
import matplotlib.pyplot as plt


dataset_train = pd.read_csv("/home/gusta/Área de Trabalho/Dados Rossman Store/train.csv")

dataset_lojas = pd.read_csv("/home/gusta/Área de Trabalho/Dados Rossman Store/store.csv")

O primeiro ponto importante é verificar a existência de dados não disponíveis (NA). O seguinte comando verifica para ambas bases.

dataset_train.isnull().sum()
dataset_lojas.isnull().sum()

Como iremos lidar com os NA:
Os primeiros dados faltantes que temos é na CompetitionDistance. São 3 no total. Iremos usar o máximo de tal variável para subsitutuir eles. O raciocínio por traz de tal decisão é de que, como não há informação sobre a distância do concorrente mais próximo, podemos assumir que não há concorrência próxima. Logo, a maior distancia pode simular a ausência de competidor.

Outros NA são refentes ao mês e ano em que a loja concorrente foi inaugurada. Nesse caso já temos um número considerável de NA’s. Por ser tratar de datas de inauguração, é um dado dificil de se conseguir, em especial se for uma loja mais antiga. Como os NA de distância foram apenas 3, podemos assumir que a falta de dados sobre inauguração não significa ausência de concorrência, apenas que não foi possível coletar tais informações sobre as lojas já estabelecidas. Então iremos substituir tais NA pelo minimo desta variável, indicando que a loja foi inaugurada no passado.

As últimas variáveis com NA são referentes ao ínicio da Promo2 e PromoInterval. Obviamente lojas que não aderiram à promo2 não terão dados sobre o ínicio desta promoção. Desta forma, o mais adequado seria substituir os NA pelo valor máximo, que no caso a ser analisado, é uma data que ainda não ocorreu, logo nunca exisitu Promo2.
A coluna PromoInterval não traz informações relevantes, logo vamos exclui-la.

dataset_lojas['CompetitionDistance'] = dataset_lojas['CompetitionDistance'].fillna(dataset_lojas['CompetitionDistance'].max())

Col_Comp = ['CompetitionOpenSinceYear', 'CompetitionOpenSinceMonth' ] #Colunas de Competition
def Subs_Min():
    for coluna in Col_Comp:
        dataset_lojas[coluna] = dataset_lojas[coluna].fillna(dataset_lojas[coluna].min())    

Col_Pro2 = ['Promo2SinceWeek', 'Promo2SinceYear'] #Colunas de Promo2
def Subs_Max():
    for coluna in Col_Pro2:
        dataset_lojas[coluna] = dataset_lojas[coluna].fillna(dataset_lojas[coluna].max())  

Subs_Min()
Subs_Max()

dataset_lojas = dataset_lojas.drop('PromoInterval', axis = 1)    

Agora com as NA's tratadas, podemos analisar a correlação entre Vendas e Clientes. A expectativa é seja altamente correlacionados e, como no problema inicial não há menção sobre usar clientes como variável independente, o mais correto é abandonar essa coluna também. Possivelmente a inclusão de Clientes irá causar overfitting no modelo.
Como o objetivo é fazer um modelo para prever o volume de vendas, não faz sentido manter na base as observações em que as lojas estão fechadas. Obviamente essas observações terão volume zero de vendas e irão possivelmente desbalancear nossa base, visto que o mesmo exemplo pode aparecer mais de uma vez, mudando apenas a data da observação. Vamos dropar as linhas que tem Open == 0 e depois podemos eliminar a coluna open pois já não faz mais sentido manter visto que todas as observações são de lojas abertas. Ainda teremos mais de 840 mil observações após esse processo e sem possivel problema de overfitting.

corr = dataset_train.corr()
sns.heatmap(corr, annot = True, 
            xticklabels=corr.columns.values,
            yticklabels=corr.columns.values)

dataset_train = dataset_train[dataset_train['Open']==1]

dataset_train = dataset_train.drop(['Customers', 'Open'], axis = 1)

A imagem será apresentada aqui.

Dando continuidade ao tratamento dos dados, vamos transformar as informações de semana, mês e ano sobre inauguração da concorrência e adoção da promo2 em duas colunas de data, uma para cada. Para isso vamos definir uma função que irá transformar os dados em inteiros para facilitar a concatenação e depois trazer a coluna para o formato de Data. Após esse processo podemos excluir tais colunas pois teremos seus dados resumidos nas novas colunas criadas.

Col_Cal = Col_Comp+Col_Pro2 #Juntando as colunas para tratamento
def Form_int():
    for coluna in Col_Cal:
        dataset_lojas[coluna] = dataset_lojas[coluna].astype(int)
Form_int()

dataset_lojas['CompetitionSinceMonthYear'] = dataset_lojas['CompetitionOpenSinceYear'].astype(str) + '-' + dataset_lojas['CompetitionOpenSinceMonth'].astype(str)
dataset_lojas['Promo2SinceMonthYear'] = dataset_lojas['Promo2SinceYear'].astype(str)  + '-' + dataset_lojas['Promo2SinceWeek'].astype(str) + '-' +'1'

dataset_lojas['CompetitionSinceMonthYear'] = pd.to_datetime(dataset_lojas['CompetitionSinceMonthYear'])
dataset_lojas['Promo2SinceMonthYear'] = pd.to_datetime(dataset_lojas['Promo2SinceMonthYear'], format=('%G-%V-%w'))

dataset_lojas = dataset_lojas.drop(Col_Cal, axis = 1)

Com as duas bases tratadas separadamente, podemos fazer o \(\it merge\) entre elas, usando o ID da loja como referencial e depois voltar a tratar os dados, mas agora em uma base só. Primeiro ajuste é colocar a coluna Date no formato de data para tornar as comparações entre as colunas que contém formato data possível.
Tal comparação busca criar dummies referentes à presença de concorrência em determinada data e existência de Promo2 na data em questão. Após conseguir essas variáveis, as informações das colunas de data sobre competição e promo2 foram extraídas e não faz mais sentido manter las.

dataset_train_lojas = dataset_train.merge(dataset_lojas, on = 'Store', how = 'inner')
dataset_train_lojas['Date'] = pd.to_datetime(dataset_train_lojas['Date'])

dataset_train_lojas['DummyCompetition'] = np.where(dataset_train_lojas['CompetitionSinceMonthYear'] <= dataset_train_lojas['Date'], 1, 0)
dataset_train_lojas['DummyPromo2'] = np.where(dataset_train_lojas['Promo2SinceMonthYear'] <= dataset_train_lojas['Date'], 1, 0)
dataset_train_lojas = dataset_train_lojas.drop(['CompetitionSinceMonthYear', 'Promo2SinceMonthYear'], axis = 1)

Porém aqui vai existir uma inconsistência. Como os NA da Distância eram apenas 3, vão ter observações em que a distância da concorrência indica proximidade mesmo que a concorrencia não tenha sido inaugurada ainda. Para resolver tal problema, vamos subsituir, nas observações que tenha DummyCompetiton ==0, o valor de DistanceCompetition pelo max(). Assim, como anteriormente, simulamos a ausência de concorrência pela maior distância possível.

dataset_train_lojas['CompetitionDistance'] = dataset_train_lojas['CompetitionDistance'].where(dataset_train_lojas['DummyCompetition'] != 0, dataset_train_lojas['CompetitionDistance'].max())

Agora vamos tratar as variáveis categóricas. São três no total, sendo elas sobre feriado no Estado da loja, tipo de loja e variedade da loja. O código a seguir nos permite analisar melhor essas variaveis.

categoricas = {'StateHoliday', 'StoreType', 'Assortment'}

def plot_cat():
    for cat in categoricas:
        sns.factorplot(cat, data = dataset_train_lojas, kind = 'count')

plot_cat()

O resultado são os seguintes gráficos:

A imagem será apresentada aqui.
A imagem será apresentada aqui.
A imagem será apresentada aqui.

Perceba que a ausência de feriados é dominante e a distribuição de variedade fica concentrada nos tipos \(\it a\) e \(\it b\). O tipo de loja é um pouco menos concentrado mas ainda é clara a maior presença de lojas do tipo \(\it a\) e \(\it d\). Para tornar essas variáveis em dummies e colocar os nomes corretos, assim como ajustes necessários, usaremos o código a seguir.

dataset_train_lojas = pd.get_dummies(data = dataset_train_lojas, columns=(categoricas))

dataset_train_lojas = dataset_train_lojas.rename(columns={'StoreType_a':'Tipo A', 'StoreType_b':'Tipo B', 'StoreType_c':'Tipo C', 'StoreType_d':'Tipo D'})
dataset_train_lojas = dataset_train_lojas.rename(columns={'Assortment_a':'Basico', 'Assortment_b':'Extra', 'Assortment_c':'Extendido'})
dataset_train_lojas = dataset_train_lojas.rename(columns={'StateHoliday_a':'PublicHoliday', 'StateHoliday_b':'Easter', 'StateHoliday_c':'Christmas'})
dataset_train_lojas = dataset_train_lojas.drop(['StateHoliday_0'], axis = 1) #pois zero em todos = sem feriado

Agora vem a questão de divisão do dataset entre treino e teste. Estamos lidando com dados em series temporais. Note que no problema original, a base de dados destinada para teste era composta de observações posteriores às observações do treino. Como não haverá adição de dados no futuro, vamos destinar as ultimas observações para a grupo de teste e o resto para o treino. A ideia é simples: dados do futuro não podem servir para prever dados do passado, mas o contrário disso é que deve ser buscado. Além disso, ainda pode existir a possibilidade de autocorrelação da variável a ser prevista.
A data da última observção é de 31/07/2015 e existem 942 data diferentes. Podemos usar as observações dos últimos 90 dias (~10% ) para o grupo de teste e o resto fica para o treino. A divisão desta forma foge do que foi proposto pela maioria dos trabalhos apresentados no Kaggle. Importante ressaltar também que, devido a quantidade pequena de variáveis, não será adotado nenhuma medida de seleção de variavéis.

data_divisoria = datetime.datetime(2015, 4, 3)

data_treino = dataset_train_lojas[dataset_train_lojas['Date']< data_divisoria]
data_teste = dataset_train_lojas[dataset_train_lojas['Date']>= data_divisoria]
#analisar as proporções entre treino e teste
print('Total de Observações é' , (len(dataset_train_lojas)))
print('Proporção de dados da base de treino é' ,len(data_treino)/len(dataset_train_lojas) )
print('Proporção de dados da base de teste é' ,len(data_teste)/(len(dataset_train_lojas)))

A proporção 87-13 é justa para construir os modelos. Vamos manter assim. Precisamos agora colocar Data como índice dos dados e verificar se as duas bases estão balanceadas, ou seja, se a distribuição das lojas entre treino e teste está proporcional, evitando que muitas lojas estejam presentes somente em uma das bases e ausentes na outra.

dados = (data_treino, data_teste)
def Indice():
    for base in dados:
        base.set_index('Date', inplace = True)

def Check_balance():
    for base in dados:
        print(base['Store'].describe())

Indice()        
Check_balance()

Verificamos que as lojas estão bem distribuídas e agora podemos dividir as variáveis independentes a variável independente dentro das bases de treino e teste. Iremos utilizar Xtrain, ytrain, Xtest e ytest para isso, sendo Sales a variável a ser prevista.

X_train = data_treino.iloc[: , np.r_[0:2 , 3:19]].values #np.r_ permite escolher assim
y_train = data_treino.iloc[: , 2].values

X_test = data_teste.iloc[: , np.r_[0:2 , 3:19]].values #np.r_ permite escolher assim
y_test = data_teste.iloc[: , 2].values

print('Dados prontos para modelar')

Para os modelos, criamos uma função que irá rodar o modelo, buscar seu R² e já plotar as previsões com os valores reais. Para incluir outro modelo, basta importar e adicionar na lista com os parâmetros já definidos. Testei algumas mudanças nos parâmetros mas não houveram variações significativas. Também tentei padronizando as variáveis usando StandardScale da biblioteca sklearn e não houve melhoria do modelo.

#Fazendo os modelos

modelos = {LinearRegression(), KNeighborsRegressor(n_neighbors = 5), RandomForestRegressor(n_estimators = 100, max_depth = 10, random_state = 1)}

def Prev_Graf():
    for modelo in modelos:
        modelo.fit(X_train,y_train)
        y_pred = modelo.predict(X_test)
        plt.scatter(X_test[:,0], y_test, color = 'red')
        plt.scatter(X_test[:,0], y_pred, color = 'blue')
        plt.ylabel('Volume de Vendas')
        plt.xlabel('Distribuição Lojas')
        plt.title(modelo)
        plt.show()
        print('R Quadrado', modelo , r2_score( y_test, y_pred))
        print('Log MSE' ,modelo,  mean_squared_log_error( y_test, y_pred))

Prev_Graf()

Os scores:

R Quadrado LinearRegression() = 0.20600286598350637
Log MSE LinearRegression() = 0.1306481508620417
R Quadrado RandomForestRegressor(maxdepth=10, randomstate=1) = 0.490768090536966
Log MSE RandomForestRegressor(maxdepth=10, randomstate=1) = 0.09168762690788412
R Quadrado KNeighborsRegressor() = 0.820752542644737
Log MSE KNeighborsRegressor() = 0.03186551687454457

Os gráficos, onde os pontos vermelhos são os pontos reais e os azuis são os previstos:

A imagem será apresentada aqui.
A imagem será apresentada aqui.
A imagem será apresentada aqui.

Os Scores foram relativamente baixos, com exceção do modelo KNN. Possível explicação para um modelo que usualmente é tido como \(\it benchmark\) conseguir um alto score é a grande quantidade de observações e a natureza \(\it cluster\) delas. Ao usar a média dos pontos próximos ele consegue obter R² satisfatório.

comentou Dez 17, 2020 por Luiz Filippe (21 pontos)  
Gustavo, achei muito boa sua resposta à questão. Inclusive, consegui reproduzir o passo a passo no Python com os seus códigos. Particularmente, muito boa a forma de lidar os os dados missing. A minha única ressalva é algo bem simples, já no final da resposta. Você poderia colocar uma legenda nos gráficos ou explicar no parágrafo o que significam o azul e o vermelho nos 3 últimos gráficos.

Gostaria de aproveitar também para fazer uma pequena contribuição para explicar o fato de o modelo de regressão linear não ter um score tão alto. Uma das explicações poderia ser sua natureza extremamente sensível a outliers. Bastaria haver a existência de uma quantidade relativamente não muito grande deles para o modelo se afastar da distribuição real. Outro fato é a hipótese de normalidade das variáveis e dos erros, que não é necessariamente verdadeira com dados reais.
comentou Dez 18, 2020 por Gustavo Medeiros (46 pontos)  
Obrigado pelo comentário Luiz. Vou colocar mais informações sobre o gráfico para ficar mais claro. Quanto à analise sobre modelo linear você tem toda razão. Talvez, após um processo de normalização, o modelo linear consiga scores mais satisfatórios.
...