tidymodels
Vamos a terminar de ver ensambles con modelos de boosting. Como vimos en la parte teórica, estos modelos tienen la característica de realizar un aprendizaje iterativo sobre estimadores que se construyen secuencialmente. Vamos a trabajar sobre la base de la ENUT para predecir si se realiza TD o no, y comparar con los resultados de los modelos anteriores.
Primero, hacemos el preprocesamiento:
library(tidyverse)
data <- read_delim("./data/enut2021_base.txt", delim = "|")
## Rows: 14350 Columns: 327
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: "|"
## dbl (326): ID, WHOG, WPER, REGION, N_MIEMBRO, BHCV01, BHCV02, BHCV03, BHCV04...
## lgl (1): BHDC01_08_SEL
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
data <- data %>% select(ID, SEXO_SEL, EDAD_SEL, TCS_GRUPO_DOMESTICO, CONDICION_ACTIVIDAD_AGRUPADA,
NIVEL_EDUCATIVO_AGRUPADO, CANT_DEMANDANTES_TOTAL, CANT_NODEMANDANTES_TOTAL,
BHCH04_SEL, BHDC_SEL) %>%
mutate(realiza_domest = as.factor(case_when(
TCS_GRUPO_DOMESTICO > 120 ~ "Realiza",
TRUE ~ "No realiza")))
data <- data %>% mutate_at(
vars(SEXO_SEL),
~as.factor(case_when(
. == 1 ~ "Mujer",
. == 2 ~ "Varon"
)))
data <- data %>% mutate_at(vars(CONDICION_ACTIVIDAD_AGRUPADA),
~as.factor(case_when(
. == 1 ~ "Ocupado",
. == 2 ~ "No ocupado"
)))
data <- data %>% mutate_at(vars(BHCH04_SEL),
~as.factor(case_when(
. == 1 ~ "Jefe/a",
. == 2 ~ "Conyuge/pareja",
. == 3 ~ "Hijo/a",
. == 4 ~ "Hijastro/a",
. == 5 ~ "Yerno/nuera",
. == 6 ~ "Nieto/a",
. == 7 ~ "Padre o madre",
. == 8 ~ "Suegro/a",
. == 9 ~ "Hermano/a",
. == 10 ~ "Cuniado/a",
. == 11 ~ "Sobrino/a",
. == 12 ~ "Abuelo/a",
. == 13 ~ "Otro familiar",
. == 14 ~ "Otro no familiar")))
data <- data %>% mutate_at(vars(BHDC_SEL),
~as.factor(case_when(
. == 0 ~ "No es demandante de cuidado",
. == 1 ~ "Es demandante de cuidado"
)))
data<- data %>% select(-TCS_GRUPO_DOMESTICO)
Veamos, primero, un ejemplo “teórico”… Vamos a ilustrar la forma de aplicar AdaBoost a un conjunto de datos bivariados para clasificar en dos clases: -1 y +1.
Figura 7.1: Datos originales
En la siguiente figura se muestran 3 clasificadores (\(h_{1}\), \(h_{2}\), \(h_{3}\)) sencillos o árboles de profundidad uno, que fueron creados de forma secuencial. Al observar \(h_{1}\), se nota que él clasificó mal los \(+\) encerrados en círculos. Por esa razón, en la siguiente iteración esas observaciones mal clasificadas tuvieron un mayor peso o importancia en el nuevo clasificador \(h_{2}\), por eso es que esos símbolos \(+\) aparecen más grandes en la segunda figura.
Al mirar el clasificador \(h_{2}\) se ve que logró clasificar bien esos \(+\) grandes, sin embargo, él clasificó mal los \(-\) que están encerrados en círculos. Por esa razón, en la siguiente iteración esas observaciones mal clasificadas tuvieron un mayor peso o importancia en el nuevo clasificador \(h_{3}\), por eso es que esos símbolos \(-\) aparecen más grandes en la tercera figura.
Figura 2: Clasificadores
El clasificador \(h_{3}\) logra clasificar mejor esos \(-\).
Figura 3: Clasificador final
Vamos a realizar un modelo de boosting con tidymodels
.
Primero, importamos la librería:
library(tidymodels)
Hacemos la partición de datos y preprocesamiento del workflow:
set.seed(123)
split <- initial_split(data)
train <- training(split)
test <- testing(split)
recipe <- recipe(realiza_domest ~ ., data = train)%>%
update_role(ID, new_role = "id") %>%
step_other(BHCH04_SEL, threshold = 0.2)
wf <- workflow() %>%
add_recipe(recipe)
Ahora, viene la parte de especificar la información del modelo. Como
estamos trabajando con boosting, empezamos pasando el parámetro
boost_tree()
. Para elegir entre los tipos de modelos de
boosting posibles, hay que modificar el parámetro de
set_engine()
. Por default, tidymodels usa el parámetro
xgboost
para modelos de boosting. Lo vamos a cambiar por
C5.0, que sigue un procedimiento similar a AdaBoosting: el modelo final
es un ensamble de árboles que hacen un voto ponderado para asignar a la
clase final.
bt_spec <- boost_tree(
trees = tune(),
min_n = tune ()
) %>%
set_engine("C5.0") %>%
set_mode("classification")
bt_spec %>% translate()
## Boosted Tree Model Specification (classification)
##
## Main Arguments:
## trees = tune()
## min_n = tune()
##
## Computational engine: C5.0
##
## Model fit template:
## parsnip::C5.0_train(x = missing_arg(), y = missing_arg(), weights = missing_arg(),
## trials = tune(), minCases = tune())
Ahora, vamos a hacer las muestras de cross-validation para tunear los hiperparámetros. Haremos un grid de 10 cruces para distintas combinaciones de la cantidad de árboles y la mínima cantidad de particiones.
tune_wf <- wf %>%
add_model(bt_spec)
set.seed(912)
folds <- vfold_cv(train)
tictoc::tic()
tune_params <- tune_wf %>%
tune_grid(folds,
metrics = metric_set(precision, recall,
roc_auc, f_meas),
grid = 15)
tictoc::toc()
## 68.694 sec elapsed
Ahora, seleccionamos el mejor modelo en base a roc_auc
y
lo finalizamos.
best_model <- select_best(tune_params, metric = "roc_auc")
final_model <- finalize_model(bt_spec, best_model)
Para finalizar, actualizamos el modelo tuneado en el workflow y lo fiteamos.
tree_boost <- wf %>%
update_model(final_model)
## Warning: The workflow has no model to remove.
fit_tree <- tree_boost %>% fit(train)
fit_tree
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: boost_tree()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 1 Recipe Step
##
## • step_other()
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call:
## C5.0.default(x = x, y = y, trials = 51L, control = C50::C5.0Control(minCases
## = 36L, sample = 0))
##
## Classification Tree
## Number of samples: 10762
## Number of predictors: 8
##
## Number of boosting iterations: 51 requested; 22 used due to early stopping
## Average tree size: 5.9
##
## Non-standard options: attempt to group attributes, minimum number of cases: 36
Guardamos el modelo para usarlo a futuro.
Ahora, vamos a predecir sobre el test set, obtener las métricas de evaluación y compararlas con los otros modelos. Recordemos los modelos previos.
cart_final_fit <- read_rds('./models/cart_final_train.rds')
cart_test <- cart_final_fit %>%
predict(test) %>%
bind_cols(test, .)
## Warning: Novel levels found in column 'SEXO_SEL': 'Varon'. The levels have been
## removed, and values have been coerced to 'NA'.
## Warning: Novel levels found in column 'BHCH04_SEL': 'Conyuge/pareja',
## 'Cuniado/a'. The levels have been removed, and values have been coerced to
## 'NA'.
cart_test <- predict(cart_final_fit, test, type = "prob") %>%
bind_cols(cart_test, .)
## Warning: Novel levels found in column 'SEXO_SEL': 'Varon'. The levels have been removed, and values have been coerced to 'NA'.
## Novel levels found in column 'BHCH04_SEL': 'Conyuge/pareja', 'Cuniado/a'. The levels have been removed, and values have been coerced to 'NA'.
bagging_final_fit <- read_rds('./models/bagging_final_train.rds')
bagging_test <- bagging_final_fit %>%
predict(test) %>%
bind_cols(test, .)
bagging_test <- predict(bagging_final_fit, test, type = "prob") %>%
bind_cols(bagging_test, .)
rf_final_fit <- read_rds('./models/rf_final_train.rds')
rf_test <- rf_final_fit %>%
predict(test) %>%
bind_cols(test, .)
rf_test <- predict(rf_final_fit, test, type = "prob") %>%
bind_cols(rf_test, .)
datasets <- list(cart_test, bagging_test, rf_test)
class_metrics <- metric_set(precision, recall,
accuracy, f_meas)
#Hago una función para mapear y no repetir lo mismo 3 veces
metricas <- function(dataset, model_name){
metrics <- roc_auc(dataset, truth = realiza_domest, ".pred_No realiza") %>%
bind_rows(class_metrics(dataset, truth = realiza_domest, estimate = .pred_class))
return(metrics)
}
metrics_eval <- datasets %>%
map_dfr(metricas, .id = "model")
metrics_eval <- metrics_eval %>%
mutate_at(vars(model),
~as.factor(
case_when(
. %in% "1" ~ "Árbol de decisión",
. %in% "2" ~ "Bagging",
. %in% "3" ~ "Random Forest")
))
ggplot(metrics_eval, aes(x = .metric, y = .estimate, fill = model))+
geom_col(position = "dodge")+
scale_fill_viridis_d()+
theme_minimal()
- ¿Qué modelo funciona mejor? ¿Por qué?
Comparemos, ahora, con Boosting…
boost_test <- fit_tree %>%
predict(test) %>%
bind_cols(., test)
boost_test <- predict(fit_tree, test, type = "prob") %>%
bind_cols(boost_test, .)
class_metrics <- metric_set(precision, recall,
accuracy, f_meas)
boost_metrics <- roc_auc(boost_test, truth = realiza_domest, ".pred_No realiza") %>%
bind_rows(class_metrics(boost_test, truth = realiza_domest, estimate = .pred_class)) %>%
mutate(model = "Boosting")
metrics_eval <- bind_rows(metrics_eval, boost_metrics)
ggplot(metrics_eval, aes(x = .metric, y = .estimate, fill = model))+
geom_col(position = "dodge")+
scale_fill_viridis_d()+
theme_minimal()
Teniendo en cuenta qué significa cada métrica de evaluación, ¿qué pueden decir de los 4 modelos que probaron?
¿Cuál captura mayor cantidad de casos positivos?
¿Cuál captura con mayor exactitud los casos positivos?
¿Cuál funciona mejor en base a esos dos criterios? ¿Y peor?