IA para Científicos Sociales

Sesión 1.3: Laboratorio - Primer flujo de trabajo con tidymodels

Danilo Freire

Departament of Data and Decision Sciences
Emory University

Laboratorio 1: Primer flujo de trabajo con tidymodels

Objetivos del laboratorio

Lo que vamos a hacer:

  1. Configurar el entorno de trabajo
  2. Cargar y explorar datos
  3. Preprocesar variables
  4. Dividir datos (train/test)
  5. Entrenar un modelo de clasificación
  6. Evaluar el rendimiento
  7. Interpretar resultados

Lo que vamos a aprender:

  • El ecosistema tidymodels
  • El flujo completo de ML en R
  • Cómo evaluar un clasificador
  • Buenas prácticas de reproducibilidad


Trabajen en sus computadoras y pregunten si tienen dudas.

Parte 1: Configuración y exploración

Configuración del entorno

Necesitamos instalar y cargar los paquetes. Ejecuten este código en R:

# Instalar paquetes (solo la primera vez)
install.packages(c("tidymodels", "tidyverse"))

# Cargar paquetes
library(tidymodels)
library(tidyverse)
  • Si ya tienen tidyverse instalado, solo necesitan instalar tidymodels
  • tidymodels carga automáticamente: rsample, parsnip, yardstick, y otros paquetes que necesitaremos
  • Si la instalación falla, intenten:
install.packages("tidymodels", dependencies = TRUE)

Recapitulando: el ecosistema tidy

tidymodels es un conjunto de paquetes que comparten la filosofía del tidyverse. Los que usaremos hoy:

  • rsample: división de datos
  • parsnip: especificación de modelos
  • yardstick: métricas de evaluación

Ventaja: misma sintaxis para cualquier modelo, sea regresión logística, random forest o redes neuronales. La lista completa queda en la documentación.

Cargar los datos

Vamos a trabajar con un dataset de indicadores socioeconómicos de países de todo el mundo (datos simulados inspirados en el Banco Mundial):

  • Es un corte transversal (cross-section): una observación por país, sin dimensión temporal
  • Los datos fueron generados a partir de un factor latente de “desarrollo” con ruido propio por indicador, lo que produce correlaciones moderadas entre predictores (r ~ 0.10-0.40)
# Cargar los datos
datos <- read_csv("datos/indicadores_mundiales.csv")

# Ver las primeras filas
glimpse(datos)
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"…

Exploración inicial

Antes de modelar, siempre hay que explorar los datos:

# Resumen estadístico
summary(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                            

Explorar la variable de resultado

  • Problema: predecir si un país tendrá crecimiento alto del PIB (>= 3%) en un año dado
  • Variable de resultado: crecimiento_alto (sí/no)
  • Predictores: gasto en educación, acceso a internet, urbanización, gasto en salud, inflación, desempleo, inversión extranjera, índice de gobierno digital
  • Esto es un problema de clasificación binaria
  • Nota: |> es el operador de pipe nativo de R 4.1+ (la fuente de estas diapositivas tiene ligaduras tipográficas, por eso se ve diferente)
# Verificar la distribución 
# del outcome
datos |>
  count(crecimiento_alto) |>
  mutate(prop = n / sum(n))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <chr>            <int> <dbl>
1 no                  93 0.520
2 si                  86 0.480

Ejercicio 1: Exploración

Instrucciones: Antes de continuar, exploren los datos ustedes mismos.

  1. ¿Cuántas observaciones y variables tiene el dataset?
  2. ¿Hay valores faltantes (NA)?
  3. ¿Cómo se distribuye cada variable numérica?
  4. ¿Hay correlaciones fuertes entre las variables?

Tómense 5 minutos para explorar.

Apéndice 1: Solución

Parte 2: Preprocesamiento y división

Preparar los datos

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…

¿Por qué convertir a factor?

  • En R, los modelos de clasificación esperan que la variable objetivo sea un factor
  • Un factor es un tipo de dato para variables categóricas
  • Tiene niveles ordenados: el primer nivel es la clase “negativa”
  • En nuestro caso: levels = c("no", "si")
    • “no” = clase negativa (referencia)
    • “si” = clase positiva (la que queremos predecir)
    • Usamos levels(...) para asegurarnos de que el orden es correcto
  • El orden importa para interpretar métricas como precisión y recall
# Ver los niveles del factor
levels(datos_modelo$crecimiento_alto)
[1] "no" "si"

Dividir los datos

Usamos 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
cat("Prueba:", nrow(datos_test), "filas\n")
Prueba: 46 filas
  • strata = crecimiento_alto asegura que la proporción de sí/no sea similar en ambos conjuntos
  • La estratificación es buena práctica, especialmente con clases desbalanceadas

Verificar la estratificación

Comprobemos que las proporciones son similares en ambos conjuntos:

# Proporciones en entrenamiento
datos_train |>
  count(crecimiento_alto) |>
  mutate(prop = round(n / sum(n), 3), conjunto = "train")
# 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   
# Proporciones en prueba
datos_test |>
  count(crecimiento_alto) |>
  mutate(prop = round(n / sum(n), 3), conjunto = "test")
# 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!)

Variaciones de la división (demo)

Tres parámetros moldean la partición. Vean el efecto:

# (1) Menos datos de entrenamiento
set.seed(2026)
split_50 <- initial_split(datos_modelo, prop = 0.50, strata = crecimiento_alto)
cat("prop = 0.50 -> Train:", nrow(training(split_50)),
    "| Test:", nrow(testing(split_50)), "\n")
prop = 0.50 -> Train: 89 | Test: 90 
# (2) Sin estratificacion (las proporciones pueden diferir)
set.seed(1234)
split_sin <- initial_split(datos_modelo, prop = 0.75)
training(split_sin) |> count(crecimiento_alto) |>
  mutate(prop = round(n / sum(n), 3))
# 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 ajuste
  • sin strata: proporciones de clase pueden diferir, sesga las métricas
  • otra semilla: resultados levemente distintos, la estratificación los mantiene comparables
  • Más ejemplos en el Apéndice 2

Parte 3: Entrenamiento y evaluación

Especificar el modelo

Usamos logistic_reg() de parsnip para definir una regresión logística:

# Especificar el modelo
modelo_log <- logistic_reg() |>
  set_engine("glm") |>
  set_mode("classification")

modelo_log
Logistic Regression Model Specification (classification)

Computational engine: glm 
  • logistic_reg() define qué tipo de modelo queremos
  • set_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ón
  • Noten que aún no hemos entrenado nada: solo definimos la especificación
  • Otros modelos disponibles en parsnip: lista completa

Ajustar el modelo

Ahora ajustamos el modelo a los datos de entrenamiento:

# Ajustar el modelo
ajuste <- modelo_log |>
  fit(crecimiento_alto ~ ., data = datos_train)

# Ver los coeficientes
tidy(ajuste)
# 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 variables
  • fit() entrena el modelo con los datos de entrenamiento
  • tidy() nos da una tabla limpia con coeficientes, errores estándar y p-valores

Interpretar los coeficientes

Recordatorio: regresión logística

  • Los coeficientes están en escala de log-odds
  • Un coeficiente positivo aumenta la probabilidad de “sí”
  • Un coeficiente negativo la disminuye
  • Para convertir a odds ratio: exp(coeficiente)

Preguntas para discutir:

  • ¿Qué variables tienen coeficientes significativos (p < 0.05)?
  • ¿Los signos tienen sentido teórico?
  • ¿La inflación alta se asocia con menor crecimiento?
# Ver los odds ratios
tidy(ajuste) |>
  mutate(odds_ratio = exp(estimate)) |>
  select(term, estimate, odds_ratio, p.value)
# 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 

Generar predicciones

  • Generamos predicciones en los datos de prueba
  • .pred_class es la clase predicha por el modelo
  • Comparamos con crecimiento_alto (la verdad)
# Predecir clases
predicciones <- ajuste |>
  predict(datos_test) |>  # predice clases por defecto
  bind_cols(datos_test)   # combinar con datos originales para evaluación

# Ver las primeras predicciones
predicciones |>
  select(crecimiento_alto, .pred_class) |>
  head(8)
# 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         

Matriz de confusión

La matriz de confusión muestra todos los resultados posibles:

# Matriz de confusión (conf_mat = confusion matrix)
conf_mat(predicciones, truth = crecimiento_alto, estimate = .pred_class)
          Truth
Prediction no si
        no 19  4
        si  5 18
  • Verdaderos Negativos (VN): predijo “no” y era “no”
  • Falsos Positivos (FP): predijo “sí” pero era “no”
  • Falsos Negativos (FN): predijo “no” pero era “sí”
  • Verdaderos Positivos (VP): predijo “sí” y era “sí”

Métricas de evaluación

# Conjunto completo de métricas
predicciones |>
  conf_mat(truth = crecimiento_alto, estimate = .pred_class) |>
  summary()
# 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.

Ejercicio 3: Interpretación

Instrucciones: Reflexionen sobre los resultados.

  1. ¿El modelo tiene mejor precisión o mejor recall?
  2. ¿Qué significa eso en términos prácticos?
    • Si predecimos “crecimiento alto” cuando no lo es, ¿qué pasa?
    • Si no detectamos un caso de crecimiento alto, ¿qué pasa?
  3. ¿Qué métrica priorizarían ustedes y por qué?

Discutan en grupos de 2-3 personas durante 5 minutos.

Apéndice 3: Reflexión

Parte 4: Más allá de la clasificación binaria

Probabilidades y umbral (demo)

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
  • Umbral bajo (0.3): predice “sí” con facilidad, alta recall, baja precisión
  • Umbral alto (0.7): predice “sí” con cautela, alta precisión, baja recall
  • Detalle de observaciones inciertas: Apéndice 4

Curva ROC y AUC (demo)

La curva ROC resume el rendimiento en todos los umbrales a la vez:

pred_probs |>
  roc_curve(truth = crecimiento_alto, .pred_si, event_level = "second") |>
  autoplot()

pred_probs |>
  roc_auc(truth = crecimiento_alto, .pred_si, event_level = "second")
# 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 invertido
  • AUC ≈ 0.5 es azar; 0.7–0.8 aceptable; > 0.9 excelente (revisen data leakage)

Más allá de esta sesión

Dos temas que no tocaremos hoy pero aparecen en los apéndices:

  • Comparación de umbrales en detalle: tabla completa y discusión en Apéndice 5
  • Validación cruzada (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 6

Material opcional: carguen el script laboratorio-01.R para ejecutar todos los ejemplos.

Resumen y cierre

Lo que aprendimos

Flujo de trabajo completo:

  1. Cargar y explorar datos
  2. Preprocesar (factores, selección)
  3. Dividir (train/test con estratificación)
  4. Especificar modelo (parsnip)
  5. Ajustar (fit)
  6. Predecir (predict)
  7. Evaluar (métricas, matriz de confusión)

Conceptos clave:

  • tidymodels unifica el modelado en R
  • La estratificación mantiene proporciones de clases
  • Las probabilidades dan más información que las clases
  • El umbral afecta el balance precision/recall
  • La validación cruzada da estimaciones más robustas

Próximo: Laboratorio 2

En el Laboratorio 2 (Sesión 1.4) vamos a:

  • Trabajar con un dataset diferente
  • Explorar más a fondo el preprocesamiento
  • Comparar múltiples modelos
  • Practicar la interpretación de resultados


Guarden su trabajo y tómense un descanso! 😉

Apéndice: Soluciones

Apéndice 1: Exploración

Comandos para cada pregunta:

# 1. Dimensiones del dataset
dim(datos)            # filas y columnas
[1] 179  11
nrow(datos)           # solo filas
[1] 179
ncol(datos)           # solo columnas
[1] 11
# 2. Valores faltantes
sum(is.na(datos))         # total de NAs
[1] 0
colSums(is.na(datos))     # NAs por columna
                   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 
# 3. Distribución de variables numéricas
datos |>
  select(where(is.numeric)) |>
  summary()
 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         
# 4. Correlaciones entre variables
datos |>
  select(where(is.numeric)) |>
  cor() |>
  round(2)
                        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:

  • 179 observaciones (un país por fila), 11 variables: pais, continente, 8 predictores numéricos y crecimiento_alto
  • 0 valores faltantes: datos simulados. En datos reales esto casi nunca ocurre, siempre verifiquen
  • acceso_internet y urbanizacion tienen distribuciones amplias: países con muy poco acceso vs. muy conectados
  • indice_gobierno_digital se correlaciona positivamente con acceso_internet y negativamente con inflacion

Pista: ggplot2 permite visualizar distribuciones con geom_histogram() y correlaciones con el paquete corrplot o ggcorrplot.

Volver al ejercicio

Apéndice 2: División de datos

# Pregunta 1: prop = 0.50 (menos datos de entrenamiento)
set.seed(2026)
split_50 <- initial_split(datos_modelo, prop = 0.50, strata = crecimiento_alto)
cat("Train:", nrow(training(split_50)), "/ Test:", nrow(testing(split_50)))
Train: 89 / Test: 90
# Pregunta 2: sin estratificación
set.seed(2026)
split_sin <- initial_split(datos_modelo, prop = 0.75)
training(split_sin) |> count(crecimiento_alto) |> mutate(prop = round(n / sum(n), 3))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <fct>            <int> <dbl>
1 no                  68 0.507
2 si                  66 0.493
testing(split_sin)  |> count(crecimiento_alto) |> mutate(prop = round(n / sum(n), 3))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <fct>            <int> <dbl>
1 no                  25 0.556
2 si                  20 0.444
# Las proporciones pueden diferir entre train y test

# Pregunta 3: diferente semilla
set.seed(999)
split_999 <- initial_split(datos_modelo, prop = 0.75, strata = crecimiento_alto)
training(split_999) |> count(crecimiento_alto) |> mutate(prop = round(n / sum(n), 3))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <fct>            <int> <dbl>
1 no                  69 0.519
2 si                  64 0.481
# Los resultados del modelo varían, pero la distribución de clases
# se mantiene gracias a la estratificación
  • Con menos datos de entrenamiento (prop = 0.50), el modelo puede tener peor rendimiento
  • Sin strata, las proporciones de clases pueden ser distintas en train y test, lo que sesga las métricas
  • Cambiar la semilla produce una partición diferente; los resultados varían algo, pero no deberían ser muy distintos

Volver al ejercicio

Apéndice 3: Interpretación

Calcular las métricas:

predicciones |>
  precision(truth = crecimiento_alto,
            estimate = .pred_class,
            event_level = "second")
# A tibble: 1 × 3
  .metric   .estimator .estimate
  <chr>     <chr>          <dbl>
1 precision binary         0.783
predicciones |>
  recall(truth = crecimiento_alto,
         estimate = .pred_class,
         event_level = "second")
# A tibble: 1 × 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 recall  binary         0.818
# O ambas a la vez:
predicciones |>
  conf_mat(truth = crecimiento_alto,
           estimate = .pred_class) |>
  summary(event_level = "second")
# 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:

  • Precisión alta, recall bajo: el modelo predice “crecimiento alto” con cautela. Pocos falsos positivos, pero se pierden casos reales
  • Recall alto, precisión baja: predice “sí” con frecuencia. No pierde casos, pero genera más falsas alarmas

¿Qué métrica priorizar?

  • Si el costo de perder un caso es alto (p. ej., decisiones de inversión): prioricen recall
  • Si el costo de una falsa alarma es alto (p. ej., asignar recursos mal): prioricen precisión
  • Sin preferencia clara: usen F1, el promedio armónico de ambas

No hay respuesta universal. El contexto define la métrica correcta.

Volver al ejercicio

Apéndice 4: Probabilidades

¿Cómo identificar predicciones inciertas?

# Observaciones con probabilidad cercana a 0.5
pred_probs |>
  select(crecimiento_alto, .pred_no, .pred_si) |>
  mutate(incertidumbre = abs(.pred_si - 0.5)) |>
  filter(incertidumbre < 0.1) |>
  arrange(incertidumbre)
# 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
  • Las observaciones con .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
  • En contextos reales, estas observaciones merecen análisis adicional o revisión manual antes de tomar decisiones
  • Si hay muchas predicciones inciertas, puede indicar que el modelo necesita más variables o más datos

Volver al ejercicio

Apéndice 5: Cambio de umbral

# 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.

Volver al ejercicio

Apéndice 6: Validación cruzada

# collect_metrics() devuelve media y error estándar de cada métrica
collect_metrics(cv_results)
# 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
# Comparar con las métricas de la división única
predicciones |>
  metrics(truth = crecimiento_alto, estimate = .pred_class)
# 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)
  • Un error estándar bajo indica resultados consistentes entre folds (buena señal)
  • Si las métricas de CV son peores que las de la división única, la partición original fue demasiado favorable al modelo
  • La validación cruzada es más confiable porque cada observación aparece exactamente una vez en validación

Volver al ejercicio

Apéndice 7: Curva ROC

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)
  • Una curva que sube rápido al inicio indica que el modelo identifica los positivos más claros primero
  • El AUC es independiente del umbral: evalúa el rendimiento en todos los posibles umbrales a la vez
  • Es especialmente útil para comparar modelos sin tener que elegir un umbral

Volver al ejercicio