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.
Índice
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.
Seja o primeiro a saber das novidades em Machine Learning. Me siga no LinkedIn.