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

Exercício Machine Learning usando base de dados "Restaurant Revenue Prediction" do Kaggle

0 votos
89 visitas
perguntada Dez 17, 2020 em Ciência da Computação por Gustavo Medeiros (46 pontos)  

TFI é uma empresa que opera mais de 1200 fast foods ao redor do mundo. Alguma de suas marcas são: Burger King, Sbarro, Popeyes dentre outras igualmente famosas. Mais de 20 mil trabalhadores estão por tras da companhia nas regiões da Europa e Ásia.
Atualmente, a decisão de abrir novas franquias é um processo puramente subjetivo baseado nas experiências e conhecimentos pessoais dos envolvidos na decisão. Quando um restaurante novo não consegue se estabelecer, ele geralmente fecha dentro de 18 meses e o investimento é totalmente perdido.
O foco do problema é achar um modelo capaz prever a receita de determinado restaurante, baseado em variaveis demográficas, geográficas e comerciais para auxliar decisões de investimento e reduzir a subjetividade das mesmas.

Compartilhe

1 Resposta

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

Os dados disponibilizados no site são referentes a 137 restaurantes distintos na base de treino e 100 mil restaurantes na base de teste. A target do problema é receita e, como não temos dados sobre receita na base de teste, iremos trabalhar somente com a base de treino

O primeiro problema que temos ao analisar os dados é o tamanho da base de treino. 137 observações e 43 colunas é receita para overfit, pois as poucas observações disponíveis não permitem que o modelo seja generalizado. Em outras palavras, fica muito mais fácil encaixar modelos com uma amostragem de treino pequena.

O segundo ponto que chama atenção é que 37 das variavés disponíveis são anônimas. Não sabemos o que elas representam. Apenas que dentre elas temos: população em determinada area, idade, distribuição de gênero, custo do metro quadrado, disponibilidade de estacionamento, presença de escolas, bancos. São, em geral, dados demográficos, geográficos e comerciais. Já podemos esperar alta multicolinearidade nessa base.

No primeiro momento, após importar as bibliotecas e os dados, vamos tentar conhecer melhor nossa base. Temos variáveis categóricas (essas especificadas) e contínuas (anônimas). Vamos começar pelas categóricas. São 4 no total, uma referente ao ano de inauguração da loja, uma indicando a cidade onde está localizada, uma indicando se está em região de metropole ou não e a última indica o tipo de restaurante.

import pandas as pd
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt

dataset = pd.read_csv('/home/gusta/Área de Trabalho/Restaurant pred compt/train.csv')

dataset.dtypes
dataset.isnull().sum()
dataset = dataset.set_index('Id') 

sns.factorplot(x='City Group',data=dataset,kind='count',size=3,aspect=0.75)
plt.title('Quantidade de cidades')
dataset[["City Group","revenue"]].groupby(["City Group"]).mean().plot(kind="bar")
plt.title('Média de receita')
dataset[["City","revenue"]].groupby(["City"]).mean().plot(kind="bar")
plt.title('Receita Media Cidades')
sns.countplot(x='Type', data=dataset)
plt.title('Distribuição Tipos')

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

O que podemos inferir destar variáveis categóricas? Fica claro que restaurantes situados em metrópoles tem receita maior. Vamos transformar essa primeira coluna em dummie. Agora já em relação às cidades, não podemos inferir nada. Na verdade, já tendo a informação sobre metropole e as possíveis variáveis geograficas e demograficas escondidas nas outras colunas não faz sentido manter a coluna de cidade.
Outra coluna categórica que chamou atenção foi a que define o tipo de restaurante. No total existem 4 tipos de restaurante possíveis, porem na amostra dedicada ao treino temos a clara dominâcia de duas delas, enquanto uma categoria tem apenas uma observação e a outra restante não tem nenhuma. Vamos eliminar essa coluna pois não queremos manter uma variável desbalanceada para nosso treino. A última coluna com informação categórica diz respeito à data de inauguração do restaurante. Vamos usar essa informação, mas não do jeito que está disponível. Iremos utilizar a data que os dados foram disponibilizados como o dia em que as amostras foram colhidas e iremos, a partir desta data, calcular o tempo, em anos, que o restaurante está aberto. Segue o código

dataset = pd.get_dummies(dataset, columns = ['City Group'])
dataset = dataset.rename(columns={'City Group_Big Cities':'Big City', 'City Group_Other':'Other'})
dataset['Open Date'] = pd.to_datetime(dataset['Open Date']).dt.year
dataset['Hoje'] = pd.DataFrame({'Date':np.repeat(['01/01/2015'],[len(dataset)]) })
dataset['Hoje'] = pd.to_datetime(dataset['Hoje']).dt.year
dataset['Anos Aberto'] = dataset['Hoje'] - dataset['Open Date']

dataset = dataset.drop(['City', 'Type', 'Other', 'Open Date', 'Hoje'], axis = 1 )

Agora temos somente variáveis contínuas e uma dummie. Vamos analisar o mapa de correlação como todo. Isso pode nos ajudar a entender as variáveis anônimas. Vamos tambem buscar informações mais claras da relação direta com o \( \it Target\) e a distribuição algumas colunas anônimas (apesar do código gerar infomação sobre a distribuição de todas colunas anônimas, achei melhor não colocar aqui para não poluir com muitas imagens).

plt.figure(figsize=(50,30))
corr = dataset.corr()
sns.heatmap(corr,
            xticklabels=corr.columns.values)

corr_revenue = dataset.corr()['revenue'].sort_values(ascending=False)
plt.figure(figsize=(10,7))
corr_revenue.drop('revenue').plot.bar()
plt.show()

for i in range(1,38):
    plt.subplots(1, figsize=(5, 2))

    sns.distplot(dataset['P{}'.format(i)], kde=False);
    plt.show()

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

Como era de se esperar, existe grande correlação entre várias colunas. Uma forma de reduzirmos a quantidade de colunas e ao mesmo tempo trabalhar para diminuir a correlação entre elas, reduzindo a possibilidade de overfitting, é por meio do \(\it Principal\) \(\it Component\) \(\it Analysis\), o PCA.
Tal metódo consiste, de forma resumida, em transformar um conjunto de variáveis correlacionadas em um conjunto menor, chamados de componentes principais. Os componentes principais são projeções ortogonais das variáveis originais e são ordenados de acordo com sua contribuição para explicar a variação nos dados. E é essa variância que é usada para informação, ou seja, para definir em quantos componentes as colunas serão transformadas.
Como se trata de dados diversos, trazendo informações que com certeza não estão no mesmo padrão, iremos padronizar os features para evitar que o PCA seja prejudicado caso haja variâncias muito grandes ou pequenas. Usaremos a ferramenta StandartScale do sklearn para esse processo. Note que vamos usar apenas os dados do treino para moldar os parâmetros da uniformização, mantendo os dados de teste isolados. Para isso vamos separar a base entre \(\it features\) e \(\it target\) e usar 15% dos dados para teste. Note que não estamos normalizando a última coluna, que é a única dummie da base.

X = dataset.iloc[:, np.r_[0:37, 38:40]].values 
y = dataset.iloc[:, 37].values

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.15)

SCx = StandardScaler()
X_train[:, :-1] = SCx.fit_transform(X_train[:, :-1])
X_test[:, :-1] = SCx.transform(X_test[:, :-1])

Agora vamos ao PCA. Novamente iremos recorrer ao sklearn para os próximos passos. No primeiro momento vamos fazer a função do PCA sem parâmetros, apenas iremos usar a base de treino das features e iremos calcular o número de componentes para explicar a variância. Vamos mostrar a contribuição de cada componente e mostrar o acumulado da variancia explicada também. Nesse momento estabelecemos até onde queremos a variância acumulada e escolhemos o número de componentes baseado nessa informação.

from sklearn.decomposition import PCA

pca = PCA()
pca.fit(X_train) #Apenas fit. Transform vem depois de achar o número de componentes

cumulative_explained_variance = np.cumsum(pca.explained_variance_ratio_)
plt.plot(range(len(cumulative_explained_variance)), cumulative_explained_variance)

plt.bar(range(1, (len(pca.explained_variance_ratio_) +1)), pca.explained_variance_ratio_, alpha=0.5, align='center', 
        label='Explicação individual')
plt.step(range(1, (len(pca.explained_variance_ratio_) +1)), np.cumsum(pca.explained_variance_ratio_), where='mid', 
        label='Variância explicada acumulada')
plt.ylabel('Taxa de Variância Explicada')
plt.xlabel('Nº Componentes')
plt.legend(loc='best')
plt.tight_layout()
plt.show()

variance = pd.DataFrame(pca.explained_variance_ratio_)
np.cumsum(pca.explained_variance_ratio_)

A imagem será apresentada aqui.

Após rodas o código acima, concluímos que o número de componentes para explicar a variação de 90 da variância é 9. Agora vamos treinar novamente a função PCA , pórem ocorrerá a transformação dos dados também. Vale frisar que o \( \it fit\) ocorre somente na base de treino, tanto na padronização quanto na aplicação do PCA. Isso evita que ocorra contaminação da base de teste. Ambos os processos também só ocorrem nos \(\it features\). A \(\it Target\) não é tocada em nenhum dos momentos.

pca = PCA(n_components = 9)
X_train = pca.fit_transform(X_train)
X_test = pca.transform(X_test)

Agora com nossos \( \it features\) transformados, vamos verificar qual modelo é mais adequado.
Para isso vamos utilizar a técnica de Cross Validation. CV consiste em dividir a base de treino em k subgrupos de mesma dimensão. Após a divisão, usamos k-1 subgrupos para treinar o modelo e testar no subgrupo que foi deixado de fora. Tal processo é feito repetivamente até todos k subgrupos tenham sidos usados como teste pelo menos uma vez, ou seja, a quantidade de \( \it output\) vai depender de quantos subgrupos foram formados. Nesse processo de validação pode haver um otimismo nos resultados. Isso porque, diferentemente da base de teste separada no começo do código, que não teve nenhum contato com os processos elaborados na base de treino, os subgrupos usados no CV foram "contaminados" pela base de treino.

from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score


kfold = KFold(n_splits=5)

kfold.get_n_splits(X_train)
print(kfold)
for train_index, test_index in kfold.split(X_train):

    X_train_val, X_test_val = X[train_index], X[test_index]
    y_train_val, y_test_val = y[train_index], y[test_index]


mse_medias, mse_mins, mse_maxs = list(),list(),list()
r2_medias, r2_mins, r2_maxs = list(),list(),list()
models = {LinearRegression(), DecisionTreeRegressor()}

def CV(model):


        scores = cross_val_score(model, X_train_val, y_train_val, scoring='r2', cv=kfold)
        mse = cross_val_score(model, X_train_val, y_train_val, scoring='neg_mean_squared_error', cv=kfold)


        r_mean, r_min, r_max = scores.mean(), scores.min(), scores.max()

        m_mean, m_min, m_max = mse.mean(), mse.min(), mse.max()


        r2_medias.append(r_mean)
        r2_mins.append(r_mean - r_min)
        r2_maxs.append(r_max - r_mean)

        mse_medias.append(m_mean)
        mse_mins.append(m_mean - m_min)
        mse_maxs.append(m_max - m_mean)


for model in models:
    CV(model)

Modelos = ['Linear Reg','DT', 'KNN', 'RF' ]

CV_r2 = pd.DataFrame(list(zip(Modelos, r2_mins, r2_medias, r2_maxs)),
              columns=['Modelo','R2 Min', 'R2 Media', 'R2 Max'])

CV_mse = pd.DataFrame(list(zip(Modelos, mse_mins, mse_medias, mse_maxs)),
              columns=['Modelo','MSE Min', 'MSE Media', 'MSE Max'])

sns.factorplot(y='Modelo',x='R2 Max',data=CV_r2,kind='bar',size=5,aspect=2)
sns.factorplot(y='Modelo',x='MSE Min',data=CV_mse,kind='bar',size=5,aspect=2)

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

Após todo o processo, definimos o melhor modelo baseado no R2 máximo e menor Mean Squared Error. O modelo escolhido é o KNN.

def KNN():
    RegKNN = KNeighborsRegressor()
    RegKNN.fit(X_test, y_test)
    y_pred = RegKNN.predict(X_test)
    plt.scatter(X_test[:,0], y_test, color = 'red')
    plt.scatter (X_test[:,0], y_pred, color = 'blue')
    plt.ylabel('Receita')
    plt.xlabel('Restaurantes')
    plt.title('KNN - n = 5')
    plt.show()
    print (r2_score( y_test, y_pred))

KNN()

A imagem será apresentada aqui.

Vemos que a previsão está longe de ser satisfatória (score = 0.086). Era esperado dado o resultado no CV, apesar do maior entre os testados. A falta de disponibilidade de dados também prejudicou o modelo. Uma base de treino maior e mais heterogênea poderia contribuir. Além disso, informações claras sobre quais informações as colunas trazem facilitariam o processo de preparo e escolha dos \( \it features\).

comentou Dez 19, 2020 por Camilaspinto (21 pontos)  
Oi, Gustavo! Concordo com você sobre o fato de as variáveis anônimas dificultarem a análise e a escolha do input do modelo. Caso fossem conhecidas seria possível realizar exclusões objetivas. Achei uma boa ideia utilizar o recurso do PCA para identificar a quantidade de variáveis necessárias para previsão e reduzir a chance de overfit. De toda forma, como primeiro passo, acho que seria uma boa ideia rodar os modelos e verificar o comportamento e acurácia com todas as variáveis disponibilizadas.
No mais, parabéns pela sua resposta, será bastante útil para o meu processo de aprendizado.
...