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
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:
- Todas as features da base de ddos eram anonimas;
- 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
porNaN
, imputar a média, a mediana e via outros modelosNã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
Share this post
Twitter
LinkedIn
Email