workflowsets: comparar modelos en serio

r
machine-learning
tidymodels
Cruzar múltiples recipes × múltiples modelos en una matriz, entrenar todo en paralelo con workflow_map(), rankear con rank_results() y visualizar con autoplot().

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…     6

Lectura típica:

  • Top 1: base_xgb con 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 con vfold_cv(v=10), la comparación no es válida. Asegúrate de que el resamples es el mismo objeto.
  • Comparar modelos tuneados vs no tuneados. Si glmnet tiene penalty = tune() y rf tiene 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 el std_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.