Sesión 1.4: Laboratorio 2 - Validación cruzada y comparación de modelos
Lo que vamos a hacer:
Lo que vamos a practicar:
Hay menos código predefinido. ¡Ustedes van a escribir más!
¡No se preocupen! Les damos una estructura para que puedan completar 😉
En el lab 1 hicimos:
indicadores_mundiales.csvHoy vamos un paso más allá:
El objetivo sustantivo sigue siendo el mismo: predecir crecimiento_alto a partir de indicadores macro
O de una forma más automatizada:
paquetes <- c("tidymodels", "tidyverse", "rpart")
# require(): intenta cargar el paquete y devuelve TRUE/FALSE
# Si no está instalado (FALSE), lo instala y carga
# character.only = TRUE: pkg es una variable; sin esto, R buscaría un paquete llamado "pkg"
for (pkg in paquetes) {
if (!require(pkg, character.only = TRUE)) {
install.packages(pkg, dependencies = TRUE)
library(pkg, character.only = TRUE)
}
}Volvemos al dataset de indicadores mundiales que ya usamos en el lab 1:
# Cargar datos
datos <- read_csv("datos/indicadores_mundiales.csv")
# Lean el archivo directo desde la web:
# datos <- read_csv("https://raw.githubusercontent.com/danilofreire/introduccion-ia-ucu/main/clases/dia-01/datos/indicadores_mundiales.csv")
# Ver estructura
glimpse(datos) # Ver las variables y tiposRows: 179
Columns: 11
$ pais <chr> "Angola", "Argelia", "Benín", "Botsuana", "Bur…
$ continente <chr> "África", "África", "África", "África", "Áfric…
$ gasto_educacion <dbl> 5.7, 4.0, 3.2, 5.0, 4.6, 4.8, 4.6, 4.3, 5.9, 5…
$ acceso_internet <dbl> 50.2, 48.0, 34.3, 46.2, 62.5, 66.0, 55.5, 39.1…
$ urbanizacion <dbl> 69.1, 61.8, 62.9, 68.6, 54.5, 32.3, 57.6, 52.1…
$ gasto_salud <dbl> 8.3, 7.6, 4.5, 5.5, 4.3, 4.6, 8.3, 6.9, 4.8, 6…
$ inflacion <dbl> 9.4, 9.2, 10.3, 14.7, 12.2, 11.8, 11.2, 23.4, …
$ desempleo <dbl> 5.9, 10.0, 7.2, 13.1, 12.7, 9.7, 9.5, 14.0, 9.…
$ inversion_extranjera <dbl> 2.4, 4.8, 2.5, 4.1, 2.3, 2.3, 5.5, 3.6, 5.2, 1…
$ indice_gobierno_digital <dbl> 0.38, 0.78, 0.13, 0.49, 0.52, 0.33, 0.57, 0.33…
$ crecimiento_alto <chr> "si", "si", "no", "no", "no", "si", "no", "no"…
Variable objetivo: crecimiento_alto (sí/no) - ¿El país tuvo crecimiento del PIB mayor o igual a 3%?
| Variable | Descripción | Tipo |
|---|---|---|
crecimiento_alto |
PIB creció ≥ 3% (sí/no) | Categórica |
gasto_educacion |
Gasto en educación (% del PIB) | Numérica |
acceso_internet |
Acceso a internet (% de la población) | Numérica |
urbanizacion |
Población urbana (% del total) | Numérica |
gasto_salud |
Gasto en salud (% del PIB) | Numérica |
inflacion |
Tasa de inflación anual (%) | Numérica |
desempleo |
Tasa de desempleo (%) | Numérica |
inversion_extranjera |
IED (% del PIB) | Numérica |
indice_gobierno_digital |
Índice de gobierno digital (0-1) | Numérica |
pais |
Nombre del país | Categórica |
continente |
Continente | Categórica |
Antes de modelar, conviene ver cómo se distribuyen las variables y si difieren entre los grupos de crecimiento_alto. Mostramos acá 3 de los 8 predictores
gasto_educacion acceso_internet urbanizacion
Min. :2.100 Min. : 5.00 Min. :16.40
1st Qu.:4.400 1st Qu.:46.60 1st Qu.:44.70
Median :5.000 Median :57.50 Median :58.10
Mean :5.027 Mean :57.52 Mean :58.42
3rd Qu.:5.600 3rd Qu.:68.35 3rd Qu.:73.15
Max. :8.000 Max. :99.00 Max. :97.60
# A tibble: 2 × 4
crecimiento_alto educacion_media internet_medio urbanizacion_media
<chr> <dbl> <dbl> <dbl>
1 no 4.75 51.8 54.8
2 si 5.33 63.7 62.3
Detalle por variable en Apéndice 2; relaciones con el objetivo en Apéndice 3; visualizaciones en Apéndice 4
Tarea: Exploren una de las variables que no mostramos en la diapositiva anterior.
crecimiento_alto?inflacion, desempleo, inversion_extranjera o indice_gobierno_digital y comparen su media (o mediana) entre países con crecimiento_alto = "si" y "no". ¿La diferencia parece grande?acceso_internet y urbanizacion? Si lo están, podríamos no necesitar a los dos en el modelo# Código para empezar
dim(datos)
datos |> count(crecimiento_alto) |> mutate(prop = n / sum(n))
# Ejemplo para la primera pregunta: media de gasto_salud por grupo
datos |>
group_by(crecimiento_alto) |>
summarise(media_gasto_salud = mean(gasto_salud))
# Ejemplo para la segunda pregunta: correlación entre dos predictores
cor(datos$acceso_internet, datos$urbanizacion)Tómense 5-10 minutos
App 1: exploración inicial · App 2: ¿media o mediana? · App 3: comparación numérica por grupo · App 4: versión visual (opcional)
Convertimos la variable objetivo a factor y descartamos las columnas que no usaremos como predictores. pais tiene 179 valores únicos y continente la dejamos fuera para quedarnos con predictores numéricos, igual que en el lab 1.
Creamos un nuevo objeto datos_modelo y mantenemos datos intacto para los apéndices y la exploración:
Rows: 179
Columns: 9
$ gasto_educacion <dbl> 5.7, 4.0, 3.2, 5.0, 4.6, 4.8, 4.6, 4.3, 5.9, 5…
$ acceso_internet <dbl> 50.2, 48.0, 34.3, 46.2, 62.5, 66.0, 55.5, 39.1…
$ urbanizacion <dbl> 69.1, 61.8, 62.9, 68.6, 54.5, 32.3, 57.6, 52.1…
$ gasto_salud <dbl> 8.3, 7.6, 4.5, 5.5, 4.3, 4.6, 8.3, 6.9, 4.8, 6…
$ inflacion <dbl> 9.4, 9.2, 10.3, 14.7, 12.2, 11.8, 11.2, 23.4, …
$ desempleo <dbl> 5.9, 10.0, 7.2, 13.1, 12.7, 9.7, 9.5, 14.0, 9.…
$ inversion_extranjera <dbl> 2.4, 4.8, 2.5, 4.1, 2.3, 2.3, 5.5, 3.6, 5.2, 1…
$ indice_gobierno_digital <dbl> 0.38, 0.78, 0.13, 0.49, 0.52, 0.33, 0.57, 0.33…
$ crecimiento_alto <fct> si, si, no, no, no, si, no, no, si, no, no, no…
Mantenemos el orden c("no", "si") para que "si" sea la clase positiva. Más adelante usaremos event_level = "second"
El feature engineering puede mejorar el rendimiento del modelo:
# Crear nuevas variables sobre el frame que va al modelo
datos_modelo <- datos_modelo |>
mutate(
# Grupos de urbanización (rural, mixto, urbano)
grupo_urbanizacion = cut(urbanizacion,
breaks = c(0, 50, 75, 100),
labels = c("rural", "mixto", "urbano"),
include.lowest = TRUE),
# Intensidad fiscal social: cuánto del PIB va a salud + educación
gasto_social = gasto_educacion + gasto_salud,
# Indicador de alto desempleo (lo usaremos como ejemplo más adelante)
alto_desempleo = factor(if_else(desempleo > 7, "alto", "bajo"))
)
# Ver las nuevas variables
datos_modelo |> select(grupo_urbanizacion, gasto_social, alto_desempleo) |> head()# A tibble: 6 × 3
grupo_urbanizacion gasto_social alto_desempleo
<fct> <dbl> <fct>
1 mixto 14 bajo
2 mixto 11.6 alto
3 mixto 7.7 alto
4 mixto 10.5 alto
5 mixto 8.9 alto
6 rural 9.4 alto
Tarea: Elijan una variable derivada y créenla
Opciones sugeridas (elijan la que más les interese). Entre paréntesis, la variable base:
acceso_internet)gasto_educacion)inflacion)inversion_extranjera)¡Tómense 3-5 minutos!
# Fijar semilla
set.seed(2026)
# Dividir datos (usando el frame con la variable objetivo como factor)
datos_split <- initial_split(datos_modelo, prop = 0.75, strata = crecimiento_alto)
datos_train <- training(datos_split)
datos_test <- testing(datos_split)
# Crear folds para validación cruzada
folds <- vfold_cv(datos_train, v = 5, strata = crecimiento_alto)
cat("Train:", nrow(datos_train), "| Test:", nrow(datos_test))Train: 133 | Test: 46
initial_split(): Divide el dataset en train/testtraining() y testing(): Extraen los datasets de train y testvfold_cv(): Crea folds para validación cruzada (5 folds, estratificados por crecimiento_alto)Con 179 países y 5 folds, cada partición tiene pocos países en validación. Es suficiente para entrenar, pero las métricas tendrán más ruido
Vamos a comparar dos modelos de naturaleza distinta: uno lineal y uno no lineal
La misma sintaxis sirve para ambos: esa es la ventaja de parsnip. Otros motores (kknn, ranger, xgboost) funcionan igual cambiando solo set_engine()
Una vez más, la lista de modelos disponibles está en https://www.tidymodels.org/find/parsnip/
Decidamos qué variables usar:
Más adelante añadiremos una variable derivada para ver si mejora el ajuste
Validación cruzada en 5 folds y resumen de métricas:
# Las tres métricas juntas en un objeto que vamos a reusar todo el lab
mis_metricas <- metric_set(precision, recall, roc_auc)
eval_logistico <- fit_resamples(
modelo_logistico, formula_basica,
resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second") # "si" es la clase positiva
) |>
collect_metrics() |>
mutate(modelo = "Logístico")
eval_logistico# A tibble: 3 × 7
.metric .estimator mean n std_err .config modelo
<chr> <chr> <dbl> <int> <dbl> <chr> <chr>
1 precision binary 0.768 5 0.0714 pre0_mod0_post0 Logístico
2 recall binary 0.782 5 0.0559 pre0_mod0_post0 Logístico
3 roc_auc binary 0.846 5 0.0512 pre0_mod0_post0 Logístico
fit_resamples() ajusta el modelo en cada fold de la validación cruzadametric_set() junta varias métricas en un solo objeto. Lo guardamos en mis_metricas y lo reusamos en cada evaluacióncollect_metrics() devuelve la media y el error estándar de cada métrica entre foldscontrol = control_resamples(event_level = "second") indica que la clase positiva es "si".metric guarda el nombre de cada métrica (precision, recall, roc_auc) y mean y std_err# El mismo bloque que antes, cambiando modelo_logistico → modelo_arbol
eval_arbol <- fit_resamples(
modelo_arbol, formula_basica,
resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second")
) |>
collect_metrics() |>
mutate(modelo = "Árbol")
# Combinar y mostrar la comparación
resultados <- bind_rows(eval_logistico, eval_arbol)
# Una fila por modelo y métrica, ordenadas para comparar de a pares
resultados |>
select(modelo, .metric, mean, std_err) |>
arrange(.metric, modelo)# A tibble: 6 × 4
modelo .metric mean std_err
<chr> <chr> <dbl> <dbl>
1 Logístico precision 0.768 0.0714
2 Árbol precision 0.686 0.0453
3 Logístico recall 0.782 0.0559
4 Árbol recall 0.706 0.0813
5 Logístico roc_auc 0.846 0.0512
6 Árbol roc_auc 0.717 0.0501
Versión visual de esta comparación (barras con error estándar) en Apéndice 7
Para discutir: las diferencias entre los dos modelos son chicas y sus errores estándar se solapan. ¿Pueden afirmar que un modelo es claramente mejor? ¿Cuál reportarían y por qué?
Antes de pedirles que prueben su propia variable, miremos un ejemplo trabajado. Reemplazamos desempleo (continuo) por alto_desempleo (binario, la creamos antes en la demo) y re-evaluamos el modelo logístico:
# Fórmula extendida con un feature derivado
formula_ext <- crecimiento_alto ~ acceso_internet + gasto_educacion + urbanizacion + gasto_salud + inflacion +
alto_desempleo + inversion_extranjera + indice_gobierno_digital
# Logístico con formula_ext
eval_log_ext <- fit_resamples(modelo_logistico, formula_ext, resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second")) |>
collect_metrics() |> mutate(modelo = "Logístico (ext)")
# Comparar el logístico básico vs. el extendido
bind_rows(eval_logistico, eval_log_ext) |>
select(modelo, .metric, mean, std_err) |>
arrange(.metric, modelo)# A tibble: 6 × 4
modelo .metric mean std_err
<chr> <chr> <dbl> <dbl>
1 Logístico precision 0.768 0.0714
2 Logístico (ext) precision 0.787 0.0640
3 Logístico recall 0.782 0.0559
4 Logístico (ext) recall 0.799 0.0514
5 Logístico roc_auc 0.846 0.0512
6 Logístico (ext) roc_auc 0.850 0.0580
alto_desempleo cambia las métricas del logístico apenas unas milésimas. No todo feature engineering mejora el modelo. Hay que medirlo, no asumirloTarea: Agreguen un feature a la fórmula y re-evalúen: la variable que crearon en el Ejercicio 2, o una interacción entre dos variables (como en el ejemplo)
# Ejemplo: una interacción entre dos variables que se refuerzan
# (gobierno digital y acceso a internet). En una fórmula se escribe con ":"
formula_estudiante <- crecimiento_alto ~ gasto_educacion + acceso_internet +
urbanizacion + gasto_salud + inflacion +
desempleo + inversion_extranjera +
indice_gobierno_digital +
indice_gobierno_digital:acceso_internet
# Repetir el mismo bloque, ahora con formula_estudiante
eval_log_est <- fit_resamples(modelo_logistico, formula_estudiante, resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second")) |>
collect_metrics() |> mutate(modelo = "Logístico (estudiante)")
# Lo mismo cambiando modelo_logistico → modelo_arbol
eval_arb_est <- fit_resamples(modelo_arbol, formula_estudiante, resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second")) |>
collect_metrics() |> mutate(modelo = "Árbol (estudiante)")
# Comparar los 4 modelos: básicos + el suyo
bind_rows(eval_logistico, eval_log_est, eval_arbol, eval_arb_est) |>
filter(.metric == "roc_auc") |> select(modelo, mean, std_err)¡Tómense 5 minutos!
Una vez elegido el mejor modelo por validación cruzada, lo entrenamos con todos los datos de train y lo evaluamos una sola vez en test (los datos que el modelo nunca vio):
# Entrenar el modelo elegido con todos los datos de train
ajuste_final <- modelo_logistico |>
fit(formula_ext, data = datos_train)
# augment() junta predicciones (clases + probabilidades) con los datos originales
# en una sola línea. Equivale a hacer dos predict() + bind_cols().
pred_test <- ajuste_final |> augment(datos_test)
# Métricas finales: el mismo mis_metricas de la validación cruzada
pred_test |> mis_metricas(truth = crecimiento_alto, estimate = .pred_class, .pred_si, event_level = "second")# A tibble: 3 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 precision binary 0.783
2 recall binary 0.818
3 roc_auc binary 0.888
augment() toma un modelo ajustado y un conjunto de datos, y añade las predicciones como columnas nuevas: .pred_class (clase predicha), .pred_no, .pred_si (probabilidades). es la convención de tidymodels para columnas que el paquete crea por ustedespredict() y dos bind_cols() por una sola líneamis_metricas devuelve las tres métricas en una sola tabla. Necesita las dos cosas: estimate = .pred_class para precision y recall, y .pred_si (la probabilidad) para el roc_aucHay una tarea para las dos sesiones con preguntas prácticas que aplican todo lo que vimos hoy.
Las respuestas ya están disponibles para que verifiquen su trabajo 😉
Sesión 1.1: ¿Qué es la IA?
Sesión 1.2: Fundamentos de ML
Sesión 1.3: Laboratorio 1
Sesión 1.4: Laboratorio 2
Día 2: Aprendizaje supervisado
Para profundizar (todo gratis): Tidy Modeling with R – R for Data Science – Get Started with tidymodels
¡Descansen y vengan con energía mañana! 😄
[1] 179 11
# A tibble: 2 × 3
crecimiento_alto n prop
<chr> <int> <dbl>
1 no 93 0.52
2 si 86 0.48
pais continente gasto_educacion
0 0 0
acceso_internet urbanizacion gasto_salud
0 0 0
inflacion desempleo inversion_extranjera
0 0 0
indice_gobierno_digital crecimiento_alto
0 0
gasto_educacion acceso_internet urbanizacion gasto_salud
Min. :2.100 Min. : 5.00 Min. :16.40 Min. :3.200
1st Qu.:4.400 1st Qu.:46.60 1st Qu.:44.70 1st Qu.:5.800
Median :5.000 Median :57.50 Median :58.10 Median :6.700
Mean :5.027 Mean :57.52 Mean :58.42 Mean :6.631
3rd Qu.:5.600 3rd Qu.:68.35 3rd Qu.:73.15 3rd Qu.:7.450
Max. :8.000 Max. :99.00 Max. :97.60 Max. :9.500
inflacion desempleo inversion_extranjera indice_gobierno_digital
Min. : 5.20 Min. : 1.500 Min. :0.200 Min. :0.0800
1st Qu.: 9.35 1st Qu.: 6.700 1st Qu.:3.050 1st Qu.:0.4050
Median :11.70 Median : 8.400 Median :4.200 Median :0.5200
Mean :12.73 Mean : 8.434 Mean :4.079 Mean :0.5136
3rd Qu.:14.70 3rd Qu.:10.100 3rd Qu.:5.100 3rd Qu.:0.6350
Max. :37.40 Max. :16.100 Max. :7.400 Max. :0.9600
El Ejercicio 1 les pide comparar la media o la mediana. ¿Cuándo conviene usar la mediana? Cuando la variable tiene cola larga y la media deja de representarla bien. Tomemos inflacion: tiene cola larga, así que la media (≈ 12.7) es mayor que la mediana (≈ 11.7), y hay países con inflación de hasta ~37% que estiran el promedio:
Min. 1st Qu. Median Mean 3rd Qu. Max.
5.20 9.35 11.70 12.73 14.70 37.40
Cambien el nombre de la variable (gasto_salud, desempleo, indice_gobierno_digital, etc.) para repetir el análisis con otra. La mayoría de los predictores son casi simétricos; inflacion es la excepción
Esta es la comparación numérica que pide el ejercicio: las medias por grupo son la pista más rápida, porque si difieren mucho, la variable probablemente ayudará al modelo. Acá las 5 que no mostramos en el cuerpo:
# A tibble: 2 × 6
crecimiento_alto salud inflacion desempleo ied gob_digital
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 no 6.40 14.7 9.56 3.50 0.438
2 si 6.88 10.6 7.22 4.70 0.595
gasto_educacion acceso_internet urbanizacion
gasto_educacion 1.00 0.07 0.24
acceso_internet 0.07 1.00 0.10
urbanizacion 0.24 0.10 1.00
gasto_salud 0.26 0.09 0.21
inflacion -0.08 -0.05 -0.13
desempleo -0.22 -0.13 -0.28
inversion_extranjera 0.21 0.17 0.14
indice_gobierno_digital 0.18 0.20 0.21
gasto_salud inflacion desempleo inversion_extranjera
gasto_educacion 0.26 -0.08 -0.22 0.21
acceso_internet 0.09 -0.05 -0.13 0.17
urbanizacion 0.21 -0.13 -0.28 0.14
gasto_salud 1.00 -0.17 -0.15 0.09
inflacion -0.17 1.00 0.18 -0.18
desempleo -0.15 0.18 1.00 -0.18
inversion_extranjera 0.09 -0.18 -0.18 1.00
indice_gobierno_digital 0.32 -0.28 -0.30 0.39
indice_gobierno_digital
gasto_educacion 0.18
acceso_internet 0.20
urbanizacion 0.21
gasto_salud 0.32
inflacion -0.28
desempleo -0.30
inversion_extranjera 0.39
indice_gobierno_digital 1.00
Welch Two Sample t-test
data: indice_gobierno_digital by crecimiento_alto
t = -6.8831, df = 175.49, p-value = 9.985e-11
alternative hypothesis: true difference in means between group no and group si is not equal to 0
95 percent confidence interval:
-0.2023819 -0.1121868
sample estimates:
mean in group no mean in group si
0.4380645 0.5953488
Versión visual (opcional) de la primera pregunta: las mismas diferencias de medias del Apéndice 3, ahora como boxplots. ¿Alguna de las cuatro variables que no aparecen en las estadísticas descriptivas separa bien crecimiento_alto?
datos |>
select(crecimiento_alto, gasto_salud, desempleo, inversion_extranjera, indice_gobierno_digital) |>
pivot_longer(-crecimiento_alto, names_to = "variable", values_to = "valor") |>
ggplot(aes(x = crecimiento_alto, y = valor, fill = crecimiento_alto)) +
geom_boxplot(alpha = 0.7) +
facet_wrap(~variable, scales = "free_y") +
scale_fill_manual(values = c("#e74c3c", "#27ae60")) +
labs(title = "Las 4 variables no mostradas, por nivel de crecimiento", x = NULL, y = NULL) +
theme(legend.position = "none")Versión visual (opcional) de la segunda pregunta. Si la nube de puntos cae cerca de una recta, los dos predictores capturan información parecida (colinealidad):
[1] 0.1032229
# Una posible solución para cada una de las cuatro opciones sugeridas
datos_modelo <- datos_modelo |>
mutate(
# 1. Gasto en educación alto (gasto_educacion)
educacion_alta = if_else(gasto_educacion > median(gasto_educacion), "alta", "baja"),
# 2. Acceso a internet por terciles (acceso_internet)
internet_grupos = cut(acceso_internet,
breaks = quantile(acceso_internet, c(0, 1/3, 2/3, 1)),
labels = c("bajo", "medio", "alto"),
include.lowest = TRUE),
# 3. Inflación alta (inflacion)
inflacion_alta = if_else(inflacion > 15, "si", "no"),
# 4. Apertura financiera: IED por encima de la mediana (inversion_extranjera)
apertura_financiera = if_else(inversion_extranjera > median(inversion_extranjera), "alta", "baja")
)
# Verificar las nuevas variables
datos_modelo |> count(educacion_alta)# A tibble: 2 × 2
educacion_alta n
<chr> <int>
1 alta 87
2 baja 92
# A tibble: 3 × 2
internet_grupos n
<fct> <int>
1 bajo 60
2 medio 59
3 alto 60
# A tibble: 2 × 2
inflacion_alta n
<chr> <int>
1 no 140
2 si 39
# A tibble: 2 × 2
apertura_financiera n
<chr> <int>
1 alta 83
2 baja 96
# Una interacción no necesita una columna nueva: se agrega a la fórmula con ":".
# Probamos indice_gobierno_digital × acceso_internet (dos variables que se refuerzan)
formula_int <- crecimiento_alto ~ gasto_educacion + acceso_internet +
urbanizacion + gasto_salud + inflacion +
desempleo + inversion_extranjera +
indice_gobierno_digital +
indice_gobierno_digital:acceso_internet
# Evaluar el logístico (reusamos los mismos folds del cuerpo del lab)
eval_log_int <- fit_resamples(
modelo_logistico, formula_int, resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second")
) |>
collect_metrics() |> mutate(modelo = "Logístico (interacción)")
# Evaluar el árbol con la misma fórmula
eval_arb_int <- fit_resamples(
modelo_arbol, formula_int, resamples = folds,
metrics = mis_metricas,
control = control_resamples(event_level = "second")
) |>
collect_metrics() |> mutate(modelo = "Árbol (interacción)")
bind_rows(eval_logistico, eval_log_int, eval_arbol, eval_arb_int) |>
filter(.metric == "roc_auc") |>
select(modelo, mean, std_err) |>
arrange(desc(mean))# A tibble: 4 × 3
modelo mean std_err
<chr> <dbl> <dbl>
1 Logístico (interacción) 0.851 0.0483
2 Logístico 0.846 0.0512
3 Árbol (interacción) 0.738 0.0687
4 Árbol 0.717 0.0501
La interacción indice_gobierno_digital:acceso_internet mejora un poco el ROC-AUC de ambos modelos: capta el efecto conjunto de dos variables que se refuerzan, algo que las variables por separado no expresan (la mejora es chica y está dentro del error estándar, así que no conviene sobre-interpretarla). El contraste con alto_desempleo, que no ayudó, deja la moraleja: reemplazar una variable continua por su versión discretizada suele ser redundante, mientras que una interacción o una transformación no lineal sí puede agregar información nueva
Una alternativa visual al cuadro de comparación:
resultados |>
ggplot(aes(x = modelo, y = mean, fill = modelo)) +
geom_col(alpha = 0.8) +
geom_errorbar(aes(ymin = mean - std_err, ymax = mean + std_err),
width = 0.2) +
facet_wrap(~.metric, scales = "free_y") +
scale_fill_manual(values = c("#2d4563", "#27ae60")) +
labs(title = "Comparación de modelos", y = "Valor", x = "") +
theme(legend.position = "none")