IA para Científicos Sociales

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

Danilo Freire

Department 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:

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")
# 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 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  
 N.unique :179   N.unique :  5   1st Qu.:4.400   1st Qu.:46.60  
 N.blank  :  0   N.blank  :  0   Median :5.000   Median :57.50  
 Min.nchar:  4   Min.nchar:  4   Mean   :5.027   Mean   :57.52  
 Max.nchar: 20   Max.nchar:  7   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          N.unique :  2    
 Median :4.200        Median :0.5200          N.blank  :  0    
 Mean   :4.079        Mean   :0.5136          Min.nchar:  2    
 3rd Qu.:5.100        3rd Qu.:0.6350          Max.nchar:  2    
 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?

Pista: estos comandos les sirven para las cuatro preguntas. ¡Tómense unos minutos para explorar! 🤓

dim(datos)              # 1. filas y columnas
glimpse(datos)          #    tipo de cada variable

sum(is.na(datos))       # 2. total de valores faltantes
colSums(is.na(datos))   #    faltantes por columna

datos |>                # 3. mínimo, mediana, máximo de cada variable
  select(where(is.numeric)) |> summary()

datos |>                # 4. correlaciones entre predictores
  select(where(is.numeric)) |> cor() |> round(2)

Apéndice 1: Solución

Parte 2: Preprocesamiento y división

Preparar los datos

Convertimos la variable de resultado a factor y descartamos las columnas que no usaremos como predictores (pais tiene 179 valores únicos y rompería el modelo; continente la dejamos fuera para mantenernos en predictores numéricos):

datos_modelo <- datos |>
  mutate(crecimiento_alto = factor(crecimiento_alto, levels = c("no", "si"))) |>
  select(-pais, -continente)

glimpse(datos_modelo)
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…

¿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))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <fct>            <int> <dbl>
1 no                  69 0.519
2 si                  64 0.481
# Proporciones en prueba
datos_test |>
  count(crecimiento_alto) |>
  mutate(prop = round(n / sum(n), 3))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <fct>            <int> <dbl>
1 no                  24 0.522
2 si                  22 0.478

Las proporciones deberían ser muy similares (¡y realmente lo son en este caso!)

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

Apéndice 7: interpretar los coeficientes (odds ratios)

Generar predicciones

  • Generamos predicciones en los datos de prueba
  • .pred_class es la clase predicha por el modelo. El punto al principio es la convención de tidymodels para columnas que el paquete crea por ustedes (evita chocar con columnas del usuario)
  • 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

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

Recordatorio: accuracy = (VP + VN)/(VP + VN + FP + FN): de todas las predicciones, cuántas acertó; precision (también aparece como ppv) = VP/(VP + FP): de las que predijo “sí”, cuántas eran “sí”; recall (también sens) = VP/(VP + FN): de los “sí” reales, cuántos detectó

Ejercicio 2: 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 3-5 minutos 🤓

Apéndice 2: Reflexión

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

Probabilidades y umbral

El modelo no solo predice clases: también devuelve probabilidades. Por defecto, predict() usa un umbral de 0.5, pero podemos cambiarlo

pred_probs <- ajuste |>
  predict(datos_test, type = "prob") |>
  bind_cols(datos_test)

pred_probs |> select(crecimiento_alto, .pred_no, .pred_si) |> head(5)
# A tibble: 5 × 3
  crecimiento_alto .pred_no .pred_si
  <fct>               <dbl>    <dbl>
1 si                  0.622  0.378  
2 no                  0.935  0.0648 
3 no                  0.994  0.00588
4 si                  0.586  0.414  
5 no                  0.331  0.669  
Umbral Efecto sobre las predicciones
0.3 predice “sí” con facilidad: alta recall, baja precisión
0.5 punto de equilibrio (por defecto)
0.7 predice “sí” con cautela: alta precisión, baja recall

Apéndice 3: Predicciones inciertas · Apéndice 4: Comparación completa

Curva ROC y AUC

La curva ROC resume el rendimiento en todos los umbrales a la vez: cada punto de la curva es un umbral distinto

  • Sensibilidad (eje Y) = VP / (VP + FN): de los países que sí crecieron, ¿cuántos detecta el modelo? Es el recall
  • 1 - especificidad (eje X) = FP / (FP + VN): de los países que no crecieron, ¿cuántos marcamos por error? Tasa de falsas alarmas
  • Bajar el umbral nos mueve hacia arriba y a la derecha: detectamos más casos reales, pero pagamos con más falsas alarmas. La diagonal es el azar
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

Apéndice 6: Interpretar el AUC

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 4
  • 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 5

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 modelos más complejos (árboles de decisión)
  • 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: 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 (f_meas), el promedio armónico de ambas

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

Volver al ejercicio

Apéndice 3: 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 4: Cambio de umbral

# 1. Tres copias de las predicciones, cada una con su umbral
pred_umbrales <- bind_rows(
  pred_probs |> mutate(umbral = 0.3),
  pred_probs |> mutate(umbral = 0.5),
  pred_probs |> mutate(umbral = 0.7)
) |>
  # 2. Clasificar como "si" si la probabilidad supera el umbral de esa copia
  mutate(
    clase_predicha = if_else(.pred_si >= umbral, "si", "no"),
    clase_predicha = factor(clase_predicha, levels = c("no", "si"))
  )

# 3. Calcular precisión y recall dentro de cada umbral
pred_umbrales |>
  group_by(umbral) |>
  summarise(
    precision = precision_vec(crecimiento_alto, clase_predicha,
                              event_level = "second"),
    recall = recall_vec(crecimiento_alto, clase_predicha,
                        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

Apéndice 5: Validación cruzada

# Crear los folds para validación cruzada (5 particiones, estratificadas)
set.seed(2026)
folds <- vfold_cv(datos_train, v = 5, strata = crecimiento_alto)

# Ajustar el modelo en cada fold
cv_results <- modelo_log |>
  fit_resamples(
    crecimiento_alto ~ .,
    resamples = folds,
    metrics = metric_set(accuracy, roc_auc),
    control = control_resamples(event_level = "second")
  )

# collect_metrics() devuelve media y error estándar de cada métrica
collect_metrics(cv_results)
# A tibble: 2 × 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 roc_auc  binary     0.864     5  0.0136 pre0_mod0_post0
# Comparar con la accuracy de la división única
accuracy(predicciones, truth = crecimiento_alto, estimate = .pred_class)
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy binary         0.804
  • 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

Apéndice 6: 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

Apéndice 7: 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)

Pregunta para discutir:

  • ¿Los signos tienen sentido teórico? Por ejemplo, ¿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 

Volver