Solução Final - Porto Seguro Data Challenge [3º lugar]

Confira a estratégia aplicada para a competição de machine learning do Porto Seguro hospedada no Kaggle

Fellipe Gomes

16 minute read


Introdução

Em Agosto e 2021 a Porto Seguro lançou um desafio no Kaggle que consistia em estimar a propensão de aquisição de novos produtos. Tratava-se de um problema de classificação e foi bem desafiador principalmente por 2 motivos:

  1. Todas as features da base de ddos eram anonimas;
  2. A métrica de avaliação foi a F1 Score (sensível à um ponto de corte)

Depois de 2 longos meses e dezenas de notebooks desenvolvidos, muitas submissões frustradas e muitas horas a menos de sono, cheguei em uma solução final que envole um blending de modelos e pseudo-labels e quando a competição acabou, percebi que uma solução mais simples de implementar teria um resultado privado ainda maior do que o notebook que selecionei. 😅

⚠️ Atenção!

Neste post abordarei uma solução mais simples e eficiente mas caso tenha interesse em conferir a solução final completa (um grande frankstein), já está publica la no github.

Este notebook é uma reescritura do meu notebook publicado no Kaggle em linguagem Python. Para quem acompanha meus posts de R pode achar meio estranho este notebook mas convido-o a tentar entender a solução pois foi desenvolvida pela perspectiva de um usuário nativo de R.

Espero que gostem! 🤘

Definição do problema de negócio

Segundo a descrição da competição:

Você provavelmente já recebeu uma ligação de telemarketing oferecendo um produto que você não precisa. Essa situação de estresse é minimizada quando você oferece um produto que o cliente realmente precisa.

Nessa competição você será desafiado a construir um modelo que prediz a probabilidade de aquisição de um produto.

Sobre a métrica de avaliação:

O critério utilizado para definição da melhor solução será o F1-Score, veja sua formula:

\[ F_1 = 2 \times \frac{precision \times recall}{precision + recall} \]

Note que tanto a Precision quanto a Recall precisam de um ponto de corte para obter as classes e por isso busquei otmizar as métricas ROC-AUC e Log Loss para obter estimativas de probabilidades com qualidade para finalmente calcular os pontos de corte que maximizam a F1.

Análise Exploratória (em R)

Antes de partir para modelagem fiz uma análise exploratória utilizando a linguagem R. Neste post tratarei de maneira bem breve e quem tiver interesse em conferir mais detalhes bem como os códigos dos gráficos basta acessar o notebook que deixei aberto no Kaggle.

Veja alguns gráficos:

📌 Interpretação:

  • Categóricas:
    • Qualitativo nominal: Possuem muitas classes, poderia ser o nome do produto, região, um texto o que torna o desafio ainda maior para criar novas features;
    • Qualitativo ordinal: Basicamente deixei como veio pois já tava como numerico;
  • Numéricas:
    • Quantitativo continua: Todas estão normalizadas (0, 1), algumas são bimodais, algumas assimétricas a direita (pode ser tempo ate alguma coisa);
    • Quantitativo discreto: Sem muito o que fazer, observação apenas a feature var52 que parece idade
  • Dados missing: Parece haver algum padrão na maneira como os dados missing ocorrem e tentei substituir os -999 por NaN, imputar a média, a mediana e via outros modelos

Não achei que seria muito produtivo ficar adivinhando o que poderia ser cada feature pois praticamente todos as transformações e novas features que gerei não superavam o resultado do modelo ajustado nos dados da maneira que vinham portanto procurei investir mais tempo na modelagem mesmo.

Machine Learning (em Python)

Veja a estratégia de modelagem de maneira visual:



Importar dependências

Carregar pacotes do Python

# general packages
import pandas as pd
import numpy as np
import time
# knn features
from gokinjo import knn_kfold_extract
from gokinjo import knn_extract
# ml tools
from sklearn.model_selection import StratifiedKFold, KFold
from sklearn.metrics import f1_score, log_loss, roc_auc_score
# models
from catboost import CatBoostClassifier
from xgboost import XGBClassifier
# optimization
import optuna
# interpretable ml
import shap
# automl
from autogluon.tabular import TabularPredictor
# ignore specific warnings
import warnings
warnings.filterwarnings("ignore", message="ntree_limit is deprecated, use `iteration_range` or model slicing instead.")

Definir funções auxiliares para calcular o ponto de corte que maximiza a F1:

def get_threshold(y_true, y_pred):
    thresholds = np.arange(0.0, 1.0, 0.01)
    f1_scores = []
    for thresh in thresholds:
        f1_scores.append(
            f1_score(y_true, [1 if m>thresh else 0 for m in y_pred]))
    f1s = np.array(f1_scores)
    return thresholds[f1s.argmax()]
    
def custom_f1(y_true, y_pred):
    max_f1_threshold =  get_threshold(y_true, y_pred)
    y_pred = np.where(y_pred>max_f1_threshold, 1, 0)
    return f1_score(y_true, y_pred) 

Carregar dados da competição:

# load data
train = pd.read_csv('../input/porto-seguro-data-challenge/train.csv').drop('id', axis=1)
test = pd.read_csv('../input/porto-seguro-data-challenge/test.csv').drop('id', axis=1)
sample_submission = pd.read_csv('../input/porto-seguro-data-challenge/submission_sample.csv')
meta = pd.read_csv('../input/porto-seguro-data-challenge/metadata.csv')

# get data types
cat_nom = [x for x in meta.iloc[1:-1, :].loc[(meta.iloc[:,1]=="Qualitativo nominal")].iloc[:,0]] 
cat_ord = [x for x in meta.iloc[1:-1, :].loc[(meta.iloc[:,1]=="Qualitativo ordinal")].iloc[:,0]] 
num_dis = [x for x in meta.iloc[1:-1, :].loc[(meta.iloc[:,1]=="Quantitativo discreto")].iloc[:,0]] 
num_con = [x for x in meta.iloc[1:-1, :].loc[(meta.iloc[:,1]=="Quantitativo continua")].iloc[:,0]] 

Stage 0: Feature Extraction com KNN

Esta técnica gera \(k \times c\) novas features, onde \(c\) é o número de classes da target. As novas features são calculadas a partir das distâncias entre as observações e seus k vizinhos mais próximos dentro de cada classe;

O valor para os \(K\) vizinhos mais próximos selecionado foi \(K=1\) e para isso utilizei a biblioteca gokinjo que foi inspirada nas idéias apresentadas na solução vencedora do Otto Group Product Classification Challenge.

# convert to numpy because gokinjo expects np arrays
X = train[cat_nom+cat_ord+num_dis+num_con].to_numpy()
y = train.y.to_numpy()
X_test = test[cat_nom+cat_ord+num_dis+num_con].to_numpy()

# extract on train data
KNN_feat_train = knn_kfold_extract(X, y, k=1, normalize='standard')
print("KNN features for training set, shape: ", np.shape(KNN_feat_train))

# extract on test data
KNN_feat_test = knn_extract(X, y, X_test, k=1, normalize='standard')
print("KNN features for test set, shape: ", np.shape(KNN_feat_test))

# convert to dataframe
knn_feat_train = pd.DataFrame(KNN_feat_train, columns=["knn"+str(x) for x in range(knn_feat_train.shape[1])])
knn_feat_test = pd.DataFrame(KNN_feat_test, columns=["knn"+str(x) for x in range(knn_feat_test.shape[1])])
## KNN features for training set, shape:  (14123, 2)
## KNN features for test set, shape:  (21183, 2)

Stage 1: Tuning XGBoost com Optuna

Testei e otimizei muitos modelos como XGBoost, NGBoost, LightGBM, CatBoost, TabNet, HistGradientBoosting e algumas DNNs e em todos os casos (exceto DNNs) utilizei o Optuna para a seleção dos hiperparâmetros.

Também inclui nas tentativas iniciais de otimização alguns métodos de remostrarem como Random Under Sampling, Smote, Tomek, Adasyn dentre outros mas não tive muito sucesso.. apenas a combinação Tomek + CatBoost pareceu trazer algum ganho.

Claro que minhas tentativas não foram exautivas e devido ao tempo limitado acabei selecionando o XGBoost que foi o que apresentou as melhores métricas depois de otimizado e também o CatBoost com alguns hiperparâmetros fixos para serem a base deste pipeline.

Principais Informações 📌 :

  • Nenhum pré-processamento;
  • KFold K=10;
  • Otimização de hiperparâmetros com Optuna;
  • Loss do XGBoost: Log Loss;
  • Loss do Otimizador: Log Loss;
  • Sem resampling;
  • Previsão final com a probabilidade média de 10 seeds diferentes
X_test = test[cat_nom+cat_ord+num_dis+num_con]
X = train[cat_nom+cat_ord+num_dis+num_con]
y = train.y

K=10
SEED=314
kf = KFold(n_splits=K, random_state=SEED, shuffle=True)
fixed_params = {
    'random_state': 9,
    "objective": "binary:logistic",
    "eval_metric": 'logloss',
    'use_label_encoder':False,
    'n_estimators':10000,
}

def objective(trial):
    
    hyperparams = {
        'clf':{
        "booster": trial.suggest_categorical("booster", ["gbtree"]),
        "lambda": trial.suggest_float("lambda", 1e-8, 5.0, log=True),
        "alpha": trial.suggest_float("alpha", 1e-8, 5.0, log=True)
        }
    }
    
    if hyperparams['clf']["booster"] == "gbtree" or hyperparams['clf']["booster"] == "dart":
        hyperparams['clf']["max_depth"] = trial.suggest_int("max_depth", 1, 9)
        hyperparams['clf']["eta"] = trial.suggest_float("eta", 0.01, 0.1, log=True)
        hyperparams['clf']["gamma"] = trial.suggest_float("gamma", 1e-8, 1.0, log=True)
        hyperparams['clf']["grow_policy"] = trial.suggest_categorical("grow_policy", ["depthwise", "lossguide"])
        hyperparams['clf']['min_child_weight'] = trial.suggest_int('min_child_weight', 5, 20)
        hyperparams['clf']["subsample"] = trial.suggest_float("subsample", 0.03, 1)
        hyperparams['clf']["colsample_bytree"] = trial.suggest_float("colsample_bytree", 0.03, 1)
        hyperparams['clf']['max_delta_step'] = trial.suggest_float('max_delta_step', 0, 10)
        
    if hyperparams['clf']["booster"] == "dart":
        hyperparams['clf']["sample_type"] = trial.suggest_categorical("sample_type", ["uniform", "weighted"])
        hyperparams['clf']["normalize_type"] = trial.suggest_categorical("normalize_type", ["tree", "forest"])
        hyperparams['clf']["rate_drop"] = trial.suggest_float("rate_drop", 1e-8, 1.0, log=True)
        hyperparams['clf']["skip_drop"] = trial.suggest_float("skip_drop", 1e-8, 1.0, log=True)
    
    params = dict(**fixed_params, **hyperparams['clf'])
    xgb_oof = np.zeros(X.shape[0])

    for fold, (train_idx, val_idx) in enumerate(kf.split(X=X, y=y)):
        X_train = X.iloc[train_idx]
        y_train = y.iloc[train_idx]
        X_val = X.iloc[val_idx]
        y_val = y.iloc[val_idx]
        
        model = XGBClassifier(**params)
        
        model.fit(X_train, y_train,
                  eval_set=[(X_val, y_val)],
                  early_stopping_rounds=150,
                  verbose=False)
    
        xgb_oof[val_idx] = model.predict_proba(X_val)[:,1]

        del model

    return log_loss(y, xgb_oof)

Como no Kaggle existe o limite de aproximadamente 8h para executar um notebook, coloquei um limite de 7.5 horas para a busca de hiperparâmetros:

study_xgb = optuna.create_study(direction='minimize')

study_xgb.optimize(objective, 
               timeout=60*60*7.5, 
               gc_after_trial=True)

Resultados da busca:

print('-> Number of finished trials: ', len(study_xgb.trials))
print('-> Best trial:')
trial = study_xgb.best_trial
print('\tValue: {}'.format(trial.value))
print('-> Params: ')
trial.params
## -> Number of finished trials:  197
## -> Best trial:
##  Value: 0.3028443879614926
## -> Params: 
## {'booster': 'gbtree',
##  'lambda': 9.012384508756378e-07,
##  'alpha': 0.7472040331088792,
##  'max_depth': 5,
##  'eta': 0.01507605562231303,
##  'gamma': 1.0214961302342215e-08,
##  'grow_policy': 'lossguide',
##  'min_child_weight': 5,
##  'subsample': 0.9331005225916879,
##  'colsample_bytree': 0.25392142363325004,
##  'max_delta_step': 5.685109389498008}

Acompanhar o histórico de cada etapa da otimização:

plot_optimization_history(study_xgb)

Avaliar as combinações de hiperparâmetros mais bem sucedidas:

optuna.visualization.plot_parallel_coordinate(study_xgb)

Quais hiperparâmetros tiveram mais impacto na modelagem:

plot_param_importances(study_xgb)

Após as 7.5 horas de busca, a melhor combinação encontrada para o XGBoost foi a seguinte:

# After 7.5 hours...
study_xgb = {'booster': 'gbtree',
 'lambda': 9.012384508756378e-07,
 'alpha': 0.7472040331088792,
 'max_depth': 5,
 'eta': 0.01507605562231303,
 'gamma': 1.0214961302342215e-08,
 'grow_policy': 'lossguide',
 'min_child_weight': 5,
 'subsample': 0.9331005225916879,
 'colsample_bytree': 0.25392142363325004,
 'max_delta_step': 5.685109389498008}

Preparar lista de hiperparâmetros do XGBoost:

final_params_xgb = dict()
final_params_xgb['clf']=dict(**fixed_params, **study_xgb)

Stage 2: Calcular Out-Of-Fold SHAP values

Após obter a melhor combinação de hiperparâmetros para o XGBoost e encontrar resultados formidáveis com o CatBoost modificando apenas alguns hiperparâmetros, resolvi tentar utilizar a informação adquirida pelo SHAP values desses modelos como entrada para novos modelos.

Algumas vantagens de se usar o shap values como um método de encoder dos dados, segundo este notebook publicado no Kaggle (muito interessante por sinal):

  • Normaliza os dados;
  • Mais ou menos Linearizado pois as features são transformadas em suas importâncias;
  • Recursos categóricos codificados de maneira mais inteligente (A codificação não é linear e depende de outros recursos da amostra);
  • Tratamento mais inteligente para valores missing.

Para evitar data leak, o SHAP values foi calculado em cima dos dados out-of-fold para os dados de treino e a média da previsão de todos os fold nos dados de teste.

Definir estratégia de validação cruzada:

X_test = test[cat_nom+cat_ord+num_dis+num_con]
X = train[cat_nom+cat_ord+num_dis+num_con]
y = train.y

K=15 # number of bins with Sturge’s rule
SEED=123
kf = StratifiedKFold(n_splits=K, random_state=SEED, shuffle=True)

XGBoost

Obter out-of-fold SHAP do modelo XGBoost tunado:

shap1_oof = np.zeros((X.shape[0], X.shape[1]))
shap1_test = np.zeros((X_test.shape[0], X_test.shape[1]))
model_shap1_oof = np.zeros(X.shape[0])

for fold, (train_idx, val_idx) in enumerate(kf.split(X=X, y=y)):
    print(f"➜ FOLD :{fold}")
    X_train = X.iloc[train_idx]
    y_train = y.iloc[train_idx]
    X_val = X.iloc[val_idx]
    y_val = y.iloc[val_idx]
    
    start = time.time()
    
    model = XGBClassifier(**final_params_xgb['clf'])
    
    model.fit(X_train, y_train,
              eval_set=[(X_val, y_val)],
              early_stopping_rounds=150,
              verbose=False)
    
    model_shap1_oof[val_idx] += model.predict_proba(X_val)[:,1]
    
    print("Final F1     :", custom_f1(y_val, model_shap1_oof[val_idx]))
    print("Final AUC    :", roc_auc_score(y_val, model_shap1_oof[val_idx]))
    print("Final LogLoss:", log_loss(y_val, model_shap1_oof[val_idx]))

    explainer = shap.TreeExplainer(model)
    shap1_oof[val_idx] = explainer.shap_values(X_val)
    shap1_test += explainer.shap_values(X_test) / K

    print(f"elapsed: {time.time()-start:.2f} sec\n")
    
shap1_oof = pd.DataFrame(shap1_oof, columns = [x+"_shap1" for x in X.columns])
shap1_test = pd.DataFrame(shap1_test, columns = [x+"_shap1" for x in X_test.columns])

print("Final F1     :", custom_f1(y, model_shap1_oof))
print("Final AUC    :", roc_auc_score(y, model_shap1_oof))
print("Final LogLoss:", log_loss(y, model_shap1_oof))
## ➜ FOLD :0
## Final F1     : 0.7032967032967034
## Final AUC    : 0.902330627099664
## Final LogLoss: 0.2953604946129216
## elapsed: 62.58 sec
## 
## ➜ FOLD :1
## Final F1     : 0.6193853427895981
## Final AUC    : 0.8613101903695408
## Final LogLoss: 0.34227429854659686
## elapsed: 45.96 sec
## 
## ➜ FOLD :2
## Final F1     : 0.6793478260869567
## Final AUC    : 0.8945898656215007
## Final LogLoss: 0.3085819148842589
## elapsed: 58.84 sec
## 
## ➜ FOLD :3
## Final F1     : 0.7073791348600509
## Final AUC    : 0.9058020716685331
## Final LogLoss: 0.2881665477053405
## elapsed: 62.24 sec
## 
## ➜ FOLD :4
## Final F1     : 0.7239583333333334
## Final AUC    : 0.9053121500559911
## Final LogLoss: 0.29320601468396107
## elapsed: 93.74 sec
## 
## ➜ FOLD :5
## Final F1     : 0.7009803921568627
## Final AUC    : 0.9076567749160134
## Final LogLoss: 0.2872539995859452
## elapsed: 73.34 sec
## 
## ➜ FOLD :6
## Final F1     : 0.6736292428198434
## Final AUC    : 0.8822788353863381
## Final LogLoss: 0.320014158050091
## elapsed: 55.16 sec
## 
## ➜ FOLD :7
## Final F1     : 0.7135416666666666
## Final AUC    : 0.9016657334826428
## Final LogLoss: 0.29617989833438774
## elapsed: 74.49 sec
## 
## ➜ FOLD :8
## Final F1     : 0.7135135135135134
## Final AUC    : 0.8893825776158104
## Final LogLoss: 0.29351621553572266
## elapsed: 93.71 sec
## 
## ➜ FOLD :9
## Final F1     : 0.7391304347826086
## Final AUC    : 0.9064054944284814
## Final LogLoss: 0.28033187155768635
## elapsed: 95.65 sec
## 
## ➜ FOLD :10
## Final F1     : 0.684863523573201
## Final AUC    : 0.9031046324199313
## Final LogLoss: 0.29823173886367804
## elapsed: 64.70 sec
## 
## ➜ FOLD :11
## Final F1     : 0.704225352112676
## Final AUC    : 0.8882052000840984
## Final LogLoss: 0.30525241732057884
## elapsed: 50.06 sec
## 
## ➜ FOLD :12
## Final F1     : 0.6666666666666666
## Final AUC    : 0.8905529469479291
## Final LogLoss: 0.313654842143217
## elapsed: 78.45 sec
## 
## ➜ FOLD :13
## Final F1     : 0.6500000000000001
## Final AUC    : 0.8745111780783517
## Final LogLoss: 0.3300786509821235
## elapsed: 59.54 sec
## 
## ➜ FOLD :14
## Final F1     : 0.7135416666666666
## Final AUC    : 0.9063284042329526
## Final LogLoss: 0.29314716930177404
## elapsed: 70.28 sec
## 
## Final F1     : 0.6822461331540014
## Final AUC    : 0.8945288307257988
## Final LogLoss: 0.30301717097927483

CatBoost

Obter out-of-fold SHAP do modelo CatBoost + features extratídas via KNN:

X = pd.concat([X, knn_feat_train], axis=1)
X_test = pd.concat([X_test, knn_feat_test], axis=1)
shap2_oof = np.zeros((X.shape[0], X.shape[1]))
shap2_test = np.zeros((X_test.shape[0], X_test.shape[1]))
model_shap2_oof = np.zeros(X.shape[0])

for fold, (train_idx, val_idx) in enumerate(kf.split(X=X, y=y)):
    print(f"➜ FOLD :{fold}")
    X_train = X.iloc[train_idx]
    y_train = y.iloc[train_idx]
    X_val = X.iloc[val_idx]
    y_val = y.iloc[val_idx]
    
    start = time.time()
    
    model = CatBoostClassifier(random_seed=SEED,
                               verbose = 0,
                               n_estimators=10000,
                               loss_function= 'Logloss',
                               use_best_model=True,
                               eval_metric= 'Logloss')
    
    model.fit(X_train, y_train, 
              eval_set = [(X_val,y_val)], 
              early_stopping_rounds = 100,
              verbose = False)
    
    model_shap2_oof[val_idx] += model.predict_proba(X_val)[:,1]
    
    print("Final F1     :", custom_f1(y_val, model_shap2_oof[val_idx]))
    print("Final AUC    :", roc_auc_score(y_val, model_shap2_oof[val_idx]))
    print("Final LogLoss:", log_loss(y_val, model_shap2_oof[val_idx]))

    explainer = shap.TreeExplainer(model)
    shap2_oof[val_idx] = explainer.shap_values(X_val)
    shap2_test += explainer.shap_values(X_test) / K

    print(f"elapsed: {time.time()-start:.2f} sec\n")
    
shap2_oof = pd.DataFrame(shap2_oof, columns = [x+"_shap" for x in X.columns])
shap2_test = pd.DataFrame(shap2_test, columns = [x+"_shap" for x in X_test.columns])

print("Final F1     :", custom_f1(y, model_shap2_oof))
print("Final AUC    :", roc_auc_score(y, model_shap2_oof))
print("Final LogLoss:", log_loss(y, model_shap2_oof))
## ➜ FOLD :0
## Final F1     : 0.6972010178117048
## Final AUC    : 0.8954157334826428
## Final LogLoss: 0.29952314366911725
## elapsed: 22.84 sec
## 
## ➜ FOLD :1
## Final F1     : 0.6348448687350835
## Final AUC    : 0.8628429451287795
## Final LogLoss: 0.3407490151943705
## elapsed: 12.59 sec
## 
## ➜ FOLD :2
## Final F1     : 0.6809651474530831
## Final AUC    : 0.8949538073908175
## Final LogLoss: 0.3066089330852162
## elapsed: 18.03 sec
## 
## ➜ FOLD :3
## Final F1     : 0.702247191011236
## Final AUC    : 0.9107992721164613
## Final LogLoss: 0.2877216893570601
## elapsed: 15.66 sec
## 
## ➜ FOLD :4
## Final F1     : 0.7131367292225201
## Final AUC    : 0.9018687010078387
## Final LogLoss: 0.2976481761596595
## elapsed: 29.35 sec
## 
## ➜ FOLD :5
## Final F1     : 0.7055837563451777
## Final AUC    : 0.909231522956327
## Final LogLoss: 0.28834373773423566
## elapsed: 15.35 sec
## 
## ➜ FOLD :6
## Final F1     : 0.6631578947368421
## Final AUC    : 0.8796402575587906
## Final LogLoss: 0.32303153676573987
## elapsed: 19.13 sec
## 
## ➜ FOLD :7
## Final F1     : 0.6997389033942559
## Final AUC    : 0.901637737961926
## Final LogLoss: 0.2985978485411335
## elapsed: 23.30 sec
## 
## ➜ FOLD :8
## Final F1     : 0.6965699208443271
## Final AUC    : 0.8825565912117177
## Final LogLoss: 0.3009859242847037
## elapsed: 20.19 sec
## 
## ➜ FOLD :9
## Final F1     : 0.7435897435897436
## Final AUC    : 0.9042469689536757
## Final LogLoss: 0.28276851015512977
## elapsed: 24.39 sec
## 
## ➜ FOLD :10
## Final F1     : 0.6767676767676767
## Final AUC    : 0.902712173242694
## Final LogLoss: 0.29999812838692497
## elapsed: 16.14 sec
## 
## ➜ FOLD :11
## Final F1     : 0.7013698630136986
## Final AUC    : 0.8865022075828719
## Final LogLoss: 0.3081393413008847
## elapsed: 13.50 sec
## 
## ➜ FOLD :12
## Final F1     : 0.6630434782608696
## Final AUC    : 0.8920456934613498
## Final LogLoss: 0.31338640296724246
## elapsed: 24.48 sec
## 
## ➜ FOLD :13
## Final F1     : 0.6485148514851485
## Final AUC    : 0.8689887167986544
## Final LogLoss: 0.3369797070301582
## elapsed: 17.17 sec
## 
## ➜ FOLD :14
## Final F1     : 0.7108753315649867
## Final AUC    : 0.8994743850304856
## Final LogLoss: 0.301420230674656
## elapsed: 16.51 sec
## 
## Final F1     : 0.6823234134098244
## Final AUC    : 0.892656043550729
## Final LogLoss: 0.305726567456891
train = pd.concat([train, shap1_oof], axis=1)
test = pd.concat([test, shap1_test], axis=1)

train = pd.concat([train, shap2_oof], axis=1)
test = pd.concat([test, shap2_test], axis=1)

Stage 3: Modelo Final com AutoGluon

AutoGluon é um AutoML desenvolvido pela Amazon muito fácil de utilizar (no melhor estilo sklearn com métodos .fit() e .predict()).

Principais Informações 📌 :

  • Inputs: Dataset original + knn features + Shapt values do XGBoost tunado e do CatBoost;
  • Loss do XGBoost: Log Loss;
  • Loss do CatBoost: AUC;
  • Loss do AutoGluon: Log Loss;
  • Tempo de processamento: 7h30m

💡 Insight

Um recurso muito útil do AutoGluon é poder acessar as previsões out-of-folds, o que facilita no cálculo do threshold que maximiza a F1 Score.

predictor = TabularPredictor(label="y",
                             problem_type='binary',
                             eval_metric="log_loss",
                             path='./AutoGlon/',
                             verbosity=1)

predictor.fit(train, presets='best_quality', time_limit=60*60*7.5) 

results = predictor.fit_summary()
## *** Summary of fit() ***
## Estimated performance of each model:
##                       model  score_val  pred_time_val      fit_time  pred_time_val_marginal  fit_time_marginal  stack_level  can_infer  fit_order
## 0       WeightedEnsemble_L2  -0.299310      30.410467   8888.826963                0.001654           2.456810            2       True         14
## 1           CatBoost_BAG_L1  -0.301038       3.051793   2376.887100                3.051793        2376.887100            1       True          7
## 2       WeightedEnsemble_L3  -0.301722     194.034947  22907.669139                0.001541           2.008858            3       True         26
## 3         LightGBMXT_BAG_L2  -0.302135     131.534432  17201.299530                1.378576         389.400378            2       True         15
## 4         LightGBMXT_BAG_L1  -0.302562       3.570399    969.385833                3.570399         969.385833            1       True          3
## 5           CatBoost_BAG_L2  -0.302646     131.912474  17619.939451                1.756617         808.040299            2       True         19
## 6           LightGBM_BAG_L2  -0.303002     131.422007  17281.518763                1.266150         469.619612            2       True         16
## 7           LightGBM_BAG_L1  -0.303264       2.964433   1038.037160                2.964433        1038.037160            1       True          4
## 8            XGBoost_BAG_L1  -0.303471       4.475003   2036.551052                4.475003        2036.551052            1       True         11
## 9    NeuralNetFastAI_BAG_L1  -0.304455      19.917584   3434.894841               19.917584        3434.894841            1       True         10
## 10           XGBoost_BAG_L2  -0.304499     132.757505  17834.135370                2.601648        1022.236218            2       True         23
## 11   NeuralNetFastAI_BAG_L2  -0.306339     142.018741  18777.287244               11.862885        1965.388093            2       True         22
## 12     LightGBMLarge_BAG_L2  -0.306606     131.701429  18260.504603                1.545573        1448.605452            2       True         25
## 13    NeuralNetMXNet_BAG_L2  -0.308237     177.769179  19273.211899               47.613322        2461.312748            2       True         24
## 14     LightGBMLarge_BAG_L1  -0.309686       3.042399   2629.185346                3.042399        2629.185346            1       True         13
## 15    ExtraTreesEntr_BAG_L2  -0.314045     132.017535  16815.886061                1.861679           3.986910            2       True         21
## 16  RandomForestEntr_BAG_L2  -0.314454     132.061970  16843.769642                1.906114          31.870490            2       True         18
## 17    ExtraTreesGini_BAG_L2  -0.314960     132.123651  16816.087081                1.967794           4.187930            2       True         20
## 18    NeuralNetMXNet_BAG_L1  -0.317156      81.677096   4258.886806               81.677096        4258.886806            1       True         12
## 19  RandomForestGini_BAG_L2  -0.321702     132.035970  16835.326491                1.880114          23.427339            2       True         17
## 20    ExtraTreesEntr_BAG_L1  -0.323283       1.794093      4.051307                1.794093           4.051307            1       True          9
## 21  RandomForestEntr_BAG_L1  -0.324296       1.966043     33.380685                1.966043          33.380685            1       True          6
## 22    ExtraTreesGini_BAG_L1  -0.325897       1.796291      3.748723                1.796291           3.748723            1       True          8
## 23  RandomForestGini_BAG_L1  -0.328218       1.778995     22.705248                1.778995          22.705248            1       True          5
## 24    KNeighborsDist_BAG_L1  -1.070156       2.010938      2.075571                2.010938           2.075571            1       True          2
## 25    KNeighborsUnif_BAG_L1  -1.071373       2.110790      2.109480                2.110790           2.109480            1       True          1
## Number of models trained: 26
## Types of models trained:
## {'StackerEnsembleModel_RF', 'StackerEnsembleModel_NNFastAiTabular', 'WeightedEnsembleModel', 'StackerEnsembleModel_XGBoost', 'StackerEnsembleModel_CatBoost', 'StackerEnsembleModel_KNN', 'StackerEnsembleModel_LGB', 'StackerEnsembleModel_XT', 'StackerEnsembleModel_TabularNeuralNet'}
## Bagging used: True  (with 10 folds)
## Multi-layer stack-ensembling used: True  (with 3 levels)
## Feature Metadata (Processed):
## (raw dtype, special dtypes):
## ('float', [])     : 152 | ['var55', 'var56', 'var57', 'var58', 'var59', ...]
## ('int', [])       :  48 | ['var1', 'var2', 'var3', 'var4', 'var5', ...]
## ('int', ['bool']) :   6 | ['var27', 'var31', 'var44', 'var49', 'var50', ...]
## Plot summary of models saved to file: ./AutoGlon/SummaryOfModels.html
## *** End of fit() summary ***

Nota: Os resultados podem variar devido à natureza estocástica do algoritmo ou procedimento de avaliação.

# get final predictions
y_oof = predictor.get_oof_pred_proba().iloc[:,1]
y_pred = predictor.predict_proba(test).iloc[:,1]
final_threshold = get_threshold(train.y, y_oof)
final_threshold
## 0.31
print("Final F1     :", custom_f1(y, y_oof))
print("Final AUC    :", roc_auc_score(y, y_oof))
print("Final LogLoss:", log_loss(y, y_oof))
## Final F1     : 0.6846193682030037
## Final AUC    : 0.8961328807692966
## Final LogLoss: 0.2993098559321765

Após submissão:

Conclusão

Gostaria de agradecer imensamente ao time do Porto Seguro pela iniciativa, pois esse tipo de competição (tão detalhada e desafiadora) não tem sido muito comum no Brasil e é muito importante para fomentar a comunidade brasileira de ciência de dados!

Sabemos que o “mundo real” é diferente do mundo das competições (onde buscamos o melhor score a todo custo) porém, na minha visão, não deixa de ser um ótimo exercício para treinar o raciocínio analítico.. além de ser muito empolgante e divertido!

Tive o enorme prazer de trocar idéias e conhecer pessoas fora da curva bem como me tornar fã de alguns competidores! A cada semana q passava o nível estava cada vez mais alto!

Com certeza este pipeline poderia ser muito melhor, sinto que poderia ter gasto mais tempo com feature engineering e tido mais paciencia com alguns modelos. Tentei fazer o melhor que pude com o tempo disponível e me sinto muito grato pela experiência de apresentar os resultados e aprender bastante com a solução dos top colocados.

Não acaba por aqui! Agora é hora de voltar aos estudos, continuar praticando com as TPS’s do Kaggle e, quem sabe, ir melhor na próxima!

comments powered by Disqus