Grupo Bimbo Inventory Demand

06 de março, 2020

1. Descrição geral do problema


Grupo Bimbo

O Grupo Bimbo, se esforça para atender a demanda diária dos consumidores por produtos frescos de panificação nas prateleiras de mais de 1 milhão de lojas ao longo das suas 45.000 lojas em todo o México.

Atualmente, os cálculos diários de estoque são realizados por funcionários de vendas de entregas diretas, que devem, sozinhos, prever a necessidade de estoque dos produtos e demanda com base em suas experiências pessoais em cada loja. Como alguns pães têm uma vida útil de uma semana, a margem aceitável para o erro é pequena.

Objetivo: neste projeto de aprendizado de máquina, vamos desenvolver um modelo para prever com precisão a demanda de estoque com base nos dados históricos de vendas. Isso fará com que os consumidores dos mais de 100 produtos de panificação não fiquem olhando para as prateleiras vazias, além de reduzir o valor gasto com reembolsos para os proprietários de lojas com produtos excedentes impróprios para venda.

Para a construção desse projeto, utilizaremos a linguagem R e os datasets disponíveis no Kaggle em:


2. Carregando dados

2.1 Importando bibliotecas necessárias

Vamos começar nosso projeto importanto todas as bilbiotecas necessárias para a realização das fases iniciais de exploração e transformação dos dados (Data Munging).

In [ ]:
# Caso não possua uma das bibliotecas importadas abaixo, a instale com um dos comandos a seguir:

install.packages(c(
    'data.table',
    'bigreadr',
    'dplyr',
    'ggplot2',
    'fasttime',
    'lubridate',
    'corrplot',
    'anomalize',
    'stringr'
))
In [ ]:
# Definindo a oculatação de warnings.

options(warn = -1)

# Importando bibliotecas.

library(data.table)
library(bigreadr)
library(dplyr)
library(ggplot2)
library(fasttime)
library(lubridate)
library(corrplot)
library(anomalize)
library(stringr)

2.2 Carregando dados do dataset cliente_tabla

In [6]:
# Importando dataset.

client <- fread('/content/datasets/cliente_tabla.csv')

# Verificando as primeiras linhas do dataset.

head(client)
A data.table: 6 × 2
Cliente_IDNombreCliente
<int><chr>
0SIN NOMBRE
1OXXO XINANTECATL
2SIN NOMBRE
3EL MORENO
4SDN SER DE ALIM CUERPO SA CIA DE INT
4SDN SER DE ALIM CUERPO SA CIA DE INT

2.3 Carregando dados do dataset producto_tabla

In [7]:
# Importando dataset.

product <- fread('/content/datasets/producto_tabla.csv')

# Verificando as primeiras linhas do dataset.

head(product)
A data.table: 6 × 2
Producto_IDNombreProducto
<int><chr>
0NO IDENTIFICADO 0
9Capuccino Moka 750g NES 9
41Bimbollos Ext sAjonjoli 6p 480g BIM 41
53Burritos Sincro 170g CU LON 53
72Div Tira Mini Doradita 4p 45g TR 72
73Pan Multigrano Linaza 540g BIM 73

2.4 Carregando dados do dataset town_state

In [8]:
# Importando dataset.

town <- fread('/content/datasets/town_state.csv')

# Verificando as primeiras linhas do dataset.

head(town)
A data.table: 6 × 3
Agencia_IDTownState
<int><chr><chr>
11102008 AG. LAGO FILT MÉXICO, D.F.
11112002 AG. AZCAPOTZALCOMÉXICO, D.F.
11122004 AG. CUAUTITLAN ESTADO DE MÉXICO
11132008 AG. LAGO FILT MÉXICO, D.F.
11142029 AG.IZTAPALAPA 2 MÉXICO, D.F.
11162011 AG. SAN ANTONIO MÉXICO, D.F.

2.5 Carregando dados de treino

In [9]:
# Importando dataset.

train <- fread('/content/datasets/train.csv')

# Verificando as primeiras linhas do dataset.

head(train)
A data.table: 6 × 11
SemanaAgencia_IDCanal_IDRuta_SAKCliente_IDProducto_IDVenta_uni_hoyVenta_hoyDev_uni_proximaDev_proximaDemanda_uni_equil
<int><int><int><int><int><int><int><dbl><int><dbl><int>
3111073301157661212325.14003
3111073301157661216433.52004
3111073301157661238439.32004
3111073301157661240433.52004
3111073301157661242322.92003
3111073301157661250538.20005

2.6 Carregando dados de teste

In [10]:
# Importando dataset.

test <- fread('/content/datasets/test.csv')

# Verificando as primeiras linhas do dataset.

head(test)
A data.table: 6 × 7
idSemanaAgencia_IDCanal_IDRuta_SAKCliente_IDProducto_ID
<int><int><int><int><int><int><int>
011403712209463907835305
1112237112264705135 1238
210204512831454976932940
311122714448471785543066
411121911130 966351 1277
5111146466011741414 972

3. Data Munging - Eliminando inconsistências nos datasets

A documentação nos alerta para a existência de alguns problemas que devem ser tratados dentro do conjunto de dados, como por exemplo registros duplicados. Por isso, iremos fazer uma breve exploração dos datasets para eliminar todas as inconsistências que possuam.

3.1 Dataset client

In [11]:
# Visualizando as primeiras 10 linhas do dataset.

head(client, 10)
A data.table: 10 × 2
Cliente_IDNombreCliente
<int><chr>
0SIN NOMBRE
1OXXO XINANTECATL
2SIN NOMBRE
3EL MORENO
4SDN SER DE ALIM CUERPO SA CIA DE INT
4SDN SER DE ALIM CUERPO SA CIA DE INT
5LA VAQUITA
6LUPITA
7I M EL GUERO
8MINI SUPER LOS LUPES

Podemos observar que existem IDs que se repetem e nomes de clientes desconhecidos (denominados como "SIN NOMBRE") no conjunto de dados que precisarão ser tratados.

Vamos contabilizar o número de registros duplicados no dataset.

In [12]:
# Verificando linhas duplicadas no dataset.

table(duplicated(client))
 FALSE 
935362 

Não encontramos nenhum registro duplicado o que nos leva a crer que a variável NombreCliente apresenta strings com diferentes tamanhos para cada ID duplicado. Vamos confirmar esta teoria.

Ao listar as primeira linhas do conjunto de dados, vimos que o ID 4 se repete duas vezes. Com base nisso, vamos capturar e analisar o valor da variável NombreCliente associado a este ID em cada observação.

In [13]:
# Definindo o número do ID que deve ser capturado.

id <- 4

# Capturando linhas que contenham o ID especificado.

client[client$Cliente_ID == id,]
A data.table: 2 × 2
Cliente_IDNombreCliente
<int><chr>
4SDN SER DE ALIM CUERPO SA CIA DE INT
4SDN SER DE ALIM CUERPO SA CIA DE INT
In [14]:
# Capturando nomes associados a cada um dos registros duplicados.

fName <- client[client$Cliente_ID == id,][1, 'NombreCliente']
sName <- client[client$Cliente_ID == id,][2, 'NombreCliente']

# Definindo o número de caracteres de cada um dos nomes dos registros duplicados.

nchar(fName)
nchar(sName)
NombreCliente: 39
NombreCliente: 36

A partir deste resultado podemos confirmar que há uma diferença entre os valores das variáveis NombreCliente de cada registro duplicado. Isto provavelmente ocorre devido a diferença do número de espaços existentes em cada nome.

Iremos contabilizar o número de registros duplicados a partir da variável Cliente_ID.

In [15]:
# Verificando número de IDs duplicados no dataset.

table(duplicated(client$Cliente_ID))
 FALSE   TRUE 
930500   4862 
In [ ]:
# Removendo registros com número de ID duplicado.

client <- client[!duplicated(client$Cliente_ID),]

Definiremos o número de registros sem o nome do cliente.

In [17]:
# Verificando número de registros sem o nome do cliente.

nrow(client[client$NombreCliente == 'SIN NOMBRE', ])
356

356 observações sem o nome do cliente.

In [18]:
# Verificando se existem valores nulos no dataset.

anyNA(client)
FALSE

Não há valores nulos no dataset.

In [19]:
# Verificando o tipo de dado das variáveis do dataset.

glimpse(client)
Rows: 930,500
Columns: 2
$ Cliente_ID    <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
$ NombreCliente <chr> "SIN NOMBRE", "OXXO XINANTECATL", "SIN NOMBRE", "EL MOR…

O conjunto de dados contém registros de 930.500 clientes.

3.2 Dataset product

In [20]:
# Visualizando as primeiras 10 linhas do dataset.

head(product, 10)
A data.table: 10 × 2
Producto_IDNombreProducto
<int><chr>
0NO IDENTIFICADO 0
9Capuccino Moka 750g NES 9
41Bimbollos Ext sAjonjoli 6p 480g BIM 41
53Burritos Sincro 170g CU LON 53
72Div Tira Mini Doradita 4p 45g TR 72
73Pan Multigrano Linaza 540g BIM 73
98Tostado Integral 180g WON 98
99Pan Blanco 567g WON 99
100Super Pan Bco Ajonjoli 680g SP WON 100
106Wonder 100pct mediano 475g WON 106

Vamos contabilizar o número de registros duplicados no dataset.

In [21]:
# Verificando linhas duplicadas no dataset.

table(duplicated(product))
FALSE 
 2592 

Nenhuma observação duplicada foi encontrada.

In [22]:
# Verificando número de IDs duplicados no dataset.

table(duplicated(product$Producto_ID))
FALSE 
 2592 

Nenhum número de ID duplicado foi encontrado.

Porém, o produto com o ID 0 não possui o nome do produto (possui o valor "NO IDENTIFICADO"). Iremos verificar se isto só ocorre neste registro.

In [23]:
# Capturando a substring especificada.

pattern <- "NO IDENTIFICADO"

# Definindo linhas que não identifiquem o nome do produto.

rows <- grep(pattern, product[, 'NombreProducto'], value = F)

# Visualizando linhas que não contenham o nome do produto.

product[rows, ]
A data.table: 1 × 2
Producto_IDNombreProducto
<int><chr>
0NO IDENTIFICADO 0

Concluímos que apenas o ID 0 não apresenta um nome de produto. Talvez vez isto ocorra porque um único ID pode estar sendo utilizado para identificar produtos que ainda não foram devidamente registrados no conjunto de dados.

In [24]:
# Verificando se existem valores nulos no dataset.

anyNA(product)
FALSE

Não há valores nulos no dataset.

In [25]:
# Verificando o tipo de dado das variáveis do dataset.

glimpse(product)
Rows: 2,592
Columns: 2
$ Producto_ID    <int> 0, 9, 41, 53, 72, 73, 98, 99, 100, 106, 107, 108, 109,…
$ NombreProducto <chr> "NO IDENTIFICADO 0", "Capuccino Moka 750g NES 9", "Bim…

O conjunto de dados contém registros de 2.592 produtos.

Repare que a variável NombreProducto contém outras informações além do nome do produto. Parece que a string segue o seguinte padrão:

NombreProducto Nome do produto Número de peças Peso Sigla do fabricante ID do produto

Veja que este padrão não está presente em todos os valores da variável, mas predomina em grande parte dos dados. Bom, não precisamos de todas estas informações para a análise que iremos fazer e por isso iremos extair apenas o nome, o peso e a sigla do fabricante de cada produto.

In [ ]:
## Extraindo a unidade de medida de massa do produto.

# Extraindo a substring com as informações brutas para uma variável temporária.

tmp <- str_extract(product$NombreProducto, "([0-9 ] |[0-9])+(G|g|Kg|kg|ml)")

# Criando uma varíavel para armazenar o número associado ao peso do produto.

product$Npeso <- as.integer(str_extract(tmp, "[0-9]+"))

# Criando uma varíavel para armazenar a unidade de medida do peso do produto.

product$UniPeso <- tolower(str_extract(tmp, "[A-z]+"))

# Criando uma variável para armazenar a sigla referente ao fabricante.

product$Productor <- toupper(str_extract(
    str_extract(
        product$NombreProducto, 
        "( [A-Z]+[a-z ]+[A-Z]+ [A-Z ]+ [0-9]+$| [A-Z ]+[A-Z ]+ [0-9]+$)"),
        "( [A-Z]+[a-z ]+[A-Z]+ [A-Z ]+ | [A-Z ]+[A-Z ]+ )"
))

# Extraindo o nome do produto.

product$NombreProducto <- str_extract(product$NombreProducto, "[A-z ]+")
In [27]:
# Visualizando dataset após a extração das informações desejadas.

head(product)
A data.table: 6 × 5
Producto_IDNombreProductoNpesoUniPesoProductor
<int><chr><int><chr><chr>
0NO IDENTIFICADO NANA IDENTIFICADO
9Capuccino Moka 750g NES
41Bimbollos Ext sAjonjoli 480g BIM
53Burritos Sincro 170g CU LON
72Div Tira Mini Doradita 45g TR
73Pan Multigrano Linaza 540g BIM
In [28]:
# Verificando se existem valores nulos em cada variável do dataset.

sapply(product, function(v){
    table(is.na(v))
})
$Producto_ID

FALSE 
 2592 

$NombreProducto

FALSE 
 2592 

$Npeso

FALSE  TRUE 
 2541    51 

$UniPeso

FALSE  TRUE 
 2541    51 

$Productor

FALSE  TRUE 
 2591     1 

Como resultado final, verificamos que não foi possível determinar o peso de 51 produtos e nem o sigla de 1 fabricante.

3.3 Dataset town

In [29]:
# Visualizando as primeiras 10 linhas do dataset.

head(town, 10)
A data.table: 10 × 3
Agencia_IDTownState
<int><chr><chr>
11102008 AG. LAGO FILT MÉXICO, D.F.
11112002 AG. AZCAPOTZALCO MÉXICO, D.F.
11122004 AG. CUAUTITLAN ESTADO DE MÉXICO
11132008 AG. LAGO FILT MÉXICO, D.F.
11142029 AG.IZTAPALAPA 2 MÉXICO, D.F.
11162011 AG. SAN ANTONIO MÉXICO, D.F.
11172001 AG. ATIZAPAN ESTADO DE MÉXICO
11182007 AG. LA VILLA MÉXICO, D.F.
11192013 AG. MEGA NAUCALPANESTADO DE MÉXICO
11202018 AG. TEPALCATES 2 MÉXICO, D.F.

Vamos verificar se há registros ou IDs de agência duplicados no dataset.

In [30]:
# Verificando linhas duplicadas no dataset.

table(duplicated(town))
FALSE 
  790 

Nenhum registro duplicado foi encontrado.

In [31]:
# Verificando número de IDs duplicados no dataset.

table(duplicated(town$Agencia_ID))
FALSE 
  790 

Nenhum registro contém um número de ID duplicado.

In [32]:
# Verificando se existem valores nulos no dataset.

anyNA(town)
FALSE

Não há valores nulos no dataset.

In [33]:
# Verificando o tipo de dado das variáveis do dataset.

glimpse(town)
Rows: 790
Columns: 3
$ Agencia_ID <int> 1110, 1111, 1112, 1113, 1114, 1116, 1117, 1118, 1119, 1120…
$ Town       <chr> "2008 AG. LAGO FILT", "2002 AG. AZCAPOTZALCO", "2004 AG. C…
$ State      <chr> "MÉXICO, D.F.", "MÉXICO, D.F.", "ESTADO DE MÉXICO", "MÉXIC…

O conjunto de dados contém registros de 790 cidades e seus respectivos estados.

3.4 Dataset train

In [34]:
# Visualizando as primeiras 10 linhas do dataset.

head(train, 10)
A data.table: 10 × 11
SemanaAgencia_IDCanal_IDRuta_SAKCliente_IDProducto_IDVenta_uni_hoyVenta_hoyDev_uni_proximaDev_proximaDemanda_uni_equil
<int><int><int><int><int><int><int><dbl><int><dbl><int>
3111073301157661212325.14003
3111073301157661216433.52004
3111073301157661238439.32004
3111073301157661240433.52004
3111073301157661242322.92003
3111073301157661250538.20005
3111073301157661309320.28003
3111073301157663894656.10006
3111073301157664085424.60004
3111073301157665310631.68006
In [35]:
# Verificando linhas duplicadas no dataset.

table(duplicated(train))
   FALSE 
74180464 

Não há registros duplicados no dataset.

In [36]:
# Verificando se existem valores nulos no dataset.

anyNA(train)
FALSE

Não há valores nulos no dataset.

In [37]:
# Verificando o tipo de dado das variáveis do dataset.

glimpse(train)
Rows: 74,180,464
Columns: 11
$ Semana            <int> 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, …
$ Agencia_ID        <int> 1110, 1110, 1110, 1110, 1110, 1110, 1110, 1110, 111…
$ Canal_ID          <int> 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, …
$ Ruta_SAK          <int> 3301, 3301, 3301, 3301, 3301, 3301, 3301, 3301, 330…
$ Cliente_ID        <int> 15766, 15766, 15766, 15766, 15766, 15766, 15766, 15…
$ Producto_ID       <int> 1212, 1216, 1238, 1240, 1242, 1250, 1309, 3894, 408…
$ Venta_uni_hoy     <int> 3, 4, 4, 4, 3, 5, 3, 6, 4, 6, 8, 4, 12, 7, 10, 5, 3…
$ Venta_hoy         <dbl> 25.14, 33.52, 39.32, 33.52, 22.92, 38.20, 20.28, 56…
$ Dev_uni_proxima   <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Dev_proxima       <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Demanda_uni_equil <int> 3, 4, 4, 4, 3, 5, 3, 6, 4, 6, 8, 4, 12, 7, 10, 5, 3…

O conjunto de dados de treino contém 74.180.464 registros e 11 colunas.

3.5 Dataset test

In [38]:
# Visualizando as primeiras 10 linhas do dataset.

head(test, 10)
A data.table: 10 × 7
idSemanaAgencia_IDCanal_IDRuta_SAKCliente_IDProducto_ID
<int><int><int><int><int><int><int>
011403712209463907835305
1112237112264705135 1238
210204512831454976932940
311122714448471785543066
411121911130 966351 1277
5111146466011741414 972
6112057145074659766 1232
710161212837441401235305
810134911223 397854 1240
911146111203164691543203
In [39]:
# Verificando linhas duplicadas no dataset.

table(duplicated(test))
  FALSE 
6999251 

Não há registros duplicados no dataset.

In [40]:
# Verificando se existem valores nulos no dataset.

anyNA(test)
FALSE

Não há valores nulos no dataset.

In [41]:
# Verificando o tipo de dado das variáveis do dataset.

glimpse(test)
Rows: 6,999,251
Columns: 7
$ id          <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,…
$ Semana      <int> 11, 11, 10, 11, 11, 11, 11, 10, 10, 11, 11, 10, 11, 10, 1…
$ Agencia_ID  <int> 4037, 2237, 2045, 1227, 1219, 1146, 2057, 1612, 1349, 146…
$ Canal_ID    <int> 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ Ruta_SAK    <int> 2209, 1226, 2831, 4448, 1130, 6601, 4507, 2837, 1223, 120…
$ Cliente_ID  <int> 4639078, 4705135, 4549769, 4717855, 966351, 1741414, 4659…
$ Producto_ID <int> 35305, 1238, 32940, 43066, 1277, 972, 1232, 35305, 1240, …

O conjunto de dados de treino contém 6.999.251 registros e 7 colunas.

4. Análise exploratória dos dados

4.1 Visão geral

Segundo a documentação referente ao projeto, cada linha dos dados de treinamento contém um registro de vendas com as seguintes variáveis:

Variável Descrição
Semana é o número da semana (de quinta a quarta-feira);
Agencia_ID é o ID do depósito de vendas;
Canal_ID é o ID do canal de vendas;
Ruta_SAK é o ID da rota (várias rotas = depósito de vendas);
Cliente_ID é o ID do cliente;
NombreCliente é o nome do cliente;
Producto_ID é o ID do produto;
NombreProducto é o nome do produto;
Venta_uni_hoy é o número de unidades vendidas na semana;
Venta_hoy é o valor de venda na semana (unidade monetária: pesos);
Dev_uni_proxima é o número de unidades retornadas na próxima semana;
Dev_proxima é o valor retornado na próxima semana e (unidade monetária: pesos) e;
Demanda_uni_equil (Target) é a variável a ser prevista, define a demanda ajustada.

Nesta etapa vamos buscar entender a disposição e as características dos dados dentro do dataset de treino além de extrair insigths que possam auxiliar no processo de criação do modelo preditivo.

In [42]:
# Verificando o tipo de dado das variáveis do dataset.

glimpse(train)
Rows: 74,180,464
Columns: 11
$ Semana            <int> 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, …
$ Agencia_ID        <int> 1110, 1110, 1110, 1110, 1110, 1110, 1110, 1110, 111…
$ Canal_ID          <int> 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, …
$ Ruta_SAK          <int> 3301, 3301, 3301, 3301, 3301, 3301, 3301, 3301, 330…
$ Cliente_ID        <int> 15766, 15766, 15766, 15766, 15766, 15766, 15766, 15…
$ Producto_ID       <int> 1212, 1216, 1238, 1240, 1242, 1250, 1309, 3894, 408…
$ Venta_uni_hoy     <int> 3, 4, 4, 4, 3, 5, 3, 6, 4, 6, 8, 4, 12, 7, 10, 5, 3…
$ Venta_hoy         <dbl> 25.14, 33.52, 39.32, 33.52, 22.92, 38.20, 20.28, 56…
$ Dev_uni_proxima   <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Dev_proxima       <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Demanda_uni_equil <int> 3, 4, 4, 4, 3, 5, 3, 6, 4, 6, 8, 4, 12, 7, 10, 5, 3…

O conjunto de dados de treino contém 74.180.464 registros e 11 colunas. Todas as variáveis apresentam o tipo de dado numérico.

In [43]:
# Verificando o número de valores únicos para cada variável do dataset.

t <- sapply(train, function(c) {
    length(unique(c))
})

# Exibindo os resultados.

print(t)
           Semana        Agencia_ID          Canal_ID          Ruta_SAK 
                7               552                 9              3603 
       Cliente_ID       Producto_ID     Venta_uni_hoy         Venta_hoy 
           880604              1799              2116             78140 
  Dev_uni_proxima       Dev_proxima Demanda_uni_equil 
              558             14707              2091 

Estes resultados são interessantes. Destacamos que podemos observar a existência de registros de 7 semanas; 880604 IDs de clientes e; 1799 IDs de Produtos diferentes no dataset.

4.2 Analisando cada variável separadamente

4.2.1 Criando funções auxiliares

Criaremos algumas funções para padronizar as plotagens de gráficos que efetuaremos.

In [ ]:
# Definindo uma função para criar gráficos de barra.

barPlot <- function(col, data) {
    
    data %>%
        mutate_at(c(var = col), as.factor) %>%
        group_by(var) %>%
        summarise(absFreq = n()) %>%
        ggplot(aes(x = var, y = absFreq)) +
        geom_bar(stat = 'identity', alpha = 0.75, fill = '#086788') +
        ylab('Frequency') +
        xlab(col) +
        labs(title = paste('Bar plot for variable:', col)) +
        theme_bw()
}
In [ ]:
# Definindo uma função para criar gráficos de boxplot.

boxPlot <- function(col, data) {

    data %>%
        group_by_at(col) %>%
        summarise(absFreq = n()) %>%
        ggplot(aes(x = absFreq)) +
        geom_boxplot(fill = '#566E3D', color = '#373D20', alpha = 0.8) +
        theme_bw() +
        theme(axis.text.y = element_blank()) +
        xlab(paste(col, 'frequency')) +
        labs(title = paste('Boxplot for variable:', col)) 
}
4.2.2 Variável Semana
In [46]:
# Definindo o nome da variável a ser analisada.

col <- 'Semana'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, train)

O gráfico nos permite observar há existênica de uma distribuição aproximadamente uniforme dos registros entre as semanas.

In [47]:
# Contabilizando o número de registros por semana.

table(Semana = train$Semana)
Semana
       3        4        5        6        7        8        9 
11165207 11009593 10615397 10191837 10382849 10406868 10408713 
4.2.3 Variável Agencia_ID
In [48]:
# Criando um boxplot com as frequências com que os ID das agências aparecem no conjunto de dados.

# Definindo o nome da variável a ser analisada.

col <- 'Agencia_ID'

# Criando um gráfico boxplot para a variável especificada.

boxPlot(col, train)

Vemos que há outliers nas frequências com que os IDs das agências aparecem no conjunto de dados. Vamos determinar a cidade e o estado destas agências com que a frequência do ID foi discrepante.

In [49]:
# Determinando o endereço, o estado e ordenando de forma decrescente a frequência dos IDs das agências no dataset.

AgencyIdFreq <- train %>%
    select(Agencia_ID) %>%
    group_by(Agencia_ID) %>%
    summarise(absFreq = n()) %>%
    arrange(desc(absFreq)) %>%
    inner_join(town, by = 'Agencia_ID')

# Visualizando os 5 IDs de agências mais frequentes.

head(AgencyIdFreq)
A tibble: 6 × 4
Agencia_IDabsFreqTownState
<int><int><chr><chr>
19118062832309 NORTE JALISCO
11237291032094 CHALCO_BM ESTADO DE MÉXICO
20136661252177 AGENCIA SUANDY ESTADO DE MÉXICO
12206639342048 AG. IXTAPALUCA 1 ESTADO DE MÉXICO
13476006872251 AGUASCALIENTES NORTE AGUASCALIENTES
13515955622252 AGUASCALIENTES SIGLO XXIAGUASCALIENTES
In [50]:
# Extraindo outliers do dataset.

AgencyIdStates <- AgencyIdFreq %>%
    anomalize(absFreq, method = 'iqr', alpha = 0.10) %>%
    filter(anomaly == 'Yes') %>%
    select(-c(absFreq_l1, absFreq_l2, anomaly))

# Visualizando as informações dos outliers.

AgencyIdStates
A tibble: 12 × 4
Agencia_IDabsFreqTownState
<int><int><chr><chr>
19118062832309 NORTE JALISCO
11237291032094 CHALCO_BM ESTADO DE MÉXICO
20136661252177 AGENCIA SUANDY ESTADO DE MÉXICO
12206639342048 AG. IXTAPALUCA 1 ESTADO DE MÉXICO
13476006872251 AGUASCALIENTES NORTE AGUASCALIENTES
13515955622252 AGUASCALIENTES SIGLO XXIAGUASCALIENTES
19125797702294 GUADALAJARA JALISCO
11265769792017 AG. SANTA CLARA ESTADO DE MÉXICO
20155751222154 ATLACOMULCO ESTADO DE MÉXICO
12195729662042 AG. TEPOZOTLAN ESTADO DE MÉXICO
19455703842322 ZAMORA MADERO MICHOACÁN
13125671402278 ZAPOPAN BIMBO JALISCO

Parece haver um grupo de estados mais frequente dentro das informações destas agências discrepantes. Para facilitar esta análise, iremos criar um gráfico de barras.

In [51]:
# Criando um gráfico de barras para os estados das agências que apresentaram uma frequência discrepante.

# Definindo o nome da variável a ser analisada.

col <- 'State'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, AgencyIdStates)
In [52]:
# Determinando a proporção dos estados identificados entre os IDs das agências que apresentaram uma frequência discrepante.

prop.table(table(AgencyIdStates$State))
  AGUASCALIENTES ESTADO DE MÉXICO          JALISCO        MICHOACÁN 
      0.16666667       0.50000000       0.25000000       0.08333333 

Concluímos que o Estado do México apresenta 6 das agências mais recorrentes no conjunto de dados e que o estado de Jalisco é o que possui a agência mais frequente.

4.2.4 Variável Canal_ID
In [53]:
# Plotando um gráfico de barras para o conjunto de dados da variável.

# Definindo o nome da variável a ser analisada.

col <- 'Canal_ID'

# Criando um gráfico de barras para a variável especificada.

barPlot(col, train)

Observamos que o canal de vendas com ID 1 é o mais frequente. Determinaremos a proporção da frequência de cada um destes canais dentro do conjunto de dados.

In [54]:
# Determinando a propoção de cada um dos canais.

train %>%
    mutate(Canal_ID = as.factor(Canal_ID)) %>%
    group_by(Canal_ID) %>%
    summarise(Prop = round(n() / length(train$Canal_ID) * 100, digits = 3))
A tibble: 9 × 2
Canal_IDProp
<fct><dbl>
1 90.907
2 1.132
4 5.066
5 0.197
6 0.379
7 0.905
8 0.090
9 0.001
11 1.324

Concluímos que aproximadamente 91% dos registros do conjunto de dados está associado ao Canal com ID = 1.

4.2.5 Variável Ruta_SAK
In [55]:
# Criando um boxplot com as frequências com que as rotas usadas aparecem no conjunto de dados.

# Definindo o nome da variável a ser analisada.

col <- 'Ruta_SAK'

# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, train)

Há um grande número de outliers dentro desta variável. Pode ser interessante extraí-los e análisá-los separadamente.

In [56]:
# Determinando em ordem decrescente as rotas mais frequentes dentro do conjunto de dados.

routeFreq <- train %>%
    group_by(Ruta_SAK) %>%
    summarise(absFreq = n()) %>%
    arrange(desc(absFreq))

# Visualizando as primeiras linhas do dataset.

head(routeFreq)
A tibble: 6 × 2
Ruta_SAKabsFreq
<int><int>
1201461918
1203438474
1202427767
1204418744
1205409597
1206391245
In [57]:
# Extraindo outliers do dataset.

routeOutFreq <- routeFreq %>%
    anomalize(absFreq, method = 'iqr', alpha = 0.10) %>%
    filter(anomaly == 'Yes') %>%
    select(-c(absFreq_l1, absFreq_l2, anomaly))

# Determinando número de outliers.

nrow(routeOutFreq)
605

Bom, detectamos a existência de 605 rotas com frequências discrepantes dentro do conjunto de dados.

In [58]:
# Determinando a rota mais frequente dentro do conjunto de dados.

routeOutFreq[routeOutFreq$absFreq == max(routeOutFreq$absFreq), ]
A tibble: 1 × 2
Ruta_SAKabsFreq
<int><int>
1201461918

Constatamos que a rota 1201 é a mais recorrente no dataset.

In [59]:
# Determinando a proporção de rotas com frequências discrepantes.

length(routeOutFreq$Ruta_SAK) / length(unique(train$Ruta_SAK)) * 100
16.7915625867333
In [60]:
# Determinando a proporção de registros associados as rotas discrepantes.

sum(routeOutFreq$absFreq) / length(train$Ruta_SAK) * 100
86.3783623677522

Por fim, concluímos que aproximadamente 16.8% das rotas apresentam frequências discrepantes e são responsáveis por 86.38% das entregas.

4.2.6 Variável Cliente_ID
In [61]:
# Criando um boxplot com as frequências com que os IDs dos clientes que estão presentes no dataset aparecem.

# Definindo o nome da variável a ser analisada.

col <- 'Cliente_ID'

# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, train)

Veja que interessante, há um cliente que apresenta uma frequência extremamente alta em relação aos demais e acaba distorcendo o gráfico. Vamos identificar o nome deste cliente junto com os dos demais outliers.

In [62]:
# Determinando em ordem decrescente os clientes mais frequentes dentro do conjunto de dados.

clientFreq <- train %>%
    select(Cliente_ID) %>%
    group_by(Cliente_ID) %>%
    summarise(absFreq = n()) %>%
    arrange(desc(absFreq)) %>%
    inner_join(client, by = 'Cliente_ID')

# Visualizando as primeiras linhas do dataset.

head(clientFreq)
A tibble: 6 × 3
Cliente_IDabsFreqNombreCliente
<int><int><chr>
653378124059PUEBLA REMISION
653039 5306QUERETARO DE ARTEAGA REMISION
652850 2622JALISCO REMISION
652889 1620CHIHUAHUA REMISION
1093627 1394SUPERAMA RIO MAYO
653124 1362TAMAULIPAS REMISION

O cliente mais discrepante possui uma frequência que é aproximadamente 23.4 vezes maior do que a do cliente que ocupa a segunda posição nos dando uma breve noção do quão distante este primeiro colocado está dos demais.

In [63]:
# Extraindo outliers do dataset.

clientOutFreq <- clientFreq %>%
    anomalize(absFreq, method = 'iqr', alpha = 0.0415) %>%
    filter(anomaly == 'Yes') %>%
    select(-c(absFreq_l1, absFreq_l2, anomaly))

# Determinando número de outliers.

nrow(clientOutFreq)
1622

Verificamos a existência de 1622 IDs de clientes com frequências discrepantes dentro do conjunto de dados.

In [64]:
# Determinando o cliente mais frequente dentro do conjunto de dados.

mostFrequentClient <- clientOutFreq[clientOutFreq$absFreq == max(clientOutFreq$absFreq), ]

# Visualizando o cliente mais frequente dentro do conjunto de dados.

mostFrequentClient
A tibble: 1 × 3
Cliente_IDabsFreqNombreCliente
<int><int><chr>
653378124059PUEBLA REMISION

Identificamos que o nome do cliente que possuí o ID mais recorrente dentro do dataset é "Puebla Remision".

In [65]:
# Determinando a proporção de registros que contém o ID do Cliente com a frequência mais discrepante.

mostFrequentClient$absFreq / length(train$Cliente_ID) * 100
0.167239449998587

O cliente "Puebla Remision" está associado a aproximadamente 0.167% dos registros do conjunto de dados.

In [66]:
# Determinando a proporção de registros que contém os IDs dos Clientes com as frequências mais discrepantes.

sum(clientOutFreq$absFreq) / length(train$Cliente_ID) * 100
1.37362311457097

Todos os registros associados a IDs de clientes que possuem uma frequência discrepante correspondem a aproximadamente 1.37% do total. Isto nos indica que a maior parte dos dados que estamos manipulando estão relacionados a muitos clientes que efetuam compras com uma frequência que não foge do padrão.

4.2.7 Variável Producto_ID
In [67]:
# Criando um boxplot com as frequências com que os IDs dos produtos que estão presentes no dataset aparecem.

# Definindo o nome da variável a ser analisada.

col <- 'Producto_ID'

# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, train)

Existem muitos outliers entre as frequências dos produtos. Isto indica que há um subconjunto de itens que fogem do padrão de venda do resto dos demais produtos.

In [68]:
# Determinando em ordem decrescente os produtos mais frequentes dentro do conjunto de dados.

productFreq <- train %>%
    select(Producto_ID) %>%
    group_by(Producto_ID) %>%
    summarise(absFreq = n()) %>%
    arrange(desc(absFreq)) %>%
    inner_join(product, by = 'Producto_ID')

# Visualizando as primeiras linhas do dataset.

head(productFreq)
A tibble: 6 × 6
Producto_IDabsFreqNombreProductoNpesoUniPesoProductor
<int><int><chr><int><chr><chr>
12402146655Mantecadas Vainilla 125g BIM
12422043864Donitas Espolvoreadas 105g BIM
22331975550Pan Blanco 640g BIM
12501860488Donas Azucar 105g BIM
12841670190Rebanada 55g BIM
12321472082Panque Nuez 255g BIM
In [69]:
# Extraindo outliers do dataset.

productOutFreq <- productFreq %>%
    anomalize(absFreq, method = 'iqr', alpha = 0.1) %>%
    filter(anomaly == 'Yes') %>%
    select(-c(absFreq_l1, absFreq_l2, anomaly))

# Determinando número de outliers.

nrow(productOutFreq)
333

Observamos que existem 333 frequências de produtos discreprantes.

In [70]:
# Determinando o produto mais frequente dentro do conjunto de dados.

mostFrequentProduct <- productOutFreq[productOutFreq$absFreq == max(productOutFreq$absFreq), ]

# Visualizando o produto mais frequente dentro do conjunto de dados.

mostFrequentProduct
A tibble: 1 × 6
Producto_IDabsFreqNombreProductoNpesoUniPesoProductor
<int><int><chr><int><chr><chr>
12402146655Mantecadas Vainilla 125g BIM

Detectamos que o produto que mais apresenta registros de vendas dentro do nosso conjunto de dados é denominado "Mantecadas Vainilla".

In [71]:
# Determinando a proporção de registros que contém os IDs dos produtos com as frequências discrepantes.

sum(productOutFreq$absFreq) / length(train$Producto_ID) * 100
96.7568132224139

Os produtos que apresentam uma frequência discrepante dentro do conjunto de dados são responsáveis por aproximadamente 96.76% dos registros.

In [72]:
# Determinando a sigla do fabricante mais recorrente dentro do conjunto de dados que contém os IDs dos produtos 
# com as frequências discrepantes.

manufacturersOut <- productOutFreq %>%
    group_by(Productor) %>%
    summarise(absFreq = n()) %>%
    arrange(desc(absFreq))

# Visualizando as primeiras linhas do dataset.

head(manufacturersOut)
A tibble: 6 × 2
ProductorabsFreq
<chr><int>
BIM 62
MLA 30
MTA BIM 29
TR 28
MTB MLA 21
MTA MLA 13

Concluímos que o fabricante identificado pela sigla BIM é o mais recorrente dentro dos produtos que apresentam uma frequência discrepante.

4.2.8 Variável Venta_uni_hoy
In [73]:
# Verificando a distribuição dos dados.

summary(train$Venta_uni_hoy)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   0.00    2.00    3.00    7.31    7.00 7200.00 
In [74]:
# Verificando a frequência com que cada número de unidades aparece no dataset.

t <- train %>%
    group_by(Venta_uni_hoy) %>%
    summarise(absFreq = n()) 

# Ordenando dados das frequências em ordem decrescente.

t <- t[order(t$absFreq, decreasing = T), ]

# Visualizando o número de unidades mais frequentes no dataset.

head(t, 10)
A tibble: 10 × 2
Venta_uni_hoyabsFreq
<int><int>
215299482
113496679
3 9271143
4 7260737
5 5707842
6 4272687
10 2929543
8 2423855
7 1762233
9 1191273

Concluímos que as vendas com 2 unidades são as mais frequentes dentro do conjunto de dados e que o número das 10 unidades mais frequentes varia entre 1 e 10.

4.2.9 Variável Venta_hoy
In [75]:
# Verificando a distribuição dos dados.

summary(train$Venta_hoy)
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
     0.0     16.8     30.0     68.5     56.1 647360.0 
In [76]:
# Definindo o valor total de vendas por semana.

train %>%
    group_by(Semana) %>%
    summarise(total_Venta_hoy = sum(Venta_hoy))
A tibble: 7 × 2
Semanatotal_Venta_hoy
<int><dbl>
3748577947
4755607344
5727925232
6701524062
7721662485
8713412753
9715954667
In [77]:
# Definindo o valor total de vendas por cliente.

train %>%
    group_by(Cliente_ID ) %>%
    summarise(total_Venta_hoy = sum(Venta_hoy)) %>%
    arrange(desc(total_Venta_hoy)) %>%
    head(10)
A tibble: 10 × 2
Cliente_IDtotal_Venta_hoy
<int><dbl>
653378154662268
653039 7697623
827594 4814696
652850 4018867
1216931 3325557
5903732 2931618
1200400 2819876
2502084 2650769
19260 2599059
60 2148013
4.2.10 Variável Dev_uni_proxima
In [78]:
# Verificando a distribuição dos dados.

summary(train$Dev_uni_proxima)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.0e+00 0.0e+00 0.0e+00 1.3e-01 0.0e+00 2.5e+05 
In [79]:
# Definindo o número total de unidades retornadas na próxima semana.

train %>%
    group_by(Semana) %>%
    summarise(total_Dev_uni_proxima = sum(Dev_uni_proxima))
A tibble: 7 × 2
Semanatotal_Dev_uni_proxima
<int><int>
31286055
41322852
51220967
61321510
71417450
81452369
91641371
In [80]:
# Definindo o número total de unidades retornadas na próxima semana por cliente.

train %>%
    group_by(Cliente_ID) %>%
    summarise(total_Dev_uni_proxima = sum(Dev_uni_proxima)) %>%
    arrange(desc(total_Dev_uni_proxima)) %>%
    head()
A tibble: 6 × 2
Cliente_IDtotal_Dev_uni_proxima
<int><int>
6533781131794
652850 59664
1050905 50778
4102610 38653
653058 18938
652889 17752
4.2.11 Variável Dev_proxima
In [81]:
# Verificando a distribuição dos dados.

summary(train$Dev_proxima)
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
     0.00      0.00      0.00      1.24      0.00 130760.00 
In [82]:
# Definindo o valor total retornado na próxima semana.

train %>%
    group_by(Semana) %>%
    summarise(total_Dev_proxima = sum(Dev_proxima))
A tibble: 7 × 2
Semanatotal_Dev_proxima
<int><dbl>
312794465
413371045
512336363
612886385
713693831
813885240
913257387
In [83]:
# Definindo o valor total retornado na próxima semana por cliente.

train %>%
    group_by(Cliente_ID) %>%
    summarise(total_Dev_proxima = sum(Dev_proxima)) %>%
    arrange(desc(total_Dev_proxima)) %>%
    head()
A tibble: 6 × 2
Cliente_IDtotal_Dev_proxima
<int><dbl>
6533787367474.2
652850 495570.2
1050905 390252.8
4102610 364382.0
653058 160844.6
652889 149645.1
4.2.12 Variável Demanda_uni_equil
In [84]:
# Criando um boxplot para visualizar a distribuição dos dados da variável Demanda_uni_equil.

# Definindo o nome da variável a ser analisada.

col <- 'Demanda_uni_equil'

# Criando um gráfico de boxplot para a variável especificada.

train %>%
    ggplot(aes(x = Demanda_uni_equil)) +
    geom_boxplot(fill = '#566E3D', color = '#373D20', alpha = 0.8) +
    theme_bw() +
    theme(axis.text.y = element_blank()) +
    xlab('Values') +
    labs(title = 'Boxplot for variable: Demanda uni equil')

Vemos que há uma distorção muito grande dentro dos valores da variável a ser prevista devido a presença de outliers muito discrepantes. Deveremos tratar este problema para conseguirmos aumentar a performance dos modelos preditivos que criarmos.

4.2.13 Variável Town

Agora, iremos verificar quantos e quais são os estados e as agências por cidades presentes no conjunto de dados.

In [85]:
# Contabilizando a frequência de agências por cidade dentro do dataset.

t <- town %>%
    group_by(Town) %>%
    summarise(absFreq = n())

# Ordenando resultados.

t <- t[order(t$absFreq, decreasing = T),]

# Visualizando as primeiras 10 linhas da tabela.

head(t, 10)
A tibble: 10 × 2
TownabsFreq
<chr><int>
2013 AG. MEGA NAUCALPAN 8
2011 AG. SAN ANTONIO 7
2355 JALAPA I 7
2029 AG.IZTAPALAPA 2 6
2036 APIZACO MARINELA 6
2161 IRAPUATO GUADALUPE 6
2269 PUERTO VALLARTA BIMBO6
2290 DURANGO BIMBO 6
2322 ZAMORA MADERO 6
2396 RUIZ CORTINEZ 6
In [86]:
# Determinando o número de agências por cidade atendidas.

nrow(t)
260

Detectamos 260 agências por cidades atendidas.

Criaremos um boxplot para verificar se existem outliers dentro das frequências de agências por cidade.

In [87]:
# Definindo o nome da variável a ser analisada.

col <- 'Town'

# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, town)

Concluímos que a agência 2013 AG. MEGA NAUCALPAN apresenta um frequência absoluta que saí do pradrão das demais presentes no conjunto de dados.

4.2.14 Variável State
In [88]:
# Contabilizando a frequência de cada estado dentro do dataset.

t <- town %>%
    group_by(State) %>%
    summarise(absFreq = n())

# Ordenando resultados.

t <- t[order(t$absFreq, decreasing = T),]

# Visualizando as primeiras 10 linhas da tabela.

head(t, 10)
A tibble: 10 × 2
StateabsFreq
<chr><int>
ESTADO DE MÉXICO 71
MÉXICO, D.F. 65
JALISCO 55
VERACRUZ 45
GUANAJUATO 39
NUEVO LEÓN 34
PUEBLA 34
SONORA 34
MICHOACÁN 33
BAJA CALIFORNIA NORTE32
In [89]:
# Determinando o número de estados atendidos.

nrow(t)
33

Detectamos que 33 estados são atendidos.

In [90]:
# Definindo o nome da variável a ser analisada.

col <- 'State'

# Criando um gráfico de boxplot para a variável especificada.

boxPlot(col, town)

Concluímos que o ESTADO DE MÉXICO apresenta um frequência absoluta que saí do pradrão dos demais presentes no conjunto de dados.

5. Análise Preditiva

5.1 Importando bibliotecas necessárias

Importaremos todas as bilbiotecas necessárias para a realização dos processos de modelagem preditiva.

In [ ]:
# Caso não possua uma das bibliotecas importadas abaixo, a instale com um dos comandos a seguir:

install.packages(c(
    'Metrics',
    'xgboost',
    'randomForest',
    'caret'
))
In [ ]:
# Importando bibliotecas.

library(Metrics)
library(xgboost)
library(randomForest)
library(caret)

5.2 Feature Selection

Observe que as variáveis Venta_uni_hoy, Venta_hoy, Dev_uni_proxima e Dev_proxima não estão presentes no conjunto de dados de teste e por isso iremos excluí-las do conjunto de dados de treino.

In [ ]:
# Selecionando as variáveis que serão utilizadas na fase de modelagem preditiva dentro do dataset de treino.

train <- train %>% select(Semana, Agencia_ID, Canal_ID, Ruta_SAK, Cliente_ID, Producto_ID, Demanda_uni_equil)

5.3 Feature Engineering I - Transformando variável Target

Agora, iremos retornar ao problema da distorção dos valores do conjunto de dados da variável alvo. Criaremos mais uma vez um boxplot para visualizar a distribuição dos dados bem como um gráfico de densidade.

In [94]:
# Criando um boxplot para visualizar a distribuição dos dados da variável Demanda_uni_equil.

train %>%
    ggplot(aes(x = Demanda_uni_equil)) +
    geom_boxplot(fill = '#566E3D', color = '#373D20', alpha = 0.8) +
    theme_bw() +
    theme(axis.text.y = element_blank()) +
    xlab('Values') +
    labs(title = 'Boxplot for variable: Demanda uni equil')
In [95]:
# Criando um gráfico de densidade para visualizar a distribuição dos dados da variável Demanda_uni_equil.

train %>%
    ggplot(aes(x = Demanda_uni_equil)) + 
    geom_density(fill = '#A6EBC9') + 
    theme_bw() + 
    labs(title = 'Density graph for variable: Demanda uni equil') +
    xlab('Demanda uni equil')

Muito bem, para contornar este problema da distorção nos dados usaremos a função de transformação log1p (ou log(x + 1)) para diminuir a irregularidade nos dados e tornar os padrões que esta variável possuí mais visíveis. Também utilizaremos a função expm1 (ou exp(x) - 1) para realizar o processo inverso de transformação dos resultados obtidos ao se aplicar a função log1p.

Ou seja, transformaremos a variável a ser prevista para realizar a execução da análise preditiva e no final converteremos os resultados gerados para a sua escala original.

Para mais informações sobre como a função log atua sobre dados distorcidos consulte este link. Para entender melhor as funções log1p e expm1 consulte este link.

Destacamos que o principal motivo de utilizarmos a função log1p é há existência de valores nulos dentro dos dados da variável o que inviabiliza o uso da função log pois o log(0) é um valor indefinido.

In [96]:
# Verificando a existência de valores nulos dentro do conjunto de dados da variável Demanda_uni_equil.

prop.table(table(train$Demanda_uni_equil == 0))
    FALSE      TRUE 
0.9819788 0.0180212 

Detectamos que aproximadamente 1.8% dos dados da variável a ser prevista são iguais a 0.

Vamos aplicar a função log1p ao conjunto de dados.

In [ ]:
# Calcula o logaritmo natural de cada valor da variável Demanda_uni_equil acrescido de 1 unidade, ou seja, 
# log(Demanda_uni_equil + 1).

train$Demanda_uni_equil <- log1p(train$Demanda_uni_equil)

Criaremos mais uma vez um gráfico de boxplot e um gráfico de densidade para visualizar o efeito da transformação aplicada sobre os dados.

In [98]:
# Criando um boxplot para visualizar a distribuição dos dados da variável Demanda_uni_equil transformada.

train %>%
    ggplot(aes(x = Demanda_uni_equil)) +
    geom_boxplot(fill = '#566E3D', color = '#373D20', alpha = 0.8) +
    theme_bw() +
    theme(axis.text.y = element_blank()) +
    xlab('log(Demanda uni equil + 1)') +
    labs(title = 'Boxplot for variable: Demanda uni equil')
In [99]:
# Criando um gráfico de densidade para visualizar a distribuição dos dados da variável Demanda_uni_equil transformada.

train %>%
    ggplot(aes(x = Demanda_uni_equil)) + 
    geom_density(fill = '#A6EBC9') + 
    theme_bw() + 
    labs(title = 'Density graph for variable: Demanda uni equil') +
    xlab('log(Demanda uni equil + 1)')

Concluímos que a aplicação da função log1p diminuiu a distorção causada pelos valores discrepantes dentro do conjunto de dados da variável a ser prevista e isso nos ajudará a alçancar valores de acurácia melhores nos modelos que criarmos.

5.4 Unindo dados de treino e de teste em um mesmo dataset

Nosso objetivo nesta etapa é criar um único dataset contendo tanto os dados de treino quanto os dados de teste. Mas, antes de executarmos esta ação devemos observar que há variáveis excluivas para cada um destes conjuntos de dados.

O dataset de teste possui a variável id que não está contida nos dados de treino e por isso a criaremos para este conjunto de dados. O mesmo processo será efetuado para a variável Demanda_uni_equil no dataset de teste.

Para distinguir os registros que pertencem ao conjunto de dados de treino dos que pertencem aos dados de teste criaremos uma variável binária denominada toTest (0: são dados de treino; 1: são dados de teste).

In [ ]:
# Criando variável ID para o conjunto de dados de treino com um valor auxiliar.

train$id <- 0

# Criando a variável para indicar se o registro pertence ou não ao dados de treino ou aos dados de teste.

train$toTest <- 0
In [ ]:
# Criando variável Demanda_uni_equil para o conjunto de dados de teste com um valor auxiliar.

test$Demanda_uni_equil <- 0

# Criando a variável para indicar se o registro pertence ou não ao dados de treino ou aos dados de teste.

test$toTest <- 1

Para começar esta junção entre os datasets, iremos capturar os registros de apenas uma das semanas presentes no dataset de treino e unir com os dados de teste.

Os registros dos dados de treino que não irão ser manipulados nesta etapa serão utilizados na etapa a seguir de Feature Engennier.

In [ ]:
# Unindo os registros do dataset de treino em que a variável Semana é igual a 9 com todos os registros de teste.

data <- rbind(train[Semana == 9], test)

Como já mesclamos todos os registros do dataset de teste, podemos liberar memória excluindo a variável test.

In [ ]:
# Removendo o dataset test.

rm(test)

5.4.1 Feature Engennier II - Criando novas variáveis preditoras

Os registros de treino das semanas que não foram usados na etapa anterior serão agrupados ao novo dataset que estamos gerando para criar novas variáveis.

In [104]:
# Determinando a média da demanda ajustada de clientes por produto e a quantidade de registros de clientes por produto.

train[Semana <= 8][ , .(meanClientProd  = mean(Demanda_uni_equil), countClientProd = .N),  
    by = .(Producto_ID, Cliente_ID)] %>% 
        merge(data, all.y = TRUE, by = c("Producto_ID", "Cliente_ID")) -> data

# Determinando a média da demanda ajustada por produto e a quantidade de registros por produto.

train[Semana <= 8][ , .(meanProd = mean(Demanda_uni_equil), countProd = .N),
    by = .(Producto_ID)] %>% 
        merge(data, all.y = TRUE, by = c("Producto_ID")) -> data

# Determinando a média da demanda ajustada por cliente e a quantidade de registros por cliente.

train[Semana <= 8][ , .(meanClient = mean(Demanda_uni_equil), countCliente = .N),
    by = .(Cliente_ID)] %>% 
        merge(data, all.y = TRUE, by = c("Cliente_ID")) -> data

# Visualizando as primeiras linhas do dataset.

head(data)
A data.table: 6 × 15
Cliente_IDmeanClientcountClienteProducto_IDmeanProdcountProdmeanClientProdcountClientProdSemanaAgencia_IDCanal_IDRuta_SAKDemanda_uni_equilidtoTest
<int><dbl><int><int><dbl><int><dbl><int><int><int><int><int><dbl><dbl><dbl>
262.853926192302353.117261 2551 NANA 92655241894.574711 00
262.853926192313931.559129193282.917141 6 92061272122.639057 00
262.853926192315182.514937 2142.397895 1102655241890.00000015693521
262.853926192315203.066581 1792 NANA112655241890.00000047286741
262.853926192329622.635926 88671.386294 1 92061272122.397895 00
262.853926192342042.673286207913.669958 6 92061272123.784190 00

Observe que a partir da execução desta fase conseguimos eliminar o problema da existência de produtos dentro dos dados de teste que não estão presentes dentro dos dados de treino, pois passamos a avaliar os valores médios e quantidades de cada variável por agrupamentos.

Agora também podemos eliminar a variável train.

In [ ]:
# Removendo o dataset train.

rm(train)

5.5 Feature Engineering III - Transformando variáveis preditoras

Nesta etapa vamos escalar os valores das variáveis preditoras entre 0 e 1.

In [106]:
# Definindo método de pré-processamento.

params <- preProcess(data[, !c('id', 'Demanda_uni_equil', 'toTest')], method = 'range')

# Transformando os dados.

data <- predict(params, data)

# Visualizando as primeiras linhas do dataset.

head(data)
A data.table: 6 × 15
Cliente_IDmeanClientcountClienteProducto_IDmeanProdcountProdmeanClientProdcountClientProdSemanaAgencia_IDCanal_IDRuta_SAKDemanda_uni_equilidtoTest
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
00.34326350.0017952650.60441190.39799510.0013729304 NA NA0.00.062680030.10.42010234.574711 00
00.34326350.0017952650.62759230.19906120.01040573590.34363780.0022007040.00.038581690.10.72334242.639057 00
00.34326350.0017952650.63009450.32109360.00011468010.28247090.0000000000.50.062680030.10.42010230.00000015693521
00.34326350.0017952650.63013450.39152450.0009642817 NA NA1.00.062680030.10.42010230.00000047286741
00.34326350.0017952650.65899990.33654080.00477349070.16330480.0000000000.00.038581690.10.72334242.397895 00
00.34326350.0017952650.68386180.34131080.01119342110.43231920.0022007040.00.038581690.10.72334243.784190 00

5.6 Segmentando dados de treino e de teste

Iremos separar os dados de treino e de teste do conjunto de dados que criamos nas etapas anteriores.

In [107]:
# Extraindo registros de treino.

train <- data %>% 
    filter(toTest == 0) %>%
    select(-c(id, toTest))

# Visualizando as primeiras linhas do dataset.

head(train)
A data.frame: 6 × 13
Cliente_IDmeanClientcountClienteProducto_IDmeanProdcountProdmeanClientProdcountClientProdSemanaAgencia_IDCanal_IDRuta_SAKDemanda_uni_equil
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
100.34326350.0017952650.60441190.39799510.001372930 NA NA00.062680030.10.42010234.574711
200.34326350.0017952650.62759230.19906120.0104057360.34363780.00220070400.038581690.10.72334242.639057
300.34326350.0017952650.65899990.33654080.0047734910.16330480.00000000000.038581690.10.72334242.397895
400.34326350.0017952650.68386180.34131080.0111934210.43231920.00220070400.038581690.10.72334243.784190
500.34326350.0017952650.68390180.22096310.0566584170.51609390.00220070400.038581690.10.72334244.682131
600.34326350.0017952650.68398190.55722730.0069141850.41338080.00220070400.038581690.10.72334243.688879
In [108]:
# Extraindo registros de teste.

test <- data %>% 
    filter(toTest == 1) %>%
    select(-c(Demanda_uni_equil, toTest))

# Visualizando as primeiras linhas do dataset.

head(test)
A data.frame: 6 × 13
Cliente_IDmeanClientcountClienteProducto_IDmeanProdcountProdmeanClientProdcountClientProdSemanaAgencia_IDCanal_IDRuta_SAKid
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
100.34326350.0017952650.63009450.32109360.00011468010.28247090.0000000000.50.062680030.10.42010231569352
200.34326350.0017952650.63013450.39152450.0009642817 NA NA1.00.062680030.10.42010234728674
300.34326350.0017952650.68390180.22096310.05665841650.51609390.0022007041.00.038581690.10.72334241547831
400.34326350.0017952650.68398190.55722730.00691418540.41338080.0022007040.50.038581690.10.72334246667200
500.34326350.0017952650.69549200.50984250.00692226140.34813910.0013204230.50.038581690.10.72334241592616
600.34326350.0017952650.69549200.50984250.00692226140.34813910.0013204231.00.038581690.10.72334246825659

Agora podemos remover a variável data.

In [ ]:
# Removendo o dataset data.

rm(data)

5.7 Criando função para gerar modelos com diferentes valores de parametrização baseados no algoritmo XGboost

Bom, optamos por utilizar o algoritmo XGboost para a criação do nosso modelo preditivo por apresentar uma boa performance para gerar os scores da métrica de avaliação a ser utilizada e por ser consideravelmente mais rápido quando comparado a outros algoritmos.

Como não sabemos quais valores utilizar para sua configuração, criaremos uma função que gere diferentes modelos com diferentes ajustes e selecionaremos aquele que obtiver o melhor desempenho para os dados de teste.

Iremos avaliar a performance dos modelos a serem criados com base nos dados de treino e por isso deveremos ter atenção ao overfitting quando formos selecionar qual deverá ser utilizado para fazer as previsões dos dados de teste.

Adotando esta estratégia conseguiremos extrair o melhor que este algoritmo pode nos oferecer.

In [ ]:
# Definindo uma função para gerar diferentes modelos com diferentes valores de parametrização baseados no algoritmo XGboost.

getBetterXGboostParameters <- function(data, label, maxDepth = 13, nEta = 0.2, nRounds = 86,  subsample = 0.85, 
                                       colsample = 0.7, statusPrint = F) {

    # Criando o dataframe para salvar os resultados dos modelos.

    featuresXGboost <- data.frame()

    # Define uma varíavel auxiliar para permitir o acompanhamento do progresso na avaliação dos modelos criados.

    count <- 0

    # Define o número total de modelos a serem criados.

    total <- length(maxDepth) * length(nEta) * length(nRounds) * length(subsample) * length(colsample)

    # Convertendo os dados das variáveis do dataset para o tipo DMatrix (uma matriz densa).

    dTrain <- xgb.DMatrix(
        data  = data,      # Define as variáveis preditoras.
        label = label      # Define a variável a ser prevista.
    )

    for(m in maxDepth) {
        for(e in nEta) {
            for(r in nRounds) {
                for(s in subsample) {
                    for(c in colsample) {

                        # Define um seed para permitir que o mesmo resultado do experimento seja reproduzível.

                        set.seed(100)

                        # Criando o modelo baseado no algoritmo XGboost.

                        model_xgb <-  xgb.train(
                            params = list(  
                                objective        = "reg:linear", # Define que o modelo deve ser baseado em uma regressão 
                                                                 # logistica linear.
                                booster          = "gbtree",     # Definindo o booster a ser utilizado.
                                eta              = e,            # Define a taxa de aprendizado do modelo.
                                max_depth        = m,            # Define o tamanho máximo da árvore.
                                subsample        = s, # Define a proporção de subamostra da instância de treinamento.
                                colsample_bytree = c # Define a proporção da subamostra de colunas ao construir cada árvore.
                            ),
                            data          = dTrain,  # Define as variáveis preditoras e a variável a ser prevista.
                            feval         = rmsle,   # Define a função de avaliação a ser utilizada.
                            nrounds       = r,       # Define o número de iterações que o algoritmo deve executar.
                            verbose       = F,       # Define a exibição da queda da taxa de erro durante o treinamento.
                            maximize      = FALSE,   # Define que a pontuação da avaliação deve ser minimizada.
                            nthread       = 16       # Define o número de threads que devem ser usadas. 
                                                     # Quanto maior for esse número, mais rápido será o treinamento.
                        )
                                

                        # Realizando as previsões com o modelo baseado no algoritmo XGboost.

                        pred <- predict(model_xgb, data)

                        # Armazena os parâmetros utilizados para criação do modelo e o score da métrica RMSLE obtido 
                        # em um dataframe.
                        
                        featuresXGboost <- rbind(featuresXGboost, data.frame(
                            maxDepth = m, 
                            eta      = e,
                            nRounds  = r,
                            s        = s,
                            c        = c, 
                            rmsle    = rmsle(label, pred)
                        ))

                        # Incrementa o número de modelos avaliados.

                        count <- count + 1

                        # Imprime a porcetagem de progresso do treinamento e o melhor score da métrica RMSLE já alcançado.

                        print(paste(100 * count / total, '%, best rmsle: ', min(featuresXGboost$rmsle)))

                        # Salvando dataframe com os resultados gerados em um arquivo .csv.

                        write.csv(
                            x         = featuresXGboost,                        # Determinando o conjunto de dados a ser 
                                                                                # salvo.
                            file      = "/content/outputs/featuresXGboost.csv", # Define o nome com o qual o conjunto de
                                                                                # dados deve ser salvo.
                            row.names = FALSE                                   # Indica que o nome das linhas não deve ser
                                                                                # gravado no arquivo a ser salvo.
                        )
                    }
                }
            }
        }
    }

    # Retorna o dataframe com os resultados obtidos pelo treinamento de cada modelo.
    
    featuresXGboost
}

5.8 Criando modelo XGboost

O algoritmo XGboost tem a capacidade de lidar com valores NA e por isso não vamos transformar os dados para tratar estas aparições.

Dito isto, podemos criar nosso modelo.

In [ ]:
# Gerando diferentes modelos baseados no algoritmo XGboost e determinando os scores para a métrica RMSLE de cada um.

featuresXGboost <- getBetterXGboostParameters(
    data        = as.matrix(train %>% select(- Demanda_uni_equil)), 
    label       = train$Demanda_uni_equil, 
    maxDepth    = 12:14, 
    nEta        = 0.2, 
    nRounds     = 85:87, 
    subsample   = 0.85,
    colsample   = 0.7, 
    statusPrint = F
)
In [ ]:
# Salvando dataframe com os resultados gerados em um arquivo .csv.

fwrite(featuresXGboost, '/content/outputs/featuresXGboost.csv')

Caso deseje pular a execução do bloco de código anterior, basta carregar os resultados já processados que estão salvos no arquivo CSV abaixo:

In [ ]:
# Carregando dataframe com os resultados obtidos para cada modelo XGboost criado.

featuresXGboost <- fread('/content/outputs/featuresXGboost.csv')

Imprimiremos os registros dos modelos criados.

In [115]:
# Visualizando dataframe com os resultados obtidos no treinamento.

featuresXGboost
A data.table: 9 × 6
maxDepthetanRoundsscrmsle
<int><dbl><int><dbl><dbl><dbl>
120.2850.850.70.1856601
120.2860.850.70.1856144
120.2870.850.70.1855794
130.2850.850.70.1831419
130.2860.850.70.1830719
130.2870.850.70.1830108
140.2850.850.70.1793361
140.2860.850.70.1792510
140.2870.850.70.1791681

Após utilizar cada uma das configurações acima para realizar as previsões dos dados de teste, observamos que aquela que obteve o melhor resultado está descrita na linha 5.

Os modelos registrados após está linha apresentam desempenhos inferiores pois começam a apresentar overfitting.

In [116]:
# Visualizando a melhor configuração para realizar as previsões dos dados de teste.

bestXGboost <- featuresXGboost[5, ]

bestXGboost
A data.table: 1 × 6
maxDepthetanRoundsscrmsle
<int><dbl><int><dbl><dbl><dbl>
130.2860.850.70.1830719
In [ ]:
# Convertendo os dados das variáveis do dataset para o tipo DMatrix (uma matriz densa).

dTrain <- xgb.DMatrix(
    data  = as.matrix(train %>% select(- Demanda_uni_equil)), # Define as variáveis preditoras.
    label = train$Demanda_uni_equil                           # Define a variável a ser prevista.
)
In [ ]:
# Define um seed para permitir que o mesmo resultado do experimento seja reproduzível.

set.seed(100)

# Criando o modelo baseado no algoritmo XGboost.

model_xgb <-  xgb.train(
    params = list(  
        objective        = "reg:linear",         # Define que o modelo deve ser baseado em uma regressão logistica linear.
        booster          = "gbtree",             # Definindo o booster a ser utilizado.
        eta              = bestXGboost$eta,      # Define a taxa de aprendizado do modelo.
        max_depth        = bestXGboost$maxDepth, # Define o tamanho máximo da árvore.
        subsample        = bestXGboost$s,        # Define a proporção de subamostra da instância de treinamento.
        colsample_bytree = bestXGboost$c         # Define a proporção da subamostra de colunas ao construir cada árvore.
    ),
    data          = dTrain,                      # Define as variáveis preditoras e a variável a ser prevista.
    feval         = rmsle,                       # Define a função de avaliação a ser utilizada.
    nrounds       = bestXGboost$nRounds,         # Define o número de iterações que o algoritmo deve executar.
    verbose       = T,                           # Define a exibição da queda da taxa de erro durante o treinamento.
    print_every_n = 5,                           # Define o número de iterações que devem ocorrer para que a impressão 
                                                 # da mensagem de avaliação seja efetuada.
    maximize      = FALSE,                       # Define que a pontuação da avaliação deve ser minimizada.
    nthread       = 16                           # Define o número de threads que devem ser usadas. Quanto maior for esse
                                                 # número, mais rápido será o treinamento.
)
In [ ]:
# Realizando as previsões com o modelo baseado no algoritmo XGboost.

pred <- predict(model_xgb, as.matrix(test %>% select(- id)))
In [ ]:
# Convertendo os resultados previsto para a escala original da variável alvo (exp(Demanda_uni_equil) - 1).

pred <- expm1(pred)

# Transformando qualquer previsão negativa em um valor nulo.

pred[pred < 0] <- 0
In [ ]:
# Salvando os resultados gerados em um arquivo CSV. 

write.csv(
    x         = data.frame(id = as.integer(test$id), Demanda_uni_equil = pred),  # Determinando o conjunto de dados a ser
                                                                                 # salvo.
    file      = "/content/outputs/results.csv",                                  # Define o nome com o qual o conjunto de 
                                                                                 # dados deve ser salvo.
    row.names = FALSE                                                            # Indica que o nome das linhas não deve 
                                                                                 # ser gravado no arquivo a ser salvo.
)

Por fim, obtemos as seguintes pontuações com a métrica RMSLE no Kaggle ao submeter as previsões geradas pelo nosso modelo para os dados de teste:

  • Pontuação privada: 0.46821
  • Pontuação pública: 0.45352

Entre em contato comigo!

Caso tenha alguma dúvida, sugestão ou apenas queira trocar uma ideia sobre este projeto, não hesite em entrar em contato comigo!