Prévision de défaut du paiement avec Xgboost
Dans ce post, nous allons entraîner et régler les hyperparamètres du modèle XGBoost. On va utiliser le package tidymodels, qui se compose de plusieurs library, un workflow pour entrainer un modèle de classification:
- recipes : framework pour le Preprocessing
- rsample : Cross Validation et sample
- parsnip : framework de train des Machines Learnings
- tune : Tuning des hyperparamètres
- yardstick : evaluation sur les données Test
Ce projet consiste à construire la meilleure fonction score possible sur une base de données classique en machine learning : la base Credit Data disponible dane le package modeldata.
Load packages necessaires
# Load les library
library(tidymodels)
library(dplyr)
library(modeldata)
# for EDA
library(summarytools)
library(skimr)
library(DataExplorer)
# Helper packages
library(modeldata) # for Data score
library(vip)
# Parallel processing
library(doParallel)
Load les données
# set random seed for reproduction
set.seed(123)
data("credit_data", package = "modeldata")
credit_data %>% glimpse()
## Rows: 4,454
## Columns: 14
## $ Status <fct> good, good, bad, good, good, good, good, good, good, bad, go~
## $ Seniority <int> 9, 17, 10, 0, 0, 1, 29, 9, 0, 0, 6, 7, 8, 19, 0, 0, 15, 33, ~
## $ Home <fct> rent, rent, owner, rent, rent, owner, owner, parents, owner,~
## $ Time <int> 60, 60, 36, 60, 36, 60, 60, 12, 60, 48, 48, 36, 60, 36, 18, ~
## $ Age <int> 30, 58, 46, 24, 26, 36, 44, 27, 32, 41, 34, 29, 30, 37, 21, ~
## $ Marital <fct> married, widow, married, single, single, married, married, s~
## $ Records <fct> no, no, yes, no, no, no, no, no, no, no, no, no, no, no, yes~
## $ Job <fct> freelance, fixed, freelance, fixed, fixed, fixed, fixed, fix~
## $ Expenses <int> 73, 48, 90, 63, 46, 75, 75, 35, 90, 90, 60, 60, 75, 75, 35, ~
## $ Income <int> 129, 131, 200, 182, 107, 214, 125, 80, 107, 80, 125, 121, 19~
## $ Assets <int> 0, 0, 3000, 2500, 0, 3500, 10000, 0, 15000, 0, 4000, 3000, 5~
## $ Debt <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2500, 260, 0, 0, 0, 2000~
## $ Amount <int> 800, 1000, 2000, 900, 310, 650, 1600, 200, 1200, 1200, 1150,~
## $ Price <int> 846, 1658, 2985, 1325, 910, 1645, 1800, 1093, 1957, 1468, 15~
Exploaration des données - EDA avec skimr
skim(credit_data)
Name | credit_data |
Number of rows | 4454 |
Number of columns | 14 |
_______________________ | |
Column type frequency: | |
factor | 5 |
numeric | 9 |
________________________ | |
Group variables | None |
Variable type: factor
skim_variable | n_missing | complete_rate | ordered | n_unique | top_counts |
---|---|---|---|---|---|
Status | 0 | 1 | FALSE | 2 | goo: 3200, bad: 1254 |
Home | 6 | 1 | FALSE | 6 | own: 2107, ren: 973, par: 783, oth: 319 |
Marital | 1 | 1 | FALSE | 5 | mar: 3241, sin: 977, sep: 130, wid: 67 |
Records | 0 | 1 | FALSE | 2 | no: 3681, yes: 773 |
Job | 2 | 1 | FALSE | 4 | fix: 2805, fre: 1024, par: 452, oth: 171 |
Variable type: numeric
skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
---|---|---|---|---|---|---|---|---|---|---|
Seniority | 0 | 1.00 | 7.99 | 8.17 | 0 | 2.00 | 5 | 12.0 | 48 | ▇▃▁▁▁ |
Time | 0 | 1.00 | 46.44 | 14.66 | 6 | 36.00 | 48 | 60.0 | 72 | ▁▂▅▃▇ |
Age | 0 | 1.00 | 37.08 | 10.98 | 18 | 28.00 | 36 | 45.0 | 68 | ▆▇▆▃▁ |
Expenses | 0 | 1.00 | 55.57 | 19.52 | 35 | 35.00 | 51 | 72.0 | 180 | ▇▃▁▁▁ |
Income | 381 | 0.91 | 141.69 | 80.75 | 6 | 90.00 | 125 | 170.0 | 959 | ▇▂▁▁▁ |
Assets | 47 | 0.99 | 5403.98 | 11574.42 | 0 | 0.00 | 3000 | 6000.0 | 300000 | ▇▁▁▁▁ |
Debt | 18 | 1.00 | 343.03 | 1245.99 | 0 | 0.00 | 0 | 0.0 | 30000 | ▇▁▁▁▁ |
Amount | 0 | 1.00 | 1038.92 | 474.55 | 100 | 700.00 | 1000 | 1300.0 | 5000 | ▇▆▁▁▁ |
Price | 0 | 1.00 | 1462.78 | 628.13 | 105 | 1117.25 | 1400 | 1691.5 | 11140 | ▇▁▁▁▁ |
Splitting – la répartition des données
Maintenant, on divise les données en données de validation et de test. Les données de validation sont utilisées pour l’entrainement du modèle et l’ajustement des hyperparamètres. Une fois le modèle construit, il peut être évalué par rapport aux données test pour évaluer la qualité et la précision du nouveau modèle.
split <- initial_split(credit_data, prop = 0.8, strata = Status)
split
## <Analysis/Assess/Total>
## <3563/891/4454>
train_data <- training(split)
test_data <- testing(split)
Preprocessing
Le prétraitement modifie les données pour rendre notre modèle plus prédictif et le processus de formation moins intensif. De nombreux modèles nécessitent un prétraitement minutieux et exhaustif des variables pour produire des prévisions précises. XGBoost, cependant, est robuste contre les données fortement asymétriques et/ou corrélées. Néanmoins, nous pouvons encore bénéficier d’un certain prétraitement
Dans tidymodels, nous utilisons recipes pour définir ces étapes de prétraitement. Le preprocessing a été déini au départ pour entrainer le modéle LightGBM
recipe_credit <- train_data %>%
recipe(Status ~ .) %>%
step_unknown(all_nominal(), -Status) %>%
step_medianimpute(all_numeric()) %>%
step_other(all_nominal(), -Status, other = "infrequent_combined") %>%
step_novel(all_nominal(), -Status, new_level = "unrecorded_observation") %>%
step_dummy(all_nominal(), -Status, one_hot = TRUE) %>%
step_mutate(
ratio_expense_income = Expenses / (Income + 0.001),
ratio_asset_income = Assets / (Income + 0.001),
ratio_debt_asset = Debt / (Assets + 0.001),
ratio_debt_income = Debt / (Income + 0.001),
ratio_amount_price = Amount / (Price + 0.010)
)
# Add upsmpling
# step_upsample(Status, over_ratio = tune())
recipe_credit
## Data Recipe
##
## Inputs:
##
## role #variables
## outcome 1
## predictor 13
##
## Operations:
##
## Unknown factor level assignment for all_nominal(), -Status
## Median Imputation for all_numeric()
## Collapsing factor levels for all_nominal(), -Status
## Novel factor level assignment for all_nominal(), -Status
## Dummy variables from all_nominal(), -Status
## Variable mutation for ratio_expense_income, ratio_asset_income, ratio_debt_asset, ratio_debt_income, ratio_amount_price
Splitting pour la validation croisée
cv_fold <- vfold_cv(train_data, v = 5, strata = Status)
cv_fold
## # 5-fold cross-validation using stratification
## # A tibble: 5 x 2
## splits id
## <list> <chr>
## 1 <split [2850/713]> Fold1
## 2 <split [2850/713]> Fold2
## 3 <split [2850/713]> Fold3
## 4 <split [2851/712]> Fold4
## 5 <split [2851/712]> Fold5
Specifications du modéle XGboost
On utilise le package parsnip pour définir la spécification du modèle. Parsnip permet d’accéder à plusieurs packages d’apprentissage automatique les 3 étapes:
1- définir le type du modèle à entrainer
2- Décider quel moteur (engine) de calcul à utiliser
3- définir le mode “classification”
xgb_spec <- boost_tree(
trees = tune(),
tree_depth = tune(), min_n = tune(),
loss_reduction = tune(),
sample_size = tune(), mtry = tune(),
learn_rate = tune()
) %>%
set_engine("xgboost") %>%
set_mode("classification")
xgb_spec %>% translate()
## Boosted Tree Model Specification (classification)
##
## Main Arguments:
## mtry = tune()
## trees = tune()
## min_n = tune()
## tree_depth = tune()
## learn_rate = tune()
## loss_reduction = tune()
## sample_size = tune()
##
## Computational engine: xgboost
##
## Model fit template:
## parsnip::xgb_train(x = missing_arg(), y = missing_arg(), colsample_bytree = tune(),
## nrounds = tune(), min_child_weight = tune(), max_depth = tune(),
## eta = tune(), gamma = tune(), subsample = tune(), nthread = 1,
## verbose = 0)
Grid specification
On va utiliser grid_latin_hypercube
afin que nous puissions couvrir l’espace des hyperparamètres le mieux possible
xgb_grid <- grid_latin_hypercube(
trees(),
tree_depth(),
min_n(),
loss_reduction(),
sample_size = sample_prop(),
finalize(mtry(), train_data),
learn_rate(),
size = 50
)
xgb_grid
## # A tibble: 50 x 7
## trees tree_depth min_n loss_reduction sample_size mtry learn_rate
## <int> <int> <int> <dbl> <dbl> <int> <dbl>
## 1 32 1 33 0.00122 0.128 12 7.92e- 5
## 2 1667 6 23 0.00000428 0.754 14 4.51e- 5
## 3 1846 9 7 0.0000000700 0.513 2 4.77e- 2
## 4 697 7 11 0.000000112 0.651 4 5.39e- 3
## 5 235 13 21 0.00000260 0.240 5 1.19e- 6
## 6 785 13 19 0.0000000144 0.211 12 1.70e- 3
## 7 1336 11 26 0.0852 0.270 9 5.46e- 8
## 8 1185 2 2 0.0000319 0.354 12 2.47e-10
## 9 1623 1 29 0.0000566 0.538 8 6.50e-10
## 10 181 7 15 0.0000170 0.967 10 4.41e- 6
## # ... with 40 more rows
Définir workflow
xgb_wf <- workflow() %>%
add_recipe(recipe_credit) %>%
add_model(xgb_spec)
xgb_wf
## == Workflow ====================================================================
## Preprocessor: Recipe
## Model: boost_tree()
##
## -- Preprocessor ----------------------------------------------------------------
## 6 Recipe Steps
##
## * step_unknown()
## * step_medianimpute()
## * step_other()
## * step_novel()
## * step_dummy()
## * step_mutate()
##
## -- Model -----------------------------------------------------------------------
## Boosted Tree Model Specification (classification)
##
## Main Arguments:
## mtry = tune()
## trees = tune()
## min_n = tune()
## tree_depth = tune()
## learn_rate = tune()
## loss_reduction = tune()
## sample_size = tune()
##
## Computational engine: xgboost
Tune des hyperparamètres
Pour accélérer le calcul, on utilise paralell processing. Pour en savoir plus https://curso-r.github.io/treesnip/articles/parallel-processing.html
all_cores <- parallel::detectCores(logical = FALSE)
registerDoParallel(cores = all_cores)
set.seed(452)
library(tictoc)
cat("Nombre de cores :", all_cores)
## Nombre de cores : 4
Le tuning prends un peu de temps pour évaluer l’ensemble des paramétres.
tic()
xgb_tune <- tune_grid(
xgb_wf,
resamples = cv_fold,
# metrics = metric_set(roc_auc, j_index, sens, spec),
control = control_grid(verbose = FALSE, save_pred = TRUE)
)
toc()
## 108.1 sec elapsed
xgb_tune
## # Tuning results
## # 5-fold cross-validation using stratification
## # A tibble: 5 x 5
## splits id .metrics .notes .predictions
## <list> <chr> <list> <list> <list>
## 1 <split [2850/713]> Fold1 <tibble [20 x 11]> <tibble [0 x 1]> <tibble>
## 2 <split [2850/713]> Fold2 <tibble [20 x 11]> <tibble [0 x 1]> <tibble>
## 3 <split [2850/713]> Fold3 <tibble [20 x 11]> <tibble [0 x 1]> <tibble>
## 4 <split [2851/712]> Fold4 <tibble [20 x 11]> <tibble [0 x 1]> <tibble>
## 5 <split [2851/712]> Fold5 <tibble [20 x 11]> <tibble [0 x 1]> <tibble>
Evaluer le Résultat
On va sortir les paramètres de tous ces modèles.
DT::datatable(xgb_tune %>% collect_metrics())
ROC curve vs gain curve
library(patchwork)
p1 <- xgb_tune %>%
collect_predictions() %>%
group_by(.config) %>%
roc_curve(Status, .pred_bad) %>%
autoplot() + theme(legend.position = "none")
p2 <- xgb_tune %>%
collect_predictions() %>%
group_by(.config) %>%
gain_curve(Status, .pred_bad) %>%
autoplot() + theme(legend.position = "none")
# p1 / p2
p1 + p2
Quels sont les combinaisons de paramètres les plus performants?
Et aprés, finaliser le modèle XGBoost avec les meilleurs paramètres.
xgb_best <- select_best(xgb_tune, "roc_auc")
xgb_final_wf <- finalize_workflow(xgb_wf, xgb_best)
xgb_best
## # A tibble: 1 x 8
## mtry trees min_n tree_depth learn_rate loss_reduction sample_size .config
## <int> <int> <int> <int> <dbl> <dbl> <dbl> <chr>
## 1 28 1974 12 10 0.00187 0.0458 0.798 Preprocess~
xgb_tune %>%
collect_metrics(parameters = xgb_best)
## # A tibble: 20 x 13
## mtry trees min_n tree_depth learn_rate loss_reduction sample_size .metric
## <int> <int> <int> <int> <dbl> <dbl> <dbl> <chr>
## 1 3 1181 40 2 8.02e- 5 4.49e- 4 0.954 accuracy
## 2 3 1181 40 2 8.02e- 5 4.49e- 4 0.954 roc_auc
## 3 6 883 24 4 1.24e- 5 4.81e- 7 0.875 accuracy
## 4 6 883 24 4 1.24e- 5 4.81e- 7 0.875 roc_auc
## 5 10 287 5 14 4.05e- 4 5.58e- 5 0.166 accuracy
## 6 10 287 5 14 4.05e- 4 5.58e- 5 0.166 roc_auc
## 7 14 1337 19 13 2.24e- 9 7.30e- 9 0.283 accuracy
## 8 14 1337 19 13 2.24e- 9 7.30e- 9 0.283 roc_auc
## 9 17 1610 26 11 1.70e- 8 7.21e- 3 0.485 accuracy
## 10 17 1610 26 11 1.70e- 8 7.21e- 3 0.485 roc_auc
## 11 20 41 30 3 1.28e-10 4.59e- 1 0.273 accuracy
## 12 20 41 30 3 1.28e-10 4.59e- 1 0.273 roc_auc
## 13 22 507 33 7 1.70e- 7 6.73e+ 0 0.372 accuracy
## 14 22 507 33 7 1.70e- 7 6.73e+ 0 0.372 roc_auc
## 15 27 1421 8 6 2.17e- 6 1.79e- 7 0.669 accuracy
## 16 27 1421 8 6 2.17e- 6 1.79e- 7 0.669 roc_auc
## 17 28 1974 12 10 1.87e- 3 4.58e- 2 0.798 accuracy
## 18 28 1974 12 10 1.87e- 3 4.58e- 2 0.798 roc_auc
## 19 31 740 14 9 2.20e- 2 1.97e-10 0.585 accuracy
## 20 31 740 14 9 2.20e- 2 1.97e-10 0.585 roc_auc
## # ... with 5 more variables: .estimator <chr>, mean <dbl>, n <int>,
## # std_err <dbl>, .config <chr>
Déterminer les paramètres les plus importants
library(vip)
xgb_final_wf %>%
fit(data = train_data) %>%
pull_workflow_fit() %>%
vip(geom = "point")
## [22:34:34] WARNING: amalgamation/../src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
last_fit() pour adapter notre modèle une dernière fois sur les données de validation et évaluer notre modèle sur les données de tests
xgb_final <- xgb_final_wf %>%
last_fit(split, metrics = metric_set(roc_auc, j_index, sens, spec, accuracy))
xgb_final %>% collect_metrics()
## # A tibble: 5 x 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 j_index binary 0.410 Preprocessor1_Model1
## 2 sens binary 0.482 Preprocessor1_Model1
## 3 spec binary 0.928 Preprocessor1_Model1
## 4 accuracy binary 0.802 Preprocessor1_Model1
## 5 roc_auc binary 0.847 Preprocessor1_Model1
La courbe ROC nous donnera une idée de la performance de notre modèle avec les données tests. Vous devriez savoir que si AUC (Area Under Curve) est proche de 50%, alors le modèle est aussi bon qu’un sélection aléatoire; par contre, si AUC est proche de 100%, vous avez un «modèle parfait»
xgb_final %>%
collect_predictions() %>%
roc_curve(Status, .pred_bad) %>%
autoplot()
xgb_final %>%
collect_predictions() %>%
roc_curve(Status, .pred_bad) %>%
ggplot(aes(x = 1 - specificity, y = sensitivity)) +
geom_path(lwd = 1.5, alpha = 0.8, color = "#421269") +
geom_abline(lty = 3) +
geom_hline(yintercept = 0.42) +
geom_vline(xintercept = 1 - 0.9186228) +
coord_equal() +
# scale_color_viridis_d(option = "plasma", end = .6)+
theme_bw()
Analyse de la densité pour chaque classe vs prévision, ce graphe permet de fournir des informations visuelles sur l’asymétrie, la distribution, et la qualité de prévision de notre modèle.
xgb_auc <- xgb_final %>% collect_predictions()
h1 <- xgb_auc %>%
ggplot() +
geom_density(aes(x = .pred_bad, fill = Status), alpha = 0.5) +
theme_bw()
h2 <- xgb_auc %>%
ggplot() +
geom_density(aes(x = .pred_good, fill = Status), alpha = 0.5) +
theme_bw()
h1 / h2
La performance du modèle XGboost n’est aussi pas optimale qu’attendue…Il faut revoir le process, peut-être en incrémentant d’autres données, revoir notre stratégie du tuning et du preprocessing, chnager le thresholds ou changer le modèle comme le Catboost ou LightGBM ; il y a toujours des pistes 😊