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')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')# Carregando os datasets
df_mercado = pd.read_excel('mercado-desafio.xlsx')
df_transacoes = pd.read_excel('transações-desafio.xlsx')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.
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)
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
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.
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)
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.
# 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 |
# 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
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')
df_mercado.drop('data', axis=1, inplace=True)
df_transacoes.drop('data', axis=1, inplace=True)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
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.
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
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
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.
Inicialmente, iremos averiguar por dados nulos.
# 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.
# 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.
# 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)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.
# 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.
# 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.
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
#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:
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 |
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.
df_transacoes = df_transacoes[df_transacoes['Amount'] > 0]# 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.
# 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.
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.
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.
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.
# 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.
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.
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()# 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.
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:
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.
# 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.
# 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.
# 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.
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')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 |
sns.heatmap(df_full.corr(), annot = True)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
# 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.
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.
df_transacoes['Seller ID'].value_counts(normalize=True)[:10].sum() * 10011.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.
A partir daqui, faremos um backup dos dados e começaremos a preparar os dados para a modelagem.
df_trans_pre_pros = df_transacoes.copy()
df_merc_pre_proc = df_mercado.copy()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.
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 |
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:
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.
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.
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.
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.
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'
)display(df_completo_v1.isna().sum())
df_completo_v1.shapedate 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)
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:
msno.matrix(df_completo_v1)
plt.show()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.
# 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
msno.matrix(df_completo_v1)
plt.show()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.
# 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
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 |
assert df_merc_pre_proc_sem_intersecao['cbot'].isin(df_completo_v1['cbot']).sum() == 0Agora vamos criar variáveis dummies na tabela de transações final, de modo a permitir que haja o concat.
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.
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)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
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 |
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
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
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
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
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.
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
A intenção para a predição dessa série temporal será por meio de modelos de árvore pelos seguintes motivos:
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 |
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
# 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
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.
df_completo_v3 = df_completo_v2.drop(columns=['price_trans', 'amount'])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 |
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).
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.
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.
# 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.
# 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()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.
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.
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 gcdisplay(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)
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.
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.
# 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
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.
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_resultsDecisionTreeClassifier - 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
# 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.
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_resultsDummyClassifier - 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])}}
# 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)
Removendo vendedores sem transação nos ultimos 90 dias
vendedores_ultimos_90 = df_pre_modelo_[df_pre_modelo_['date'] >= '2024-08-04'].seller_id.unique()
vendedores_ultimos_90array(['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)
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]
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
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
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.
(df_pre_modelo_v2.value_counts() >= 4).sum()84
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
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.
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 |
Agora repetiremos os testes dos modelos de machine learning tradicionais e de séries temporais.
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.
#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.
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.
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
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.
if 'price_dol' in df_pre_modelo.columns:
df_pre_modelo.drop(['cbot_dol','price_dol','attempt','pric_cbot_sqrt'], axis=1, inplace=True)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 |
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).
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.
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).
# 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.
# 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
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.
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.
# 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.
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
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.
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 |
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 |
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
# 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# 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)# Make predictions
predicts_v2 = tsfc_model_v2.predict(X_test_no_date)
predicts_v2_probs = tsfc_model_v2.predict_proba(X_test_no_date)# 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
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!
df_trans = pd.read_excel('transações-desafio.xlsx')top_100_sellers_04_11.indexIndex(['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)
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:
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 |
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:
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.
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:
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.