workflowsets: comparar modelos en serio
El problema: comparar muchos modelos
En cualquier proyecto serio de ML pruebas varios modelos antes de quedarte con uno: una regresión logística como baseline, un random forest como caballo de batalla, un XGBoost para presumir. Sin estructura, esto es:
# Versión manual — frágil
res_lm <- fit_resamples(wf_lm, resamples = folds, metrics = mis_metricas)
res_rf <- fit_resamples(wf_rf, resamples = folds, metrics = mis_metricas)
res_xgb <- fit_resamples(wf_xgb, resamples = folds, metrics = mis_metricas)
collect_metrics(res_lm)
collect_metrics(res_rf)
collect_metrics(res_xgb)Y todavía falta compararlos a mano. Si añades otra receta (con y sin PCA, por ejemplo), tienes 6 combinaciones, el código duplicado se descontrola rápido.
workflowsets automatiza el cruce recipes × modelos y los corre como una matriz. Una llamada para entrenar, una llamada para comparar.
workflow_set(): combinaciones recipes × modelos
library(tidymodels)
library(workflowsets)
# Dos recetas alternativas
receta_base <- recipe(price ~ ., data = train) |>
step_log(price, base = 10) |>
step_normalize(all_numeric_predictors()) |>
step_dummy(all_nominal_predictors())
receta_pca <- receta_base |>
step_pca(all_numeric_predictors(), num_comp = 5)
# Tres especificaciones de modelo
spec_lm <- linear_reg() |> set_engine("lm")
spec_rf <- rand_forest(trees = 500) |> set_engine("ranger") |> set_mode("regression")
spec_xgb <- boost_tree(trees = 500) |> set_engine("xgboost") |> set_mode("regression")
# Cruce
wfs <- workflow_set(
preproc = list(base = receta_base, pca = receta_pca),
models = list(lm = spec_lm, rf = spec_rf, xgb = spec_xgb)
)
wfs
#> # A workflow set/tibble: 6 × 4
#> wflow_id info option result
#> <chr> <list> <list> <list>
#> 1 base_lm <tibble [1 × 4]> <opts[0]> <list [0]>
#> 2 base_rf <tibble [1 × 4]> <opts[0]> <list [0]>
#> 3 base_xgb <tibble [1 × 4]> <opts[0]> <list [0]>
#> 4 pca_lm <tibble [1 × 4]> <opts[0]> <list [0]>
#> 5 pca_rf <tibble [1 × 4]> <opts[0]> <list [0]>
#> 6 pca_xgb <tibble [1 × 4]> <opts[0]> <list [0]>Las dos listas en preproc y models se cruzan: 2 × 3 = 6 workflows. Cada uno tiene un wflow_id basado en los nombres (base_lm, pca_xgb, etc.). Nombrar las listas es importante, los nombres son las etiquetas en el ranking final.
workflow_map(): correr todo en paralelo
library(future)
plan(multisession, workers = 4) # paralelismo
set.seed(123)
resultados <- workflow_map(
wfs,
fn = "fit_resamples", # qué función aplicar
resamples = folds,
metrics = metric_set(rmse, rsq),
verbose = TRUE
)workflow_map() aplica una función (fit_resamples, tune_grid, tune_bayes) a cada workflow del set. Devuelve el mismo wfs pero con la columna result llena.
Con plan(multisession, workers = 4), los 6 workflows se distribuyen en 4 procesos paralelos, la mejora es casi proporcional al número de cores.
Si quieres tunear modelos en lugar de solo evaluar:
resultados_tune <- workflow_map(
wfs,
fn = "tune_grid",
resamples = folds,
grid = 10,
metrics = metric_set(rmse)
)Esto hace tuning de hiperparámetros (los marcados con tune()) para cada modelo del set. Es el patrón completo: explora 6 workflows, cada uno con 10 combinaciones de hiperparámetros, todos sobre los mismos folds.
rank_results(): el ranking honesto
Una vez todos los modelos han corrido sobre los mismos folds, rank_results() te da el ranking:
resultados |>
rank_results(rank_metric = "rmse", select_best = TRUE)
#> # A tibble: 6 × 9
#> wflow_id .config .metric mean std_err n preprocessor model rank
#> <chr> <chr> <chr> <dbl> <dbl> <int> <chr> <chr> <int>
#> 1 base_xgb Preprocessor1_M… rmse 0.085 0.0015 5 recipe boost_… 1
#> 2 base_rf Preprocessor1_M… rmse 0.092 0.0019 5 recipe rand_f… 2
#> 3 pca_xgb Preprocessor1_M… rmse 0.103 0.0021 5 recipe boost_… 3
#> 4 pca_rf Preprocessor1_M… rmse 0.109 0.0024 5 recipe rand_f… 4
#> 5 base_lm Preprocessor1_M… rmse 0.156 0.0031 5 recipe linear… 5
#> 6 pca_lm Preprocessor1_M… rmse 0.158 0.0029 5 recipe linear… 6Lectura típica:
- Top 1:
base_xgbcon RMSE = 0.085 ± 0.0015, el mejor. - Diferencia con #2: 0.007, vs
std_err≈ 0.002, diferencia significativa. - PCA empeora todo: las versiones con PCA caen al fondo del ranking.
Esa información de un vistazo es lo que justifica la inversión en workflowsets. Comparando a mano hubieras llegado al mismo número, pero con más fricción.
autoplot(): comparación visual
autoplot(resultados, metric = "rmse")Un boxplot o gráfico de puntos con la métrica de cada workflow + intervalos. Mucho más legible que la tabla cuando comparas con stakeholders.
autoplot(resultados, metric = "rmse", select_best = TRUE)Con select_best = TRUE agrupa los resultados de tuning y muestra solo el mejor por workflow.
option_add(): parámetros compartidos
Si quieres aplicar la misma opción a todos los workflows del set (un control_* específico, una semilla, un parámetro de tuning):
wfs <- wfs |>
option_add(
control = control_grid(save_pred = TRUE, verbose = TRUE)
)Útil cuando vas a usar collect_predictions() después y quieres asegurar que todos los workflows guarden las predicciones.
Patrón completo con tuning
El patrón end-to-end más usado:
library(tidymodels)
library(workflowsets)
library(finetune)
# 1. Datos divididos + folds
set.seed(123)
split <- initial_split(datos, prop = 0.8, strata = outcome)
train <- training(split)
folds <- vfold_cv(train, v = 5, strata = outcome)
# 2. Receta común (puedes tener varias)
receta <- recipe(outcome ~ ., data = train) |>
step_normalize(all_numeric_predictors()) |>
step_dummy(all_nominal_predictors())
# 3. Modelos a comparar (con tune)
spec_glmnet <- logistic_reg(penalty = tune(), mixture = tune()) |>
set_engine("glmnet")
spec_rf <- rand_forest(mtry = tune(), min_n = tune(), trees = 500) |>
set_engine("ranger") |> set_mode("classification")
spec_xgb <- boost_tree(
trees = 500, learn_rate = tune(), tree_depth = tune(), min_n = tune()
) |>
set_engine("xgboost") |> set_mode("classification")
# 4. Workflow set
wfs <- workflow_set(
preproc = list(base = receta),
models = list(glmnet = spec_glmnet, rf = spec_rf, xgb = spec_xgb)
)
# 5. Tune en paralelo con racing (descarta candidatos malos pronto)
plan(multisession, workers = 6)
set.seed(456)
res <- workflow_map(
wfs,
fn = "tune_race_anova",
resamples = folds,
grid = 20,
metrics = metric_set(roc_auc),
control = control_race(verbose_elim = TRUE)
)
# 6. Ver ranking
rank_results(res, rank_metric = "roc_auc", select_best = TRUE)
autoplot(res, metric = "roc_auc", select_best = TRUE)
# 7. Extraer el ganador y finalizar
mejor_id <- res |>
rank_results(rank_metric = "roc_auc", select_best = TRUE) |>
slice(1) |>
pull(wflow_id)
mejores_params <- res |>
extract_workflow_set_result(mejor_id) |>
select_best(metric = "roc_auc")
wf_final <- res |>
extract_workflow(mejor_id) |>
finalize_workflow(mejores_params)
# 8. Evaluación final
last_fit(wf_final, split) |>
collect_metrics()Ocho pasos, todo automatizable. Esta plantilla cubre el 90 % de los proyectos de ML tabular.
Cuándo workflowsets vs scripting manual
Usa workflowsets cuando:
- Comparas ≥ 3 modelos sobre los mismos datos.
- Pruebas varias recetas (con/sin PCA, con/sin imputación, con distintas codificaciones).
- Necesitas paralelismo sin escribir orquestación manual.
NO uses workflowsets cuando:
- Solo tienes un modelo y quieres tunearlo. Llama
tune_grid()directamente. - Los workflows son tan distintos entre sí que cada uno necesita métricas y resamples distintos. Mejor mantenerlos separados.
Trampas habituales
- Olvidar
plan(multisession). Sin paralelismo,workflow_map()es secuencial, perdería todo el beneficio. Especialmente con racing o tuning, paralelismo es donde está el speedup. - Mezclar workflows con diferentes folds. Si un workflow se entrena con
vfold_cv(v=5)y otro convfold_cv(v=10), la comparación no es válida. Asegúrate de que elresampleses el mismo objeto. - Comparar modelos tuneados vs no tuneados. Si
glmnettienepenalty = tune()yrftiene parámetros fijos, glmnet probablemente gane porque eligió su mejor hiperparámetro. Asegura que todos los modelos están en condiciones equivalentes. - Confiar en el ranking sin mirar
std_err. Si las diferencias entre top 1 y top 3 son menores que elstd_err, son empates estadísticos.select_by_one_std_err()aplicada al workflow ganador suele ser la decisión robusta.
En la siguiente entrega
Has aprendido a comparar modelos sistemáticamente. La última pieza pedagógica de la ruta es interpretar lo que el modelo aprendió. Un random forest con AUC 0.92 que es una caja negra te puede dejar sin defensa frente a un comité de revisión. Vemos DALEX, vip y SHAP. Lo siguiente.