Sesión 1.3: Laboratorio - Primer flujo de trabajo con tidymodels
Lo que vamos a hacer:
Lo que vamos a aprender:
Trabajen en sus computadoras y pregunten si tienen dudas.
Necesitamos instalar y cargar los paquetes. Ejecuten este código en R:
tidymodels es un conjunto de paquetes que comparten la filosofía del tidyverse. Los que usaremos hoy:
Ventaja: misma sintaxis para cualquier modelo, sea regresión logística, random forest o redes neuronales. La lista completa queda en la documentación.
Vamos a trabajar con un dataset de indicadores socioeconómicos de países de todo el mundo (datos simulados inspirados en el Banco Mundial):
Rows: 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"…
Antes de modelar, siempre hay que explorar los datos:
pais continente gasto_educacion acceso_internet
Length:179 Length:179 Min. :2.100 Min. : 5.00
Class :character Class :character 1st Qu.:4.400 1st Qu.:46.60
Mode :character Mode :character Median :5.000 Median :57.50
Mean :5.027 Mean :57.52
3rd Qu.:5.600 3rd Qu.:68.35
Max. :8.000 Max. :99.00
urbanizacion gasto_salud inflacion desempleo
Min. :16.40 Min. :3.200 Min. : 5.20 Min. : 1.500
1st Qu.:44.70 1st Qu.:5.800 1st Qu.: 9.35 1st Qu.: 6.700
Median :58.10 Median :6.700 Median :11.70 Median : 8.400
Mean :58.42 Mean :6.631 Mean :12.73 Mean : 8.434
3rd Qu.:73.15 3rd Qu.:7.450 3rd Qu.:14.70 3rd Qu.:10.100
Max. :97.60 Max. :9.500 Max. :37.40 Max. :16.100
inversion_extranjera indice_gobierno_digital crecimiento_alto
Min. :0.200 Min. :0.0800 Length:179
1st Qu.:3.050 1st Qu.:0.4050 Class :character
Median :4.200 Median :0.5200 Mode :character
Mean :4.079 Mean :0.5136
3rd Qu.:5.100 3rd Qu.:0.6350
Max. :7.400 Max. :0.9600
crecimiento_alto (sí/no)Instrucciones: Antes de continuar, exploren los datos ustedes mismos.
Tómense 5 minutos para explorar.
Convertimos la variable de resultado a factor (necesario para clasificación) y seleccionamos las variables:
# Asegurar que el outcome sea un factor
datos <- datos |>
mutate(crecimiento_alto = factor(crecimiento_alto, levels = c("no", "si")))
# Seleccionar variables para el modelo (solo numéricas)
datos_modelo <- datos |>
select(crecimiento_alto, gasto_educacion, acceso_internet,
urbanizacion, gasto_salud, inflacion, desempleo,
inversion_extranjera, indice_gobierno_digital)
# Verificar
glimpse(datos_modelo)Rows: 179
Columns: 9
$ crecimiento_alto <fct> si, si, no, no, no, si, no, no, si, no, no, no…
$ 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…
levels = c("no", "si")
levels(...) para asegurarnos de que el orden es correctoUsamos initial_split() de rsample para crear la división train/test:
# Fijar semilla para reproducibilidad
set.seed(2026)
# Dividir: 75% entrenamiento, 25% prueba
datos_split <- initial_split(datos_modelo, prop = 0.75, strata = crecimiento_alto)
# Extraer los conjuntos
datos_train <- training(datos_split)
datos_test <- testing(datos_split)
# Verificar tamaños
cat("Entrenamiento:", nrow(datos_train), "filas\n")Entrenamiento: 133 filas
Prueba: 46 filas
strata = crecimiento_alto asegura que la proporción de sí/no sea similar en ambos conjuntosComprobemos que las proporciones son similares en ambos conjuntos:
# A tibble: 2 × 4
crecimiento_alto n prop conjunto
<fct> <int> <dbl> <chr>
1 no 69 0.519 train
2 si 64 0.481 train
# A tibble: 2 × 4
crecimiento_alto n prop conjunto
<fct> <int> <dbl> <chr>
1 no 24 0.522 test
2 si 22 0.478 test
Las proporciones deberían ser muy similares (¡y realmente lo son en este caso!)
Tres parámetros moldean la partición. Vean el efecto:
prop = 0.50 -> Train: 89 | Test: 90
# A tibble: 2 × 3
crecimiento_alto n prop
<fct> <int> <dbl>
1 no 75 0.56
2 si 59 0.44
prop baja: menos datos para aprender, peor ajustestrata: proporciones de clase pueden diferir, sesga las métricasUsamos logistic_reg() de parsnip para definir una regresión logística:
Logistic Regression Model Specification (classification)
Computational engine: glm
logistic_reg() define qué tipo de modelo queremosset_engine("glm") define qué implementación usar (glm es la función base de R)set_mode("classification") indica que es un problema de clasificaciónAhora ajustamos el modelo a los datos de entrenamiento:
# A tibble: 9 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) -4.64 2.56 -1.81 0.0701
2 gasto_educacion 0.588 0.271 2.17 0.0299
3 acceso_internet 0.0634 0.0186 3.41 0.000658
4 urbanizacion -0.00648 0.0159 -0.408 0.683
5 gasto_salud -0.168 0.219 -0.767 0.443
6 inflacion -0.166 0.0638 -2.60 0.00938
7 desempleo -0.318 0.112 -2.85 0.00438
8 inversion_extranjera 0.355 0.182 1.96 0.0505
9 indice_gobierno_digital 5.35 2.03 2.63 0.00848
crecimiento_alto ~ . significa: predecir crecimiento_alto usando todas las demás variablesfit() entrena el modelo con los datos de entrenamientotidy() nos da una tabla limpia con coeficientes, errores estándar y p-valoresRecordatorio: regresión logística
exp(coeficiente)Preguntas para discutir:
# A tibble: 9 × 4
term estimate odds_ratio p.value
<chr> <dbl> <dbl> <dbl>
1 (Intercept) -4.64 0.00963 0.0701
2 gasto_educacion 0.588 1.80 0.0299
3 acceso_internet 0.0634 1.07 0.000658
4 urbanizacion -0.00648 0.994 0.683
5 gasto_salud -0.168 0.845 0.443
6 inflacion -0.166 0.847 0.00938
7 desempleo -0.318 0.727 0.00438
8 inversion_extranjera 0.355 1.43 0.0505
9 indice_gobierno_digital 5.35 211. 0.00848
.pred_class es la clase predicha por el modelocrecimiento_alto (la verdad)# A tibble: 8 × 2
crecimiento_alto .pred_class
<fct> <fct>
1 si no
2 no no
3 no no
4 si no
5 no si
6 no si
7 no no
8 si si
La matriz de confusión muestra todos los resultados posibles:
Truth
Prediction no si
no 19 4
si 5 18
# A tibble: 13 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.804
2 kap binary 0.609
3 sens binary 0.792
4 spec binary 0.818
5 ppv binary 0.826
6 npv binary 0.783
7 mcc binary 0.609
8 j_index binary 0.610
9 bal_accuracy binary 0.805
10 detection_prevalence binary 0.5
11 precision binary 0.826
12 recall binary 0.792
13 f_meas binary 0.809
Observen las diferentes métricas y piensen cuál es más relevante para este problema.
Instrucciones: Reflexionen sobre los resultados.
Discutan en grupos de 2-3 personas durante 5 minutos.
El modelo no solo predice clases: también da probabilidades. Por defecto, predict() usa un umbral de 0.5, pero podemos cambiarlo.
# Obtener probabilidades en test
pred_probs <- ajuste |>
predict(datos_test, type = "prob") |>
bind_cols(datos_test)
# Precision y recall para tres umbrales
purrr::map_df(c(0.3, 0.5, 0.7), function(u) {
pred_probs |>
mutate(.pred_u = factor(
dplyr::if_else(.pred_si >= u, "si", "no"),
levels = c("no", "si")
)) |>
summarise(
umbral = u,
precision = precision_vec(crecimiento_alto, .pred_u, event_level = "second"),
recall = recall_vec(crecimiento_alto, .pred_u, event_level = "second")
)
})# A tibble: 3 × 3
umbral precision recall
<dbl> <dbl> <dbl>
1 0.3 0.688 1
2 0.5 0.783 0.818
3 0.7 0.85 0.773
La curva ROC resume el rendimiento en todos los umbrales a la vez:
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.911
event_level = "second" fija “si” como el evento positivo. Sin esto, el AUC sale invertidoDos temas que no tocaremos hoy pero aparecen en los apéndices:
vfold_cv + fit_resamples): en lugar de una sola división train/test, el modelo se evalúa en varios folds y se promedian las métricas. Da estimaciones más estables. La veremos en Laboratorio 2. Ejemplo en Apéndice 6Material opcional: carguen el script laboratorio-01.R para ejecutar todos los ejemplos.
Flujo de trabajo completo:
Conceptos clave:
En el Laboratorio 2 (Sesión 1.4) vamos a:
Guarden su trabajo y tómense un descanso! 😉
Comandos para cada pregunta:
[1] 179 11
[1] 179
[1] 11
[1] 0
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
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
Respuestas esperadas:
pais, continente, 8 predictores numéricos y crecimiento_altoacceso_internet y urbanizacion tienen distribuciones amplias: países con muy poco acceso vs. muy conectadosindice_gobierno_digital se correlaciona positivamente con acceso_internet y negativamente con inflacionPista: ggplot2 permite visualizar distribuciones con geom_histogram() y correlaciones con el paquete corrplot o ggcorrplot.
Train: 89 / Test: 90
# A tibble: 2 × 3
crecimiento_alto n prop
<fct> <int> <dbl>
1 no 68 0.507
2 si 66 0.493
# A tibble: 2 × 3
crecimiento_alto n prop
<fct> <int> <dbl>
1 no 25 0.556
2 si 20 0.444
# A tibble: 2 × 3
crecimiento_alto n prop
<fct> <int> <dbl>
1 no 69 0.519
2 si 64 0.481
prop = 0.50), el modelo puede tener peor rendimientostrata, las proporciones de clases pueden ser distintas en train y test, lo que sesga las métricasCalcular las métricas:
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 precision binary 0.783
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 recall binary 0.818
# A tibble: 13 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.804
2 kap binary 0.609
3 sens binary 0.818
4 spec binary 0.792
5 ppv binary 0.783
6 npv binary 0.826
7 mcc binary 0.609
8 j_index binary 0.610
9 bal_accuracy binary 0.805
10 detection_prevalence binary 0.5
11 precision binary 0.783
12 recall binary 0.818
13 f_meas binary 0.8
Cómo interpretar la diferencia:
¿Qué métrica priorizar?
No hay respuesta universal. El contexto define la métrica correcta.
¿Cómo identificar predicciones inciertas?
# A tibble: 5 × 4
crecimiento_alto .pred_no .pred_si incertidumbre
<fct> <dbl> <dbl> <dbl>
1 no 0.527 0.473 0.0265
2 no 0.460 0.540 0.0399
3 no 0.580 0.420 0.0804
4 si 0.582 0.418 0.0823
5 si 0.586 0.414 0.0856
.pred_si cercano a 0.5 son las más difíciles de clasificar: pequeños cambios en los predictores pueden cambiar la clase predicha# Comparar precisión y recall para tres umbrales
purrr::map_df(c(0.3, 0.5, 0.7), function(u) {
pred_probs |>
mutate(.pred_u = factor(
dplyr::if_else(.pred_si >= u, "si", "no"),
levels = c("no", "si")
)) |>
summarise(
umbral = u,
precision = precision_vec(crecimiento_alto, .pred_u, event_level = "second"),
recall = recall_vec(crecimiento_alto, .pred_u, event_level = "second")
)
})# A tibble: 3 × 3
umbral precision recall
<dbl> <dbl> <dbl>
1 0.3 0.688 1
2 0.5 0.783 0.818
3 0.7 0.85 0.773
| Umbral | Precisión | Recall | Interpretación |
|---|---|---|---|
| 0.3 | baja | alta | predice “sí” fácilmente, más falsos positivos |
| 0.5 | media | media | punto de equilibrio (por defecto) |
| 0.7 | alta | baja | predice “sí” con cautela, más falsos negativos |
Este es el compromiso precisión-recall en acción. El umbral óptimo depende del problema.
# A tibble: 3 × 6
.metric .estimator mean n std_err .config
<chr> <chr> <dbl> <int> <dbl> <chr>
1 accuracy binary 0.759 5 0.0236 pre0_mod0_post0
2 precision binary 0.763 5 0.0244 pre0_mod0_post0
3 recall binary 0.732 5 0.0562 pre0_mod0_post0
# A tibble: 2 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.804
2 kap binary 0.609
collect_metrics() devuelve la media de cada métrica sobre los 5 folds junto con su error estándar (std_err)Interpretar el AUC:
| AUC | Interpretación |
|---|---|
| 0.5 | No mejor que el azar |
| 0.7–0.8 | Aceptable |
| 0.8–0.9 | Bueno |
| > 0.9 | Excelente (verifiquen data leakage) |