challenge_v2

Carregando os datasets, primeira visualização e descrição dos dados

Code
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno
import requests
import warnings
warnings.filterwarnings('ignore')
Code
# Carregando os datasets
df_mercado = pd.read_excel('mercado-desafio.xlsx')
df_transacoes = pd.read_excel('transações-desafio.xlsx')
Code
display(df_mercado.head())
df_transacoes.head()
Date Company Origin_city Origin_state Destination_city Destination_state Product Price CBOT
0 2024-01-30 Polaris Abelardo Luz SC Joaçaba SC Soja 114.231354 1260.025702
1 2024-01-30 Polaris Alegrete RS Rio Grande RS Soja 118.031576 1241.320557
2 2024-01-30 Polaris Alta Floresta MT Barcarena PA Milho 31.075042 501.491344
3 2024-01-30 Polaris Alta Floresta MT Barcarena PA Soja 94.684088 1173.122729
4 2024-01-30 Polaris Alta Floresta MT Santos SP Milho 23.563284 419.767221
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state
0 2024-02-28 11:44:47 Polaris 100000001 200000001 99.045785 569.703694 Soja Uberlândia MG
1 2024-06-13 14:01:54 Polaris 100000002 200000002 132.086256 4859.382786 Soja Rondonópolis MT
2 2024-06-13 14:12:18 Polaris 100000003 200000002 135.136955 15571.087533 Soja Rondonópolis MT
3 2024-06-26 14:27:55 Polaris 100000004 200000003 138.452293 10029.322768 Soja Itiquira MT
4 2024-06-26 14:30:48 Polaris 100000003 200000002 145.808870 19155.138463 Soja Rondonópolis MT

Separando os dados de treino e teste desde já para ambas as bases de dados, podemos evitar vazamento de dados e garantir que o modelo seja treinado e testado de forma correta.

Code
df_mercado_teste = df_mercado[df_mercado['Date'] >= '2024-11-04']
display(df_mercado_teste.tail())
display(df_mercado_teste.shape)


df_mercado = df_mercado[df_mercado['Date'] < '2024-11-04']
display(df_mercado.tail())
display(df_mercado.shape)

df_transacoes_teste = df_transacoes[df_transacoes['Date'] >= '2024-11-04']
display(df_transacoes_teste.tail())
display(df_transacoes_teste.shape)

df_transacoes = df_transacoes[df_transacoes['Date'] < '2024-11-04']
display(df_transacoes.tail())
display(df_transacoes.shape)
Date Company Origin_city Origin_state Destination_city Destination_state Product Price CBOT
307641 2024-11-05 Solara Sinop MT Sinop MT Milho 42.180754 450.889315
307642 2024-11-05 Solara Tangará da Serra MT Matupá MT Soja 101.448044 1012.835732
307643 2024-11-05 Solara Vicentinópolis GO Alto Araguaia MT Milho 46.357904 404.817656
307644 2024-11-05 Solara Vicentinópolis GO Primavera do Leste MT Milho 60.947772 407.502802
307645 2024-11-05 Solara Vicentinópolis GO Água Boa MT Milho 55.576690 397.344017
(3310, 9)
Date Company Origin_city Origin_state Destination_city Destination_state Product Price CBOT
308464 2024-07-03 Solara Boa Esperança do Sul SP Sorriso MT Milho 32.922475 437.848752
308465 2024-07-03 Solara Boa Esperança do Sul SP Sorriso MT Soja 107.164837 1214.925647
308466 2024-07-03 Solara Lucas do Rio Verde MT Alta Floresta MT Milho 39.081253 376.084426
308467 2024-07-03 Solara Lucas do Rio Verde MT Alta Floresta MT Soja 109.361530 1213.899519
308468 2024-07-03 Solara Sorriso MT Sorriso MT Milho 31.889917 443.184324
(305157, 9)
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state
9176 2024-11-04 10:28:03 Polaris 100001343 200000170 58.948311 18257.265904 Milho Santa Rita do Trivelato MT
9328 2024-11-04 15:10:16 Polaris 100002402 200000184 115.212700 9788.617441 Soja Balsas MA
9387 2024-11-04 12:14:51 Lunarix 100001098 200000020 121.846881 924.004785 Soja Barcarena PA
9428 2024-11-04 11:26:35 Lunarix 100001362 200000265 125.121518 4538.786917 Soja Santos SP
9498 2024-11-04 11:46:41 Polaris 100002426 200000148 130.503032 9584.640097 Soja Campo Grande MS
(18, 10)
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state
9508 2024-10-31 11:25:44 Polaris 100002391 200000148 126.603868 10956.428539 Soja Campo Grande MS
9509 2024-10-31 10:58:30 Polaris 100002391 200000148 121.091274 9097.242207 Soja Campo Grande MS
9510 2024-10-22 11:56:12 Polaris 100000898 200000151 110.655079 4920.229226 Soja Balsas MA
9511 2024-10-15 14:54:45 Polaris 100000898 200000151 97.880818 4839.009249 Soja Balsas MA
9512 2024-10-24 12:44:03 Polaris 100002428 200000151 99.734466 10342.657887 Soja Balsas MA
(9495, 10)
Code
df_transacoes_teste.isna().sum()
Date            0
Time            0
Company         0
Seller ID       0
Buyer ID        0
Price           0
Amount          0
Product         0
origin_city     0
origin_state    0
dtype: int64
Code
df_mercado_teste.isna().sum()
Date                 0
Company              0
Origin_city          0
Origin_state         0
Destination_city     0
Destination_state    0
Product              0
Price                0
CBOT                 0
dtype: int64

Como é percebido, não precisaremos imputar dados em valores de teste, entao podemos prosseguir com a análise com todos os dados, atentando para não vazar dados de treino para teste.

Code
df_mercado = pd.concat([df_mercado, df_mercado_teste])
df_transacoes = pd.concat([df_transacoes, df_transacoes_teste])

display(df_mercado.shape, df_transacoes.shape)
(308467, 9)
(9513, 10)
Code
display(df_mercado.info())
df_transacoes.info()
<class 'pandas.core.frame.DataFrame'>
Index: 308467 entries, 0 to 307645
Data columns (total 9 columns):
 #   Column             Non-Null Count   Dtype         
---  ------             --------------   -----         
 0   Date               308467 non-null  datetime64[ns]
 1   Company            308467 non-null  object        
 2   Origin_city        308464 non-null  object        
 3   Origin_state       308465 non-null  object        
 4   Destination_city   308464 non-null  object        
 5   Destination_state  308466 non-null  object        
 6   Product            308467 non-null  object        
 7   Price              308462 non-null  float64       
 8   CBOT               308464 non-null  float64       
dtypes: datetime64[ns](1), float64(2), object(6)
memory usage: 23.5+ MB
None
<class 'pandas.core.frame.DataFrame'>
Index: 9513 entries, 0 to 9498
Data columns (total 10 columns):
 #   Column        Non-Null Count  Dtype         
---  ------        --------------  -----         
 0   Date          9513 non-null   datetime64[ns]
 1   Time          9513 non-null   object        
 2   Company       9513 non-null   object        
 3   Seller ID     9513 non-null   int64         
 4   Buyer ID      9513 non-null   int64         
 5   Price         9513 non-null   float64       
 6   Amount        9512 non-null   float64       
 7   Product       9513 non-null   object        
 8   origin_city   9511 non-null   object        
 9   origin_state  9513 non-null   object        
dtypes: datetime64[ns](1), float64(2), int64(2), object(5)
memory usage: 817.5+ KB

Iremos inicialmente fazer uma rápida conversao nos dados do dataframe de transações, uma vez que o mesmo possui Seller e Buyer ID como valores numéricos, o que não é o ideal para a análise que pretendemos fazer.

Junto a isso, transformaremos dados object em categorical para termos maior performance.

Também já iremos adicionar os dados de dólar para ambos os datasets, uma vez que a cotação do dólar é um fator importante para a análise.

E uniremos as colunas date e time and date-time, uma vez que ambas são formatos de data, além de ranquear os valores de data em ordem crescente.

Code
# Função para buscar dados do dólar
def get_dollar_data(start_date, end_date):
    url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.10813/dados?formato=json&dataInicial={start_date}&dataFinal={end_date}"
    response = requests.get(url)
    data = response.json()
    df_dollar = pd.DataFrame(data)
    df_dollar['data'] = pd.to_datetime(df_dollar['data'], format='%d/%m/%Y')
    df_dollar['Dolar'] = df_dollar['valor'].astype(float)
    return df_dollar

# Buscando os dados do dólar
df_dollar = get_dollar_data('01/01/2024', '06/11/2024')
df_dollar['data'] = pd.to_datetime(df_dollar['data'], format='%d/%m/%Y')
df_dollar.drop('valor', axis=1, inplace=True)
display(df_dollar.head())
data Dolar
0 2024-01-02 4.8910
1 2024-01-03 4.9206
2 2024-01-04 4.9182
3 2024-01-05 4.8893
4 2024-01-08 4.8844
Code
# Convertendo seller e buyer ID para category
df_transacoes['Seller ID'] = df_transacoes['Seller ID'].astype('category')
df_transacoes['Buyer ID'] = df_transacoes['Buyer ID'].astype('category')

# Convertendo categorical objects para df_transacoes
for col in df_transacoes.select_dtypes(include='object').columns:
    df_transacoes[col] = df_transacoes[col].astype('category')

# Convertendo categorical objects para df_mercado
for col in df_mercado.select_dtypes(include='object').columns:
    df_mercado[col] = df_mercado[col].astype('category')

# Combinando colunas de data e hora em uma única coluna de data-hora em df_transacoes
df_transacoes['date-time'] = pd.to_datetime(df_transacoes['Date'].astype(str) + ' ' + df_transacoes['Time'].astype(str))

#Adicionando a coluna de dólar aos dataframes
df_mercado = df_mercado.merge(df_dollar, left_on='Date', right_on='data', how='left')
df_transacoes = df_transacoes.merge(df_dollar, left_on='Date', right_on='data', how='left')

# Ranqueando os valores de data em ordem crescente
df_mercado.sort_values('Date', inplace=True)
df_transacoes.sort_values('date-time', inplace=True)

Removendo colunas redundantes

Code
display(df_mercado.columns)
display(df_transacoes.columns)
Index(['Date', 'Company', 'Origin_city', 'Origin_state', 'Destination_city',
       'Destination_state', 'Product', 'Price', 'CBOT', 'data', 'Dolar'],
      dtype='object')
Index(['Date', 'Time', 'Company', 'Seller ID', 'Buyer ID', 'Price', 'Amount',
       'Product', 'origin_city', 'origin_state', 'date-time', 'data', 'Dolar'],
      dtype='object')
Code
df_mercado.drop('data', axis=1, inplace=True)
df_transacoes.drop('data', axis=1, inplace=True)
Code
display(df_mercado.info())
display(df_transacoes.info())
<class 'pandas.core.frame.DataFrame'>
Index: 308467 entries, 0 to 308466
Data columns (total 10 columns):
 #   Column             Non-Null Count   Dtype         
---  ------             --------------   -----         
 0   Date               308467 non-null  datetime64[ns]
 1   Company            308467 non-null  category      
 2   Origin_city        308464 non-null  category      
 3   Origin_state       308465 non-null  category      
 4   Destination_city   308464 non-null  category      
 5   Destination_state  308466 non-null  category      
 6   Product            308467 non-null  category      
 7   Price              308462 non-null  float64       
 8   CBOT               308464 non-null  float64       
 9   Dolar              307600 non-null  float64       
dtypes: category(6), datetime64[ns](1), float64(3)
memory usage: 13.9 MB
None
<class 'pandas.core.frame.DataFrame'>
Index: 9513 entries, 3318 to 9507
Data columns (total 12 columns):
 #   Column        Non-Null Count  Dtype         
---  ------        --------------  -----         
 0   Date          9513 non-null   datetime64[ns]
 1   Time          9513 non-null   category      
 2   Company       9513 non-null   category      
 3   Seller ID     9513 non-null   category      
 4   Buyer ID      9513 non-null   category      
 5   Price         9513 non-null   float64       
 6   Amount        9512 non-null   float64       
 7   Product       9513 non-null   category      
 8   origin_city   9511 non-null   category      
 9   origin_state  9513 non-null   category      
 10  date-time     9513 non-null   datetime64[ns]
 11  Dolar         9512 non-null   float64       
dtypes: category(7), datetime64[ns](2), float64(3)
memory usage: 969.3 KB
None
Code
display(df_mercado.head())
display(df_transacoes.head())
Date Company Origin_city Origin_state Destination_city Destination_state Product Price CBOT Dolar
0 2024-01-30 Polaris Abelardo Luz SC Joaçaba SC Soja 114.231354 1260.025702 4.9632
290100 2024-01-30 Celestix Marialva PR Ponta Grossa PR Soja 109.867495 1238.676890 4.9632
290101 2024-01-30 Celestix Primeiro de Maio PR Ponta Grossa PR Soja 103.027031 1108.410995 4.9632
290102 2024-01-30 Celestix Sertanópolis PR Ponta Grossa PR Soja 110.006327 1135.565386 4.9632
290103 2024-01-30 Celestix Toledo PR Paranaguá PR Milho 52.468228 483.132550 4.9632
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state date-time Dolar
3318 2024-01-02 13:11:05 Polaris 100000864 200000001 135.227337 936.856515 Soja Uberlândia MG 2024-01-02 13:11:05 4.8910
3319 2024-01-03 11:37:41 Polaris 100000865 200000027 137.791300 107351.263706 Soja Rondonópolis MT 2024-01-03 11:37:41 4.9206
447 2024-01-03 12:15:56 Lunarix 100000094 200000011 107.246483 2009.361816 Soja Porto Velho RO 2024-01-03 12:15:56 4.9206
1301 2024-01-03 13:26:05 Lunarix 100000314 200000009 112.163921 962.326195 Soja Boa Vista RR 2024-01-03 13:26:05 4.9206
3320 2024-01-03 14:24:58 Polaris 100000866 200000133 127.947271 1353.466033 Soja Rondonópolis MT 2024-01-03 14:24:58 4.9206

Agora que os dados não apresentam estrutura de dados incorretas, iremos inicialmente dar uma olhada no resumo dos dados.

Code
display(df_mercado.select_dtypes(include='category').describe())
display(df_mercado.select_dtypes(exclude='category').describe())
Company Origin_city Origin_state Destination_city Destination_state Product
count 308467 308464 308465 308464 308466 308467
unique 4 538 19 66 16 2
top Polaris Sorriso MT Santos SP Soja
freq 161680 3544 125083 82741 82958 197305
Date Price CBOT Dolar
count 308467 308462.000000 308464.000000 307600.000000
mean 2024-06-28 01:48:47.048922880 85.619782 867.405537 5.352366
min 2024-01-30 00:00:00 0.078774 331.276369 4.929700
25% 2024-04-15 00:00:00 43.866456 455.819876 5.075900
50% 2024-07-09 00:00:00 100.598188 1024.532517 5.441000
75% 2024-09-11 00:00:00 114.428814 1135.067290 5.581300
max 2024-11-05 00:00:00 158.409209 1374.068160 5.806700
std NaN 36.111364 334.027225 0.269215

Sobre o resumo dos dados da tabela de mercado

  • Ao que parece, há 4 companhias fornecedoras de grãos.
  • Os dados de mercado de MT e SP (estados) foram os mais presentes.
  • Ainda não é possível dizer com precisão, mas parece haver um outlier no preço dos grãos, uma vez que há uma mínima de 0.07.
  • Os dados de mercado começam em 1 de janeiro de 2024 e se extendem até 11 de maio de 2024.
Code
display(df_transacoes.select_dtypes(include='category').describe())
display(df_transacoes.select_dtypes(exclude='category').describe())
Time Company Seller ID Buyer ID Product origin_city origin_state
count 9513 9513 9513 9513 9513 9511 9513
unique 7507 4 2428 268 2 225 15
top 15:05:35 Polaris 100000011 200000011 Soja Porto Velho RO
freq 6 5308 174 1422 8750 1761 2015
Date Price Amount date-time Dolar
count 9513 9513.000000 9512.000000 9513 9512.000000
mean 2024-06-09 09:06:08.968779520 114.097366 10675.543855 2024-06-09 22:13:23.929990656 5.313521
min 2024-01-02 00:00:00 6.844928 0.000000 2024-01-02 13:11:05 4.853700
25% 2024-04-09 00:00:00 108.417159 1479.214086 2024-04-09 14:36:53 5.060400
50% 2024-05-24 00:00:00 117.688357 3645.921404 2024-05-24 15:27:48 5.250600
75% 2024-08-06 00:00:00 127.310136 9571.714745 2024-08-06 11:54:02 5.580100
max 2024-11-04 00:00:00 166.048427 485800.495674 2024-11-04 15:47:34 5.806700
std NaN 23.864692 25708.589974 NaN 0.275208

Sobre o resumo dos dados da tabela de transações

  • Novamente, podemos perceber que a fornecedora de grãos que mais se mostrou presente é a Polaris.
  • O vendedor que mais fez transações no período foi o vendedor cujo ID é 100000011. Essa informação pode vir a ser bastante relevante, uma vez que o objetivo da atual análise é prever os vendedores com maior probabilidade de realizar uma transação no dia 04/11/2024.
  • De todas as transações, o comprador 2000000011 foi o que se apresentou como sendo o que mais comprou no período, com 1422 (o que equivale a uma boa porcentagem, se comparado ao total de transações, que é de 9513).
  • A Soja é o grão mais comercializado, com boa margem, representando quase que a completude dos dados de transações.
  • As cidades e estado de origem Porto Velho e RO se apresentaram como muito relevantes em transações, o que pode indicar que sejam potenciais compradores ou vendedores.
  • Ao que parece, houve uma transação que aparenta ser um outlier, uma vez que o valor zerado não parece fazer sentido (Não temos dados de transações eventualmente canceladas)

Uma vez realizada essa análise exploratória inicial, iremos agora realizar averiguar por dados inconsistentes (outliers, valores nulos e ou duplicados), de modo a lidar com eles de maneira adequada.

Limpeza dos dados

Tratamento dos valores nulos na tabela de mercado

Inicialmente, iremos averiguar por dados nulos.

Code
# Verificando dados nulos no dataset df_mercado
nulls_mercado = df_mercado.isnull().sum()
print("Dados nulos em df_mercado:")
print(nulls_mercado)

# Verificando dados nulos no dataset df_transacoes
nulls_transacoes = df_transacoes.isnull().sum()
print("Dados nulos em df_transacoes:")
print(nulls_transacoes)
Dados nulos em df_mercado:
Date                   0
Company                0
Origin_city            3
Origin_state           2
Destination_city       3
Destination_state      1
Product                0
Price                  5
CBOT                   3
Dolar                867
dtype: int64
Dados nulos em df_transacoes:
Date            0
Time            0
Company         0
Seller ID       0
Buyer ID        0
Price           0
Amount          1
Product         0
origin_city     2
origin_state    0
date-time       0
Dolar           1
dtype: int64

Uma vez que foram identificados dados nulos, vamos fazer uma análise mais aprofundada para entender o motivo de tais valores nulos.

Code
# Filtrando df_mercado onde há valores nulos
df_mercado_nulos = df_mercado[df_mercado.isnull().any(axis=1)]
display(df_mercado_nulos)
# Analisando os valores nulos com a matriz do missingno
msno.matrix(df_mercado)
plt.show()

# Filtrando df_transacoes onde há valores nulos
df_transacoes_nulos = df_transacoes[df_transacoes.isnull().any(axis=1)]
display(df_transacoes_nulos)

msno.matrix(df_transacoes)
plt.show()
Date Company Origin_city Origin_state Destination_city Destination_state Product Price CBOT Dolar
212621 2024-02-12 Celestix Feliz Natal MT Alto Araguaia MT Soja 88.232731 1165.839655 NaN
212637 2024-02-12 Celestix Pontes e Lacerda MT Rondonópolis MT Milho 28.181567 440.703643 NaN
212636 2024-02-12 Celestix Ponta Grossa PR Paranaguá PR Milho 54.869316 415.379391 NaN
212635 2024-02-12 Celestix Paranatinga MT Alto Araguaia MT Soja 102.213627 1260.847684 NaN
212632 2024-02-12 Celestix Marialva PR Ponta Grossa PR Soja 109.192229 1197.322407 NaN
... ... ... ... ... ... ... ... ... ... ...
305138 2024-06-25 Solara Boa Esperança do Sul SP Sorriso MT Milho NaN 445.064312 5.4283
305143 2024-06-25 Solara Primavera do Leste MT Sorriso MT Milho 39.269856 NaN 5.4283
305144 2024-06-25 Solara Sorriso MT Sorriso MT Soja NaN 1024.359826 5.4283
305147 2024-06-26 Solara Lucas do Rio Verde MT NaN MT Soja 110.722521 1084.896351 5.5091
305145 2024-06-26 Solara Boa Esperança do Sul NaN Sorriso MT Milho 32.648673 427.025622 5.5091

883 rows × 10 columns

Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state date-time Dolar
4817 2024-04-22 14:31:21 Polaris 100001319 200000176 103.578445 9148.845615 Soja NaN RS 2024-04-22 14:31:21 5.2037
1158 2024-05-30 10:30:07 Lunarix 100000275 200000011 121.605271 4304.842664 Soja Porto Velho RO 2024-05-30 10:30:07 NaN
5962 2024-06-13 14:19:49 Polaris 100001543 200000142 112.140439 1945.221917 Soja NaN RO 2024-06-13 14:19:49 5.3968
6432 2024-07-02 09:56:06 Polaris 100001054 200000138 126.847615 NaN Soja Capinópolis MG 2024-07-02 09:56:06 5.6671

À primeira vista, não parece existir um padrão exato para os valores nulos para os dados da tabela de transações, o que pode indicar que os mesmos não são fruto de um erro de preenchimento, mas sim de uma falta de informação.

No entanto, é perceptível que algum padrão para os valores nulos na tabela de mercado, principalmente nos dias 2024-02-23, 2024-05-29 e 2024-06-25 - 2024-06-26.

Inicialmente, faremos um forward fill para valores de dolar.

Code
# Fazendo forward fill para ambas as tabelas na coluna de dolar
df_mercado['Dolar'].fillna(method='ffill', inplace=True)
df_transacoes['Dolar'].fillna(method='ffill', inplace=True)
Code
display(df_mercado.isna().sum())
display(df_transacoes.isna().sum())
Date                 0
Company              0
Origin_city          3
Origin_state         2
Destination_city     3
Destination_state    1
Product              0
Price                5
CBOT                 3
Dolar                0
dtype: int64
Date            0
Time            0
Company         0
Seller ID       0
Buyer ID        0
Price           0
Amount          1
Product         0
origin_city     2
origin_state    0
date-time       0
Dolar           0
dtype: int64

Agora, como restou um valor muito baixo de valores nulos, podemos dropá-los, uma vez que não haverá grande impacto.

Code
# Removendo dados nulos de df_mercado
df_mercado.dropna(inplace=True)

# Removendo dados nulos de df_transacoes
df_transacoes.dropna(inplace=True)

# Verificando se ainda há dados nulos em df_mercado
nulls_mercado = df_mercado.isnull().sum()
print("Dados nulos em df_mercado após remoção:")
print(nulls_mercado)

# Verificando se ainda há dados nulos em df_transacoes
nulls_transacoes = df_transacoes.isnull().sum()
print("Dados nulos em df_transacoes após remoção:")
print(nulls_transacoes)
Dados nulos em df_mercado após remoção:
Date                 0
Company              0
Origin_city          0
Origin_state         0
Destination_city     0
Destination_state    0
Product              0
Price                0
CBOT                 0
Dolar                0
dtype: int64
Dados nulos em df_transacoes após remoção:
Date            0
Time            0
Company         0
Seller ID       0
Buyer ID        0
Price           0
Amount          0
Product         0
origin_city     0
origin_state    0
date-time       0
Dolar           0
dtype: int64

Uma vez removidos os dados nulos, iremos agora observar um pouco melhor por outliers.

Para isso, iremos visualizar a distribuição das variaveis numéricas para ambas tabelas.

Tratamento de outliers

Code
# Selecionando apenas as variáveis numéricas de cada dataset
numeric_cols_mercado = df_mercado.select_dtypes(include=['float64', 'int64']).columns
numeric_cols_transacoes = df_transacoes.select_dtypes(include=['float64', 'int64']).columns

# Número de variáveis numéricas em cada dataset
num_numeric_cols_mercado = len(numeric_cols_mercado)
num_numeric_cols_transacoes = len(numeric_cols_transacoes)

# Configurando o estilo do Seaborn
sns.set(style="whitegrid")

# Criando subplots para df_mercado
fig, axes = plt.subplots(num_numeric_cols_mercado, 3, figsize=(15, 5 * num_numeric_cols_mercado))
fig.suptitle('Distribuição das Variáveis Numéricas - df_mercado', fontsize=16)

for i, col in enumerate(numeric_cols_mercado):
    sns.histplot(df_mercado[col], kde=True, ax=axes[i, 0], color='skyblue')
    axes[i, 0].set_title(f'Distribuição com KDE - {col}')
    
    sns.boxplot(x=df_mercado[col], ax=axes[i, 1], color='lightgreen')
    axes[i, 1].set_title(f'Boxplot - {col}')
    
    sns.violinplot(x=df_mercado[col], ax=axes[i, 2], color='lightcoral')
    axes[i, 2].set_title(f'Violin Plot - {col}')

plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.show()

# Criando subplots para df_transacoes
fig, axes = plt.subplots(num_numeric_cols_transacoes, 3, figsize=(15, 5 * num_numeric_cols_transacoes))
fig.suptitle('Distribuição das Variáveis Numéricas - df_transacoes', fontsize=16)

for i, col in enumerate(numeric_cols_transacoes):
    sns.histplot(df_transacoes[col], kde=True, ax=axes[i, 0], color='skyblue')
    axes[i, 0].set_title(f'Distribuição com KDE - {col}')
    
    sns.boxplot(x=df_transacoes[col], ax=axes[i, 1], color='lightgreen')
    axes[i, 1].set_title(f'Boxplot - {col}')
    
    sns.violinplot(x=df_transacoes[col], ax=axes[i, 2], color='lightcoral')
    axes[i, 2].set_title(f'Violin Plot - {col}')

plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.show()

Ao que tange o número de outliers, o gráfico de mercado nao apresenta outliers nem distribuições incomuns.

No entanto, para os dados de transações foram observados alguns potenciais problemas: - Amount com muitos valores centrados próximos de 0 e outliers para a direita na distribuição. Faz sentido existir tantos valores próximos de 0? - Price mostra uma distribuição similar ao gráfico de mercado, no entanto também foram observados outliers em ambas as caudas.

Code
def detect_outliers_iqr(df, column):
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    return outliers

# Detectando outliers para as variáveis 'Price' e 'Amount'
outliers_price = detect_outliers_iqr(df_transacoes, 'Price')
outliers_amount = detect_outliers_iqr(df_transacoes, 'Amount')

# Mostrando os outliers detectados
display(outliers_price)
display(outliers_amount)
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state date-time Dolar
2043 2024-01-15 14:21:17 Solara 100000526 200000038 54.681622 56717.940367 Milho Caarapó MS 2024-01-15 14:21:17 4.8759
2044 2024-01-17 11:32:37 Solara 100000527 200000039 63.023598 52969.781954 Milho Castro PR 2024-01-17 11:32:37 4.9340
2046 2024-01-22 12:13:30 Solara 100000529 200000041 51.422882 10117.616126 Milho Uberlândia MG 2024-01-22 12:13:30 4.9484
2047 2024-01-23 11:21:56 Solara 100000530 200000042 55.020528 6391.805907 Milho Uberlândia MG 2024-01-23 11:21:56 4.9709
2048 2024-01-31 11:17:02 Solara 100000531 200000043 49.602382 9288.175973 Milho Uberlândia MG 2024-01-31 11:17:02 4.9529
... ... ... ... ... ... ... ... ... ... ... ... ...
9411 2024-11-01 11:18:14 Lunarix 100000770 200000265 156.013449 2987.451617 Soja Santos SP 2024-11-01 11:18:14 5.8067
9002 2024-11-01 11:26:37 Lunarix 100000068 200000007 41.684132 15053.920427 Milho Tangará da Serra MT 2024-11-01 11:26:37 5.8067
9233 2024-11-01 11:40:20 Polaris 100001657 200000195 57.301925 66.208256 Milho Sambaíba MA 2024-11-01 11:40:20 5.8067
9508 2024-11-04 10:28:03 Polaris 100001343 200000170 58.948311 18257.265904 Milho Santa Rita do Trivelato MT 2024-11-04 10:28:03 5.7892
9501 2024-11-04 12:54:55 Polaris 100001835 200000199 56.417243 4833.072191 Milho Itaituba PA 2024-11-04 12:54:55 5.7892

792 rows × 12 columns

Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state date-time Dolar
3319 2024-01-03 11:37:41 Polaris 100000865 200000027 137.791300 107351.263706 Soja Rondonópolis MT 2024-01-03 11:37:41 4.9206
2043 2024-01-15 14:21:17 Solara 100000526 200000038 54.681622 56717.940367 Milho Caarapó MS 2024-01-15 14:21:17 4.8759
3332 2024-01-16 11:37:02 Polaris 100000009 200000135 130.173796 36529.168747 Soja Joaçaba SC 2024-01-16 11:37:02 4.9032
3335 2024-01-16 14:13:16 Polaris 100000874 200000134 127.035627 35137.580644 Soja Campos Novos SC 2024-01-16 14:13:16 4.9032
2044 2024-01-17 11:32:37 Solara 100000527 200000039 63.023598 52969.781954 Milho Castro PR 2024-01-17 11:32:37 4.9340
... ... ... ... ... ... ... ... ... ... ... ... ...
8976 2024-11-01 10:35:26 Lunarix 100000037 200000018 48.711198 83106.205466 Milho Cerejeiras RO 2024-11-01 10:35:26 5.8067
9368 2024-11-01 11:22:38 Lunarix 100001098 200000020 144.931861 23079.344924 Soja Barcarena PA 2024-11-01 11:22:38 5.8067
9073 2024-11-01 14:58:52 Celestix 100000302 200000130 119.823906 58085.077972 Soja Nova Santa Helena MT 2024-11-01 14:58:52 5.8067
8971 2024-11-01 15:26:00 Lunarix 100000519 200000020 124.331593 84831.143395 Soja Barcarena PA 2024-11-01 15:26:00 5.8067
9497 2024-11-04 12:20:33 Polaris 100002332 200000199 121.641336 25427.175976 Soja Terra Nova do Norte MT 2024-11-04 12:20:33 5.7892

926 rows × 12 columns

Code
#Observando melhor os dados dos outliers

display(outliers_price.describe())
display(outliers_amount.describe())
Date Price Amount date-time Dolar
count 792 792.000000 792.000000 792 792.000000
mean 2024-07-16 08:27:16.363636224 48.773635 22772.785352 2024-07-16 21:16:00.297979648 5.466686
min 2024-01-15 00:00:00 6.844928 0.000000 2024-01-15 14:21:17 4.875900
25% 2024-06-12 00:00:00 39.275004 3335.971777 2024-06-12 14:38:57.249999872 5.362400
50% 2024-07-23 00:00:00 45.021491 9644.915552 2024-07-23 10:49:43.500000 5.519600
75% 2024-08-26 00:00:00 51.891810 21514.534745 2024-08-26 11:51:59 5.634000
max 2024-11-04 00:00:00 166.048427 442922.185198 2024-11-04 12:54:55 5.806700
std NaN 20.768480 41107.228758 NaN 0.211966
Date Price Amount date-time Dolar
count 926 926.000000 926.000000 926 926.000000
mean 2024-06-23 09:54:02.332613376 108.630023 64507.354427 2024-06-23 22:53:17.712742912 5.367029
min 2024-01-03 00:00:00 6.844928 21747.379061 2024-01-03 11:37:41 4.875900
25% 2024-04-23 06:00:00 102.784529 30188.879687 2024-04-23 20:36:44.249999872 5.135000
50% 2024-06-26 00:00:00 119.833132 45108.101467 2024-06-26 12:48:50.500000 5.440600
75% 2024-08-26 00:00:00 131.328196 75470.461394 2024-08-26 12:20:55.249999872 5.616600
max 2024-11-04 00:00:00 164.145931 485800.495674 2024-11-04 12:20:33 5.806700
std NaN 34.341409 57989.353693 NaN 0.259136

Parece não haver grandes problemas com os dados de Price e Amount para transações, pois:

  • Os valores de price parecem aceitáveis dentro de um contexto geral, mas iremos investigar um pouco melhor para os diferentes tipos de grãos. Pois pode haver forte diferença nos preços de diferentes grãos.
  • Amount com valores iguais a zero realmente parecem algo preocupante, também faremos essa investigação.
Code
df_transacoes[df_transacoes['Amount'] == 0]
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state date-time Dolar
989 2024-07-23 12:23:44 Lunarix 100000230 200000011 48.274026 0.0 Milho Porto Velho RO 2024-07-23 12:23:44 5.5801
Code
sns.histplot(data=df_transacoes, x='Price', hue='Product', multiple='stack')

plt.show()

Sobre o valor de amount igual a 0: Como há apenas um ponto com Amount equivalente a zero, não parece ser um grande problema removê-lo.

Sobre os valores de price em torno de 6, podemos reparar com facilidade que o valor do milho tende a ser mais baixo que o valor da soja, o que pode explicar a diferença de preços e um valor baixo pode ter sido referente a variações de mercado.

Mas ainda parece um pouco estranho a distribuição do valor de Amount próximo de zero, logo iremos investigar por percentil o valor de Amount.

Code
df_transacoes = df_transacoes[df_transacoes['Amount'] > 0]
Code
# Calculando os percentis
percentiles = np.percentile(df_transacoes['Amount'], np.arange(0, 101))

# Configurando o estilo do Seaborn
sns.set(style="whitegrid")

# Criando o gráfico
plt.figure(figsize=(12, 6))
sns.lineplot(x=np.arange(0, 101), y=percentiles, marker='o', color='b')

# Adicionando títulos e rótulos
plt.title('Distribuição dos Percentis de Amount', fontsize=16)
plt.xlabel('Percentil', fontsize=14)
plt.ylabel('Amount', fontsize=14)

# Adicionando ticks e valores em 45 graus a cada 5 percentis
for i in range(0, 101, 5):
    plt.text(i, percentiles[i], f'{percentiles[i]:.2f}', ha='right', va='bottom', rotation=45, fontsize=8)

# Exibindo o gráfico
plt.show()

Após avaliar os percentis (e entendo que Amount seja o número de sacas do produto), parecem haver valores condizentes (por exemplo, pode ser incomum, mas uma transação pode ser de apenas uma saca até um número altíssimo - principalmente se considerado grandes players)

Por esse motivo, não faremos mais nenhuma remoção de outliers e agora iremos buscar entender um pouco melhor sobre valores duplicados.

Como não foram feitas muitas mudanças nos dados para o tratamento de outliers, seria ligeiramente redundante plotar novamente as distribuições, logo iremos avançar pra próxima etapa.

Tratamento de dados duplicados

Code
# Verificando dados duplicados no dataset df_mercado
duplicated_mercado = df_mercado.duplicated()
print(f"Número de linhas duplicadas em df_mercado: {duplicated_mercado.sum()}")

# Exibindo as linhas duplicadas em df_mercado
if duplicated_mercado.sum() > 0:
    display(df_mercado[duplicated_mercado])

# Verificando dados duplicados no dataset df_transacoes
duplicated_transacoes = df_transacoes.duplicated()
print(f"Número de linhas duplicadas em df_transacoes: {duplicated_transacoes.sum()}")

# Exibindo as linhas duplicadas em df_transacoes
if duplicated_transacoes.sum() > 0:
    display(df_transacoes[duplicated_transacoes])
Número de linhas duplicadas em df_mercado: 0
Número de linhas duplicadas em df_transacoes: 0

Não foram identificados dados duplicados, dessa forma podemos seguir para a próxima etapa de descobrimento de análise exploratória dos dados.

Análise exploratória dos dados

Como já vimos anteriormente, não há mais valores nulos nos dados, logo podemos atribuir as colunas de data como index, uma vez que estamos trabalhando com data series.

Code
df_mercado.set_index('Date', inplace=True)
df_transacoes.set_index('date-time', inplace=True)

display(df_mercado.info())
display(df_transacoes.info())
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 308451 entries, 2024-01-30 to 2024-11-05
Data columns (total 9 columns):
 #   Column             Non-Null Count   Dtype   
---  ------             --------------   -----   
 0   Company            308451 non-null  category
 1   Origin_city        308451 non-null  category
 2   Origin_state       308451 non-null  category
 3   Destination_city   308451 non-null  category
 4   Destination_state  308451 non-null  category
 5   Product            308451 non-null  category
 6   Price              308451 non-null  float64 
 7   CBOT               308451 non-null  float64 
 8   Dolar              308451 non-null  float64 
dtypes: category(6), float64(3)
memory usage: 11.5 MB
None
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 9509 entries, 2024-01-02 13:11:05 to 2024-11-04 15:47:34
Data columns (total 11 columns):
 #   Column        Non-Null Count  Dtype         
---  ------        --------------  -----         
 0   Date          9509 non-null   datetime64[ns]
 1   Time          9509 non-null   category      
 2   Company       9509 non-null   category      
 3   Seller ID     9509 non-null   category      
 4   Buyer ID      9509 non-null   category      
 5   Price         9509 non-null   float64       
 6   Amount        9509 non-null   float64       
 7   Product       9509 non-null   category      
 8   origin_city   9509 non-null   category      
 9   origin_state  9509 non-null   category      
 10  Dolar         9509 non-null   float64       
dtypes: category(7), datetime64[ns](1), float64(3)
memory usage: 894.8 KB
None

Inicialmente, vamos buscar entender melhor as variáveis numéricas e posteriormente as variáveis categóricas.

Análise das variáveis numéricas

Como já foi feita análise de outliers e pudemos ver a distribuição dos dados, a primeira análise a ser feita aqui será a de correlação entre as variáveis numéricas para cada um dos dataframes.

Code
# Heatmap for df_mercado
plt.figure(figsize=(10, 8))
sns.heatmap(df_mercado.select_dtypes(include=['float64', 'int64']).corr(), annot=True, cmap='coolwarm')
plt.title('Heatmap de correlação para df_mercado')
plt.show()

# Heatmap for df_transacoes
plt.figure(figsize=(10, 8))
sns.heatmap(df_transacoes.select_dtypes(include=['float64', 'int64']).corr(), annot=True, cmap='coolwarm')
plt.title('Heatmap de correlação para df_transacoes')
plt.show()

Percebe-se uma correlação extremamente alta entre CBOT e Price para os dados de mercado. Vamos tentar entender melhor essa correlação por meio de um scatterplot.

Já para o dado de transações, nenhuma correlação foi considerada relevante.

Code
sns.scatterplot(data=df_mercado,
            x = 'Price',
            y = 'CBOT')

plt.title('Scatterplot de Price x CBOT')
plt.xlabel('Price')
plt.ylabel('CBOT')
plt.show()

Parece haver duas nuvens separadas de pontos, o que sugere dizer que essas nuvens separadas se dão pela diferença de grãos.

Code
sns.scatterplot(data=df_mercado,
            x = 'Price',
            y = 'CBOT',
            hue = 'Product',
            )

plt.title('Scatterplot de Price x CBOT')
plt.xlabel('Price')
plt.ylabel('CBOT')
plt.show()

Code
# Calculando a correlação entre Price e CBOT para cada produto
correlation_soja = df_mercado[df_mercado['Product'] == 'Soja'][['Price', 'CBOT']].corr().iloc[0, 1]
correlation_milho = df_mercado[df_mercado['Product'] == 'Milho'][['Price', 'CBOT']].corr().iloc[0, 1]

print(f"Correlação entre Price e CBOT para Soja: {correlation_soja}")
print(f"Correlação entre Price e CBOT para Milho: {correlation_milho}")
Correlação entre Price e CBOT para Soja: -0.09765353853241913
Correlação entre Price e CBOT para Milho: -0.1965302638151687

Essa acaba por ser uma correlação bastante traiçoeira, uma vez que embora a correlação global seja alta, quando separa-se entre os grãos, praticamente não há correlação. Essa informação será bastante importante para nossa análise.

Vamos explorar um pouco melhor todas as correlações por meio de scatterplots, pode ser que encontremos mais detalhes nos dados.

Code
plt.figure(figsize=(15, 9))
sns.pairplot(df_mercado, hue='Product')
plt.show()

sns.pairplot(df_transacoes, hue='Product')
plt.show()
<Figure size 1440x864 with 0 Axes>

Nenhuma correlação potencialmente relevante foi encontrada, mas pode-se perceber que para quase todas as distribuições, O milho diverge completamente da Soja, quase que formando clusters completamente diferentes.

Pode ser que seja interessante aplicar a mesma análise para as companhias. Vamos descobrir:

Code
plt.figure(figsize=(15, 9))
sns.pairplot(df_mercado, hue='Company')
plt.show()

sns.pairplot(df_transacoes, hue='Company')
plt.show()
<Figure size 1440x864 with 0 Axes>

As análises se misturam bastante, e nenhum padrão salta aos olhos.

Agora vamos tentar entender o CBOT e o PRICE médios por no mercado pelo tempo, junto do valor do dólar.

Code
# Group by 'Date' and plot each group
df_mercado.select_dtypes(include=['float64', 'int64']).plot(subplots=True, figsize=(15, 10))
array([<Axes: xlabel='Date'>, <Axes: xlabel='Date'>,
       <Axes: xlabel='Date'>], dtype=object)

O CBOT e o Price se apresentam em séries bastante instáveis, mas aparentemente estacionárias. Mas não apresenta muita informação. Vamos quebrar o CBOT e o Price por grão, muito embora não tenha sido notável a correlação entre o Price e o CBOT.

Code
# Plotando as variáveis numéricas com hue pelo 'Product'
fig, axes = plt.subplots(len(numeric_cols_mercado), 1, figsize=(15, 5 * len(numeric_cols_mercado)))

for i, col in enumerate(numeric_cols_mercado):
    sns.lineplot(data=df_mercado, x=df_mercado.index, y=col, hue='Product', ax=axes[i])
    axes[i].set_title(f'{col} ao longo do tempo por Produto')
    axes[i].set_xlabel('Data')
    axes[i].set_ylabel(col)

plt.tight_layout()
plt.show()

O gráfico acima já se mostrou mais informativo, de modo que é perceptível que as tendências do Milho e da Soja são extremamente parecidas em gráficos temporais, muito embora a correlação entre CBOT e PRICE quando comparados os grãos não tenha se mostrado alta.

Agora vamos analisar a variação do price e do amount ao longo do tempo, para tentar entender melhor a relação entre essas variáveis.

Code
# Plotando as variáveis numéricas com hue pelo 'Product'
fig, axes = plt.subplots(len(numeric_cols_transacoes), 1, figsize=(15, 5 * len(numeric_cols_transacoes)))

resampled_df_transacoes = df_transacoes.resample('D').last()

for i, col in enumerate(numeric_cols_transacoes):
    sns.lineplot(data=resampled_df_transacoes, x=resampled_df_transacoes.index, y=col, hue='Product', ax=axes[i])
    axes[i].set_title(f'{col} ao longo do tempo por Produto')
    axes[i].set_xlabel('Data')
    axes[i].set_ylabel(col)

plt.tight_layout()
plt.show()

Aparentemente, apresenta-se uma mesma tendência para o milho e a soja, no entanto foi observado um outlier extremo para a soja no mês de Maio.

Esse outlier pode ser potencialmente algum erro nos dados, mas não pode ser descartado que o mercado possa ter oscilado muito. A opção será por mantê-lo.

Caso o modelo apresente resultados ruins, podemos retornar e fazer a remoção do mesmo.

Agora vamos agrupar os dados por mês e tentar entender alguma relação potencialmente existente

Code
df_mercado_M = df_mercado.select_dtypes(exclude='category').resample('M').mean()
df_transacoes_M = df_transacoes.select_dtypes(exclude='category').resample('M').mean().iloc[:, 1:]

df_full = df_mercado_M.join(df_transacoes_M, lsuffix='_mercado', rsuffix='_transacoes')
Code
df_full.head()
Price_mercado CBOT Dolar_mercado Price_transacoes Amount Dolar_transacoes
Date
2024-01-31 74.808098 942.624095 4.958046 110.659245 6857.659970 4.929115
2024-02-29 73.922377 897.243020 4.964153 104.083740 4867.544854 4.964358
2024-03-31 81.887653 913.864918 4.979460 111.177098 6575.839736 4.976851
2024-04-30 82.073445 901.341247 5.128669 114.188471 10876.314722 5.128612
2024-05-31 87.220198 949.152019 5.134886 119.745228 10155.877420 5.120572
Code
sns.heatmap(df_full.corr(), annot = True)

  • Dólar e CBOT têm forte impacto nas variáveis relacionadas a preço e quantidade no mês.
  • Preço de mercado e preço de transações são altamente correlacionados, o que é esperado.

Avaliando as variáveis categóricas

Code
display(df_transacoes.head())
display(df_mercado.head())
Date Time Company Seller ID Buyer ID Price Amount Product origin_city origin_state Dolar
date-time
2024-01-02 13:11:05 2024-01-02 13:11:05 Polaris 100000864 200000001 135.227337 936.856515 Soja Uberlândia MG 4.8910
2024-01-03 11:37:41 2024-01-03 11:37:41 Polaris 100000865 200000027 137.791300 107351.263706 Soja Rondonópolis MT 4.9206
2024-01-03 12:15:56 2024-01-03 12:15:56 Lunarix 100000094 200000011 107.246483 2009.361816 Soja Porto Velho RO 4.9206
2024-01-03 13:26:05 2024-01-03 13:26:05 Lunarix 100000314 200000009 112.163921 962.326195 Soja Boa Vista RR 4.9206
2024-01-03 14:24:58 2024-01-03 14:24:58 Polaris 100000866 200000133 127.947271 1353.466033 Soja Rondonópolis MT 4.9206
Company Origin_city Origin_state Destination_city Destination_state Product Price CBOT Dolar
Date
2024-01-30 Polaris Abelardo Luz SC Joaçaba SC Soja 114.231354 1260.025702 4.9632
2024-01-30 Celestix Marialva PR Ponta Grossa PR Soja 109.867495 1238.676890 4.9632
2024-01-30 Celestix Primeiro de Maio PR Ponta Grossa PR Soja 103.027031 1108.410995 4.9632
2024-01-30 Celestix Sertanópolis PR Ponta Grossa PR Soja 110.006327 1135.565386 4.9632
2024-01-30 Celestix Toledo PR Paranaguá PR Milho 52.468228 483.132550 4.9632

As variáveis potencialmente interessantes para análises de variáveis categóricas são:

  • Company

  • Product

  • Origin_state

Code
# Creating crosstab for df_mercado
crosstab_mercado = pd.crosstab(index=df_mercado['Origin_state'], columns=[df_mercado['Company'], df_mercado['Product']])

# Crosstab for df_mercado
plt.figure(figsize=(15, 6))
sns.heatmap(crosstab_mercado, annot=True, fmt="d", cmap="YlGnBu", annot_kws={'size': 8})  
plt.title('Heatmap de Company e Product por Origin_state - df_mercado', fontsize=10)  
plt.xticks(fontsize=8)  
plt.yticks(fontsize=8)
plt.show()

# Creating crosstab for df_transacoes
crosstab_transacoes = pd.crosstab(index=df_transacoes['origin_state'], columns=[df_transacoes['Company'], df_transacoes['Product']])

# Crosstab for df_transacoes 
plt.figure(figsize=(15, 6))
sns.heatmap(crosstab_transacoes, annot=True, fmt="d", cmap="YlGnBu", annot_kws={'size': 8})  
plt.title('Heatmap de Company e Product por origin_state - df_transacoes', fontsize=10)  
plt.xticks(fontsize=8)  
plt.yticks(fontsize=8) 
plt.show()

MG (tem a ver com a grão direto?! haha), MT e RO se mostraram como os estado que mais tiveram exportações de soja .

Ao que parece, o MT também apresenta influencia relevante nas vendas de Milho.

A companhia Polaris também se mostrou como forte exportadora de soja, potencialmente como a principal empresa de exportação tanto da soja quanto dos grãos em geral.

O milho parece não ser muito vendido, e provavelmente a compra da soja seja mais atrativa para os compradores.

Vamos tentar entender um pouco melhor sobre os vendedores, que parece ser uma variável extremamente importante para a análise atual.

Code
df_transacoes['Seller ID'].value_counts().head(10).plot(kind='bar', figsize=(15, 6), color='skyblue')

Parecem existir vendedores que atuam muito mais fortemente no mercado, vamos entender qual a porcentagem desses top 10 vendedores é representativa do total.

Code
df_transacoes['Seller ID'].value_counts(normalize=True)[:10].sum() * 100
11.042170575244505

Os top 10 vendedores representam apenas em torno de 11% do total de vendedores. E pode ser que os mesmos possam ter maior probabilidade de realizar uma transação em 04/11 e 05/11.

Pré processamento dos dados

A partir daqui, faremos um backup dos dados e começaremos a preparar os dados para a modelagem.

Code
df_trans_pre_pros = df_transacoes.copy()
df_merc_pre_proc = df_mercado.copy()
Code
import gc

gc.collect()
181651

Inicialmente, removeremos as colunas de Time de transações, que não serão será utilizada.

A coluna Buyer ID também não será utilizada para a análise.

Como potencialmente haverá manipulações com data, iremos extrair dados de data para ambas tabelas.

Também irei alterar o nome das colunas para facilitar a manipulação dos dados.

Code
df_merc_pre_proc.rename_axis('date_index', inplace=True)
df_merc_pre_proc.rename_axis('date_index', inplace=True)

# Aplicando a coluna 'Date' ao índice
df_merc_pre_proc['date'] = df_merc_pre_proc.index

# Removendo as colunas 'Date' e 'Time'
df_trans_pre_pros.drop(columns=['Time', 'Buyer ID'], inplace=True)


df_trans_pre_pros.columns = ['date', 'company', 'seller_id', 'price', 'amount', 'product',
       'origin_city', 'origin_state', 'dolar']

df_merc_pre_proc.columns = ['company', 'origin_city', 'origin_state', 'destination_city',
       'destination_state', 'product', 'price', 'cbot', 'dolar', 'date',
       ]


display(df_trans_pre_pros.tail())
display(df_merc_pre_proc.tail())
date company seller_id price amount product origin_city origin_state dolar
date-time
2024-11-04 14:21:17 2024-11-04 Polaris 100000060 124.447569 2290.909287 Soja Porto Velho RO 5.7892
2024-11-04 15:10:16 2024-11-04 Polaris 100002402 115.212700 9788.617441 Soja Balsas MA 5.7892
2024-11-04 15:13:15 2024-11-04 Polaris 100002339 114.543597 9715.149462 Soja Sambaíba MA 5.7892
2024-11-04 15:16:05 2024-11-04 Polaris 100002339 114.041989 9691.241875 Soja Tasso Fragoso MA 5.7892
2024-11-04 15:47:34 2024-11-04 Lunarix 100000348 149.946259 989.589355 Soja Santos SP 5.7892
company origin_city origin_state destination_city destination_state product price cbot dolar date
date_index
2024-11-05 Polaris Uruaçu GO Tubarão SC Soja 122.733502 1033.708378 5.784 2024-11-05
2024-11-05 Polaris Uruaçu GO Santos SP Soja 119.679794 996.462259 5.784 2024-11-05
2024-11-05 Polaris União do Sul MT Barcarena PA Milho 49.221543 441.941709 5.784 2024-11-05
2024-11-05 Polaris Vila Bela da Santíssima Trindade MT Santos SP Milho 44.600848 415.780537 5.784 2024-11-05
2024-11-05 Solara Vicentinópolis GO Água Boa MT Milho 55.576690 397.344017 5.784 2024-11-05

Join dos dados

Neste momento, faremos o join das tabelas de mercado com as transações, de modo a termos um único dataframe para a modelagem.

Faremos Um join de muitos para muitos, que usualmente costuma ser uma “Caixa preta”.

Para o join, utilizaremos as colunas equivalentes em ambas tabelas, que são:

  • Data,
  • Product,
  • Origin_state,
  • Origin_city,
  • Company.

No entanto, precisamos posteriormente fazer um concat, para não perder os dados de mercado.

Note que precisaremos primeiramente averiguar valores duplicados na tabela de mercado.

Ademais, iremos fazer uma extra limpeza de dados, de modo a manter apenas os dados que tiveram transações, uma vez que queremos predizer potenciais vendedores, dados sem transação adicionam ruído e aumentam os recursos computacionais a serem utilizados.

Code
mask = df_merc_pre_proc.duplicated(['date', 'product', 'origin_state', 'origin_city', 'company'], keep=False)

df_merc_pre_proc[mask].sort_values(['date', 'product', 'origin_state', 'origin_city', 'company'])
company origin_city origin_state destination_city destination_state product price cbot dolar date
date_index
2024-01-30 Polaris Boa Esperança ES Santos SP Milho 25.142881 433.155449 4.9632 2024-01-30
2024-01-30 Polaris Boa Esperança ES Barcarena PA Milho 29.160256 433.693689 4.9632 2024-01-30
2024-01-30 Celestix Caiapônia GO São Simão GO Milho 41.827155 506.542431 4.9632 2024-01-30
2024-01-30 Celestix Caiapônia GO Santos SP Milho 38.948770 501.699789 4.9632 2024-01-30
2024-01-30 Celestix Chapadão do Céu GO São Simão GO Milho 42.184684 479.746676 4.9632 2024-01-30
... ... ... ... ... ... ... ... ... ... ...
2024-11-05 Polaris Santa Rosa do Tocantins TO Barcarena PA Soja 114.267406 1056.955698 5.7840 2024-11-05
2024-11-05 Polaris Santa Rosa do Tocantins TO Palmeirante TO Soja 119.764620 1070.872022 5.7840 2024-11-05
2024-11-05 Polaris Silvanópolis TO Barcarena PA Soja 106.193980 1087.471183 5.7840 2024-11-05
2024-11-05 Polaris Silvanópolis TO São Luís MA Soja 110.884395 945.922078 5.7840 2024-11-05
2024-11-05 Polaris Silvanópolis TO Palmeirante TO Soja 104.204235 1047.814036 5.7840 2024-11-05

192639 rows × 10 columns

Há duplicatas para as quebras selecionadas, e agruparemos pela mediana, afim de evitar que a média seja influenciada por outliers.

Code
df_merc_join = (
    df_merc_pre_proc
    .groupby(['date', 'product', 'origin_state', 'origin_city', 'company'])
    .agg({'price': 'median', 'cbot': 'median'}).reset_index().dropna()
)

Vamos conferir se ainda há essas duplicatas.

Code
mask = df_merc_join.duplicated(['date', 'product', 'origin_state', 'origin_city', 'company'], keep=False)

df_merc_join[mask].sort_values(['date', 'product', 'origin_state', 'origin_city', 'company'])
date product origin_state origin_city company price cbot

Sem duplicatas, podemos fazer o join.

Code
df_completo_v1  = df_trans_pre_pros.merge(
    df_merc_join,
    left_on=['date', 'product', 'origin_state', 'origin_city', 'company'],
    right_on=['date', 'product', 'origin_state', 'origin_city','company'],
    suffixes=('_trans','_merc'),
    how='left'
)
Code
display(df_completo_v1.isna().sum())

df_completo_v1.shape
date              0
company           0
seller_id         0
price_trans       0
amount            0
product           0
origin_city       0
origin_state      0
dolar             0
price_merc      846
cbot            846
dtype: int64
(9509, 11)

Averiguando nulos gerados

Code
df_completo_v1
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot
0 2024-01-02 Polaris 100000864 135.227337 936.856515 Soja Uberlândia MG 4.8910 NaN NaN
1 2024-01-03 Polaris 100000865 137.791300 107351.263706 Soja Rondonópolis MT 4.9206 NaN NaN
2 2024-01-03 Lunarix 100000094 107.246483 2009.361816 Soja Porto Velho RO 4.9206 NaN NaN
3 2024-01-03 Lunarix 100000314 112.163921 962.326195 Soja Boa Vista RR 4.9206 NaN NaN
4 2024-01-03 Polaris 100000866 127.947271 1353.466033 Soja Rondonópolis MT 4.9206 NaN NaN
... ... ... ... ... ... ... ... ... ... ... ...
9504 2024-11-04 Polaris 100000060 124.447569 2290.909287 Soja Porto Velho RO 5.7892 113.301882 950.812555
9505 2024-11-04 Polaris 100002402 115.212700 9788.617441 Soja Balsas MA 5.7892 122.683527 997.535892
9506 2024-11-04 Polaris 100002339 114.543597 9715.149462 Soja Sambaíba MA 5.7892 118.333486 1087.257588
9507 2024-11-04 Polaris 100002339 114.041989 9691.241875 Soja Tasso Fragoso MA 5.7892 111.886052 1054.265425
9508 2024-11-04 Lunarix 100000348 149.946259 989.589355 Soja Santos SP 5.7892 131.923338 1054.746493

9509 rows × 11 columns

Foram gerados valores de price_merc e cbot nulos, vamos averiguar melhor o que houve:

Code
msno.matrix(df_completo_v1)
plt.show()

Code
df_completo_v1.tail()
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot
9504 2024-11-04 Polaris 100000060 124.447569 2290.909287 Soja Porto Velho RO 5.7892 113.301882 950.812555
9505 2024-11-04 Polaris 100002402 115.212700 9788.617441 Soja Balsas MA 5.7892 122.683527 997.535892
9506 2024-11-04 Polaris 100002339 114.543597 9715.149462 Soja Sambaíba MA 5.7892 118.333486 1087.257588
9507 2024-11-04 Polaris 100002339 114.041989 9691.241875 Soja Tasso Fragoso MA 5.7892 111.886052 1054.265425
9508 2024-11-04 Lunarix 100000348 149.946259 989.589355 Soja Santos SP 5.7892 131.923338 1054.746493

Ao que indica, não há um sinal aparente do erro com os joins, portanto faremos a imputação dos valores da mediana do respectivo grão, semana, estado, cidade e companhia.

Code
# Função para preencher valores nulos com a mediana com base em múltiplos agrupamentos
def preencher_nulos_com_mediana(df, colunas_grupo, coluna_alvo):
    medianas = df.groupby(colunas_grupo)[coluna_alvo].transform('median')
    df[coluna_alvo].fillna(medianas, inplace=True)

# Definir as colunas para agrupamento
df_completo_v1['week'] = df_completo_v1['date'].dt.isocalendar().week
colunas_grupo = ['product', 'week', 'origin_city', 'company']

# Preencher valores nulos para 'price_merc' e 'cbot'
preencher_nulos_com_mediana(df_completo_v1, colunas_grupo, 'price_merc')
preencher_nulos_com_mediana(df_completo_v1, colunas_grupo, 'cbot')

# Verificar se ainda há valores nulos restantes
print(df_completo_v1.isnull().sum())
date              0
company           0
seller_id         0
price_trans       0
amount            0
product           0
origin_city       0
origin_state      0
dolar             0
price_merc      562
cbot            562
week              0
dtype: int64

Ainda permaneceram valores nulos, vamos averiguar novamente

Code
msno.matrix(df_completo_v1)
plt.show()

Code
df_completo_v1[df_completo_v1.isna().any(axis=1)]
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot week
0 2024-01-02 Polaris 100000864 135.227337 936.856515 Soja Uberlândia MG 4.8910 NaN NaN 1
1 2024-01-03 Polaris 100000865 137.791300 107351.263706 Soja Rondonópolis MT 4.9206 NaN NaN 1
2 2024-01-03 Lunarix 100000094 107.246483 2009.361816 Soja Porto Velho RO 4.9206 NaN NaN 1
3 2024-01-03 Lunarix 100000314 112.163921 962.326195 Soja Boa Vista RR 4.9206 NaN NaN 1
4 2024-01-03 Polaris 100000866 127.947271 1353.466033 Soja Rondonópolis MT 4.9206 NaN NaN 1
... ... ... ... ... ... ... ... ... ... ... ... ...
8763 2024-09-30 Polaris 100002284 49.630928 5143.850536 Milho Chapadão do Sul MS 5.4475 NaN NaN 40
8801 2024-09-30 Lunarix 100000008 135.726359 79989.267438 Soja Paranaguá PR 5.4475 NaN NaN 40
8908 2024-10-09 Lunarix 100000471 157.386287 38543.499865 Soja Paranaguá PR 5.5731 NaN NaN 41
8967 2024-10-15 Lunarix 100000470 133.314977 5350.482751 Soja Paranaguá PR 5.6372 NaN NaN 42
8977 2024-10-16 Lunarix 100000008 145.852907 54469.931031 Soja Paranaguá PR 5.6743 NaN NaN 42

562 rows × 12 columns

Ainda sem algum padrão sistemático dos valores nulos, portanto vamos usar a mediana global do grão para imputar os valores nulos.

Code
# Definir as colunas para agrupamento
colunas_grupo = ['product']

# Preencher valores nulos para 'price_merc' e 'cbot'
preencher_nulos_com_mediana(df_completo_v1, colunas_grupo, 'price_merc')
preencher_nulos_com_mediana(df_completo_v1, colunas_grupo, 'cbot')

# Verificar se ainda há valores nulos restantes
print(df_completo_v1.isnull().sum())
date            0
company         0
seller_id       0
price_trans     0
amount          0
product         0
origin_city     0
origin_state    0
dolar           0
price_merc      0
cbot            0
week            0
dtype: int64

Agora conseguimos garantir que não há mais valores nulos, e iremos neste momento adotar os dados de Mercado junto aos dados de transação com um concat.

Mas primeiro vamos eliminar os dados de mercado que já estão presentes em df_completo_v1

Code
mask2 = df_merc_pre_proc['cbot'].isin(df_completo_v1['cbot'])
df_merc_pre_proc_sem_intersecao = df_merc_pre_proc[~mask2]
df_merc_pre_proc_sem_intersecao.head()
company origin_city origin_state destination_city destination_state product price cbot dolar date
date_index
2024-01-30 Polaris Abelardo Luz SC Joaçaba SC Soja 114.231354 1260.025702 4.9632 2024-01-30
2024-01-30 Celestix Marialva PR Ponta Grossa PR Soja 109.867495 1238.676890 4.9632 2024-01-30
2024-01-30 Celestix Primeiro de Maio PR Ponta Grossa PR Soja 103.027031 1108.410995 4.9632 2024-01-30
2024-01-30 Celestix Sertanópolis PR Ponta Grossa PR Soja 110.006327 1135.565386 4.9632 2024-01-30
2024-01-30 Celestix Toledo PR Paranaguá PR Milho 52.468228 483.132550 4.9632 2024-01-30
Code
assert df_merc_pre_proc_sem_intersecao['cbot'].isin(df_completo_v1['cbot']).sum() == 0

Agora vamos criar variáveis dummies na tabela de transações final, de modo a permitir que haja o concat.

Code
display(df_completo_v1.columns)

display(df_merc_pre_proc_sem_intersecao.columns)
Index(['date', 'company', 'seller_id', 'price_trans', 'amount', 'product',
       'origin_city', 'origin_state', 'dolar', 'price_merc', 'cbot', 'week'],
      dtype='object')
Index(['company', 'origin_city', 'origin_state', 'destination_city',
       'destination_state', 'product', 'price', 'cbot', 'dolar', 'date'],
      dtype='object')

Primeiro rearranjaremos as colunas para facilitar a manipulação.

Code
df_completo_v1['destination_city'] = None
df_completo_v1['destination_state'] = None

df_merc_pre_proc_sem_intersecao['seller_id'] = None
df_merc_pre_proc_sem_intersecao['week'] = None
df_merc_pre_proc_sem_intersecao.rename(columns={'price': 'price_merc'}, inplace=True)
Code
df_completo_v2 = pd.concat([df_completo_v1, df_merc_pre_proc_sem_intersecao], axis=0, ignore_index=True)

Agora vamos adicionar duas novas variáveis

  • Semana do ano
  • Dia da semana
Code
df_completo_v2.drop('week', axis=1, inplace=True)

df_completo_v2['week_of_year'] = df_completo_v2['date'].dt.isocalendar().week
df_completo_v2['day_of_week'] = df_completo_v2['date'].dt.day_name()

df_completo_v2.sort_values('date', inplace=True)
df_completo_v2.head()
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
0 2024-01-02 Polaris 100000864 135.227337 936.856515 Soja Uberlândia MG 4.8910 115.276863 1137.705266 NaN NaN 1 Tuesday
1 2024-01-03 Polaris 100000865 137.791300 107351.263706 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
2 2024-01-03 Lunarix 100000094 107.246483 2009.361816 Soja Porto Velho RO 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
3 2024-01-03 Lunarix 100000314 112.163921 962.326195 Soja Boa Vista RR 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
4 2024-01-03 Polaris 100000866 127.947271 1353.466033 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
Code
df_completo_v2
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
0 2024-01-02 Polaris 100000864 135.227337 936.856515 Soja Uberlândia MG 4.8910 115.276863 1137.705266 NaN NaN 1 Tuesday
1 2024-01-03 Polaris 100000865 137.791300 107351.263706 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
2 2024-01-03 Lunarix 100000094 107.246483 2009.361816 Soja Porto Velho RO 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
3 2024-01-03 Lunarix 100000314 112.163921 962.326195 Soja Boa Vista RR 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
4 2024-01-03 Polaris 100000866 127.947271 1353.466033 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 NaN NaN 1 Wednesday
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
313980 2024-11-05 Polaris NaN NaN NaN Milho Paracatu MG 5.7840 60.197139 421.327700 Santos SP 45 Tuesday
313979 2024-11-05 Polaris NaN NaN NaN Milho Padre Bernardo GO 5.7840 55.376171 375.730339 Santos SP 45 Tuesday
313978 2024-11-05 Polaris NaN NaN NaN Soja Padre Bernardo GO 5.7840 126.104457 1017.881801 Cristalina GO 45 Tuesday
314048 2024-11-05 Solara NaN NaN NaN Milho Ipiranga do Norte MT 5.7840 39.319555 437.737880 Alta Floresta MT 45 Tuesday
315012 2024-11-05 Solara NaN NaN NaN Milho Vicentinópolis GO 5.7840 55.576690 397.344017 Água Boa MT 45 Tuesday

315013 rows × 15 columns

Code
df_completo_v2.info()
<class 'pandas.core.frame.DataFrame'>
Index: 315013 entries, 0 to 315012
Data columns (total 15 columns):
 #   Column             Non-Null Count   Dtype         
---  ------             --------------   -----         
 0   date               315013 non-null  datetime64[ns]
 1   company            315013 non-null  category      
 2   seller_id          9509 non-null    category      
 3   price_trans        9509 non-null    float64       
 4   amount             9509 non-null    float64       
 5   product            315013 non-null  category      
 6   origin_city        315013 non-null  object        
 7   origin_state       315013 non-null  object        
 8   dolar              315013 non-null  float64       
 9   price_merc         315013 non-null  float64       
 10  cbot               315013 non-null  float64       
 11  destination_city   305504 non-null  category      
 12  destination_state  305504 non-null  category      
 13  week_of_year       315013 non-null  UInt32        
 14  day_of_week        315013 non-null  object        
dtypes: UInt32(1), category(5), datetime64[ns](1), float64(5), object(3)
memory usage: 27.4+ MB
Code
def impute_missing_values_with_mode(df, group_cols, target_cols):
    for col in target_cols:
        mode_values = df.groupby(group_cols)[col].transform(lambda x: x.mode()[0] if not x.mode().empty else x)
        df[col].fillna(mode_values, inplace=True)

# Definindo as colunas para agrupamento e as colunas alvo
group_cols = ['company', 'product']
target_cols = ['destination_city', 'destination_state']

# Imputando os valores nulos com a moda
impute_missing_values_with_mode(df_completo_v2, group_cols, target_cols)

# Verificando se ainda há valores nulos restantes
print(df_completo_v2.isnull().sum())
date                      0
company                   0
seller_id            305504
price_trans          305504
amount               305504
product                   0
origin_city               0
origin_state              0
dolar                     0
price_merc                0
cbot                      0
destination_city          0
destination_state         0
week_of_year              0
day_of_week               0
dtype: int64
Code
df_completo_v2.info()
<class 'pandas.core.frame.DataFrame'>
Index: 315013 entries, 0 to 315012
Data columns (total 15 columns):
 #   Column             Non-Null Count   Dtype         
---  ------             --------------   -----         
 0   date               315013 non-null  datetime64[ns]
 1   company            315013 non-null  category      
 2   seller_id          9509 non-null    category      
 3   price_trans        9509 non-null    float64       
 4   amount             9509 non-null    float64       
 5   product            315013 non-null  category      
 6   origin_city        315013 non-null  object        
 7   origin_state       315013 non-null  object        
 8   dolar              315013 non-null  float64       
 9   price_merc         315013 non-null  float64       
 10  cbot               315013 non-null  float64       
 11  destination_city   315013 non-null  category      
 12  destination_state  315013 non-null  category      
 13  week_of_year       315013 non-null  UInt32        
 14  day_of_week        315013 non-null  object        
dtypes: UInt32(1), category(5), datetime64[ns](1), float64(5), object(3)
memory usage: 27.4+ MB
Code
df_completo_v2.tail()
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
313980 2024-11-05 Polaris NaN NaN NaN Milho Paracatu MG 5.784 60.197139 421.327700 Santos SP 45 Tuesday
313979 2024-11-05 Polaris NaN NaN NaN Milho Padre Bernardo GO 5.784 55.376171 375.730339 Santos SP 45 Tuesday
313978 2024-11-05 Polaris NaN NaN NaN Soja Padre Bernardo GO 5.784 126.104457 1017.881801 Cristalina GO 45 Tuesday
314048 2024-11-05 Solara NaN NaN NaN Milho Ipiranga do Norte MT 5.784 39.319555 437.737880 Alta Floresta MT 45 Tuesday
315012 2024-11-05 Solara NaN NaN NaN Milho Vicentinópolis GO 5.784 55.576690 397.344017 Água Boa MT 45 Tuesday

E por fim faremos a limpeza dos dados que não apresentaram transações.

Code
df_completo_v2.dropna(subset='seller_id', inplace=True)

df_completo_v2.info()
<class 'pandas.core.frame.DataFrame'>
Index: 9509 entries, 0 to 9496
Data columns (total 15 columns):
 #   Column             Non-Null Count  Dtype         
---  ------             --------------  -----         
 0   date               9509 non-null   datetime64[ns]
 1   company            9509 non-null   category      
 2   seller_id          9509 non-null   category      
 3   price_trans        9509 non-null   float64       
 4   amount             9509 non-null   float64       
 5   product            9509 non-null   category      
 6   origin_city        9509 non-null   object        
 7   origin_state       9509 non-null   object        
 8   dolar              9509 non-null   float64       
 9   price_merc         9509 non-null   float64       
 10  cbot               9509 non-null   float64       
 11  destination_city   9509 non-null   category      
 12  destination_state  9509 non-null   category      
 13  week_of_year       9509 non-null   UInt32        
 14  day_of_week        9509 non-null   object        
dtypes: UInt32(1), category(5), datetime64[ns](1), float64(5), object(3)
memory usage: 932.1+ KB

Preparação dos dados para modelagem.

A intenção para a predição dessa série temporal será por meio de modelos de árvore pelos seguintes motivos:

  • Feature engineering necessário, mas não de suma importância.
  • Não há necessidade de normalização dos dados.
  • Modelos de árvore são robustos para séries temporais.
  • Modelos de árvore são robustos para dados desbalanceados.
  • Modelos de árvore são robustos para dados categóricos.
  • Possibilidade de predição das probabilidade (e múltiplas classes)
  • Possibilidade de interpretabilidade, por meio de feature importance.
Code
df_completo_v2.tail()
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
9503 2024-11-04 Polaris 100000060 115.562730 2622.786440 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday
9504 2024-11-04 Polaris 100000060 124.447569 2290.909287 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday
9505 2024-11-04 Polaris 100002402 115.212700 9788.617441 Soja Balsas MA 5.7892 122.683527 997.535892 Santos SP 45 Monday
9506 2024-11-04 Polaris 100002339 114.543597 9715.149462 Soja Sambaíba MA 5.7892 118.333486 1087.257588 Santos SP 45 Monday
9496 2024-11-04 Polaris 100002271 119.524125 2092.071936 Soja Uberlândia MG 5.7892 120.336453 993.482226 Santos SP 45 Monday
Code
df_completo_v2.info()
<class 'pandas.core.frame.DataFrame'>
Index: 9509 entries, 0 to 9496
Data columns (total 15 columns):
 #   Column             Non-Null Count  Dtype         
---  ------             --------------  -----         
 0   date               9509 non-null   datetime64[ns]
 1   company            9509 non-null   category      
 2   seller_id          9509 non-null   category      
 3   price_trans        9509 non-null   float64       
 4   amount             9509 non-null   float64       
 5   product            9509 non-null   category      
 6   origin_city        9509 non-null   object        
 7   origin_state       9509 non-null   object        
 8   dolar              9509 non-null   float64       
 9   price_merc         9509 non-null   float64       
 10  cbot               9509 non-null   float64       
 11  destination_city   9509 non-null   category      
 12  destination_state  9509 non-null   category      
 13  week_of_year       9509 non-null   UInt32        
 14  day_of_week        9509 non-null   object        
dtypes: UInt32(1), category(5), datetime64[ns](1), float64(5), object(3)
memory usage: 932.1+ KB
Code
# Convertendo colunas do tipo object para category
df_completo_v2 = df_completo_v2.apply(lambda x: x.astype('category') if x.dtype == 'object' else x)

# Verificando a conversão
df_completo_v2.info()
<class 'pandas.core.frame.DataFrame'>
Index: 9509 entries, 0 to 9496
Data columns (total 15 columns):
 #   Column             Non-Null Count  Dtype         
---  ------             --------------  -----         
 0   date               9509 non-null   datetime64[ns]
 1   company            9509 non-null   category      
 2   seller_id          9509 non-null   category      
 3   price_trans        9509 non-null   float64       
 4   amount             9509 non-null   float64       
 5   product            9509 non-null   category      
 6   origin_city        9509 non-null   category      
 7   origin_state       9509 non-null   category      
 8   dolar              9509 non-null   float64       
 9   price_merc         9509 non-null   float64       
 10  cbot               9509 non-null   float64       
 11  destination_city   9509 non-null   category      
 12  destination_state  9509 non-null   category      
 13  week_of_year       9509 non-null   UInt32        
 14  day_of_week        9509 non-null   category      
dtypes: UInt32(1), category(8), datetime64[ns](1), float64(5)
memory usage: 757.1 KB
Code
df_completo_v2.tail()
date company seller_id price_trans amount product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
9503 2024-11-04 Polaris 100000060 115.562730 2622.786440 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday
9504 2024-11-04 Polaris 100000060 124.447569 2290.909287 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday
9505 2024-11-04 Polaris 100002402 115.212700 9788.617441 Soja Balsas MA 5.7892 122.683527 997.535892 Santos SP 45 Monday
9506 2024-11-04 Polaris 100002339 114.543597 9715.149462 Soja Sambaíba MA 5.7892 118.333486 1087.257588 Santos SP 45 Monday
9496 2024-11-04 Polaris 100002271 119.524125 2092.071936 Soja Uberlândia MG 5.7892 120.336453 993.482226 Santos SP 45 Monday

Agora iremos remover as colunas

price_trans

e

amount

Pois são colunas dependentes de que haja uma transação, e o que queremos descobrir é se haverá uma transação, então não faz sentido mantê-las, além de potencialmente causar vazamento de dados.

Code
df_completo_v3 = df_completo_v2.drop(columns=['price_trans', 'amount'])
Code
df_completo_v3.head()
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
0 2024-01-02 Polaris 100000864 Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday
1 2024-01-03 Polaris 100000865 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
2 2024-01-03 Lunarix 100000094 Soja Porto Velho RO 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
3 2024-01-03 Lunarix 100000314 Soja Boa Vista RR 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
4 2024-01-03 Polaris 100000866 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
Code
df_completo_v4_test = df_completo_v3.copy()

# Convert 'seller_id' to string to ensure uniform data type
df_completo_v4_test['seller_id'] = df_completo_v4_test['seller_id'].astype(str)


df_completo_v4_test.head()
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
0 2024-01-02 Polaris 100000864 Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday
1 2024-01-03 Polaris 100000865 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
2 2024-01-03 Lunarix 100000094 Soja Porto Velho RO 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
3 2024-01-03 Lunarix 100000314 Soja Boa Vista RR 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
4 2024-01-03 Polaris 100000866 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday

Agora garantimos que temos variaveis globais para cada label encoder (para fazer transform para novos dados).

Escolha da abordagem para predição

A abordagem será de utilizar modelos de ML baseados em árvores adaptados para séries temporais, uma vez que estamos lidando com dados temporais e que também queremos entender probabilidades.

Para tanto, utilizaremos a coluna de Seller ID como target, e as demais colunas como features.

Precisaríamos averiguar se os dados estão balanceados. O que já se sabe é que não estão:

No entanto tecnicas como SMOTE já não se mostram muito eficazes nos dias de hoje, e como há um número muito elevado de classes, se tornaria bastante complexo tentar balancear ou atribuir pesos.

Iremos seguir com os dados desbalanceados e tentar entender se o modelo consegue aprender com os dados.

Code
df_pre_modelo = df_completo_v4_test
df_pre_modelo.sample(5)
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
3503 2024-05-02 Lunarix 100000371 Milho Porto Velho RO 5.1178 45.164727 472.008227 Barcarena PA 18 Thursday
4932 2024-06-04 Polaris 100001520 Milho Campo Novo do Parecis MT 5.2681 33.979715 422.158728 Santos SP 23 Tuesday
9257 2024-10-30 Polaris 100000095 Soja Porto Velho RO 5.7795 129.128846 1005.263977 Santos SP 44 Wednesday
3 2024-01-03 Lunarix 100000314 Soja Boa Vista RR 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
3441 2024-04-30 Polaris 100000767 Soja Ituverava SP 5.1712 113.901003 1254.593503 Santos SP 18 Tuesday

Um outro ponto muito importante é que como estamos trabalhando com um modelo de classificação, precisamos ter pelo menos 2 classes de cada label, o que não é o caso como os dados estão agora.

Isso se dá porque precisamos de um conjunto de treino e teste, e se não houver pelo menos 2 classes de cada label, não será possível fazer a divisão dos dados estratificados para Y e não conseguiríamos fazer validações precisas.

Vamos reavaliar os percentis do número de transações por vendedor.

Code
# Calculando o número de transações por vendedor
num_transacoes_por_vendedor = df_pre_modelo['seller_id'].value_counts()

# Calculando os percentis
percentis = np.percentile(num_transacoes_por_vendedor, np.arange(1, 101))

# Plotando o gráfico
plt.figure(figsize=(10, 6))
plt.plot(np.arange(1, 101), percentis, marker='o')

# Adicionando os textos aos pontos do gráfico a cada 2 percentis
for i in range(0, 100, 5):
    plt.text(i + 1, percentis[i], f'{percentis[i]:.2f}', ha='center', va='bottom', fontsize=8)

plt.xlabel('Percentis')
plt.ylabel('Número de Transações')
plt.title('Número de Transações por Percentil de Vendedor')
plt.grid(True)
plt.show()

Infelizmente iremos perder aproximadamente 50% dos dados de vendedores, mas caso não apresente bons resultados, podemos mudar completamente a abordagem.

Code
# Filter the dataframe to keep only sellers with more than 1 transaction
sellers_with_multiple_transactions = df_pre_modelo['seller_id'].value_counts()
sellers_with_multiple_transactions = sellers_with_multiple_transactions[sellers_with_multiple_transactions > 1].index

df_filtered = df_pre_modelo[df_pre_modelo['seller_id'].isin(sellers_with_multiple_transactions)]
df_pre_modelo_ = df_filtered.copy()

Modelagem

Aqui faremos a importação dos modelos e métricas de avaliação.

Inicialmente testaremos modelos tradicionais de machine learning baseados em árvore como parâmetro de comparação. Perceba que embora esses modelos não sejam os mais indicados para séries temporais, eles são robustos para dados desbalanceados e categóricos e também com o feature engineering, foram capturados dados de data como por exemplo das colunas day_of_week e week_of_year.

Posteriormente, utilizaremos modelos baseados em árvore mas que tem imbutidos em si a capacidade de lidar com séries temporais. Para mais informações, consultar a documentação https://www.sktime.net/en/stable/index.html

Espera-se que esses modelos tenham melhores resultados, e que sejam a base para as predições do dia 05/11/2024.

IMPORTANTE

Perceba que para todos os modelos, a abordagem principal sempre será pela avaliação pelo precision_weighted, ja+á que nos importa mais se o que o modelo diz está correto, buscando evitar Falsos positivos do modelo, pois já teríamos mais certeza do próximo dia, nos antecipando às demandas de mercado e executando as operações e transações potencialmente corretas.

Mas também poderia ter sido optado pelo recall, de modo a garantir de todas as vendas q foram feitas, quantas o modelo acertou, ou ainda o f1, que é uma média harmônica entre precision e recall, no entanto precision me parece mais adequada.

A abordagem é pelo precision weighted, que nos dá uma média ponderada do precision de cada classe, o que é importante para a análise, uma vez que temos dados desbalanceados e um número muito grande de classes.

Code
from sktime.classification.dummy import DummyClassifier
from sktime.classification.interval_based import TimeSeriesForestClassifier
from sktime.transformations.panel.compose import ColumnConcatenator
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, roc_auc_score
from sktime.datatypes._panel._convert import from_2d_array_to_nested
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from tqdm import tqdm
import gc
Code
display(df_pre_modelo_.tail())

df_pre_modelo_.shape
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
9502 2024-11-04 Polaris 100001525 Soja São Gabriel RS 5.7892 118.297200 1099.150289 Santos SP 45 Monday
9503 2024-11-04 Polaris 100000060 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday
9504 2024-11-04 Polaris 100000060 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday
9506 2024-11-04 Polaris 100002339 Soja Sambaíba MA 5.7892 118.333486 1087.257588 Santos SP 45 Monday
9496 2024-11-04 Polaris 100002271 Soja Uberlândia MG 5.7892 120.336453 993.482226 Santos SP 45 Monday
(8330, 13)

Separando em X e y (features e target)

Code
X = df_pre_modelo_.drop(['seller_id'],axis=1)
y = df_pre_modelo_['seller_id']

E a divisão de treino e teste seguirá a ordem dos dados.

Code
X.head()
date company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
0 2024-01-02 Polaris Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday
1 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
4 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
5 2024-01-04 Polaris Soja Joaçaba SC 4.9182 115.276863 1137.705266 Santos SP 1 Thursday
6 2024-01-05 Polaris Soja Joaçaba SC 4.8893 115.276863 1137.705266 Santos SP 1 Friday

Divisao em treino e teste considerando a ordem temporal.

Code
# Dividindo em treinamento e teste considerando a ordem temporal
X_train, X_test = X[X['date'] < '2024-11-04'], X[X['date'] >= '2024-11-04']
y_train, y_test = y[:X_train.shape[0]], y[X_train.shape[0]:]

display(X_train.shape,X_test.shape)
display(y_train.shape,y_test.shape)
(8314, 12)
(16, 12)
(8314,)
(16,)

Aqui faremos o encoding da variável Y

Code
le_target  = LabelEncoder()
y_train_encoded = le_target.fit_transform(y_train)

display(X_train.head(), X_train.shape)
display(y_train_encoded[5], y_train.shape)
date company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
0 2024-01-02 Polaris Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday
1 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
4 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
5 2024-01-04 Polaris Soja Joaçaba SC 4.9182 115.276863 1137.705266 Santos SP 1 Thursday
6 2024-01-05 Polaris Soja Joaçaba SC 4.8893 115.276863 1137.705266 Santos SP 1 Friday
(8314, 12)
568
(8314,)

Aqui aplicaremos os modelos tradicionais de machine learning e faremos a comparação com os dados de teste.

Além disso, faremos o label encoding para as variáveis categóricas.

Utilizaremos também a abordagem de time series validation, proveninente do TimeSeriesSplit posteriormente, adequado para séries temporais.

Code
import numpy as np
from sklearn.model_selection import TimeSeriesSplit, cross_validate

#garantir que o treino tem 
unique_classes = np.unique(y_train_encoded)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train_encoded])

# Drop the 'date' column from X_train
X_train_no_date = X_train.drop(columns=['date'])

# Encode categorical variables in X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Lista de modelos para validação cruzada
models = {
    'DecisionTreeClassifier': DecisionTreeClassifier(max_depth=6, min_samples_split=2),
    'RandomForestClassifier': RandomForestClassifier(n_estimators=100, max_depth=6),
    'ExtraTreesClassifier': ExtraTreesClassifier(n_estimators=100, max_depth=6),
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 500 para o time series split
tscv = TimeSeriesSplit(max_train_size=500)

# Aplicando cross-validation para cada modelo
for model_name, model in models.items():
    scores = cross_validate(model, X_train_no_date, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    cv_results[model_name] = scores
    print(f'{model_name} - Test Precision: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Train Precision: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Exibindo os resultados
cv_results
DecisionTreeClassifier - Test Precision: 0.0570 (+/- 0.0364)
DecisionTreeClassifier - Train Precision: 0.1838 (+/- 0.0506)
RandomForestClassifier - Test Precision: 0.0559 (+/- 0.0234)
RandomForestClassifier - Train Precision: 0.4142 (+/- 0.0492)
ExtraTreesClassifier - Test Precision: 0.0549 (+/- 0.0225)
ExtraTreesClassifier - Train Precision: 0.4145 (+/- 0.0351)
{'DecisionTreeClassifier': {'fit_time': array([0.02106285, 0.00800323, 0.00700068, 0.00900269, 0.00999594]),
  'score_time': array([0.01300097, 0.01299524, 0.01199985, 0.00899696, 0.01100445]),
  'test_precision_weighted': array([0.10006437, 0.10277875, 0.03151577, 0.02474675, 0.02565449]),
  'train_precision_weighted': array([0.24770888, 0.21075845, 0.19971542, 0.16185891, 0.09871545]),
  'test_accuracy': array([0.12707581, 0.17111913, 0.09097473, 0.04620939, 0.08158845]),
  'train_accuracy': array([0.318, 0.3  , 0.294, 0.204, 0.178]),
  'test_recall_weighted': array([0.12707581, 0.17111913, 0.09097473, 0.04620939, 0.08158845]),
  'train_recall_weighted': array([0.318, 0.3  , 0.294, 0.204, 0.178])},
 'RandomForestClassifier': {'fit_time': array([0.29406476, 0.39608574, 0.33652139, 0.28299832, 0.33000112]),
  'score_time': array([0.3500948 , 0.39754581, 0.4136219 , 0.44762874, 0.44362688]),
  'test_precision_weighted': array([0.07946258, 0.08753987, 0.03784021, 0.02834177, 0.04645186]),
  'train_precision_weighted': array([0.42524463, 0.48340865, 0.4451657 , 0.36339823, 0.35356808]),
  'test_accuracy': array([0.1400722 , 0.16606498, 0.09602888, 0.06642599, 0.11191336]),
  'train_accuracy': array([0.51 , 0.594, 0.534, 0.472, 0.456]),
  'test_recall_weighted': array([0.1400722 , 0.16606498, 0.09602888, 0.06642599, 0.11191336]),
  'train_recall_weighted': array([0.51 , 0.594, 0.534, 0.472, 0.456])},
 'ExtraTreesClassifier': {'fit_time': array([0.21400595, 0.21699762, 0.1619966 , 0.19200087, 0.1920023 ]),
  'score_time': array([0.22100091, 0.22800374, 0.38553119, 0.41858244, 0.40457916]),
  'test_precision_weighted': array([0.0579496 , 0.09496815, 0.03337556, 0.03344458, 0.05482399]),
  'train_precision_weighted': array([0.41145801, 0.48064448, 0.40003632, 0.37639182, 0.40418246]),
  'test_accuracy': array([0.13068592, 0.17761733, 0.0967509 , 0.07581227, 0.13068592]),
  'train_accuracy': array([0.508, 0.576, 0.496, 0.492, 0.52 ]),
  'test_recall_weighted': array([0.13068592, 0.17761733, 0.0967509 , 0.07581227, 0.13068592]),
  'train_recall_weighted': array([0.508, 0.576, 0.496, 0.492, 0.52 ])}}

Arrumando os dados do resultado do modelo

Code
# Descompactar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df

# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
results_df.T
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
DecisionTreeClassifier 0.011013 0.011599 0.056952 0.183751 0.103394 0.2588 0.103394 0.2588
RandomForestClassifier 0.327934 0.410504 0.055927 0.414157 0.116101 0.5132 0.116101 0.5132
ExtraTreesClassifier 0.195401 0.331539 0.054912 0.414543 0.122310 0.5184 0.122310 0.5184

Percebe-se que nenhum modelo teve uma capacidade preditiva eficiente, o que era esperado, uma vez que os modelos de árvore não são os mais indicados para séries temporais.

Parecem não ter sido capazes de identificar padrões a partir das informações de semana adicionadas aos dados.

Percebe-se que até possuem resultados razoáveis para o treino, mas todos modelos overfittados para os dados de teste.

Logo, partiremos para modelos de séries temporais com a abordagem de ML (sktime).

Primeiro iremos converter os dados para nested, formato necessário para o sktime

Utilizamos dois diferentes modelos disponíveis na biblioteca, de modo a escolher o modelo que possuas melhores resultados.

Um modelo é apenas um dummy classifier e o outro é um modelo baseado em árvore para séries temporais, similar a um random forest.

Code
from sktime.classification.dummy import DummyClassifier
from sktime.classification.interval_based import TimeSeriesForestClassifier
from sklearn.pipeline import make_pipeline

# Ensure y_train has consecutive integer values
unique_classes = np.unique(y_train_encoded)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train_encoded])

# Drop the 'date' column from X_train
X_train_no_date = X_train.drop(columns=['date'])

# Encode categorical variables in X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Lista de modelos para validação cruzada
models = {
    'DummyClassifier': DummyClassifier(strategy='most_frequent'),
    'TimeSeriesForestClassifier': TimeSeriesForestClassifier(n_estimators=150, n_jobs=-1, random_state=42)
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 500 para o time series split
tscv = TimeSeriesSplit(max_train_size=500)

# Aplicando cross-validation para cada modelo
for model_name, model in models.items():
    pipeline = make_pipeline(
        ColumnConcatenator(),
        model)

    scores = cross_validate(pipeline, from_2d_array_to_nested(X_train_no_date), y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    cv_results[model_name] = scores
    print(f'{model_name} - Test Precision: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Train Precision: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Exibindo os resultados
cv_results
DummyClassifier - Test Precision: 0.0005 (+/- 0.0005)
DummyClassifier - Train Precision: 0.0016 (+/- 0.0006)
TimeSeriesForestClassifier - Test Precision: 0.0941 (+/- 0.0456)
TimeSeriesForestClassifier - Train Precision: 0.5590 (+/- 0.0595)
{'DummyClassifier': {'fit_time': array([1.22814083, 1.1242764 , 1.10827065, 0.99362636, 0.85758543]),
  'score_time': array([1.28885698, 1.18563628, 1.03258038, 0.88173604, 0.83055043]),
  'test_precision_weighted': array([1.35594104e-03, 1.88194816e-04, 5.33826845e-04, 8.34104446e-06,
         4.38426149e-04]),
  'train_precision_weighted': array([0.002304, 0.001296, 0.002116, 0.000676, 0.001444]),
  'test_accuracy': array([0.0368231 , 0.01371841, 0.02310469, 0.00288809, 0.02093863]),
  'train_accuracy': array([0.048, 0.036, 0.046, 0.026, 0.038]),
  'test_recall_weighted': array([0.0368231 , 0.01371841, 0.02310469, 0.00288809, 0.02093863]),
  'train_recall_weighted': array([0.048, 0.036, 0.046, 0.026, 0.038])},
 'TimeSeriesForestClassifier': {'fit_time': array([1.82501888, 2.74240112, 3.17734575, 3.23943734, 3.44072008]),
  'score_time': array([3.4760139 , 3.29619694, 3.13205671, 2.55634499, 2.0233624 ]),
  'test_precision_weighted': array([0.10133797, 0.179185  , 0.0643197 , 0.05138096, 0.07423893]),
  'train_precision_weighted': array([0.53784607, 0.63918055, 0.59842477, 0.4628283 , 0.55679423]),
  'test_accuracy': array([0.166787  , 0.22382671, 0.11480144, 0.08736462, 0.13790614]),
  'train_accuracy': array([0.606, 0.706, 0.658, 0.564, 0.636]),
  'test_recall_weighted': array([0.166787  , 0.22382671, 0.11480144, 0.08736462, 0.13790614]),
  'train_recall_weighted': array([0.606, 0.706, 0.658, 0.564, 0.636])}}
Code
# Descompactar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df
# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
results_df.T
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
DummyClassifier 1.062380 1.043872 0.000505 0.001567 0.019495 0.0388 0.019495 0.0388
TimeSeriesForestClassifier 2.884985 2.896795 0.094093 0.559015 0.146137 0.6340 0.146137 0.6340

É perceptível que o modelo de Time series sofreu overfitting, uma vez que teve resultados razoáveis no treino, mas não teve capacidade de generalizar para o teste, bem como os modelos com abordagem tradicional de machine learning.

Talvez precisemos fazer alterações nos dados, eventualmente diminuir o número de classes para predição (reduzindo vendedores o número de vendedores)

Faremos a mudança nos dados de modo a tentar melhores modelos, iremos utilizar dois critérios:

  • Remover vendedores que não fizeram transações nos últimos 90 dias
  • Remover vendedores com poucas transações (o limite vai ser definido pela análise nos proximos passos)

Removendo vendedores sem transação nos ultimos 90 dias

Code
vendedores_ultimos_90 = df_pre_modelo_[df_pre_modelo_['date'] >= '2024-08-04'].seller_id.unique()
vendedores_ultimos_90
array(['100000540', '100000193', '100000270', '100000757', '100000169',
       '100001934', '100000980', '100000759', '100000639', '100000003',
       '100000002', '100001739', '100001238', '100000490', '100000638',
       '100001485', '100000032', '100000905', '100000705', '100001533',
       '100000953', '100000201', '100000974', '100001345', '100000242',
       '100001676', '100000431', '100001801', '100000505', '100001929',
       '100001837', '100000916', '100000481', '100001926', '100000870',
       '100000112', '100001933', '100000339', '100001719', '100000737',
       '100001327', '100001499', '100000366', '100000303', '100000195',
       '100001420', '100000829', '100001140', '100000301', '100000867',
       '100000969', '100000909', '100000210', '100001774', '100001657',
       '100001898', '100000787', '100000546', '100001646', '100001370',
       '100000040', '100001617', '100000862', '100000402', '100001760',
       '100000509', '100000357', '100000758', '100001524', '100001363',
       '100000868', '100001507', '100000861', '100001904', '100001114',
       '100001860', '100000243', '100000387', '100000076', '100000983',
       '100000077', '100001947', '100001913', '100001333', '100000055',
       '100001949', '100001952', '100001955', '100000062', '100000990',
       '100001806', '100001953', '100000702', '100001833', '100000927',
       '100001807', '100000228', '100000037', '100000289', '100000244',
       '100000783', '100000480', '100000296', '100000159', '100000449',
       '100000170', '100000023', '100000863', '100001036', '100000812',
       '100000383', '100001962', '100000500', '100000848', '100001343',
       '100000321', '100000174', '100000199', '100000268', '100000859',
       '100000044', '100001391', '100001235', '100000171', '100000039',
       '100001923', '100000009', '100001967', '100001040', '100000106',
       '100001888', '100001085', '100000520', '100001271', '100000895',
       '100000367', '100001855', '100000685', '100001751', '100000005',
       '100000521', '100000269', '100001968', '100000315', '100000515',
       '100001668', '100001388', '100001172', '100000901', '100000519',
       '100001089', '100000255', '100000024', '100000813', '100000523',
       '100001870', '100001400', '100000286', '100000028', '100000302',
       '100000294', '100000136', '100001972', '100001178', '100000043',
       '100001875', '100000437', '100000111', '100001237', '100000310',
       '100000126', '100000019', '100000102', '100001822', '100000359',
       '100001146', '100001753', '100000346', '100000208', '100000131',
       '100001832', '100000821', '100000348', '100001471', '100001973',
       '100000760', '100001280', '100001978', '100000516', '100000224',
       '100001979', '100000351', '100000258', '100000765', '100000525',
       '100000766', '100001198', '100000342', '100000440', '100000518',
       '100000138', '100001981', '100000189', '100000051', '100001649',
       '100000119', '100001842', '100002020', '100002019', '100000135',
       '100002021', '100002018', '100000190', '100002006', '100000322',
       '100000454', '100000398', '100000912', '100002007', '100002017',
       '100001683', '100001258', '100002027', '100002029', '100000309',
       '100000220', '100001859', '100000082', '100000337', '100000261',
       '100000227', '100001925', '100000272', '100002030', '100000485',
       '100000237', '100002024', '100001252', '100000127', '100000931',
       '100001740', '100001326', '100001924', '100000021', '100000407',
       '100000087', '100000292', '100000932', '100002000', '100000602',
       '100001572', '100001240', '100001865', '100001102', '100001098',
       '100000749', '100001417', '100001249', '100001206', '100001985',
       '100001990', '100001826', '100001997', '100000130', '100001106',
       '100001626', '100001495', '100001998', '100001761', '100000116',
       '100001496', '100001050', '100001993', '100000896', '100001463',
       '100001994', '100000324', '100001567', '100001995', '100001996',
       '100002040', '100000716', '100001467', '100000394', '100000875',
       '100002043', '100001714', '100002041', '100001187', '100000192',
       '100000335', '100002046', '100002047', '100001786', '100000341',
       '100000016', '100002039', '100001557', '100001528', '100001768',
       '100001543', '100000157', '100002034', '100002035', '100000276',
       '100000064', '100000240', '100002038', '100000354', '100001883',
       '100000891', '100000673', '100000397', '100002054', '100001145',
       '100000751', '100000558', '100001137', '100000369', '100000544',
       '100000456', '100000049', '100002052', '100001574', '100000156',
       '100001858', '100000637', '100000063', '100000748', '100000865',
       '100000122', '100000790', '100001704', '100001201', '100001849',
       '100000042', '100000747', '100000140', '100002079', '100001921',
       '100002060', '100002106', '100001254', '100002091', '100002088',
       '100000871', '100002093', '100000973', '100002089', '100000926',
       '100001599', '100000041', '100002086', '100000209', '100001279',
       '100001446', '100000191', '100001640', '100002064', '100002061',
       '100000587', '100001527', '100002075', '100002069', '100002071',
       '100002072', '100002063', '100000081', '100000226', '100000767',
       '100000815', '100002108', '100000814', '100001828', '100000937',
       '100001090', '100000089', '100001447', '100002095', '100002097',
       '100000275', '100001893', '100002100', '100000904', '100001746',
       '100001051', '100001127', '100002110', '100000142', '100002121',
       '100000798', '100000368', '100002114', '100000325', '100001032',
       '100000649', '100001037', '100000769', '100001732', '100000962',
       '100000200', '100001638', '100000155', '100001878', '100001628',
       '100001857', '100002117', '100002118', '100001661', '100000547',
       '100000069', '100000308', '100001334', '100000806', '100001516',
       '100002135', '100001196', '100000246', '100001184', '100001702',
       '100001712', '100001083', '100000994', '100000584', '100001687',
       '100000060', '100000471', '100001298', '100000866', '100000474',
       '100000065', '100000283', '100002140', '100000298', '100000278',
       '100000266', '100002150', '100000349', '100001784', '100002144',
       '100002146', '100000950', '100002169', '100001763', '100000035',
       '100000149', '100002153', '100002161', '100001245', '100000075',
       '100002156', '100002155', '100000854', '100001900', '100002154',
       '100002152', '100001074', '100002151', '100000924', '100002163',
       '100000413', '100000048', '100000100', '100000334', '100001779',
       '100002165', '100002166', '100000114', '100001570', '100000388',
       '100000788', '100000497', '100002178', '100002175', '100002176',
       '100001395', '100001104', '100002183', '100002185', '100002181',
       '100002196', '100001030', '100000771', '100000659', '100002192',
       '100002226', '100000223', '100001633', '100002214', '100000139',
       '100002212', '100002215', '100001895', '100001525', '100001882',
       '100001004', '100001311', '100002223', '100000852', '100001338',
       '100000550', '100002202', '100001387', '100000922', '100002201',
       '100001791', '100000304', '100000214', '100000332', '100000329',
       '100000453', '100002208', '100001375', '100002232', '100001170',
       '100002239', '100000892', '100000642', '100000162', '100000687',
       '100001766', '100001148', '100002236', '100000057', '100000971',
       '100000987', '100002241', '100001455', '100000773', '100001585',
       '100001095', '100002249', '100000355', '100001132', '100000482',
       '100000186', '100001889', '100001211', '100001308', '100001442',
       '100000445', '100002266', '100000047', '100002262', '100002256',
       '100001149', '100000147', '100002265', '100001052', '100000307',
       '100001383', '100001013', '100002271', '100002272', '100001151',
       '100000919', '100002282', '100000501', '100001472', '100000936',
       '100000260', '100001122', '100000655', '100001535', '100002280',
       '100001752', '100000770', '100000008', '100000507', '100002285',
       '100000669', '100000095', '100000508', '100000954', '100001173',
       '100001152', '100001189', '100000819', '100002300', '100002297',
       '100002303', '100001220', '100001797', '100001005', '100000251',
       '100002304', '100000985', '100000124', '100000072', '100001901',
       '100002318', '100000898', '100002319', '100002364', '100000202',
       '100001012', '100001204', '100000470', '100000738', '100002361',
       '100002368', '100001041', '100000011', '100001306', '100002328',
       '100000588', '100000316', '100002383', '100000143', '100001473',
       '100002376', '100000991', '100000992', '100002378', '100002362',
       '100001354', '100000408', '100002335', '100002321', '100001644',
       '100001111', '100000699', '100000774', '100001635', '100001809',
       '100000643', '100000133', '100000165', '100000621', '100000984',
       '100000038', '100001123', '100000104', '100002421', '100002337',
       '100002359', '100001708', '100001596', '100001034', '100002390',
       '100002326', '100001711', '100000794', '100002411', '100001021',
       '100001035', '100002332', '100000947', '100000263', '100000494',
       '100002406', '100000903', '100000280', '100000203', '100000093',
       '100000247', '100002398', '100002391', '100000101', '100001284',
       '100000362', '100001062', '100000097', '100000633', '100000918',
       '100001110', '100002334', '100001664', '100000318', '100000068',
       '100000109', '100002339', '100001362', '100000141', '100001835'],
      dtype=object)
Code
mask = [df_pre_modelo_[df_pre_modelo_['date'] < '2024-08-04'].isin(vendedores_ultimos_90).seller_id]
mask
[0       False
 1        True
 4        True
 5        True
 6        True
         ...  
 6994     True
 6995     True
 6996     True
 6997    False
 6987     True
 Name: seller_id, Length: 6224, dtype: bool]
Code
mask = df_pre_modelo_[df_pre_modelo_['date'] < '2024-08-04']['seller_id'].isin(vendedores_ultimos_90)
df_pre_modelo_[df_pre_modelo_['date'] < '2024-08-04'][mask]
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
1 2024-01-03 Polaris 100000865 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
4 2024-01-03 Polaris 100000866 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday
5 2024-01-04 Polaris 100000867 Soja Joaçaba SC 4.9182 115.276863 1137.705266 Santos SP 1 Thursday
6 2024-01-05 Polaris 100000867 Soja Joaçaba SC 4.8893 115.276863 1137.705266 Santos SP 1 Friday
7 2024-01-05 Polaris 100000868 Soja Joaçaba SC 4.8893 115.276863 1137.705266 Santos SP 1 Friday
... ... ... ... ... ... ... ... ... ... ... ... ... ...
6992 2024-08-02 Polaris 100000901 Soja Joaçaba SC 5.7360 150.498828 945.491532 Santos SP 31 Friday
6994 2024-08-02 Celestix 100000481 Soja Rio Grande RS 5.7360 152.981792 1066.873615 Santos PR 31 Friday
6995 2024-08-02 Polaris 100001032 Milho Paragominas PA 5.7360 55.445087 422.086114 Santos SP 31 Friday
6996 2024-08-02 Polaris 100001921 Soja Porto dos Gaúchos MT 5.7360 97.540517 1027.333426 Santos SP 31 Friday
6987 2024-08-02 Polaris 100001220 Milho Nova Mutum MT 5.7360 39.004638 400.438451 Santos SP 31 Friday

3695 rows × 13 columns

gerando o dataframe v2

Code
df_pre_modelo_v2 = pd.concat([df_pre_modelo_[df_pre_modelo_['date'] >= '2024-08-04'],
                              df_pre_modelo_[df_pre_modelo_['date'] < '2024-08-04'][mask]])

df_pre_modelo_v2
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
7024 2024-08-05 Polaris 100000540 Soja Uberlândia MG 5.764 125.712651 1145.497646 Santos SP 32 Monday
7079 2024-08-05 Lunarix 100000193 Soja Sinop MT 5.764 106.934372 996.212223 Santos SP 32 Monday
7078 2024-08-05 Lunarix 100000270 Soja Boa Vista RR 5.764 123.753326 1063.886595 Santos SP 32 Monday
7077 2024-08-05 Solara 100000757 Soja Barreiras BA 5.764 115.276863 1137.705266 Uberaba MT 32 Monday
7076 2024-08-05 Polaris 100000169 Soja Redenção PA 5.764 111.325147 1033.918821 Santos SP 32 Monday
... ... ... ... ... ... ... ... ... ... ... ... ... ...
6992 2024-08-02 Polaris 100000901 Soja Joaçaba SC 5.736 150.498828 945.491532 Santos SP 31 Friday
6994 2024-08-02 Celestix 100000481 Soja Rio Grande RS 5.736 152.981792 1066.873615 Santos PR 31 Friday
6995 2024-08-02 Polaris 100001032 Milho Paragominas PA 5.736 55.445087 422.086114 Santos SP 31 Friday
6996 2024-08-02 Polaris 100001921 Soja Porto dos Gaúchos MT 5.736 97.540517 1027.333426 Santos SP 31 Friday
6987 2024-08-02 Polaris 100001220 Milho Nova Mutum MT 5.736 39.004638 400.438451 Santos SP 31 Friday

5801 rows × 13 columns

Agora iremos remover os vendedores que fizeram pouquíssimas transações

Code
df_pre_modelo_v2['seller_id'].value_counts().plot(kind='bar', figsize=(15, 6), color='skyblue')
plt.show()

O gráfico não é perfeito, mas nos mostra a quantidade de vendedores com poucas e muitas vendas, vamos fazer um looping pra entender o quanto perderíamos de dados ao remover determinado número de vendedores.

Code
(df_pre_modelo_v2.value_counts() >= 4).sum()
84
Code
max_numero_vendas = df_pre_modelo_v2['seller_id'].value_counts().max()
min_numero_vendas = df_pre_modelo_v2['seller_id'].value_counts().min()

tamanho_df_agora = df_pre_modelo_v2.shape[0]

for i in range(min_numero_vendas, max_numero_vendas + 1):
    # Filtrar vendedores com pelo menos i vendas
    num_vendedores = (df_pre_modelo_v2['seller_id'].value_counts() >= i).sum()
    
    # Contar quantas linhas restariam no DataFrame após o filtro
    linhas_restantes = df_pre_modelo_v2[df_pre_modelo_v2['seller_id'].map(df_pre_modelo_v2['seller_id'].value_counts()) >= i].shape[0]
    
    # Calcular a porcentagem de perda com base nas linhas excluídas
    porcentagem_perda = (1 - linhas_restantes / tamanho_df_agora) * 100
    
    print(f"Perderíamos essa porcentagem do df com vendedores com mais de {i} vendas: {porcentagem_perda:.2f}%")
Perderíamos essa porcentagem do df com vendedores com mais de 2 vendas: 0.00%
Perderíamos essa porcentagem do df com vendedores com mais de 3 vendas: 6.79%
Perderíamos essa porcentagem do df com vendedores com mais de 4 vendas: 12.02%
Perderíamos essa porcentagem do df com vendedores com mais de 5 vendas: 16.91%
Perderíamos essa porcentagem do df com vendedores com mais de 6 vendas: 21.82%
Perderíamos essa porcentagem do df com vendedores com mais de 7 vendas: 25.55%
Perderíamos essa porcentagem do df com vendedores com mais de 8 vendas: 28.68%
Perderíamos essa porcentagem do df com vendedores com mais de 9 vendas: 31.58%
Perderíamos essa porcentagem do df com vendedores com mais de 10 vendas: 34.99%
Perderíamos essa porcentagem do df com vendedores com mais de 11 vendas: 37.58%
Perderíamos essa porcentagem do df com vendedores com mais de 12 vendas: 40.04%
Perderíamos essa porcentagem do df com vendedores com mais de 13 vendas: 42.32%
Perderíamos essa porcentagem do df com vendedores com mais de 14 vendas: 44.79%
Perderíamos essa porcentagem do df com vendedores com mais de 15 vendas: 46.96%
Perderíamos essa porcentagem do df com vendedores com mais de 16 vendas: 49.03%
Perderíamos essa porcentagem do df com vendedores com mais de 17 vendas: 50.41%
Perderíamos essa porcentagem do df com vendedores com mais de 18 vendas: 52.75%
Perderíamos essa porcentagem do df com vendedores com mais de 19 vendas: 54.92%
Perderíamos essa porcentagem do df com vendedores com mais de 20 vendas: 55.25%
Perderíamos essa porcentagem do df com vendedores com mais de 21 vendas: 57.32%
Perderíamos essa porcentagem do df com vendedores com mais de 22 vendas: 57.68%
Perderíamos essa porcentagem do df com vendedores com mais de 23 vendas: 57.68%
Perderíamos essa porcentagem do df com vendedores com mais de 24 vendas: 58.47%
Perderíamos essa porcentagem do df com vendedores com mais de 25 vendas: 60.13%
Perderíamos essa porcentagem do df com vendedores com mais de 26 vendas: 60.13%
Perderíamos essa porcentagem do df com vendedores com mais de 27 vendas: 60.58%
Perderíamos essa porcentagem do df com vendedores com mais de 28 vendas: 61.51%
Perderíamos essa porcentagem do df com vendedores com mais de 29 vendas: 62.47%
Perderíamos essa porcentagem do df com vendedores com mais de 30 vendas: 63.47%
Perderíamos essa porcentagem do df com vendedores com mais de 31 vendas: 63.99%
Perderíamos essa porcentagem do df com vendedores com mais de 32 vendas: 65.06%
Perderíamos essa porcentagem do df com vendedores com mais de 33 vendas: 65.61%
Perderíamos essa porcentagem do df com vendedores com mais de 34 vendas: 65.61%
Perderíamos essa porcentagem do df com vendedores com mais de 35 vendas: 67.95%
Perderíamos essa porcentagem do df com vendedores com mais de 36 vendas: 69.16%
Perderíamos essa porcentagem do df com vendedores com mais de 37 vendas: 71.64%
Perderíamos essa porcentagem do df com vendedores com mais de 38 vendas: 72.92%
Perderíamos essa porcentagem do df com vendedores com mais de 39 vendas: 72.92%
Perderíamos essa porcentagem do df com vendedores com mais de 40 vendas: 72.92%
Perderíamos essa porcentagem do df com vendedores com mais de 41 vendas: 73.61%
Perderíamos essa porcentagem do df com vendedores com mais de 42 vendas: 73.61%
Perderíamos essa porcentagem do df com vendedores com mais de 43 vendas: 73.61%
Perderíamos essa porcentagem do df com vendedores com mais de 44 vendas: 74.35%
Perderíamos essa porcentagem do df com vendedores com mais de 45 vendas: 74.35%
Perderíamos essa porcentagem do df com vendedores com mais de 46 vendas: 75.12%
Perderíamos essa porcentagem do df com vendedores com mais de 47 vendas: 75.92%
Perderíamos essa porcentagem do df com vendedores com mais de 48 vendas: 76.73%
Perderíamos essa porcentagem do df com vendedores com mais de 49 vendas: 76.73%
Perderíamos essa porcentagem do df com vendedores com mais de 50 vendas: 76.73%
Perderíamos essa porcentagem do df com vendedores com mais de 51 vendas: 76.73%
Perderíamos essa porcentagem do df com vendedores com mais de 52 vendas: 76.73%
Perderíamos essa porcentagem do df com vendedores com mais de 53 vendas: 76.73%
Perderíamos essa porcentagem do df com vendedores com mais de 54 vendas: 77.64%
Perderíamos essa porcentagem do df com vendedores com mais de 55 vendas: 77.64%
Perderíamos essa porcentagem do df com vendedores com mais de 56 vendas: 77.64%
Perderíamos essa porcentagem do df com vendedores com mais de 57 vendas: 77.64%
Perderíamos essa porcentagem do df com vendedores com mais de 58 vendas: 77.64%
Perderíamos essa porcentagem do df com vendedores com mais de 59 vendas: 78.64%
Perderíamos essa porcentagem do df com vendedores com mais de 60 vendas: 79.66%
Perderíamos essa porcentagem do df com vendedores com mais de 61 vendas: 79.66%
Perderíamos essa porcentagem do df com vendedores com mais de 62 vendas: 79.66%
Perderíamos essa porcentagem do df com vendedores com mais de 63 vendas: 79.66%
Perderíamos essa porcentagem do df com vendedores com mais de 64 vendas: 79.66%
Perderíamos essa porcentagem do df com vendedores com mais de 65 vendas: 80.76%
Perderíamos essa porcentagem do df com vendedores com mais de 66 vendas: 80.76%
Perderíamos essa porcentagem do df com vendedores com mais de 67 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 68 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 69 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 70 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 71 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 72 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 73 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 74 vendas: 81.90%
Perderíamos essa porcentagem do df com vendedores com mais de 75 vendas: 83.18%
Perderíamos essa porcentagem do df com vendedores com mais de 76 vendas: 83.18%
Perderíamos essa porcentagem do df com vendedores com mais de 77 vendas: 83.18%
Perderíamos essa porcentagem do df com vendedores com mais de 78 vendas: 84.50%
Perderíamos essa porcentagem do df com vendedores com mais de 79 vendas: 85.85%
Perderíamos essa porcentagem do df com vendedores com mais de 80 vendas: 85.85%
Perderíamos essa porcentagem do df com vendedores com mais de 81 vendas: 85.85%
Perderíamos essa porcentagem do df com vendedores com mais de 82 vendas: 85.85%
Perderíamos essa porcentagem do df com vendedores com mais de 83 vendas: 85.85%
Perderíamos essa porcentagem do df com vendedores com mais de 84 vendas: 85.85%
Perderíamos essa porcentagem do df com vendedores com mais de 85 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 86 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 87 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 88 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 89 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 90 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 91 vendas: 87.30%
Perderíamos essa porcentagem do df com vendedores com mais de 92 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 93 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 94 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 95 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 96 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 97 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 98 vendas: 88.86%
Perderíamos essa porcentagem do df com vendedores com mais de 99 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 100 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 101 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 102 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 103 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 104 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 105 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 106 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 107 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 108 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 109 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 110 vendas: 90.55%
Perderíamos essa porcentagem do df com vendedores com mais de 111 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 112 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 113 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 114 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 115 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 116 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 117 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 118 vendas: 92.45%
Perderíamos essa porcentagem do df com vendedores com mais de 119 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 120 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 121 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 122 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 123 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 124 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 125 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 126 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 127 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 128 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 129 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 130 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 131 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 132 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 133 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 134 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 135 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 136 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 137 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 138 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 139 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 140 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 141 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 142 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 143 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 144 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 145 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 146 vendas: 94.48%
Perderíamos essa porcentagem do df com vendedores com mais de 147 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 148 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 149 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 150 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 151 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 152 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 153 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 154 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 155 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 156 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 157 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 158 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 159 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 160 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 161 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 162 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 163 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 164 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 165 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 166 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 167 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 168 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 169 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 170 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 171 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 172 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 173 vendas: 97.00%
Perderíamos essa porcentagem do df com vendedores com mais de 174 vendas: 97.00%

Optaremos por remover vendedores com 5 ou menos transações

Code
df_modelo_v3 = df_pre_modelo_v2[df_pre_modelo_v2['seller_id'].isin(df_pre_modelo_v2['seller_id'].value_counts()[df_pre_modelo_v2['seller_id'].value_counts() >= 5].index)]

df_modelo_v3
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week
7024 2024-08-05 Polaris 100000540 Soja Uberlândia MG 5.764 125.712651 1145.497646 Santos SP 32 Monday
7073 2024-08-05 Polaris 100000980 Soja Cristalina GO 5.764 124.222422 1096.595637 Santos SP 32 Monday
7072 2024-08-05 Polaris 100000759 Soja Querência MT 5.764 111.100457 1026.749060 Santos SP 32 Monday
7071 2024-08-05 Polaris 100000639 Soja Rio Grande RS 5.764 137.267385 997.318735 Santos SP 32 Monday
7069 2024-08-05 Polaris 100000002 Soja Rondonópolis MT 5.764 109.476566 959.707359 Santos SP 32 Monday
... ... ... ... ... ... ... ... ... ... ... ... ... ...
6991 2024-08-02 Polaris 100001719 Soja Rio Grande RS 5.736 138.879297 1126.802364 Santos SP 31 Friday
6992 2024-08-02 Polaris 100000901 Soja Joaçaba SC 5.736 150.498828 945.491532 Santos SP 31 Friday
6994 2024-08-02 Celestix 100000481 Soja Rio Grande RS 5.736 152.981792 1066.873615 Santos PR 31 Friday
6995 2024-08-02 Polaris 100001032 Milho Paragominas PA 5.736 55.445087 422.086114 Santos SP 31 Friday
6987 2024-08-02 Polaris 100001220 Milho Nova Mutum MT 5.736 39.004638 400.438451 Santos SP 31 Friday

4820 rows × 13 columns

Antes da predição, irei adicionar uma última variavel que é o dia do mes, que pode ser relevante para a predição e uma vez que modelos tradicionais de ML não têm a capacidade de lidar com séries temporais, essa variável pode vir a ser relevante.

Code
df_modelo_v3['month_day'] = df_modelo_v3['date'].dt.day
df_modelo_v3.tail()
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day
6991 2024-08-02 Polaris 100001719 Soja Rio Grande RS 5.736 138.879297 1126.802364 Santos SP 31 Friday 2
6992 2024-08-02 Polaris 100000901 Soja Joaçaba SC 5.736 150.498828 945.491532 Santos SP 31 Friday 2
6994 2024-08-02 Celestix 100000481 Soja Rio Grande RS 5.736 152.981792 1066.873615 Santos PR 31 Friday 2
6995 2024-08-02 Polaris 100001032 Milho Paragominas PA 5.736 55.445087 422.086114 Santos SP 31 Friday 2
6987 2024-08-02 Polaris 100001220 Milho Nova Mutum MT 5.736 39.004638 400.438451 Santos SP 31 Friday 2

Novos modelos com dados mais selecionados

Agora repetiremos os testes dos modelos de machine learning tradicionais e de séries temporais.

Code
X = df_modelo_v3.drop(['seller_id'], axis=1)
y = df_modelo_v3['seller_id']

# Dividindo em treinamento e teste considerando a ordem temporal
X_train, X_test = X[X['date'] < '2024-11-04'], X[X['date'] >= '2024-11-04']
y_train, y_test = y[:X_train.shape[0]], y[X_train.shape[0]:]

display(X_train.shape, X_test.shape)
display(y_train.shape, y_test.shape)

# Codificação de rótulos para todas as variáveis categóricas
le_company = LabelEncoder()
le_product = LabelEncoder()
le_origin_city = LabelEncoder()
le_origin_state = LabelEncoder()
le_destination_city = LabelEncoder()
le_destination_state = LabelEncoder()
le_day_of_week = LabelEncoder()
le_target = LabelEncoder()

X['company'] = le_company.fit_transform(X['company'])
X['product'] = le_product.fit_transform(X['product'])
X['origin_city'] = le_origin_city.fit_transform(X['origin_city'])
X['origin_state'] = le_origin_state.fit_transform(X['origin_state'])
X['destination_city'] = le_destination_city.fit_transform(X['destination_city'])
X['destination_state'] = le_destination_state.fit_transform(X['destination_state'])
X['day_of_week'] = le_day_of_week.fit_transform(X['day_of_week'])
y_train_encoded = le_target.fit_transform(y_train)

display(X_train.head(), X_train.shape)
display(y_train_encoded[5], y_train.shape)

# Garantir que y_train tenha valores inteiros consecutivos
unique_classes = np.unique(y_train_encoded)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train_encoded])

# Remover a coluna 'date' de X_train
X_train_no_date = X_train.drop(columns=['date'])

# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Lista de modelos para validação cruzada
models = {
    'DecisionTreeClassifier': DecisionTreeClassifier(max_depth=6, min_samples_split=2),
    'RandomForestClassifier': RandomForestClassifier(n_estimators=100, max_depth=6),
    'ExtraTreesClassifier': ExtraTreesClassifier(n_estimators=100, max_depth=6),
    'DummyClassifier': DummyClassifier(strategy='most_frequent'),
    'TimeSeriesForestClassifier': TimeSeriesForestClassifier(n_estimators=150, n_jobs=-1, random_state=42)
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 500 para o time series split
tscv = TimeSeriesSplit(max_train_size=500)

# Aplicando validação cruzada para cada modelo
for model_name, model in models.items():
    if model_name in ['DummyClassifier', 'TimeSeriesForestClassifier']:
        pipeline = make_pipeline(ColumnConcatenator(), model)
        scores = cross_validate(pipeline, from_2d_array_to_nested(X_train_no_date), y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    else:
        scores = cross_validate(model, X_train_no_date, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    
    cv_results[model_name] = scores
    print(f'{model_name} - Precisão de Teste: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Precisão de Treinamento: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Exibindo os resultados
cv_results

# Desempacotar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df

# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
results_df.T
(4810, 13)
(10, 13)
(4810,)
(10,)
date company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day
7024 2024-08-05 Polaris Soja Uberlândia MG 5.764 125.712651 1145.497646 Santos SP 32 Monday 5
7073 2024-08-05 Polaris Soja Cristalina GO 5.764 124.222422 1096.595637 Santos SP 32 Monday 5
7072 2024-08-05 Polaris Soja Querência MT 5.764 111.100457 1026.749060 Santos SP 32 Monday 5
7071 2024-08-05 Polaris Soja Rio Grande RS 5.764 137.267385 997.318735 Santos SP 32 Monday 5
7069 2024-08-05 Polaris Soja Rondonópolis MT 5.764 109.476566 959.707359 Santos SP 32 Monday 5
(4810, 13)
285
(4810,)
DecisionTreeClassifier - Precisão de Teste: 0.0201 (+/- 0.0300)
DecisionTreeClassifier - Precisão de Treinamento: 0.2149 (+/- 0.0315)
RandomForestClassifier - Precisão de Teste: 0.0193 (+/- 0.0283)
RandomForestClassifier - Precisão de Treinamento: 0.4877 (+/- 0.0283)
ExtraTreesClassifier - Precisão de Teste: 0.0159 (+/- 0.0191)
ExtraTreesClassifier - Precisão de Treinamento: 0.4835 (+/- 0.0409)
DummyClassifier - Precisão de Teste: 0.0022 (+/- 0.0019)
DummyClassifier - Precisão de Treinamento: 0.0044 (+/- 0.0023)
TimeSeriesForestClassifier - Precisão de Teste: 0.0373 (+/- 0.0484)
TimeSeriesForestClassifier - Precisão de Treinamento: 0.6131 (+/- 0.0914)
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
DecisionTreeClassifier 0.007798 0.008204 0.020061 0.214901 0.048190 0.2584 0.048190 0.2584
RandomForestClassifier 0.266954 0.067166 0.019287 0.487743 0.055930 0.4832 0.055930 0.4832
ExtraTreesClassifier 0.150302 0.051003 0.015886 0.483481 0.048689 0.4736 0.048689 0.4736
DummyClassifier 0.470200 0.583064 0.002239 0.004389 0.040949 0.0640 0.040949 0.0640
TimeSeriesForestClassifier 1.849717 1.474826 0.037257 0.613130 0.050687 0.6076 0.050687 0.6076

Ao que parece, as mudanças apenas surtiram efeitos negativos. Vamos tentar manter apenas os dados com vendedores que realizaram transações nos ultimos 30 dias.

Code
#dados anntes das mudanças
X = df_pre_modelo_v2.drop(['seller_id'], axis=1)
y = df_pre_modelo_v2['seller_id']

X['month_day'] = X['date'].dt.day
# Dividindo em treinamento e teste considerando a ordem temporal
X_train, X_test = X[X['date'] < '2024-11-04'], X[X['date'] >= '2024-11-04']
y_train, y_test = y[:X_train.shape[0]], y[X_train.shape[0]:]

display(X_train.shape, X_test.shape)
display(y_train.shape, y_test.shape)

# Codificação de rótulos para todas as variáveis categóricas
le_company = LabelEncoder()
le_product = LabelEncoder()
le_origin_city = LabelEncoder()
le_origin_state = LabelEncoder()
le_destination_city = LabelEncoder()
le_destination_state = LabelEncoder()
le_day_of_week = LabelEncoder()
le_target = LabelEncoder()

X['company'] = le_company.fit_transform(X['company'])
X['product'] = le_product.fit_transform(X['product'])
X['origin_city'] = le_origin_city.fit_transform(X['origin_city'])
X['origin_state'] = le_origin_state.fit_transform(X['origin_state'])
X['destination_city'] = le_destination_city.fit_transform(X['destination_city'])
X['destination_state'] = le_destination_state.fit_transform(X['destination_state'])
X['day_of_week'] = le_day_of_week.fit_transform(X['day_of_week'])
y_train_encoded = le_target.fit_transform(y_train)

display(X_train.head(), X_train.shape)
display(y_train_encoded[5], y_train.shape)

# Garantir que y_train tenha valores inteiros consecutivos
unique_classes = np.unique(y_train_encoded)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train_encoded])

# Remover a coluna 'date' de X_train
X_train_no_date = X_train.drop(columns=['date'])

# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Lista de modelos para validação cruzada
models = {
    'DecisionTreeClassifier': DecisionTreeClassifier(max_depth=6, min_samples_split=2),
    'RandomForestClassifier': RandomForestClassifier(n_estimators=100, max_depth=6),
    'ExtraTreesClassifier': ExtraTreesClassifier(n_estimators=100, max_depth=6),
    'DummyClassifier': DummyClassifier(strategy='most_frequent'),
    'TimeSeriesForestClassifier': TimeSeriesForestClassifier(n_estimators=150, n_jobs=-1, random_state=42)
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 500 para o time series split
tscv = TimeSeriesSplit(max_train_size=500)

# Aplicando validação cruzada para cada modelo
for model_name, model in models.items():
    if model_name in ['DummyClassifier', 'TimeSeriesForestClassifier']:
        pipeline = make_pipeline(ColumnConcatenator(), model)
        scores = cross_validate(pipeline, from_2d_array_to_nested(X_train_no_date), y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    else:
        scores = cross_validate(model, X_train_no_date, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    
    cv_results[model_name] = scores
    print(f'{model_name} - Precisão de Teste: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Precisão de Treinamento: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Exibindo os resultados
cv_results

# Desempacotar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df

# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
results_df.T
(5785, 13)
(16, 13)
(5785,)
(16,)
date company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day
7024 2024-08-05 Polaris Soja Uberlândia MG 5.764 125.712651 1145.497646 Santos SP 32 Monday 5
7079 2024-08-05 Lunarix Soja Sinop MT 5.764 106.934372 996.212223 Santos SP 32 Monday 5
7078 2024-08-05 Lunarix Soja Boa Vista RR 5.764 123.753326 1063.886595 Santos SP 32 Monday 5
7077 2024-08-05 Solara Soja Barreiras BA 5.764 115.276863 1137.705266 Uberaba MT 32 Monday 5
7076 2024-08-05 Polaris Soja Redenção PA 5.764 111.325147 1033.918821 Santos SP 32 Monday 5
(5785, 13)
532
(5785,)
DecisionTreeClassifier - Precisão de Teste: 0.0223 (+/- 0.0244)
DecisionTreeClassifier - Precisão de Treinamento: 0.1627 (+/- 0.0647)
RandomForestClassifier - Precisão de Teste: 0.0187 (+/- 0.0205)
RandomForestClassifier - Precisão de Treinamento: 0.4374 (+/- 0.0551)
ExtraTreesClassifier - Precisão de Teste: 0.0220 (+/- 0.0229)
ExtraTreesClassifier - Precisão de Treinamento: 0.4402 (+/- 0.0527)
DummyClassifier - Precisão de Teste: 0.0011 (+/- 0.0013)
DummyClassifier - Precisão de Treinamento: 0.0030 (+/- 0.0005)
TimeSeriesForestClassifier - Precisão de Teste: 0.0403 (+/- 0.0393)
TimeSeriesForestClassifier - Precisão de Treinamento: 0.5771 (+/- 0.1072)
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
DecisionTreeClassifier 0.008181 0.010003 0.022292 0.162734 0.025311 0.2156 0.025311 0.2156
RandomForestClassifier 0.260190 0.247961 0.018715 0.437372 0.043361 0.4820 0.043361 0.4820
ExtraTreesClassifier 0.210856 0.265676 0.021966 0.440215 0.037759 0.4588 0.037759 0.4588
DummyClassifier 0.322749 0.527103 0.001143 0.002984 0.026141 0.0544 0.026141 0.0544
TimeSeriesForestClassifier 2.288895 2.546812 0.040323 0.577136 0.046680 0.6028 0.046680 0.6028

Diferentemente do que pensamos, retirar dados apenas piorou ou continuou com o overfitting, o que poderia ter sido esperado, já que mais dados favorecem menor overfitting.

A ideia que eu estava em mente é que menos classes disponíveis para o modelo melhoraria seus resultados, mas isso não foi comprovado.

Dessa forma, vamos tentar fazer uma abordagem reversa, que seria de adotar novamente os vendedores cujas transações foram de apenas uma. E novamente rodar os modelos.

Code
X = df_pre_modelo.drop(['seller_id'], axis=1)
y = df_pre_modelo['seller_id']

X['month_day'] = X['date'].dt.day

# Dividindo em treinamento e teste considerando a ordem temporal
X_train, X_test = X[X['date'] < '2024-11-04'], X[X['date'] >= '2024-11-04']
y_train, y_test = y[:X_train.shape[0]], y[X_train.shape[0]:]

display(X_train.shape, X_test.shape)
display(y_train.shape, y_test.shape)

# Codificação de rótulos para todas as variáveis categóricas
le_company = LabelEncoder()
le_product = LabelEncoder()
le_origin_city = LabelEncoder()
le_origin_state = LabelEncoder()
le_destination_city = LabelEncoder()
le_destination_state = LabelEncoder()
le_day_of_week = LabelEncoder()
le_target = LabelEncoder()

X['company'] = le_company.fit_transform(X['company'])
X['product'] = le_product.fit_transform(X['product'])
X['origin_city'] = le_origin_city.fit_transform(X['origin_city'])
X['origin_state'] = le_origin_state.fit_transform(X['origin_state'])
X['destination_city'] = le_destination_city.fit_transform(X['destination_city'])
X['destination_state'] = le_destination_state.fit_transform(X['destination_state'])
X['day_of_week'] = le_day_of_week.fit_transform(X['day_of_week'])
y_train_encoded = le_target.fit_transform(y_train)

display(X_train.head(), X_train.shape)
display(y_train_encoded[5], y_train.shape)

# Garantir que y_train tenha valores inteiros consecutivos
unique_classes = np.unique(y_train_encoded)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train_encoded])

# Remover a coluna 'date' de X_train
X_train_no_date = X_train.drop(columns=['date'])

# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Lista de modelos para validação cruzada
models = {
    'DecisionTreeClassifier': DecisionTreeClassifier(max_depth=6, min_samples_split=2),
    'RandomForestClassifier': RandomForestClassifier(n_estimators=100, max_depth=6),
    'ExtraTreesClassifier': ExtraTreesClassifier(n_estimators=100, max_depth=6),
    'DummyClassifier': DummyClassifier(strategy='most_frequent'),
    'TimeSeriesForestClassifier': TimeSeriesForestClassifier(n_estimators=50,  n_jobs=-1, random_state=42)
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 500 para o time series split
tscv = TimeSeriesSplit(max_train_size=2000)

# Aplicando validação cruzada para cada modelo
for model_name, model in models.items():
    if model_name in ['DummyClassifier', 'TimeSeriesForestClassifier']:
        pipeline = make_pipeline(ColumnConcatenator(), model)
        scores = cross_validate(pipeline, from_2d_array_to_nested(X_train_no_date), y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    else:
        scores = cross_validate(model, X_train_no_date, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    
    cv_results[model_name] = scores
    print(f'{model_name} - Precisão de Teste: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Precisão de Treinamento: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Exibindo os resultados
cv_results

# Desempacotar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df

# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
results_df.T
(9491, 13)
(18, 13)
(9491,)
(18,)
date company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day
0 2024-01-02 Polaris Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday 2
1 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3
2 2024-01-03 Lunarix Soja Porto Velho RO 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3
3 2024-01-03 Lunarix Soja Boa Vista RR 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3
4 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3
(9491, 13)
866
(9491,)
DecisionTreeClassifier - Precisão de Teste: 0.0524 (+/- 0.0294)
DecisionTreeClassifier - Precisão de Treinamento: 0.1163 (+/- 0.0320)
RandomForestClassifier - Precisão de Teste: 0.0548 (+/- 0.0212)
RandomForestClassifier - Precisão de Treinamento: 0.2496 (+/- 0.0253)
ExtraTreesClassifier - Precisão de Teste: 0.0522 (+/- 0.0174)
ExtraTreesClassifier - Precisão de Treinamento: 0.2583 (+/- 0.0345)
DummyClassifier - Precisão de Teste: 0.0004 (+/- 0.0004)
DummyClassifier - Precisão de Treinamento: 0.0009 (+/- 0.0005)
TimeSeriesForestClassifier - Precisão de Teste: 0.0902 (+/- 0.0319)
TimeSeriesForestClassifier - Precisão de Treinamento: 0.5788 (+/- 0.0272)
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
DecisionTreeClassifier 0.051199 0.026601 0.052446 0.116278 0.097407 0.187410 0.097407 0.187410
RandomForestClassifier 1.367680 1.751306 0.054838 0.249579 0.117394 0.324474 0.117394 0.324474
ExtraTreesClassifier 0.369401 1.687527 0.052224 0.258299 0.122834 0.325901 0.122834 0.325901
DummyClassifier 1.292218 0.993406 0.000419 0.000873 0.017457 0.028422 0.017457 0.028422
TimeSeriesForestClassifier 11.525586 4.293025 0.090175 0.578813 0.130297 0.619602 0.130297 0.619602

Os resultados foram melhores para o TimeSeriesForestClassifier, conseguindo alcançar um precision weighted de aproximadamente 10%, mas ainda está longe de ser ótimo, uma vez que o treino possui uma precisão muito maior (de 58%)

Como o modelo continua apresentando overfitting, podemos tentar adicionar mais dados para treino.

Irei tentar técnicas iniciais de feature engineering.

Code
df_modelo_v4 = df_pre_modelo.copy()

df_modelo_v4['month_day'] = df_modelo_v4['date'].dt.day
df_modelo_v4['cbot_dol'] = df_modelo_v4['cbot'].div(df_modelo_v4['dolar'])
df_modelo_v4['price_dol'] = df_modelo_v4['price_merc'].div(df_modelo_v4['dolar'])
df_modelo_v4['pric_cbot_sqrt'] = np.sqrt(df_modelo_v4['price_merc'].mul(df_modelo_v4['cbot']))
df_modelo_v4
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day cbot_dol price_dol pric_cbot_sqrt
0 2024-01-02 Polaris 100000864 Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday 2 232.611995 23.569181 362.147889
1 2024-01-03 Polaris 100000865 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
2 2024-01-03 Lunarix 100000094 Soja Porto Velho RO 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
3 2024-01-03 Lunarix 100000314 Soja Boa Vista RR 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
4 2024-01-03 Polaris 100000866 Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
9503 2024-11-04 Polaris 100000060 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday 4 164.239024 19.571250 328.220737
9504 2024-11-04 Polaris 100000060 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday 4 164.239024 19.571250 328.220737
9505 2024-11-04 Polaris 100002402 Soja Balsas MA 5.7892 122.683527 997.535892 Santos SP 45 Monday 4 172.309800 21.191793 349.830275
9506 2024-11-04 Polaris 100002339 Soja Sambaíba MA 5.7892 118.333486 1087.257588 Santos SP 45 Monday 4 187.807916 20.440387 358.690647
9496 2024-11-04 Polaris 100002271 Soja Uberlândia MG 5.7892 120.336453 993.482226 Santos SP 45 Monday 4 171.609588 20.786370 345.763109

9509 rows × 17 columns

Aqui incluiremos alguns outros modelos pra ver se performam melhor

Code
X = df_modelo_v4.drop(['seller_id'], axis=1)
y = df_modelo_v4['seller_id']

# Dividindo em treinamento e teste considerando a ordem temporal
X_train, X_test = X[X['date'] < '2024-11-04'], X[X['date'] >= '2024-11-04']
y_train, y_test = y[:X_train.shape[0]], y[X_train.shape[0]:]

display(X_train.shape, X_test.shape)
display(y_train.shape, y_test.shape)

display(X_train.head(), X_train.shape)
display(y_train_encoded[5], y_train.shape)

# Garantir que y_train tenha valores inteiros consecutivos
unique_classes = np.unique(y_train_encoded)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train_encoded])

# Remover a coluna 'date' de X_train
X_train_no_date = X_train.drop(columns=['date'])

# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Lista de modelos para validação cruzada
models = {
    'DecisionTreeClassifier': DecisionTreeClassifier(max_depth=6, min_samples_split=2),
    'RandomForestClassifier': RandomForestClassifier(n_estimators=100, max_depth=6),
    'ExtraTreesClassifier': ExtraTreesClassifier(n_estimators=100, max_depth=6),
    'DummyClassifier': DummyClassifier(strategy='most_frequent'),
    'TimeSeriesForestClassifier': TimeSeriesForestClassifier(n_estimators=50,  n_jobs=-1, random_state=42)
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 500 para o time series split
tscv = TimeSeriesSplit(max_train_size=2000)

# Aplicando cross-validation para cada modelo
for model_name, model in models.items():
    if model_name in ['DummyClassifier', 'TimeSeriesForestClassifier']:
        pipeline = make_pipeline(ColumnConcatenator(), model)
        scores = cross_validate(pipeline, from_2d_array_to_nested(X_train_no_date), y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    else:
        scores = cross_validate(model, X_train_no_date, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    
    cv_results[model_name] = scores
    print(f'{model_name} - Precisão de Teste: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Precisão de Treinamento: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Exibindo os resultados
cv_results

# Desempacotar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df

# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
results_df.T
(9491, 16)
(18, 16)
(9491,)
(18,)
date company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day cbot_dol price_dol pric_cbot_sqrt
0 2024-01-02 Polaris Soja Uberlândia MG 4.8910 115.276863 1137.705266 Santos SP 1 Tuesday 2 232.611995 23.569181 362.147889
1 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
2 2024-01-03 Lunarix Soja Porto Velho RO 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
3 2024-01-03 Lunarix Soja Boa Vista RR 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
4 2024-01-03 Polaris Soja Rondonópolis MT 4.9206 115.276863 1137.705266 Santos SP 1 Wednesday 3 231.212711 23.427400 362.147889
(9491, 16)
866
(9491,)
DecisionTreeClassifier - Precisão de Teste: 0.0521 (+/- 0.0281)
DecisionTreeClassifier - Precisão de Treinamento: 0.1180 (+/- 0.0328)
RandomForestClassifier - Precisão de Teste: 0.0456 (+/- 0.0170)
RandomForestClassifier - Precisão de Treinamento: 0.2596 (+/- 0.0300)
ExtraTreesClassifier - Precisão de Teste: 0.0481 (+/- 0.0199)
ExtraTreesClassifier - Precisão de Treinamento: 0.2647 (+/- 0.0342)
DummyClassifier - Precisão de Teste: 0.0004 (+/- 0.0004)
DummyClassifier - Precisão de Treinamento: 0.0009 (+/- 0.0005)
TimeSeriesForestClassifier - Precisão de Teste: 0.0896 (+/- 0.0310)
TimeSeriesForestClassifier - Precisão de Treinamento: 0.5794 (+/- 0.0267)
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
DecisionTreeClassifier 0.089291 0.017709 0.052053 0.118021 0.097913 0.187884 0.097913 0.187884
RandomForestClassifier 1.440251 1.165223 0.045601 0.259564 0.103858 0.324461 0.103858 0.324461
ExtraTreesClassifier 0.315847 1.418979 0.048115 0.264712 0.109171 0.324871 0.109171 0.324871
DummyClassifier 1.464661 1.231508 0.000419 0.000873 0.017457 0.028422 0.017457 0.028422
TimeSeriesForestClassifier 12.176355 4.199526 0.089636 0.579377 0.136496 0.619602 0.136496 0.619602

Ao que parece, não gerou resultado favorável.

portanto tentaremos alguns outros diferentes modelos do pacote sktime junto com o TimeSeriesForestClassifier para prever os vendedores. com os dados df_pre_modelo.

Code
if 'price_dol' in df_pre_modelo.columns:
    df_pre_modelo.drop(['cbot_dol','price_dol','attempt','pric_cbot_sqrt'], axis=1, inplace=True)
Code
df_pre_modelo['month_day'] = df_pre_modelo['date'].dt.day

df_pre_modelo.tail()
date company seller_id product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day
9503 2024-11-04 Polaris 100000060 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday 4
9504 2024-11-04 Polaris 100000060 Soja Porto Velho RO 5.7892 113.301882 950.812555 Santos SP 45 Monday 4
9505 2024-11-04 Polaris 100002402 Soja Balsas MA 5.7892 122.683527 997.535892 Santos SP 45 Monday 4
9506 2024-11-04 Polaris 100002339 Soja Sambaíba MA 5.7892 118.333486 1087.257588 Santos SP 45 Monday 4
9496 2024-11-04 Polaris 100002271 Soja Uberlândia MG 5.7892 120.336453 993.482226 Santos SP 45 Monday 4
Code
from sktime.classification.dictionary_based import IndividualBOSS, IndividualTDE
from sktime.classification.dictionary_based import MUSE
from sklearn.preprocessing import StandardScaler
import numpy as np
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import cross_validate, TimeSeriesSplit
from sktime.transformations.panel.compose import ColumnConcatenator
from sktime.datatypes._panel._convert import from_2d_array_to_nested
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from sklearn.dummy import DummyClassifier
from sktime.classification.interval_based import TimeSeriesForestClassifier

X = df_pre_modelo.drop(['seller_id'], axis=1)
y = df_pre_modelo['seller_id']

# Dividindo em treinamento e teste considerando a ordem temporal
X_train, X_test = X[X['date'] < '2024-11-04'], X[X['date'] >= '2024-11-04']
y_train, y_test = y[:X_train.shape[0]], y[X_train.shape[0]:]

display(X_train.shape, X_test.shape)

# Garantir que y_train tenha valores inteiros consecutivos
unique_classes = np.unique(y_train)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train])

# Remover a coluna 'date' de X_train
X_train_no_date = X_train.drop(columns=['date'])

# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])

# Garantir que todos os dados passados para o StandardScaler sejam numéricos
X_train_no_date = X_train_no_date.apply(pd.to_numeric)

# Converter X_train_no_date para um formato aninhado compatível com sktime
X_train_nested = from_2d_array_to_nested(X_train_no_date)

# Lista de modelos para validação cruzada
models = {
    'IndividualBOSS': IndividualBOSS(),
    'IndividualTDE': IndividualTDE(),
    'MUSE': MUSE(),
    'DecisionTreeClassifier': DecisionTreeClassifier(max_depth=6, min_samples_split=2),
    'RandomForestClassifier': RandomForestClassifier(n_estimators=100, max_depth=6),
    'ExtraTreesClassifier': ExtraTreesClassifier(n_estimators=100, max_depth=6),
    'DummyClassifier': DummyClassifier(strategy='most_frequent'),
    'TimeSeriesForestClassifier': TimeSeriesForestClassifier(n_estimators=50, n_jobs=-1, random_state=42),
}

# Dicionário para armazenar os resultados
cv_results = {}

# Utilizando uma janela de 2000 para o time series split
tscv = TimeSeriesSplit(max_train_size=2000)

# Aplicando validação cruzada para cada modelo
for model_name, model in models.items():
    if model_name in ['DummyClassifier', 'TimeSeriesForestClassifier',
                       'IndividualBOSS','IndividualTDE','MUSE']:
        pipeline = make_pipeline(ColumnConcatenator(), model)
        scores = cross_validate(pipeline, X_train_nested, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    else:
        scores = cross_validate(model, X_train_no_date, y_train_mapped, cv=tscv, scoring=['precision_weighted', 'accuracy', 'recall_weighted'], return_train_score=True, n_jobs=-1)
    
    cv_results[model_name] = scores
    print(f'{model_name} - Precisão de Teste: {scores["test_precision_weighted"].mean():.4f} (+/- {scores["test_precision_weighted"].std():.4f})')
    print(f'{model_name} - Precisão de Treinamento: {scores["train_precision_weighted"].mean():.4f} (+/- {scores["train_precision_weighted"].std():.4f})')

# Descompactar as listas no dicionário e criar um DataFrame
results_df = pd.DataFrame({model: {metric: scores for metric, scores in cv_results[model].items()} for model in cv_results})

# Exibir o DataFrame
results_df

# Calcular a média para cada métrica no dicionário
mean_results = {model: {metric: np.mean(scores) for metric, scores in cv_results[model].items()} for model in cv_results}

# Criar um DataFrame a partir dos resultados médios
mean_results_df = pd.DataFrame(mean_results)

# Exibir o DataFrame
mean_results_df.T
(9491, 13)
(18, 13)
IndividualBOSS - Precisão de Teste: 0.0010 (+/- 0.0006)
IndividualBOSS - Precisão de Treinamento: 0.0168 (+/- 0.0040)
IndividualTDE - Precisão de Teste: 0.0270 (+/- 0.0131)
IndividualTDE - Precisão de Treinamento: 0.1857 (+/- 0.0183)
MUSE - Precisão de Teste: 0.0307 (+/- 0.0124)
MUSE - Precisão de Treinamento: 0.2849 (+/- 0.0773)
DecisionTreeClassifier - Precisão de Teste: 0.0523 (+/- 0.0296)
DecisionTreeClassifier - Precisão de Treinamento: 0.1163 (+/- 0.0320)
RandomForestClassifier - Precisão de Teste: 0.0498 (+/- 0.0187)
RandomForestClassifier - Precisão de Treinamento: 0.2458 (+/- 0.0307)
ExtraTreesClassifier - Precisão de Teste: 0.0535 (+/- 0.0182)
ExtraTreesClassifier - Precisão de Treinamento: 0.2601 (+/- 0.0277)
DummyClassifier - Precisão de Teste: 0.0004 (+/- 0.0004)
DummyClassifier - Precisão de Treinamento: 0.0009 (+/- 0.0005)
TimeSeriesForestClassifier - Precisão de Teste: 0.0902 (+/- 0.0319)
TimeSeriesForestClassifier - Precisão de Treinamento: 0.5788 (+/- 0.0272)
fit_time score_time test_precision_weighted train_precision_weighted test_accuracy train_accuracy test_recall_weighted train_recall_weighted
IndividualBOSS 2.732303 2.441194 0.000995 0.016792 0.005440 0.022448 0.005440 0.022448
IndividualTDE 3.922540 36.197457 0.027048 0.185721 0.025427 0.176114 0.025427 0.176114
MUSE 7.396641 1.316267 0.030682 0.284899 0.072992 0.374039 0.072992 0.374039
DecisionTreeClassifier 0.034979 0.013800 0.052268 0.116278 0.097786 0.187410 0.097786 0.187410
RandomForestClassifier 0.974226 1.372157 0.049827 0.245816 0.115370 0.322457 0.115370 0.322457
ExtraTreesClassifier 0.362330 1.609855 0.053494 0.260086 0.124478 0.328192 0.124478 0.328192
DummyClassifier 0.975559 0.721989 0.000419 0.000873 0.017457 0.028422 0.017457 0.028422
TimeSeriesForestClassifier 10.015471 3.697557 0.090175 0.578813 0.130297 0.619602 0.130297 0.619602

Vamos tentar um rápido GridSearch para tentar otimizar o modelo (ainda não se trata do modelo final, que também passará por um gridsearch).

Code
from sklearn.model_selection import GridSearchCV

# Definir a grade de parâmetros
param_grid = {
    'timeseriesforestclassifier__n_estimators': range(10, 101, 10)
}
tscv = TimeSeriesSplit(max_train_size=2000)
# Criar o pipeline
pipeline = make_pipeline(ColumnConcatenator(), TimeSeriesForestClassifier(n_jobs=-1, random_state=42))

# Criar o objeto GridSearchCV
grid_search = GridSearchCV(pipeline, param_grid, cv=tscv, scoring='precision_weighted', n_jobs=-1)

# Ajustar o modelo
grid_search.fit(X_train_nested, y_train_mapped)

# Imprimir os melhores parâmetros e a melhor pontuação
print("Melhores parâmetros: ", grid_search.best_params_)
print("Melhor score encontrado: ", grid_search.best_score_)
Melhores parâmetros:  {'timeseriesforestclassifier__n_estimators': 100}
Melhor score encontrado:  0.0970069316486977

Agora temos um modelo time series com random forest com os melhores parametros para dados até dia 01-11-2024.

Code
tsfc_model_v1 = grid_search.best_estimator_

Aqui iremos fazer o predict e o predict probabilidade (a partir do qual descobriremos a probabilidade de cada vendedor fazer uma transação no dia 04-11-2024).

Code
# Remover a coluna 'date' de X_test
if 'date' in X_test.columns:
    X_test_no_date = X_test.drop(columns=['date'])
else:
    X_test_no_date = X_test

for col, le in label_encoders.items():
    X_test_no_date[col] = le.transform(X_test_no_date[col])

# Converter X_test_no_date para um formato aninhado compatível com sktime
X_test_nested = from_2d_array_to_nested(X_test_no_date)

# Fazer previsões
predicts = tsfc_model_v1.predict(X_test_nested)
predicts_prob = tsfc_model_v1.predict_proba(X_test_nested)

Por fim, fazemos a manipulação dos dados e definimos os vendedores que mais provavelmente farão transações no dia 04-11-2024.

Code
# Selecionar as 100 maiores probabilidades
top_100_indices = np.argsort(predicts_prob, axis=None)[-100:]
top_100_values = np.take(predicts_prob, top_100_indices)

# Obter as coordenadas das 100 maiores probabilidades
top_100_coords = np.unravel_index(top_100_indices, predicts_prob.shape)

# Obter os nomes dos vendedores a partir dos índices
top_100_sellers = le_target.inverse_transform(top_100_coords[1])

# Criar um DataFrame com os resultados
df_top_100 = pd.DataFrame({
    'Index': list(zip(*top_100_coords)),
    'Probability': top_100_values,
    'Seller': top_100_sellers
})

# Exibir o DataFrame
df_top_100

# Selecionar as 100 maiores probabilidades para cada linha
top_100_indices_per_row = np.argsort(predicts_prob, axis=1)[:, -100:]
top_100_values_per_row = np.take_along_axis(predicts_prob, top_100_indices_per_row, axis=1)

# Obter os nomes dos vendedores a partir dos índices
top_100_sellers_per_row = le_target.inverse_transform(top_100_indices_per_row.flatten()).reshape(top_100_indices_per_row.shape)

# Criar uma lista de dataframes para cada linha
df_list = []
for i in range(predicts_prob.shape[0]):
    df_top_100_per_row = pd.DataFrame({
        'Probability': top_100_values_per_row[i],
        'Seller': top_100_sellers_per_row[i]
    })
    df_top_100_per_row['Row'] = i
    df_list.append(df_top_100_per_row)

# Concatenar todos os dataframes em um único dataframe
df_top_100_all = pd.concat(df_list, ignore_index=True)

top_100_sellers_04_11 = df_top_100_all.pivot_table(index='Seller', values='Probability', aggfunc='max').sort_values(by='Probability', ascending=False).fillna(0)
top_100_sellers_04_11 = top_100_sellers_04_11[top_100_sellers_04_11['Probability'] > 0.01]
top_100_sellers_04_11
Probability
Seller
100002332 0.410000
100001835 0.320000
100002039 0.240000
100001739 0.235000
100000348 0.181524
... ...
100002046 0.011778
100000500 0.011667
100000490 0.011667
100000339 0.011667
100002232 0.011250

196 rows × 1 columns

Explicando probabilidades no dia 04-11

Agora veremos os top 100 vendedores com maior probabilidade de venda, para cada uma das informações de mercado.

Essa predição foi feita pra cada linha do conjunto de teste. No caso, temos 196 vendedores que poderiam fazer uma transação no dia 04/11/2024, pois para cada informação de mercado, há a probabilidade de diferentes vendedores realizarem a transação.

Treinando o modelo final

Agora iremos unir todos os dados pra treinar o modelo final para o modelo TimeSeriesForestClassifier, que foi o modelo que apresentou o melhor resultado até aqui.

E faremos o grid_search, de modo a garantir já o melhor modelo.

Mas antes iremos remover qualquer item da memória que não seja necessário.

Code
# Verificar se as variáveis estão disponíveis antes de removê-las
vars_to_delete = [
    'df_completo_v1', 'df_completo_v2', 'df_completo_v3', 'df_completo_v4_test', 'df_dollar', 'df_filtered', 
    'df_full', 'df_list', 'df_merc_join', 'df_merc_pre_proc_sem_intersecao', 
    'df_mercado_M', 'df_mercado_nulos', 'df_mercado_teste', 'df_modelo_v3', 'df_modelo_v4', 'df_top_100', 
    'df_top_100_all', 'df_top_100_per_row', 'df_trans_pre_pros', 'df_transacoes', 'df_transacoes_M', 
    'df_transacoes_nulos', 'df_transacoes_teste', 'duplicated_mercado', 'duplicated_transacoes', 'outliers_amount', 
    'outliers_price', 'resampled_df_transacoes'
]

for var in vars_to_delete:
    if var in globals():
        del globals()[var]

# Remover arrays não mais utilizados
arrays_to_delete = [
    'top_100_indices', 'top_100_values', 'top_100_coords', 'top_100_sellers', 'top_100_indices_per_row', 
    'top_100_values_per_row', 'top_100_sellers_per_row'
]

for array in arrays_to_delete:
    if array in globals():
        del globals()[array]

gc.collect()
52

Agora faremos o fit do modelo final, ja definindo os melhores parametros com o gridsearch.

Neste momento, reduziremos o número de parâmetros e substituiremos pelo RandomizedSearch, pois após diversas tentativas, a memória não foi suficiente e o modelo não rodou.

Além disso, o grid search aqui não melhorou muito as respostas (precisaríamos tentar um número grande de parâmetros para melhorar o modelo e reduzir o overfitting), então essa retirada não se faz potencialmente danosa.

Code
from sklearn.model_selection import RandomizedSearchCV

X = df_pre_modelo.drop(['seller_id'], axis=1)
y = df_pre_modelo['seller_id']

# Garantir que y_train tenha valores inteiros consecutivos
unique_classes = np.unique(y_train)
class_mapping = {cls: idx for idx, cls in enumerate(unique_classes)}
y_train_mapped = np.array([class_mapping[cls] for cls in y_train])

# Remover a coluna 'date' de X_train
X_train_no_date = X_train.drop(columns=['date'])

# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_train_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_train_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_train_no_date[col] = le.transform(X_train_no_date[col])


# Converter X_train_no_date para um formato aninhado compatível com sktime
X_train_nested = from_2d_array_to_nested(X_train_no_date)

# Definir a grade de parâmetros
param_grid = {
    'timeseriesforestclassifier__n_estimators': range(10, 51, 10) 
}
tscv = TimeSeriesSplit(max_train_size=1000)  

#criando o pipeline
pipeline = make_pipeline(ColumnConcatenator(), TimeSeriesForestClassifier(random_state=42))

# Criar o objeto RandomizedSearch
randomized_ = RandomizedSearchCV(pipeline, param_grid, cv=tscv, scoring='precision_weighted', n_jobs=2) 

# Ajustar o modelo
randomized_.fit(X_train_nested, y_train_mapped)

# Imprimir os melhores parâmetros e a melhor pontuação
print("Melhores parametros: ", grid_search.best_params_)
print("Melhores scores: ", grid_search.best_score_)
Melhores parametros:  {'timeseriesforestclassifier__n_estimators': 100}
Melhores scores:  0.0970069316486977
Code
tsfc_model_v2 = randomized_.best_estimator_

Agora temos o modelo final, iremos apenas fazer a preparação dos dados para o dia 05/11/2024.

Com os dados preparados, faremos o predict, definiremos a probabilidade dos vendedores realizarem uma transação no dia 05/11/2024 e decidiremos os próximos passos.

Ajustando os dados do dia 05/11/2024 para a predição do modelo.

Code
df_merc_pre_proc.tail()
company origin_city origin_state destination_city destination_state product price cbot dolar date
date_index
2024-11-05 Polaris Uruaçu GO Tubarão SC Soja 122.733502 1033.708378 5.784 2024-11-05
2024-11-05 Polaris Uruaçu GO Santos SP Soja 119.679794 996.462259 5.784 2024-11-05
2024-11-05 Polaris União do Sul MT Barcarena PA Milho 49.221543 441.941709 5.784 2024-11-05
2024-11-05 Polaris Vila Bela da Santíssima Trindade MT Santos SP Milho 44.600848 415.780537 5.784 2024-11-05
2024-11-05 Solara Vicentinópolis GO Água Boa MT Milho 55.576690 397.344017 5.784 2024-11-05
Code
df_merc_11_05 = df_merc_pre_proc[df_merc_pre_proc.index == '2024-11-05']
df_merc_11_05.tail()
company origin_city origin_state destination_city destination_state product price cbot dolar date
date_index
2024-11-05 Polaris Uruaçu GO Tubarão SC Soja 122.733502 1033.708378 5.784 2024-11-05
2024-11-05 Polaris Uruaçu GO Santos SP Soja 119.679794 996.462259 5.784 2024-11-05
2024-11-05 Polaris União do Sul MT Barcarena PA Milho 49.221543 441.941709 5.784 2024-11-05
2024-11-05 Polaris Vila Bela da Santíssima Trindade MT Santos SP Milho 44.600848 415.780537 5.784 2024-11-05
2024-11-05 Solara Vicentinópolis GO Água Boa MT Milho 55.576690 397.344017 5.784 2024-11-05
Code
df_merc_11_05 = df_merc_11_05.loc[:, ['company', 'product', 'origin_city', 'origin_state', 'dolar', 'price', 'cbot',
                    'destination_city', 'destination_state']]

df_merc_11_05['week_of_year'] = pd.to_datetime(df_merc_11_05.index).isocalendar().week
df_merc_11_05['day_of_week'] = pd.to_datetime(df_merc_11_05.index).dayofweek
df_merc_11_05['month_day'] = pd.to_datetime(df_merc_11_05.index).day
df_merc_11_05.reset_index(drop=True, inplace=True)

df_merc_11_05.columns = ['company', 'product', 'origin_city', 'origin_state', 'dolar', 'price_merc', 'cbot',
                    'destination_city', 'destination_state', 'week_of_year', 'day_of_week', 'month_day']

X_test = df_merc_11_05
df_merc_11_05
company product origin_city origin_state dolar price_merc cbot destination_city destination_state week_of_year day_of_week month_day
0 Polaris Soja Formosa GO 5.784 134.813068 963.577372 Cristalina GO 45 1 5
1 Polaris Soja Gaúcha do Norte MT 5.784 93.585026 1075.960638 Santos SP 45 1 5
2 Polaris Soja Guarantã do Norte MT 5.784 123.918571 1081.237240 Rondonópolis MT 45 1 5
3 Polaris Soja Guaraí TO 5.784 113.876140 1033.092819 Barcarena PA 45 1 5
4 Polaris Milho Guaraí TO 5.784 50.538869 441.374111 Guaraí TO 45 1 5
... ... ... ... ... ... ... ... ... ... ... ... ...
1536 Polaris Soja Uruaçu GO 5.784 122.733502 1033.708378 Tubarão SC 45 1 5
1537 Polaris Soja Uruaçu GO 5.784 119.679794 996.462259 Santos SP 45 1 5
1538 Polaris Milho União do Sul MT 5.784 49.221543 441.941709 Barcarena PA 45 1 5
1539 Polaris Milho Vila Bela da Santíssima Trindade MT 5.784 44.600848 415.780537 Santos SP 45 1 5
1540 Solara Milho Vicentinópolis GO 5.784 55.576690 397.344017 Água Boa MT 45 1 5

1541 rows × 12 columns

Code
# Remover a coluna 'date' de df_mercado_11_05 se existir
if 'date' in df_merc_11_05.columns:
    X_test_no_date = X_test.drop(columns=['date'])
else:
    X_test_no_date = X_test
Code
# Codificar variáveis categóricas em X_train_no_date
categorical_cols = X_test_no_date.select_dtypes(include=['category', 'object']).columns
label_encoders = {col: LabelEncoder().fit(X_test_no_date[col]) for col in categorical_cols}

for col, le in label_encoders.items():
    X_test_no_date[col] = le.transform(X_test_no_date[col])

# Converter X_test_no_date para um formato aninhado compatível com sktime
X_test_no_date = from_2d_array_to_nested(X_test_no_date)
Code
# Make predictions
predicts_v2 = tsfc_model_v2.predict(X_test_no_date)
predicts_v2_probs = tsfc_model_v2.predict_proba(X_test_no_date)
Code
# Selecionar as 100 maiores probabilidades
top_100_indices = np.argsort(predicts_v2_probs, axis=None)[-100:]
top_100_values = np.take(predicts_v2_probs, top_100_indices)

# Obter as coordenadas das 100 maiores probabilidades
top_100_coords = np.unravel_index(top_100_indices, predicts_v2_probs.shape)

# Obter os nomes dos vendedores a partir dos índices
top_100_sellers = le_target.inverse_transform(top_100_coords[1])

# Criar um DataFrame com os resultados
df_top_100 = pd.DataFrame({
    'Index': list(zip(*top_100_coords)),
    'Probability': top_100_values,
    'Seller': top_100_sellers
})

# Exibir o DataFrame
df_top_100

# Selecionar as 100 maiores probabilidades para cada linha
top_100_indices_per_row = np.argsort(predicts_v2_probs, axis=1)[:, -100:]
top_100_values_per_row = np.take_along_axis(predicts_v2_probs, top_100_indices_per_row, axis=1)

# Obter os nomes dos vendedores a partir dos índices
top_100_sellers_per_row = le_target.inverse_transform(top_100_indices_per_row.flatten()).reshape(top_100_indices_per_row.shape)

# Criar uma lista de dataframes para cada linha
df_list = []
for i in range(predicts_v2_probs.shape[0]):
    df_top_100_per_row = pd.DataFrame({
        'Probability': top_100_values_per_row[i],
        'Seller': top_100_sellers_per_row[i]
    })
    df_top_100_per_row['Row'] = i
    df_list.append(df_top_100_per_row)

# Concatenar todos os dataframes em um único dataframe
df_top_100_all = pd.concat(df_list, ignore_index=True)

top_100_sellers_04_11 = df_top_100_all.pivot_table(index='Seller', values='Probability', aggfunc='max').sort_values(by='Probability', ascending=False).fillna(0)
top_100_sellers_04_11 = top_100_sellers_04_11[top_100_sellers_04_11['Probability'] > 0.01]
top_100_sellers_04_11
Probability
Seller
100002304 0.540000
100001657 0.460000
100000819 0.340000
100000133 0.340000
100002201 0.320000
... ...
100002322 0.011111
100002323 0.011111
100002324 0.011111
100001062 0.011111
100001280 0.010714

1411 rows × 1 columns

Code
top_100_sellers_04_11[:10]
Probability
Seller
100002304 0.540000
100001657 0.460000
100000819 0.340000
100000133 0.340000
100002201 0.320000
100002378 0.320000
100000243 0.306667
100001904 0.300000
100000359 0.280000
100001997 0.280000

Será que esses vendedores fizeram bastante transações anteriormente? Vamos averiguar!

Code
df_trans = pd.read_excel('transações-desafio.xlsx')
Code
top_100_sellers_04_11.index
Index(['100002304', '100001657', '100000819', '100000133', '100002201',
       '100002378', '100000243', '100001904', '100000359', '100001997',
       ...
       '100000710', '100000456', '100001533', '100002408', '100002344',
       '100002322', '100002323', '100002324', '100001062', '100001280'],
      dtype='object', name='Seller', length=1411)
Code
df_trans['Seller ID'] = df_trans['Seller ID'].astype(str)
df_trans[df_trans['Seller ID'].isin(top_100_sellers_04_11.index.astype(str))].groupby('Seller ID').agg({
    'Amount': ['count', 'sum', 'median'],
}).sort_values(by=('Amount', 'count'), ascending=False)
Amount
count sum median
Seller ID
100000011 174 2.329851e+06 6972.469183
100000767 146 5.956219e+05 1869.478412
100000790 118 3.410177e+05 1931.217010
100000037 110 1.161185e+06 2042.319983
100000195 98 1.120023e+07 94048.471013
... ... ... ...
100001645 1 6.631726e+03 6631.725538
100001648 1 2.015435e+04 20154.347803
100001656 1 1.986823e+03 1986.822732
100001658 1 3.033805e+03 3033.804732
100002428 1 1.034266e+04 10342.657887

1411 rows × 3 columns

Conclui-se que na data de 05/11/2024, há 1411 vendedores com alguma probabilidade de realizarem pelo menos uma transação.

Agora vamos entender melhor um pouco o modelo:

Code
df_results_final = tsfc_model_v2['timeseriesforestclassifier'].feature_importances_
df_results_final.index  = X_test.columns

df_results_final
mean std slope
company 0.010784 0.016924 0.011252
product 0.025342 0.027265 0.029046
origin_city 0.061032 0.045314 0.061090
origin_state 0.071915 0.039667 0.072170
dolar 0.077591 0.043551 0.072927
price_merc 0.053597 0.029278 0.066522
cbot 0.052697 0.030642 0.067734
destination_city 0.049515 0.031737 0.066133
destination_state 0.045105 0.044422 0.049577
week_of_year 0.032772 0.035100 0.035303
day_of_week 0.010269 0.018841 0.005658
month_day 0.000000 0.000000 0.000000
Code
sns.barplot(x=df_results_final['mean'].sort_values(ascending=False), y=df_results_final.index, orient='h')
plt.ylabel('Features')
plt.title('Feature Importances')

plt.show()

Aqui podemos ver as features que mais impactaram na tomada de decisão do modelo (as features que mais reduzem a impureza nos nós das árvores de decisão).

Reiterando que essas features são as que mais impactaram na decisão do modelo, e não são necessariamente as features que efetivamente determinam o mercado, mas as features entre os dados que o modelo encontrou e que mais determinaram nas predições foram essas:

  • Company,
  • Product,
  • Origin city,
  • Origin state,
  • Dolar.

Aparentemente o dólar foi uma ótima adição ao modelo, sendo uma das features que mais influenciam e proporcionam informação para o mesmo fazer a predição.

Conclusão e Próximos passos

O projeto atual se mostrou bastante desafiador por três principais motivos: A predição de séries temporais para classificação, com um número muito grande de classes e que requisitasse probabilidade. Além da limitação de não poder utilizarmos modelos de redes neurais.

O modelo feito não teve um prediction muito alto, dada a complexidade da predição de diversas classes e a quantidade de poucos dados disponíveis e poder computacional. No entanto, alguns pontos poderiam ter sido utilizados de modo a melhorar o modelo:

  • Utilizar modelos de Deep learning, que teriam um potencial exponencialmente maior de capturar padrões nos dados.
  • Tentar mais técnicas de feature engineering, trazendo dados de negócio, como fontes do IBGE, CONAB, e outras fontes de dados do mercado de grãos.
  • Realizar um tuning de hiperparâmetros mais extenso, de modo a descobrir features melhores dos modelos e reduzir o overfitting, aumentando a capacidade de generanalização do modelo.
  • Potencialmente fazer conversões nos dados para buscar uma regressão (pensei nesta abordagem durante o projeto, mas não encontrei alguma técnica plausível).
  • Com mais tempo, fazer uma análise exploratória mais profunda, buscando encontrar padrões potencialmente ainda não encontrados na atual análise.
  • Com a presença de dados reais, poderíamos ter mais insumo pra alimentar o modelo, também potencialmente contribuindo com a reduçao de overfitting.
  • Testar outros modelos do sktime para predição de séries temporais.
  • Fazer conversões nos dados de modo a utilizar abordagem comum de séries temporais (ARIMA, SARIMA, Seasonal decomposing, suavizaçao exponencial).
  • Fazer o LAG nos dados, oo que potencialmente aumentaria a capacidade de predição de modelos de machine learning com abordagem tradicional.
  • Fazer o agrupamento dos dados com KMeans, de modo a reduzir o número de classes e fazer a predição sobre um grupo de potenciais vendedores (aprendizagem semi supervisionada).
  • Fazer uma abordagem de classificação binária, onde cada seller_id poderia ser um imput e a predição seria se ele faria ou não uma transação, no entanto após tentar realizar essa abordagem, percebeu-se dificuldade em lidar com um grande dataset gerado.

Enfim… Várias são as ideias, para muitas delas não houve tempo ou recursos computacionais que pudessem torná-las executáveis, mas no geral acredito que a abordagem tenha seguido boa e que o modelo e análises atuais tenham atendido à demanda que era a de entregar a probabilidade de potenciais compradores nos dias 04/11 e 05/11/2024.