fbpx

Categoria: Regressão

How To Predict Multiple Time Series With Scikit-Learn (With a Sales Forecasting Example)

You got a lot of time series and want to predict the next step (or steps). What should you do now? Train a model for each series? Is there a way to fit a model for all the series together? Which is better?

I have seen many data scientists think about approaching this problem by creating a single model for each product. Although this is one of the possible solutions, it’s not likely to be the best.

Here I will demonstrate how to train a single model to predict multiple time series at the same time. This technique usually creates powerful models that help teams win machine learning competitions and can be used in your project.

Como Criar um Sistema de Recomendação de Produtos Usando Machine Learning

Imagine que para cada usuário registrado em seu site você pudesse recomendar produtos diferentes, personalizados para os gostos do cliente. Isso é possível usando sistemas de recomendação automática baseados em machine learning.

Esta é uma das aplicações mais famosas de machine learning em comércio eletrônico. Quem nunca visitou o site de uma loja e dentro da página havia “outros produtos que podem te interessar”? Várias empresas já adotam este tipo de sistema, inclusive gigantes como a Amazon e Netflix.

Os métodos descritos neste artigo podem ser aplicados a qualquer produto. Aqui vou demonstrar o uso com um banco de dados de usuários de uma comunidade de leitores. O desafio é, baseado em notas de 0 a 10 dadas a livros, recomendar novos livros que o usuário possa gostar.

Formato dos Dados e Tarefa

Para criar o sistema de recomendação, basta termos os dados no seguinte formato:

Usuário – Produto – Nota

Ou seja, para cada produto que o usuário deu uma nota, teremos uma linha em nosso arquivo. Se o seu site não possui um sistema de avaliação de produtos, também é possível substituir a nota pelo número um, caso o cliente tenha comprado o produto, e zero, em caso negativo.

Nós tentaremos prever a nota que um usuário dará a um livro que ele ainda não avaliou (e provavelmente não leu). Na prática, baseado nas notas dadas aos novos livros, podemos recomendar a ele os livros com maior nota, pois estes são os livros que nosso modelo sugere que despertam o interesse deste leitor.

Fiz uma breve limpeza nos dados, e tanto eles quanto o código se encontram neste link: Github Materiais Recomendação
Os dados originais foram retirados deste site: Book-Crossing Dataset

Ferramentas

Eu utilizarei a biblioteca Surprise, em Python, que possui algoritmos de recomendação que podemos treinar usando nossos dados. Ela não é uma biblioteca muito extensa, mas possui tudo o que precisamos para fazer nosso sistema.

Para avaliar o modelo usarei a função nativa da biblioteca para dividir os exemplos em 3 partes e fazer validação cruzada.

O Primeiro Modelo

O primeiro modelo que faremos é muito simples, baseado na nota geral dos produtos, diferença da nota média do produto para a nota geral e diferença da nota média do usuário para a nota geral.

Então, por exemplo, imagine que a média de todas as notas, de todos os produtos de seu site seja 3. Esta é o que chamamos de média geral.

Agora imagine que a nota do livro que queremos recomendar ao usuário seja 4. Para obtermos a diferença da média geral, subtraímos 3 de 4 (4 – 3), e temos que o valor da diferença da nota média do produto para a nota geral é 1. Ou seja, este livro é avaliado como melhor do que a média dos livros do site.

O último componente de nossa fórmula envolve a média de notas que o usuário dá aos livros. Isso leva em consideração a personalidade mais seletiva ou não de alguns usuários. Em nosso exemplo, a média da nota do usuário é 3, significando que o usuário é mais exigente que a média. Subtraímos a média geral deste valor (3-3), e obtemos a diferença da nota média do usuário para a nota geral, que é 0.

A fórmula que usamos para prever a nota que este usuário dará a este produto é a seguinte:

Avaliação = média geral + diferença da nota média do produto para a nota geral + diferença da nota média do usuário para a nota geral

Ou seja, neste caso 3 + 1 + 0 = 4.

No código abaixo usei o modelo BaselineOnly, que calcula os coeficientes de cada usuário e produto, além da média geral, de acordo com nossos dados de treino, e armazena para podermos usar em novos produtos.

Para medir o erro, usei a Raiz Quadrada do Erro Médio Quadrado, que basicamente mostra em média quanto a nota prevista desvia da nota real.

O erro para este modelo foi de: 1,65347. Este é um erro baixo, se pensarmos que as notas vão de 1 a 10.

Testando um Modelo mais Complexo

Agora vou testar um modelo mais avançado. Em vez dos três números usados pelo modelo acima, este vai tentar encontrar representações mais complexas para cada usuário e produto. Isso dá maior capacidade do modelo capturar detalhes, e a ideia é que capturando estes detalhes o modelo possa estimar com menor erro a nota dada a um novo produto.

O erro para este modelo foi de 1,74865. Apesar de ser um erro baixo, não é melhor que nosso modelo mais simples.

Nem sempre um modelo ou algoritmo mais avançado, complexo, significa uma melhora. E em alguns casos, a melhora é tão pequena que não vale a pena. Por isso é importante que o cientista de dados saiba o que está fazendo, e conheça o funcionamento dos modelos, para saber qual é a melhor alternativa para o banco de dados e a tarefa em questão, em vez de apenas aplicar o que é mais popular ou mais avançado.

Eu ainda tentei encontrar parâmetros para otimizar este modelo, pensando que isso pudesse ajudá-lo a superar o modelo mais simples, mas não obtive êxito. Isso fortalece a hipótese que o modelo mais simples é melhor neste caso.

Conclusão

Esta foi uma demonstração bem simples e rápida de como criar um modelo de recomendação que poderia ser usado em um site de comércio eletrônico. Existem vários outros passos, como a verificação mais cautelosa dos dados, definições da melhor maneira de fazer o processo de modelagem e a otimização e testes dos modelos, que devem ser feitas para garantir que o modelo tenha uma boa performance e seja robusto, mas elas fogem ao escopo deste artigo.

Tutorial: Aumentando o Poder Preditivo de Seus Modelos de Machine Learning com Stacking Ensembles

Quem acompanha competições sabe que uma das coisas mais importantes é saber juntar vários modelos para criar uma solução poderosa. Várias pessoas já me perguntaram, por e-mail ou nas apresentações que fiz, sobre ensembles. Este é um assunto importante não apenas para competições, mas também para casos reais onde se quer extrair o máximo possível de performance dos modelos.

Ensembles são conjuntos de modelos que oferecem uma performance melhor do que cada modelo que o compõe.

Então neste artigo quero exemplificar a melhor maneira que conheço de criar ensembles: stacking. Este é um método que usei em todas as competições que tive bons resultados.

Antes de começar, uma dica: é importante pensar em Machine Learning como “processos”. Aqui não vamos apenas testar modelos em um banco de dados, mas vamos testar processos, pipelines de métodos aplicadas aos dados para saber quais são os resultados.

Este material é parte da apresentação que fiz no dia 30/11 no PyData Meetup São Paulo.

O Jupyter Notebook original está diponível neste link.

In [1]:
import pandas as pd
import numpy as np

Carregando os dados

Os dados utilizados são transações comerciais de imóveis na cidade de Ames, Iowa. Nosso objetivo é prever o preço de venda de uma casa alimentando o modelo com as características. Estes dados também são tema de uma competição no Kaggle. Os dados de treino e teste podem ser encontrados em: https://www.kaggle.com/c/house-prices-advanced-regression-techniques/

Algo muito importante é explorar os dados em busca de ideias de features e métodos para validar os modelos. Como o foco deste material é stacking, vou pular esta parte. Ainda assim, acho importante esclarecer que temos dois tipos básicos de variáveis nestes dados: categóricas e numéricas.

As variáveis categóricas possuem níveis que não podem ser ordenados. Em alguns casos é possível pensar em maneiras de ordená-los, como uma variável que descreve se uma rua é asfaltada ou não. Neste caso pode-se pensar que a rua asfaltada vai valorizar o imóvel.

As variáveis numéricas podem receber qualquer valor contínuo. Um exemplo seria o tamanho do terreno.

Nesta célula carregamos os dados e criamos um DataFrame com as features (X) e uma Série com o preço da venda (SalePrice, y) que é nosso alvo.

No total temos 79 colunas, mas limitei a 5 aqui por causa da formatação do site.

In [2]:
train = pd.read_csv('train.csv', index_col='Id')
X, y = train.drop('SalePrice', axis=1), train.SalePrice.copy()
train.head().iloc[:, :5]
Out[2]:
MSSubClass MSZoning LotFrontage LotArea Street
Id
1 60 RL 65.0 8450 Pave
2 20 RL 80.0 9600 Pave
3 60 RL 68.0 11250 Pave
4 70 RL 60.0 9550 Pave
5 60 RL 84.0 14260 Pave

Conjuntos de Variáveis (Features)

Uma das maneiras de obter modelos diferentes é variar a representação dos dados utilizada para treiná-los. Por isso nosso primeiro passo será construir estas representações.

Funções auxiliares

A métrica sugerida pelo Kaggle para avaliar os modelos é o RMSLE, este erro leva em conta a diferença entre o logaritmo das previsões e do alvo. É possível pensar neste erro como uma aproximação do erro percentual do modelo, mas com propriedades mais interessantes do ponto de vista matemático.

Criei três funções para cálculo do erro. Nós vamos fazer transformações da variável y, então para facilitar nosso trabalho, decidi criar funções personalizadas para cada transformação, assim evita alguma confusão na hora de transformá-las para computar o erro numa função só.

Para usarmos algumas funções do Scikit-learn que vão ajudar a manter nosso código mais limpo e mais eficiente, precisamos criar as funções de erro da maneira requerida pelo módulo. Neste caso, a função pecisa receber um modelo treinado, as features e o alvo. Ela computará as previsões e deverá retornar um número, que é o valor da métrica de erro.

A última linha da célula cria um objeto da validação cruzada do scikit-learn. Ele vai cuidar da divisão dos dados para nós. Este assunto merece uma série de artigos, mas basicamente é uma maneira de dividir os dados em N partes, e usar repetidamente N-1 partes como dados de treino, e a parte restante como dados de teste. Por exemplo, se temos as partes [1, 2, 3, 4, 5], numa das iterações podemos treinar usando as partes [1, 2, 3, 4] e validar na parte 5.

Apesar desta ser uma série temporal, o Kaggle decidiu tratar como dados aleatórios independentes, então por isso faremos esta validação cruzada simples.

In [3]:
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import cross_val_score, cross_val_predict, KFold
from sklearn.ensemble import RandomForestRegressor

def rmsle(estimator, X, y):
    p = estimator.predict(X)
    return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))

def rmsle_log_y(estimator, X, y):
    p = estimator.predict(X)
    return np.sqrt(mean_squared_error(y, p))

def rmsle_sqrt_y(estimator, X, y):
    p = estimator.predict(X)
    y = np.power(y, 2)
    p = np.power(p, 2)
    return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))

kf = KFold(n_splits=5, shuffle=True, random_state=1)

Feature set 1: variáveis “numéricas”

O primeiro conjunto de features que teremos será uma seleção simples das variáveis originalmente numéricas dos dados. Para isto basta selecionar todas as colunas que possuem variáveis com números inteiros ou de ponto flutuante. Além disso, para substituir os valores nulos (o scikit-learn exige a substituição), decidi colocar o valor -1. Como vamos utilizar apenas modelos baseados em árvores com estes dados originais, não há necessidade de se preocupar muito com isso para nossos propósitos.

Após armazenar estes dados na variável X1, vemos que são 36 colunas. Já nesta parte quero treinar um modelo de Random Forest dentro da validação cruzada, para sabermos como ele se sairia sozinho nos dados originais. Para isso utilizei a função cross_val_score. Ela é uma função que facilita bastante na hora de fazer a validação cruzada com scikit-learn. Basta colocar um modelo e os dados. Neste caso optei por especificar um esquema de validação e uma métrica de erro personalizada.

Esta função retorna uma lista com os erros de cada iteração da validação cruzada, então fiz a média para sabermos o erro médio das partes.

In [4]:
X1 = X.select_dtypes(include=[np.number]).fillna(-1)
print('Dims', X1.shape)
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X1, y, cv=kf, scoring=rmsle).mean()
print('RMSLE:', error)
Dims (1460, 36)
RMSLE: 0.14582352618

Feature set 2: Ordinal Encoding Categóricas

Agora vamos criar um outro conjunto de features, desta vez adicionando as variáveis categóricas. Existem várias maneiras de codificar este tipo de variável para os modelos, uma delas é usando um formato ordinal. Isso simplesmente significa substituir cada valor original por números sequenciais. Em alguns modelos isso pode ser problemático, pois eles tentarão capturar alguma relação de ordem em valores que podem não ter. No nosso caso, com modelos baseados em árvores de decisão, este problema é quase inexistente.

Após codificar desta maneira, rodamos novamente a validação cruzada, agora nestes novos dados.

In [5]:
from sklearn.preprocessing import LabelEncoder

X2 = X.copy()
for col in X2.columns:
    if X2[col].dtype == object:
        enc = LabelEncoder()
        X2[col] = enc.fit_transform(X[col].fillna('Missing'))

print('Dims', X2.shape)
X2.fillna(-1, inplace=True)
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X2, y, cv=kf, scoring=rmsle).mean()
print('RMSLE:', error)
Dims (1460, 79)
RMSLE: 0.143837364859

Bônus: OneHot Encoding Categóricas

A maneira mais popular de codificar variáveis categóricas é o One Hot Encoding. Basicamente consiste em transformar cada valor da variável em uma coluna cujo novo valor será 1 caso a variável tenha aquele valor num determinado exemplo, ou 0 em caso negativo. Existem indicativos que árvores de decisão não processem tão bem este tipo de representação, mas em alguns casos práticos já vi isto funcionar melhor que o ordinal, então veja esta como mais uma ferramenta.

Este método cria mais de 200 novas colunas, o que deixa o processo de treino mais devagar, então decidi deixar a linha da validação cruzada comentada. Caso queira ver o resultado, basta rodá-la sem o jogo da velha.

In [6]:
#from sklearn.preprocessing import OneHotEncoder
X3 = X.copy()
cats = []
for col in X3.columns:
    if X3[col].dtype == object:
        X3 = X3.join(pd.get_dummies(X3[col], prefix=col), how='left')
        X3.drop(col, axis=1, inplace=True)
    

print('Dims', X3.shape)
X3.fillna(-1, inplace=True)
model = RandomForestRegressor(n_estimators=1000, random_state=0)
#cross_val_score(model, X3, y, cv=kf, scoring=rmsle).mean()
Dims (1460, 288)

Transformações do Target

Uma maneira interessante de criar diversidade, e às vezes até obter uma melhor performance, num caso de regressão, é transformar a variável que estamos tentando prever. Neste caso testaremos duas transformações: logaritmo e raiz quadrada.

Log

É possível ver que tentar prever o logaritmo do preço nos dá um resultado melhor. Isto acontece não só pelo fato do modelo capturar padrões diferentes, mas também porque usamos uma métrica baseada na diferença de logaritmos.

In [7]:
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('RF, X1, log-target RMSLE:', error)

model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('RF, X2, log-target RMSLE:', error)
RF, X1, log-target RMSLE: 0.14518580749
RF, X2, log-target RMSLE: 0.14207134495

Raiz Quadrada

Esta transformação também nos dá um resultado melhor do que usar a variável em seu estado original. Uma das sugestões da razão pela qual vemos este efeito é que estas transformações fazem com que a variável y tenha uma distribuição mais próxima da normal, o que facilita o trabalho do modelo.

In [8]:
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('RF, X1, sqrt-target RMSLE:', error)

model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('RF, X2, sqrt-target RMSLE:', error)
RF, X1, sqrt-target RMSLE: 0.145652934484
RF, X2, sqrt-target RMSLE: 0.143004600132

Gerando modelos com modelos/algoritmos diferentes

Outra maneira de gerar diversidade para o ensemble é gerar modelos diferentes. Neste caso vou usar meu modelo preferido, o GBM. Este também é baseado em árvores de decisão, mas basicamente treina cada árvore sequencialmente focando nos erros cometidos pelas anteriores.

Nas células abaixo é possível ver a performance deste modelo nos feature sets e transformações que usamos com a Random Forest. Vemos que ele traz uma melhora significativa, capturando melhor os padrões da relação entre as variáveis e o preço de venda dos imóveis.

In [9]:
from sklearn.ensemble import GradientBoostingRegressor
                
model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('GBM, X1, log-target RMSLE:', error)

model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('GBM, X2, log-target RMSLE:', error)
GBM, X1, log-target RMSLE: 0.133492454914
GBM, X2, log-target RMSLE: 0.129806890482
In [10]:
from sklearn.ensemble import GradientBoostingRegressor
                
model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('GBM, X1, sqrt-target RMSLE:', error)

model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('GBM, X2, sqrt-target RMSLE:', error)
GBM, X1, sqrt-target RMSLE: 0.134258972813
GBM, X2, sqrt-target RMSLE: 0.130919235682

Ajustando hiperparâmetros

Para simplificar o exemplo e focar na parte do ensemble, não vou fazer o ajuste dos hiperparâmetros do modelo. Hiperparâmetros são os atributos do modelo (como a profundidade das árvores de decisão) que precisam ser ajustados usando dados separados de validação ou um ciclo de validação cruzada.

É bom saber que nem sempre os melhores modelos formam os melhores ensembles. É importante ter modelos poderosos ao fazer stacking, mas também devemos lembrar que é importante ter diversidade. Às vezes alguns modelos que possuem um erro mais alto podem capturar padrões diferentes dos melhores modelos e por isso contribuem com o ensemble.

Caso você decida fazer o ajuste dos hiperparâmetros, é importante colocá-lo dentro do ciclo de validação cruzada que veremos no próximo passo.

Stacking

Tudo o que fizemos acima é para que possamos criar o nosso ensemble. Esta é a hora juntarmos os métodos usados para melhorarmos o poder preditivo dos nossos modelos.

O Stacking é uma maneira de fazer o ensemble na qual usamos modelos para fazer previsões, e depois usamos estas previsões como features em novos modelos, no que pode ser chamado de “segundo nível”. Você pode fazer este processo várias vezes, mas a cada nível o retorno em performance com relação à computação necessária é menor.

Nesta fase precisamos de dois ciclos de validação cruzada: externo e interno. No interno, nós treinaremos os modelos nos dados originais e faremos as previsões. No externo, treinaremos o modelo usando as previsões do primeiro passo como features.

A cada passo da validação cruzada interna, vamos salvar as previsões para a parte dos dados que for usada como validação. Desta maneira teremos previsões para todos os exemplos de nossos dados de treino. Além disso treinaremos um modelo nos dados originais de treino para podermos fazer previsões para os dados de teste.

No ciclo externo treinaremos um modelo nas previsões geradas pelo ciclo interno, e as features dos dados de validação serão as previsões dos modelos de primeiro nível nos dados de teste.

No nosso caso específico, criaremos previsões usando todas as combinações de modelos (RF e GBM), transformações do target (log e raiz quadrada) e feature sets (X1 e X2). Não usaremos o X3 porque levaria muito tempo para treinar, e para os fins de demonstração do método estes dois serão o bastante.

No fim teremos previsões de 8 modelos no primeiro nível. No segundo nível usei uma regressão linear regularizada (Ridge). Tendo estas previsões podemos computar o erro nos dados fora das nossas amostras de treino e validação internas, o que nos dará uma estimativa confiável do erro do ensemble.

In [13]:
from itertools import product
from sklearn.linear_model import Ridge

kf_out = KFold(n_splits=5, shuffle=True, random_state=1)
kf_in = KFold(n_splits=5, shuffle=True, random_state=2)

cv_mean = []
for fold, (tr, ts) in enumerate(kf_out.split(X, y)):
    X1_train, X1_test = X1.iloc[tr], X1.iloc[ts]
    X2_train, X2_test = X2.iloc[tr], X2.iloc[ts]
    y_train, y_test = y.iloc[tr], y.iloc[ts]
    
    modelos = [GradientBoostingRegressor(random_state=0), RandomForestRegressor(random_state=0)]
    targets = [np.log1p, np.sqrt]
    feature_sets = [(X1_train, X1_test), (X2_train, X2_test)]
    
    
    predictions_cv = []
    predictions_test = []
    for model, target, feature_set in product(modelos, targets, feature_sets):
        predictions_cv.append(cross_val_predict(model, feature_set[0], target(y_train), cv=kf_in).reshape(-1,1))
        model.fit(feature_set[0], target(y_train))
        ptest = model.predict(feature_set[1])
        predictions_test.append(ptest.reshape(-1,1))
    
    predictions_cv = np.concatenate(predictions_cv, axis=1)
    predictions_test = np.concatenate(predictions_test, axis=1)
    
    stacker = Ridge()
    stacker.fit(predictions_cv, np.log1p(y_train))
    error = rmsle_log_y(stacker, predictions_test, np.log1p(y_test))
    cv_mean.append(error)
    print('RMSLE Fold %d - RMSLE %.4f' % (fold, error))
    
print('RMSLE CV5 %.4f' % np.mean(cv_mean))
    
RMSLE Fold 0 - RMSLE 0.1248
RMSLE Fold 1 - RMSLE 0.1449
RMSLE Fold 2 - RMSLE 0.1257
RMSLE Fold 3 - RMSLE 0.1409
RMSLE Fold 4 - RMSLE 0.1087
RMSLE CV5 0.1290

Como podemos ver, nosso melhor modelo de primeiro nível é o GBM treinado com a transformação log nos dados X2, que atinge o erro de 0,1298. Nosso ensemble atinge o valor de 0,1290. Uma melhora de 0,62%.

O objetivo deste artigo era demonstrar o método, sem se preocupar muito com a performance no fim. Um ensemble feito visando melhora de performance pode apresentar um resultado mais significativo.

Em alguns casos, como em fundos de investimento ou aplicações de saúde, uma melhora pequena pode ter um resultado bastante significativo no mundo real, que justifique a criação de uma solução mais completa usando stacking.

Como sempre na aplicação de Machine Learning, nada é garantia de sucesso, mas este método é um dos mais consistentes em oferecer uma melhora.

Melhorando o Sistema de Busca de E-commerce da Home Depot com Machine Learning

Uma das tarefas mais importantes para um site de e-commerce é apresentar produtos relevantes para o usuário. Existem várias maneiras de se criar um sistema de busca de produtos, inclusive usando machine learning em alguma parte do processo. Uma delas é usar um modelo para determinar a relevância de um produto para um termo de busca.

A Home Depot é uma rede de lojas que comercializa itens para casa, como móveis, eletrodomésticos e equipamentos de manutenção. Além das lojas físicas, existe uma loja virtual onde os consumidores podem navegar pelos produtos e fazer compras.

Nesta competição a tarefa a ser solucionada era determinar qual a relevância dos resultados do mecanismo de busca do site desta empresa. Foram disponibilizados dados sobre a frase de busca e informações dos produtos retornados, além de uma pontuação, indicando a relevância daquele resultado.

A solução apresentada aqui é parte da que usei para garantir o 13º lugar, entre mais de 2000 times, na competição.

Quais Eram as Informações Disponíveis?

Foram disponibilizados pares de termos de busca e produtos retornados. Para a maioria dos produtos havia o título e a descrição, além de alguns atributos específicos como medidas, cor, material e textura.

Por fim, uma coluna determinava a relevância, que ia de 1 a 3, sendo 1 para produtos irrelevantes para a busca, e 3 para produtos bastante relevantes. Estas notas foram dadas por vários avaliadores humanos

Durante a criação dos dados, os avaliadores tinham fotografias dos produtos para ajudar a determinar a relevância, mas estas não foram disponibilizadas durante a competição.

As Diferenças Entre Dados de Treino e Teste

O treino e o teste continham termos de busca diferentes. Apenas 30% dos termos de busca do teste apareciam no treino. No fim da competição foi revelado que alguns exemplos dos dados de teste não eram utilizados para avaliar o resultado do modelo, mas apenas para evitar que competidores determinassem a relevância manualmente.

Alguns produtos também só apareciam em um dos dois conjuntos dos dados.

Num caso real esta divisão faz sentido, já que não queremos que o modelo seja bom apenas nas buscas que já conhecemos, mas também em novas buscas e produtos.

Como Isso Influenciou a Validação

Por causa da diferença citada acima, era importante criar um ambiente de validação que não tivesse dados sobre o mesmo termo de busca no treino e no teste.

Em vez de fazer uma validação aleatória comum, sorteei termos de busca, de maneira que fossem criadas 5 divisões, cada uma contendo todas as linhas para os termos de busca atribuídos a ela.

Isso foi fundamental para obter um bom resultado na competição, já que os resultados locais estavam bastante próximos dos resultados na validação do Kaggle (Public Leaderboard), o que me dava mais segurança sobre a capacidade de meu modelo generalizar para os dados privados.

Como as Soluções Eram Avaliadas

A métrica escolhida pelos patrocinadores foi o RMSE (raiz quadrada da média dos erros elevados ao quadrado). É uma métrica bem popular e basicamente aplica uma penalidade proporcional à diferença entre a previsão e a realidade.

Como Modelar Esta Tarefa

Num primeiro momento vi muita gente tentando criar modelos baseados em representações de “bag-of-words” dos dados. Esta representação consiste em criar uma matriz cujas linhas são os exemplos e as colunas são as palavras contidas nos exemplos. Em sua forma mais simples, se a palavra está no documento, o valor para a coluna, naquela linha é 1, caso contrário 0.

Mas esta tarefa requer um pensamento diferente. Na verdade o conteúdo até tinha poder preditivo, mas o mais importante não eram as palavras, e sim a relação entre o produto e termo de busca. E isso dificilmente é capturado da melhor maneira pela abordagem acima.

Pensando nisso, decidi me focar em criar variáveis que representassem a similaridade entre os produtos e os termos de busca correspondentes.

Limpando e Preparando o Texto Original

Um dos passos para o processamento de dados textuais é descobrir se a limpeza de algumas partes do texto pode ajudar o modelo a encontrar uma solução melhor.

Uma das sugestões dadas no fórum era substituir “medidas” expressas de maneiras diferentes por um termo comum. Um exemplo seria “m2” se tornar “metro quadrado”. Isso evita que algumas métricas de similaridade determinem que estes são conceitos diferentes, apesar de se referirem à mesma coisa.

Além disso, remover toda a pontuação e excluir palavras muito comuns, com pouco valor preditivo (como artigos), também ajudou a tornar o modelo melhor.

Stemming

Uma técnica bastante útil nestes casos, stemming significa retirar partes que flexionam uma palavra. Por exemplo, transformar a palavra “training” em “train”. Isso ajuda em algumas tarefas nas quais estas diferenças entre as palavras não influenciam, ou até acabam atrapalhando o modelo.

Neste caso, fazer o stemming sobre o texto ajudou as medidas de similaridade a capturar com maior precisão os valores corretos entre termos e produtos.

Correção Ortográfica

Como estamos tratando de texto gerado por usuários, podem acontecer erros de digitação, ou até mesmo diferenças de escrita em nomes de marcas mais complicadas de produtos.

Por isso, foi importante encontrar uma maneira de corrigir os possíveis erros ortográficos. Utilizei uma tabela disponibilizada nos fóruns, na qual um usuário usou o corretor ortográfico do Google para arrumar as frases de busca. Este foi um dos passos que mais ajudou a reduzir o erro do modelo.

N-gramas

N-gramas na análise de texto normalmente podem significar trechos de caracteres ou de palavras.

Nesta competição usei os dois tipos. Quando a unidade mínima é uma palavra, um unigrama pode ser “tinta” ou “azul”. Mas também é possível utilizar bigramas, trigramas, etc. Um bigrama seria “escada branca”, simplesmente uma string com duas palavras.

Quando a unidade mínima do n-grama é um caractere, o N se refere ao número de letras do trecho, sendo “abc” um trigrama.

Apesar de existirem similaridades que podem ser aplicadas diretamente a caracteres, como a Levenshtein, usar as similaridades descritas aqui com n-gramas de caracteres ajudou o modelo.

Similaridades Entre Termos de Busca e Produto

Uma das maneiras mais simples de se retornar resultados para um termo de busca é retornar os produtos com títulos e descrições mais similares às palavras buscadas. Normalmente este método já dá resultados bons, mas claro que existem detalhes que não são capturados por ele., e itens que possuem as mesmas palavras, mas são irrelevantes.

Além disso é preciso definir como será feita a comparação entre as palavras dos termos de busca e a descrição do produto. Para fazer isso existem algumas medidas bastante populares. Neste caso específico, utilizei o valor das medidas para um determinado par de busca e produto como variável no modelo.

Coeficiente de correspondência simples

Uma das variáveis mais poderosas, a correspondência simples é apenas a proporção de palavras de um campo que estão presentes no outro campo. Neste caso, um exemplo é o número de palavras do termo de busca que estão presentes no título do produto divido pelo número total de palavras do primeiro.

Como um exemplo simples: para um termo de busca “tinta azul para exterior” e um produto com título “galão de tinta para exterior – azul”, teríamos as 4 palavras do termo presentes no título, o que daria um coeficiente 4/4 = 1. Já um produto com título “galão de tinta para interior – amarela”, teria um coeficiente de 2/4 = 0,5.

Este coeficiente foi computado nas duas direções, verificando quais palavras do termo de busca estavam no título, e também qual a proporção de palavras do título no termo de busca.

Similaridade Cosseno

Esta similaridade também é bastante pupular e parece funcionar muito bem na prática. Primeiro é necessário transformar o texto original em uma matriz. A forma mais popular de fazer isso é considerar cada documento em uma linha da matriz, criando uma coluna para cada palavra e colocando um valor na célula correspondente à presença ou ausência daquela palavra no documento da linha.

Este valor pode ser um simples indicador binário da presença da palavra, ou também o número de vezes que a palavra aparece no documento. Mas uma maneira mais avançada, e a que utilizei nesta competição, é computar o coeficiente TF-IDF e usar como valor. Este coeficiente leva em conta a frequência da palavra no documento atual e também com relação aos outros documentos.

Após transformar os campos de texto originais em matrizes, normalizei os vetores para que cada um tivesse tamanho igual a 1, e computei o produto escalar entre eles. Como exemplo, computei o produto escalar entre o termo de busca e o título do produto para cada linha.

Similaridade Jaccard

Outra métrica de similaridade bastante interessante é a Jaccard. Originalmente ela é uma medida para conjuntos. No caso, para usá-la com texto, consideramos cada frase como um conjunto de palavras (ou n-gramas).

Para computar a métrica, basta dividir o número de elementos da intersecção entre os dois grupos pelo número de elementos da união dos mesmos. Por exemplo, se considerarmos o termo de busca “tinta azul para exterior” e o título “galão de tinta para exterior – azul” teremos uma intersecção de tamanho 4 e uma união de tamanho 6, então a similaridade Jaccard é igual a 0,667.

Existe uma versão bastante similar à Jaccard, chamada Similaridade Tanimoto, que pode ser computada diretamente sobre vetores de palavras, mas aqui usei a versão tradicional.

O Modelo Utilizado

O modelo que utilizei, sem surpresas, foi Gradient Boosted Trees, através da implementação do XGBoost.

Para tentar extrair valor do texto “bruto” tentei usar um SVM linear e Random Forests, mas nenhum deles foi capaz de me dar uma performance muito boa. Isso já era esperado, já que o conteúdo, apesar de importante, não é o foco da tarefa, e sim a similaridade entre campos diferentes.

Ideias Que Não Funcionaram

Em todo projeto de ciência de dados existem vários métodos a serem testados, mas boa parte deles acaba não sendo útil na tarefa específica. Apesar de machine learning ser uma área acadêmica de exatas, aplicar os modelos a dados reais envolve bastante tentativa e erro guiada, claro, por experiências anteriores. Por isso acho importante relatar algumas coisas que tentei mas não funcionaram.

Olhando os dados parecia que a correspondência entre as palavras finais de um termo de busca e título do produto era bastante preditiva quanto à relevância. Tentei criar várias variáveis baseadas nessa ideia, mas elas não funcionaram. Entendo que qualquer padrão relacionado a isso já era capturado pelas outras variáveis.

Outro método bastante comum em competições, o ensemble de vários modelos para melhorar a pontuação, não funcionou bem desta vez. Outros times também relataram que era mais produtivo juntar as variáveis de todos os integrantes e treinar um modelo, do que tentar criar um ensemble com os modelos de cada um.

Apesar do pequeno ganho, os times com as melhores colocações usaram ensembles com uma quantidade considerável de modelos.

Como Conquistamos o 1º Lugar Entre 1300 Times Na Competição de Machine Learning da Caterpillar

A Caterpillar é uma empresa que fabrica equipamentos industriais, como tratores e motores. Para manter suas operações, eles precisam comprar tubos com diversas especificações, de vários fornecedores diferentes, para usar em sua linha de produção. Cada fornecedor e produto possui um modelo de precificação diferente.

A tarefa nesta competição era criar um modelo que fosse capaz de precificar os tubos utilizando dados históricos de fornecedores e características dos produtos.

Tive o prazer de competir no time que venceu esta competição, ultrapassando mais de 1300 times de cientistas de dados do mundo todo. Neste artigo quero descrever as partes mais importantes sobre a criação da solução que garantiu esta vitória.

Por ter uma familiaridade melhor com a minha parte da solução, boa parte deste artigo se refere a ela, mas quero deixar claro que a solução vencedora foi criada utilizando modelos diferentes desenvolvidos individualmente por cada integrante. Nenhum de nós teria conseguido vencer sozinho, e todos foram essenciais para que o time alcançasse o primeiro lugar.

Dados

Foram disponibilizados vários arquivos com características dos tubos e dos fornecedores. Existem duas categorias básicas de precificação: preço fixo e preço variável de acordo com a quantidade.

Havia cerca de 30 mil linhas para treino, e 30 mil linhas para teste. Algo importante a se levar em consideração é o fato que várias linhas se referiam aos mesmos tubos, mudando apenas a quantidade mínima de compra para obter aquele preço.

Dentre as variáveis disponíveis estavam: quantidade, ID do tubo, data da compra, previsão de uso anual, diâmetro e comprimento.

Validação

Esta é uma das partes mais importantes de qualquer tarefa de mineração de dados. É essencial criar um ambiente de validação que tenha as mesmas características, ou se aproxime bastante, do ambiente de produção. No nosso caso, dos dados de teste.

Isso significa que não adiantaria simplesmente embaralhar as linhas e distribuir em divisões como na validação cruzada “padrão”. Neste caso, decidi sortear divisões baseadas nos tubos. Na validação cruzada, em vez de distribuir linhas nas divisões dos dados, cada divisão continha todas as linhas de um determinado tubo.

Apesar de ser uma série temporal, os dados de treino e teste não estavam divididos entre passado e futuro, então levar a data em consideração para definir os dados de validação não trazia benefícios.

Este esquema de validação se mostrou bastante robusto e próximo tanto da leaderboard pública quanto da avaliação nos dados de teste (leaderboard privada).

Extração de Variáveis

Depois de um ambiente de validação robusto, a parte mais importante é encontrar características dos dados que possam ser transformadas em variáveis para alimentar o modelo.

Vou apresentar transformações que, em geral, podem ser utilizadas com outros dados, ou seja, não são específicas desta tarefa.

Representação Ordinal das Variáveis Categóricas

As implementações mais populares dos modelos baseados em decision trees disponíveis em Python tratam todas as variáveis como numéricas. Neste caso existem algumas alternativas, como fazer one-hot encoding, para que o modelo possa encontrar características únicas dos níveis das variáveis categóricas.

Mas, pela própria natureza do modelo, fazer a codificação das variáveis de maneira ordinal permite às decision trees aproximar a captura de valor de cada categoria como se estivesse codificada da maneira correta.

Neste caso temos uma variável categórica em que cada nível é representado por um número.

Contagem de Registros

O número de vezes que um registro aparece nos dados também pode ter valor preditivo.

Uma transformação comum, e bastante útil, é substituir os valores categóricos pela contagem de registros que pertencem àquela categoria. Neste caso houve uma melhora significativa no erro.

Quantidade Mínima e Máxima Disponível

Utilizando o fato que cerca de 90% dos tubos tinham seu preço variando com a quantidade comprada, percebi que os tubos mais caros tinham uma quantidade máxima de compra menor do que tubos mais baratos.

Boa parte dos tubos mais caros tinham seu preço mínimo atingido ao comprar 5 unidades, enquanto os tubos mais baratos, chegavam a 250 unidades.

Este atributo foi muito importante, e demonstra o valor que existe em procurar entender os dados e a tarefa em questão.

Vazamento de Informação nos IDs

Esta é a situação em que temos informações nos dados de treino que permitem descobrir padrões na variável dependente, mas que não deveriam existir na realidade.

Neste caso, cada tubo era identificado por um número, e ao fazer o ordenamento dos dados usando esta variável, percebi que existiam pequenos clusters de tubos com preços e IDs parecidos. Isso significa que o ordenamento dos tubos possuía valor preditivo.

Para usar esta informação, simplesmente criei uma variável com a parte numérica dos IDs.

Tanto em competições, quanto na prática, é importante procurar estes padrões. No primeiro caso, isso pode ajudar você a conseguir um resultado melhor. No segundo caso, deve-se remover todo tipo de vazamento de informações para não comprometer a performance do modelo em um ambiente de produção.

Peso dos Componentes

Cada tubo possuía componentes diferentes que seriam acoplados a ele. As características de cada componente variavam, mas uma delas se destacava, o peso.

Com acesso à quantidade e ao tipo de componente utilizado em cada tubo, calculei a soma do peso dos componentes de cada um, e esta foi uma variável com uma correlação significativa com o preço, e pouco correlacionada com as outras.

Modelos

Gradient Boosted Trees

Famoso por seu bom desempenho em competições de áreas diversas, este foi o meu melhor modelo. Utilizei a implementação do XGBoost, que é a melhor versão open source que conheço deste modelo.

O XGBoost tem a opção de otimizar o RMSE, mas como nesta competição estamos otimizando o RMSLE, ou seja o erro logarítmico, fiz a transformação da variável dependente com a função log(y + 1). Existiam sugestões de utilizar a raiz da variável, em vez do log, e meu melhor modelo acabou sendo treinado na variável transformada com a 15ª raiz.

Este modelo, sozinho, era o bastante para conseguir o 10º lugar.

Regularized Greedy Forests

Esta foi a novidade desta competição. Apesar de ter testado este modelo na competição do Facebook, eu nunca havia dedicado tempo a fazê-lo funcionar.

O funcionamento é basicamente o seguinte: ele otimiza uma função loss igual ao GBT, mas com um termo de regularização. Além disso, periodicamente reajusta os pesos de cada nó terminal das árvores do ensemble.

Em tese é um aprimoramento sobre o GBT, mas nesta competição não apresentou uma performance melhor. Acredito que com mais tempo para ajustar os parâmetros e treinar seja possível conseguir um resultado melhor.

Outros Modelos

Dois outros modelos foram utilizados por mim, ou outros integrantes, para compor o ensemble, mas que sozinhos não tinham uma boa performance:

– Redes Neurais
– Factorization Machines

Ensemble (Stacking)

O ensemble foi feito em três níveis, usando modelos de todos os integrantes do time.

No primeiro nível, os modelos recebiam os dados originais e faziam suas previsões individuais em dados fora da amostra de treino, no mesmo esquema de validação cruzada.

No segundo nível, quatro modelos utilizavam as previsões criadas no nível anterior como dados de treino, e mais uma vez faziam previsões usando um esquema de validação cruzada para evitar overfitting.

No terceiro nível, a média das previsões dos modelos do segundo nível era utilizada como previsão final.

Conclusão

No caso desta competição, era extremamente improvável vencer sem formar um bom time. O uso de soluções criadas por membros diferentes, e que acabavam melhorando as previsões no ensemble, foi essencial.

Além disso um dos fatores mais importantes para se vencer uma competição do Kaggle é o tempo. Quanto mais tempo você tiver para testar ideias, maiores as chances de sucesso.

Determinando a Relevância de Produtos num Sistema de Buscas Usando Machine Learning

Uma das tarefas mais importantes para um site de comércio eletrônico é garantir que o usuário encontre o produto que está buscando. Isto inclui um sistema de buscas que retorne produtos relevantes. Nesta competição a CrowdFlower, uma plataforma especializada na coleta de dados, disponibilizou dados sobre buscas por produtos, e a mediana da pontuação de relevância dada por três pessoas para cada um.

O objetivo era criar um modelo que pudesse calcular a relevância de um produto para uma determinada frase de busca.

Dados

Foram disponibilizados cerca de 10 mil registros para treino, e 20 mil para teste. Nestes registros havia a frase de busca, o título e a descrição do produto, a mediana da relevância e a variância da relevância atribuída pelos usuários.

Nos exemplos de teste havia uma parcela de registros “falsos”, que não eram utilizados para calcular a performance, mas foram colocados para evitar classificação humana.

Métrica

A métrica de avaliação foi o Quadratic Weighted Kappa. Ela mede a discordância entre dois “avaliadores”. Neste caso, media a discordância entre a pontuação dada pelo modelo e a pontuação dada por humanos. Ela é sensível não apenas à precisão, mas também à distribuição da pontuação. Em tese, uma atribuição de pontos aleatória resultaria num Kappa de 0, e uma concordância total, em Kappa igual a 1. Existem casos em que o Kappa se torna negativo.

Processamento dos Dados

Texto

A transformação básica do texto do título e da descrição é fazer uma bag of words. Em sua forma mais simples é uma matriz cujas linhas são os exemplos (documentos) e as colunas são palavras (termos). O valor de cada célula é o número de vezes que a palavra aparece naquele documento.

Uma transformação mais avançada é pegar esta matriz e fazer algumas operações utilizando a frequência dos termos em cada documento, e também no corpo de documentos. Esta é chamada de TF-IDF. Neste caso, os termos com maior presença dentro de um documento específico recebem um peso maior, mas que é descontado por um fator inversamente proporcional à presença dele em outros documentos.

Simplificando: palavras raras que aparecem bastante em um documento recebem maior peso, e palavras frequentes, que aparecem em todos os documentos, se tornam menos importantes.

Fora isso, é importante determinar um limite mínimo de vezes que a palavra deve aparecer no corpo de documentos, para diminuir o ruído.

Neste caso, a melhor representação que encontrei foi treinar uma matriz baseada apenas nos títulos, e outra apenas na descrição. Alguns participantes sugeriram treinar nos dois ao mesmo tempo, mas como se trata de um problema de busca, acredito que seja importante diferenciá-los.

SVD – Latent Semantic Analysis

Como você já deve imaginar, a matriz de documentos fica enorme, e é bastante esparsa. Isso dificulta o uso com alguns modelos não lineares. Neste caso existe a opção de reduzir a dimensionalidade usando o SVD. Este algoritmo de álgebra linear basicamente encontra os componentes principais da matriz de documentos, onde há maior variação. Isso reduz a dimensionalidade do problema, mas pode causar perda de informação.

Nestes casos de NLP eles acabam funcionando bem, porque filtram um pouco o ruído dos dados. Aplicar o SVD à matriz TF-IDF te dá uma representação conhecida como Latent Semantic Analysis. A interpretação é que cada componente obtido se refere a um “conceito” representado pelos documentos. Ele também é bastante usado para visualizar os documentos num espaço com menores dimensões.

Na competição utilizei SVD para reduzir a dimensionalidade da matriz TF-IDF antes de alimentá-la aos modelos.

Outros atributos

Além dos atributos acima, criei outros que pareciam fazer sentido, e alguns que encontrei em trabalhos acadêmicos relacionados. O mais importante deles foi a porcentagem de palavras da busca que estavam no título. Além disso, criei atributos baseados na similaridade média entre um produto e os outros da mesma frase de busca, e também a contagem de vezes que a frase de busca aparece nos dados.

Uma parte importante foi codificar cada frase de busca com one hot. Ou seja, para cada uma delas criei uma coluna que possuía o número 1 caso ela estivesse presente, e o número 0 caso contrário. Fiz isso para ajudar o modelo a capturar variações próprias de cada frase de busca.

Regressão ou Classificação?

Existiam duas maneiras de encarar essa tarefa: considerar cada nível da mediana como uma classe, sendo assim um problema de classificação, ou considerar como um problema de regressão e arredondar as previsões.

Num primeiro momento achei mais adequado tratar como regressão, mas como o Kaggle queria as previsões arredondadas, acabei criando vários modelos de classificação e não tive tempo de testar a outra alternativa.

Com a classificação temos duas opções: usar a classe prevista pelo modelo, ou utilizar as probabilidades de cada classe para fazer uma média ponderada para obter a pontuação. A primeira alternativa se mostrou melhor neste caso.

Modelos

SVM

Os melhores modelos foram obtidos usando SVM. Ele é um dos modelos mais utilizados para classificação de texto, então não é surpresa que tenha boa performance.

O melhor modelo individual, utilizando os componentes do SVD e os outros atributos, atingia um Kappa de 0,672 na validação cruzada, e 0,654 na LB.

Random Forest

Um dos meus modelos preferidos, mas normalmente não são a melhor opção para dados esparsos em altas dimensões. De qualquer maneira, após vários testes, consegui 0,6695 na validação cruzada, e 0,6508 na LB usando uma matriz TF-IDF junto com os outros atributos.

Uma razão que pode ajudar a explicar o fato dela se comportar melhor com a matriz TF-IDF do que com a matriz do SVD é que este é um método que reduz a variância, então acaba sendo robusto diante de muitos atributos, desde que uma parcela deles seja relevante para a previsão.

Gradient Boosted Trees

Apesar deste modelo também não ser muito recomendado para uso com dados esparsos em altas dimensões, decidi treinar vários modelos “fracos” e fazer o stacking usando as previsões deles e os outros atributos, que não envolviam a representação das palavras.

Os modelos do stacking tinham uma média de 0,60 Kappa na validação.

A pontuação deste modelo ficava por volta de 0,624 na LB. Mas a intenção real com ele era ter um modelo diferente do SVM para fazer o ensemble.

Pós Processamento

Em todas as soluções de Top 10 que eu li, havia um detalhe em comum: o pós processamento das previsões. As equipes usaram métodos especiais para fazer arredondamento, multiplicando por coeficientes, ou determinando pontos de corte diferentes para a previsão de cada “classe”.

Em um dos casos, o time ordenou as previsões e, de acordo com a proporção de cada classe nos dados de treino, arredondou as previsões. Kappa é uma métrica preocupada com a distribuição das previsões, então isso acabou ajudando.

O mais próximo que cheguei de um ajuste nas previsões foi utilizar a média geométrica em vez da média simples para o ensemble. Os modelos costumavam superestimar a relevância dos produtos, e ela puxava as previsões para a classes mais baixas.

Ensemble e Overfitting

No fim, fiz uma média geométrica com meus melhores modelos. O ensemble era formado por vários SVMs, uma Random Forest e um GBT.

Apesar de não ser uma prática recomendável, principalmente em casos com uma quantidade de dados pequena como esta competição, utilizei a LB para validar o ensemble. Isso acabou fazendo com que eu escolhesse o meu segundo melhor ensemble, mesmo assim acabei no Top 5% da competição.

Machine Learning Para Previsão de Vendas Usando Dados Meteorológicos

O WalMart é uma rede com milhares de lojas em 27 países. É possível encontrar vários artigos sobre os mecanismos tecnológicos utilizados para gerenciar a logística e distribuição dos produtos. É a segunda vez que eles oferecem uma competição no Kaggle com a intenção de encontrar candidatos para entrevistas para vagas de cientistas de dados.

Uma grande vantagem deste tipo de competição é termos acesso a dados de grandes companhias, e entender quais são os problemas que eles estão tentando resolver com modelos probabilísticos.

O objetivo da competição era criar um modelo que pudesse prever a quantidade de venda de alguns produtos, em lojas específicas, nos dias antes e depois de nevascas e tempestades. O exemplo dado por eles na descrição da tarefa foi a venda de guarda-chuvas, que intuitivamente deve ver um aumento antes de uma grande tempestade.

Dados

Para treinar o modelo foram oferecidos dois arquivos: um deles continha informações sobre a identificação das lojas, produtos, e as estações meteorológicas mais próximas. O outro continha dados meteorológicos de cada estação.

No total foram disponibilizados dados de 111 produtos cujas vendas podem ser afetadas pelas condições climáticas, 45 lojas, e 20 estações meteorológicas. O objetivo era prever a quantidade de cada produto, em cada loja, que seria vendida 3 dias antes, 3 dias depois, e no dia do evento climático.

Um evento climático era considerado caso tivesse sido registrada mais que uma polegada de chuva, ou mais que duas polegadas de neve naquele dia.

A combinação entre lojas e produtos nos dava cerca de 4,6 milhões de exemplos para treino, e cerca de 500 mil para teste. Cada exemplo se referia a um dia, loja e produto.

Métrica

A métrica utilizada foi o Root Mean Square Log Error. É basicamente o RMSE aplicado à transformação log(Y + 1) das previsões. Isso significa que erros em previsões que deveriam ser próximas de 0 seriam punidos mais severamente do que erros em previsões com números mais altos. Por exemplo, prever 5 itens quando deveria ser 0, tem uma punição maior do que prever 105 quando deveria ser 100.

Transformação dos dados

Como eu só tinha 10 dias para trabalhar nesta competição, decidi verificar até onde era possível chegar com um modelo baseado apenas nas variáveis originais e, quem sabe, fazer um ensemble.

Uma diferença desta para outras competições é que, apesar dos dados virem organizados, você era responsável por unir as variáveis climáticas com os dados identificados de produtos e lojas. Faz todo o sentido, pois o Walmart não iria querer um data scientist que não saiba manipular dados.

Para isto utilizei Pandas. Esta biblioteca Python para manipulação de dados é uma das mais utilizadas, e lembra bastante as estruturas de dados disponíveis em R.

Num primeiro momento usei todas as variáveis como numéricas, e treinei um XGBoost com leve tuning, excluindo as variáveis que codificavam alertas especiais, e usando 10% do dataset para determinar o número de árvores. Como já era de se esperar, o resultado foi ruim, cerca de 0,1643 na LB.

Binarizando as variáveis

Depois de testar o primeiro modelo, codifiquei as variáveis categóricas com one-hot encoding. Ou seja, para cada nível foi criada uma coluna com o indicador 0 ou 1, caso a variável estivesse presente naquele exemplo. Normalmente o número de colunas deve ser o número de níveis menos um, para não ter problemas com colinearidade. Como eu planejava usar modelos que não eram sensíveis a esse problema, eu não me preocupei em excluir uma das colunas.

Após fazer tuning usando um subset com 10% dos dados, e obter o RMSLE de 0,1216 na validação, enviei a solução, e obtive o valor de 0,1204 na LB, uma boa melhora sobre o anterior.

Imputação

Muitos dados meteorológicos não estavam presentes, então decidi testar um método simples de imputação: substituir os NaNs pela média dos valores da coluna. Após fazer o tuning novamente, agora adequado a esses novos valores, obtive o RMSLE de 0,1140 nos 10% de validação e 0,1095 na LB.

Variáveis temporais

Não explorei muito a dimensão temporal dos dados, em uma das tentativas adicionei informações meteorológicas do dia anterior, o que reduziu o erro para 0,1083.

Subdivisões dos dados

Um método que funcionou muito bem na minha primeira competição, e que sempre acabo tentando, é dividir o training set em pequenos subsets relacionados a alguma variável e treinar um modelo específico para cada um. Neste caso, decidi criar 19 modelos, um para cada uma das 19 estações meteorológicas presentes no test set. O RMSLE na LB foi de 0,1101. Pior do que com um modelo que trata as estações como variáveis no dataset inteiro.

Um problema grave com esta abordagem é tentar usar o mesmo modelo, com os mesmos parâmetros, para datasets diferentes. Sabendo disso, decidi fazer um pequeno tuning dos parâmetros para cada dataset, o que reduziu o RMSLE da LB para 0,1069.

Apesar da pequena diferença, parece que a divisão em modelos individuais para cada estação capturava algumas informações que não estavam presentes no modelo que considerava todas juntas.

Modelos

Dos modelos que testei, dois se destacaram: Gradient Boosted Trees (XGBoost) e Random Forests.

Random Forest

Eu tinha usado Random Forest para regressão apenas uma vez, em um trabalho, mas nunca com uma quantidade grande de dados. Após fazer o tuning dos parâmetros, aplicando o modelo nos dados imputados, ela resultou num RMSLE de 0,1116 na LB.

XGBoost

O XGBoost apresentou o melhor erro de um modelo individual. Além de ajustar parâmetros, como a profundidade das árvores, usando um subset dos dados, era necessário ajustar a quantidade de árvores e a learning rate. Normalmente uma learning rate pequena e uma grande quantidade de árvores é a receita segura para melhorar a performance, em troca de mais tempo para treinar o modelo.

Como o XGBoost é sensível à seed do RNG, decidi fazer um ensemble de XGBs mudando apenas este valor. Este método melhorou marginalmente a minha pontuação em outras competições, mas nesta o impacto dele foi maior pelo seguinte fato: o XGBoost possui uma função que permite deixar dados separados para que ele determine o número de árvores que minimiza o erro. Neste caso decidi usar a função, e deixei 5% dos dados separados. Além de variar a seed para o próprio XGB, eu variei a seed para fazer a divisão dos dados, isso fez com que os modelos ficassem mais diversos, o que é essencial para um bom ensemble.

O máximo que cheguei a treinar foram 10 XGBoosts neste esquema. Apesar de ser um modelo bastante estável, o RMSLE do ensemble foi de 0,1041, apresentando uma redução comparado a 0,1095 do modelo individual.

Solução Final

No fim, juntei todas as soluções que eu havia enviado, e acabei obtendo um RMSLE de 0,1028, garantindo uma posição entre as 20% melhores.

Possíveis Melhorias

Um dia após o término da competição eu revisei as variáveis do meu XGBoost, e descobri que as variáveis que identificavam os produtos (item_nbr) não estavam em formato binário, e eram consideradas por ele as mais importantes. Com a codificação correta acredito que seria possível diminuir mais o erro, e alcançar uma posição final melhor.

As Métricas Mais Populares para Avaliar Modelos de Machine Learning

Durante o processo de criação de um modelo de machine learning nós precisamos medir a qualidade dele de acordo com o objetivo da tarefa. Existem funções matemáticas que nos ajudam a avaliar a capacidade de erro e acerto dos nossos modelos, e agora você conhecerá algumas das mais utilizadas. No artigo, usarei a palavra métrica para me referir a essas funções.

Tão importante quanto saber escolher um bom modelo, é saber escolher a métrica correta para decidir qual é o melhor entre eles.

Existem métricas mais simples, outras mais complexas, algumas que funcionam melhor para datasets com determinadas características, ou outras personalizadas de acordo com o objetivo final do modelo.

Ao escolher uma métrica deve-se levar em consideração fatores como a proporção de dados de cada classe no dataset e o objetivo da previsão (probabilidade, binário, ranking, etc). Por isso é importante conhecer bem a métrica que será utilizada, já que isso pode fazer a diferença na prática.

Nenhuma destas funções é melhor do que as outras em todos os casos. É sempre importante levar em consideração a aplicação prática do modelo. O objetivo deste artigo não é ir a fundo em cada uma delas, mas apresentá-las para que você possa pesquisar mais sobre as que achar interessante.

Classificação

Estas métricas são utilizadas em tarefas de classificação, e a maioria delas pode ser adaptada tanto para classificação binária quanto de múltiplas classes. Nas tarefas de classificação buscamos prever qual é a categoria a que uma amostra pertence como, por exemplo, determinar se uma mensagem é spam.

Precisão Geral (Accuracy)

Precis\tilde{a}o\ Geral = \frac{P}{P\ +\ N}

Esta é a métrica mais simples. É basicamente o número de acertos (positivos) divido pelo número total de exemplos. Ela deve ser usada em datasets com a mesma proporção de exemplos para cada classe, e quando as penalidades de acerto e erro para cada classe forem as mesmas.

Em problemas com classes desproporcionais, ela causa uma falsa impressão de bom desempenho. Por exemplo, num dataset em que 80% dos exemplos pertençam a uma classe, só de classificar todos os exemplos naquela classe já se atinge uma precisão de 80%, mesmo que todos os exemplos da outra classe estejam classificados incorretamente.

F1 Score

F1 = \frac{2\ *\ precis\tilde{a}o\ *\ recall}{precis\tilde{a}o\ +\ recall}

O F1 Score é uma média harmônica entre precisão (que, apesar de ter o mesmo nome, não é a mesma citada acima) e recall. Veja abaixo as definições destes dois termos.

Ela é muito boa quando você possui um dataset com classes desproporcionais, e o seu modelo não emite probabilidades. Isso não significa que não possa ser usada com modelos que emitem probabilidades, tudo depende do objetivo de sua tarefa de machine learning.

Em geral, quanto maior o F1 score, melhor.

Precisão (Precision)

Precis\tilde{a}o = \frac{PV}{PV\ +\ FP}

Número de exemplos classificados como pertencentes a uma classe, que realmente são daquela classe (positivos verdadeiros), dividido pela soma entre este número, e o número de exemplos classificados nesta classe, mas que pertencem a outras (falsos positivos).

Recall

Recall = \frac{PV}{P}

Número de exemplos classificados como pertencentes a uma classe, que realmente são daquela classe, dividido pela quantidade total de exemplos que pertencem a esta classe, mesmo que sejam classificados em outra. No caso binário, positivos verdadeiros divididos por total de positivos.

AUC – Area Under the ROC Curve

auc
Imagem retirada da documentação do Scikit-Learn

Esta é uma métrica interessante para tarefas com classes desproporcionais. Nela, mede-se a área sob uma curva formada pelo gráfico entre a taxa de exemplos positivos, que realmente são positivos, e a taxa de falsos positivos.

Uma das vantagens em relação ao F1 Score, é que ela mede o desempenho do modelo em vários pontos de corte, não necessariamente atribuindo exemplos com probabilidade maior que 50% para a classe positiva, e menor, para a classe negativa.

Em sistemas que se interessam apenas pela classe, e não pela probabilidade, ela pode ser utilizada para definir o melhor ponto de corte para atribuir uma ou outra classe a um exemplo. Este ponto de corte normalmente é o ponto que se localiza mais à esquerda, e para o alto, no gráfico, mas depende bastante do custo do erro na previsão de uma determinada classe.

Log Loss

Log\ Loss = -\frac{1}{N}\sum_{i=1}^N {(y_i\log(p_i) + (1 - y_i)\log(1 - p_i))}

A fórmula do exemplo é para o caso binário, neste caso: p é a probabilidade do exemplo pertencer à classe 1, e y é o valor real da variável dependente.

Esta função pune previsões incorretas muito confiantes. Por exemplo, prever uma classe com uma probabilidade de 95%, e na realidade a correta ser outra. Ela pode ser utilizada para problemas binários ou com múltiplas classes, mas eu particularmente não gosto de usar em datasets com classes desproporcionais. O valor dela sempre terá a tendência de melhorar se o modelo estiver favorecendo a maior classe presente.

Tomando os cuidados acima, nas situações em que a probabilidade de um exemplo pertencer a uma classe for mais importante do que classificá-lo diretamente, esta função é preferível a usar simplesmente a precisão geral.

Regressão

Neste parte estão as funções mais comuns utilizadas para avaliar o desempenho de modelos de regressão. Na regressão buscamos prever um valor numérico, como, por exemplo, as vendas de uma empresa para o próximo mês. Nos exemplos abaixo:

y_i = valor\ real\\\hat{y}_i = valor\ previsto

Mean Squared Error – MSE

MSE = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2

Talvez seja a mais utilizada, esta função calcula a média dos erros do modelo ao quadrado. Ou seja, diferenças menores têm menos importância, enquanto diferenças maiores recebem mais peso.

Existe uma variação, que facilita a interpretação: o Root Mean Squared Error. Ele é simplesmente a raiz quadrada do primeiro. Neste caso, o erro volta a ter as unidades de medida originais da variável dependente.

Mean Absolute Error – MAE

MAE = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i|

Bastante parecido com MSE, em vez de elevar a diferença entre a previsão do modelo, e o valor real, ao quadrado, ele toma o valor absoluto. Neste caso, em vez de atribuir um peso de acordo com a magnitude da diferença, ele atribui o mesmo peso a todas as diferenças, de maneira linear.

Se imaginarmos um exemplo simples, onde temos apenas a variável que estamos tentando prever, podemos ver um fato interessante que difere o MSE do MAE, e que devemos levar em conta ao decidir entre os dois: o valor que minimizaria o primeiro erro seria a média, já no segundo caso, a mediana.

Mean Absolute Percentage Error – MAPE

MAPE = \frac{1}{n} \sum_{i=1}^{n} |\frac{y_i - \hat{y}_i}{y_i}|

Este erro calcula a média percentual do desvio absoluto entre as previsões e a realidade. É utilizado para avaliar sistemas de previsões de vendas e outros sistemas nos quais a diferença percentual seja mais interpretável, ou mais importante, do que os valores absolutos.

Métricas específicas para tarefas

Em alguns casos, o ideal é usar uma métrica que tenha um significado específico para a tarefa em questão. Por exemplo, na segmentação de anúncios, a taxa de cliques; num sistema para comprar e vender ações, verificar o retorno médio. As métricas acima são importantes e podem ser utilizadas de maneira geral, mas se houver uma alternativa melhor, mais adequada ao contexto, ela deve ser utilizada.

Como Criar um Modelo Simples para Prever Séries Temporais Usando Machine Learning em Python

Quando tratamos da previsão de séries temporais um modelo amplamente utilizado é a regressão linear. Apesar de simples, ele tem se mostrado bastante útil em aplicações reais.

Uma forma muito simples de criar um modelo para este caso é usar os dados anteriores da própria variável de interesse para prever o atual. É possível criar modelos que buscam prever estas séries utilizando outros atributos, o que em alguns casos vai melhorar a precisão dos mesmos.

Neste artigo quero demonstrar a maneira mais simples, usando uma regressão linear nos dados históricos da própria variável de interesse. O código disponibilizado está num formato adequado para que o leitor entenda o que está acontecendo, por isso ele pode ter partes que poderiam ser otimizadas num ambiente de produção, mas foi uma escolha, de acordo com o objetivo educacional do artigo, deixa-las assim.

O código completo, e os dados, estão disponíveis aqui.

Descrição dos dados

Os dados utilizados correspondem à “prime rate” no Brasil. A “prime rate” é a taxa de juros bancários para clientes preferenciais, ou seja, aplicada a clientes com baixo risco de inadimplência em operações de alto valor. Vamos usar os valores desta taxa nos últimos 6 meses para prever o próximo.

Prime Rate

Temos dados mensais de janeiro de 2005 até novembro de 2014. Eles são originalmente divulgados pelo Banco Central do Brasil, mas foram obtidos na plataforma Quandl.

Observação importante: não use as informações deste artigo como base para tomar quaisquer decisões, inclusive financeiras, ou de investimentos. Este é um artigo educacional e serve apenas para demonstrar o uso de uma ferramenta de machine learning para a previsão de séries temporais.

Modelos utilizados para comparação

Para comparar o desempenho da regressão linear neste problema, vou utilizar outros dois métodos válidos para previsão de séries temporais:

Valor do último mês: a previsão para o próximo mês é simplesmente o mesmo valor da variável no último mês.

Média Móvel: a previsão para o próximo mês é a média dos valores dos últimos 6 meses.

Métrica de avaliação

Vamos utilizar o Erro Percentual Absoluto Médio (em inglês, MAPE). É uma métrica bastante utilizada na área de previsões de séries temporais, e se refere à média do percentual de erros cometidos nas previsões, desconsiderando a direção (acima ou abaixo do real).

Além deste erro, também avaliaremos o Erro Médio Absoluto (MAE, em inglês), que é a média dos erros absolutos (ignorando o sinal positivo ou negativo). Assim sabemos melhor quanto estamos desviando dos valores reais nas unidades originais.

Do ponto de vista prático, para que possamos justificar o uso da regressão linear em vez dos métodos mais simples, ela deve apresentar um erro médio menor do que o erro das outras opções.

Para quem quiser ser um pouco mais rigoroso, vou usar um teste estatístico pareado (Wilcoxon Signed-Rank) para avaliar as duas alternativas que oferecerem os menores erros. Levando em consideração algumas suposições sobre os dados, ele basicamente nos diz a probabilidade da diferença entre os erros dos dois métodos ser puramente por sorte, e não por alguma característica especial que torne um modelo melhor do que o outro.

Na prática, na maioria das vezes a decisão de usar um modelo ou outro é feita diante da comparação entre a melhora no desempenho e o esforço necessário para coloca-lo em funcionamento. Alguns modelos que passam pelo teste estatístico, mas são muito complicados, nunca serão implementados, enquanto outros, mais simples, que não passam pelo rigor do teste, serão utilizados.

Módulos Python utilizados

Para rodar o script deste artigo são necessários os seguintes módulos Python: Numpy, Pandas e Scikit-learn. Caso queira efetuar o teste pareado também é necessário ter Scipy. Para reproduzir o gráfico, é necessário Matplotlib.

Definindo as funções das métricas de avaliação

Para o MAE vamos utilizar a implementação disponível no scikit-learn. Como ele não possui uma função para computar o MAPE, nós precisamos cria-la.

Carregando e formatando os dados

Os dados estão num CSV em formato de tabela. Após carregarmos o CSV na memória usando Pandas precisamos organizar os dados para fazer a previsão. O scikit-learn não se dá muito bem com Pandas, então além de preparar os dados no formato correto para alimentar o modelo, vamos coloca-los em arrays do numpy.

A matriz de atributos terá 6 colunas, uma para cada mês anterior ao que queremos prever, e o vetor com a variável dependente terá o valor a ser previsto (próximo mês). Para isso vamos começar pelo sétimo mês disponível, que é o número seis no loop porque, em Python, o primeiro elemento é indexado como zero.

Treinando o modelo e fazendo as previsões

Dados financeiros normalmente mudam de regime com frequência, então vamos treinar um novo modelo a cada mês. O primeiro mês a ser previsto será o 31º disponível após a transformação dos dados. Assim temos pelo menos 30 exemplos para treinar o primeiro modelo. Em tese quanto mais dados para treinar, melhor.

Para cada mês vamos armazenar o valor previsto pelos três métodos e o valor verdadeiro.

Avaliação dos resultados

Após a conclusão das previsões, transformamos as listas em arrays do numpy novamente e computamos as métricas.

Vemos que o melhor modelo é a regressão linear, seguido por usar o valor do último mês, e um resultado bastante ruim com o método da média móvel.

Prime Rate Linear Regression

Teste Wilcoxon Signed-Rank – Opcional

Vamos usar um teste pareado nos erros absolutos da regressão linear e do método que utiliza o valor do último mês para ver se a diferença entre eles é significativa do ponto de vista estatístico. Ou seja, qual é a probabilidade da diferença entre o erro dos dois modelos ser apenas por sorte.

O p-value reportado por esta função é relativo a um teste de duas caudas, ou seja, testa se as médias são diferentes. No nosso caso, estamos mais interessados em saber se uma média é menor do que a outra, ou seja, um teste de uma cauda. A forma mais fácil de converter este número para o que nós desejamos é dividi-lo por 2.

O p-value mínimo mais aceito pela comunidade científica para determinar que há uma diferença entre as médias é de 0,05. Neste caso estamos bem abaixo, nosso p-value é menor do que 0,001, então conseguimos determinar que a probabilidade da diferença de performance entre os modelos ser apenas por sorte é mínima. Podemos rejeitar a hipótese que eles oferecem o mesmo desempenho, e favorecer o uso da regressão linear.

Vale a pena ressaltar que, ao usar estes testes, é importante continuar a coletar dados mesmo após atingir significado estatístico, para confirmar o resultado. Parar de coletar assim que se atinge o nível desejado do p-value pode levar a resultados falsos.

Sugestões para melhorar o modelo

Nós criamos o modelo mais simples possível, baseado apenas nos valores históricos da série. Abaixo deixo algumas sugestões que podem ser implementadas para possivelmente reduzir o erro.

Boosting

Boosting é uma técnica que treina vários modelos nos mesmos dados, a diferença é que cada modelo é treinado nos erros residuais dos anteriores. Apesar de haver o risco de overfitting caso não seja feita uma boa validação, esta é uma técnica que apresenta bastante sucesso na prática.

Criação de mais atributos

Utilizamos apenas os valores dos últimos seis meses como variáveis independentes. Se adicionarmos mais variáveis que sejam correlacionadas com a variável dependente, podemos melhorar o modelo. Além disso, podemos adicionar atributos baseados em transformações, como elevar os valores ao quadrado, para capturar relações não lineares.

Usar outros modelos

Regressão linear não é a única opção nestes casos. Existem modelos baseados em alterações da regressão linear, redes neurais, SVMs, e decision trees que podem apresentar um desempenho melhor.

Teste