IA para Científicos Sociales

Sesión 1.4: Laboratorio 2 - Validación cruzada y comparación de modelos

Danilo Freire

Department of Data and Decision Sciences
Emory University

Laboratorio 2: Validación cruzada y comparación de modelos

Objetivos del laboratorio

Lo que vamos a hacer:

  1. Volver al dataset del laboratorio 1
  2. Análisis exploratorio extendido
  3. Feature engineering básico
  4. Validación cruzada en 5 folds
  5. Comparar regresión logística vs. árbol
  6. Evaluar el mejor modelo en test

Lo que vamos a practicar:

  • Decisiones de preprocesamiento
  • Comparación sistemática de modelos
  • Visualización de resultados
  • Interpretación de modelos

Hay menos código predefinido. ¡Ustedes van a escribir más!

¡No se preocupen! Les damos una estructura para que puedan completar 😉

Del lab 1 al lab 2

En el lab 1 hicimos:

  • Cargamos indicadores_mundiales.csv
  • Una sola división train/test
  • Un modelo: regresión logística
  • Métricas en test: precision, recall, ROC-AUC
  • Discutimos el umbral de decisión

Hoy vamos un paso más allá:

  • El mismo dataset, los mismos predictores
  • Validación cruzada (5 folds) en lugar de una sola partición
  • Dos modelos a comparar: logístico vs. árbol
  • Feature engineering: ¿una variable nueva ayuda?
  • Evaluación final en test

El objetivo sustantivo sigue siendo el mismo: predecir crecimiento_alto a partir de indicadores macro

Parte 1: Carga y exploración

Cargar los paquetes

# Cargar paquetes
# install.packages(c("tidymodels", "tidyverse", "rpart")) # Instalar si es necesario
library(tidymodels)
library(tidyverse)
library(rpart) # Para el motor de árbol de decisión

# Configurar tema de ggplot
theme_set(theme_minimal(base_size = 14))

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)
  }
}

El dataset: indicadores mundiales

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 tipos
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"…

Variable objetivo: crecimiento_alto (sí/no) - ¿El país tuvo crecimiento del PIB mayor o igual a 3%?

Descripción de las variables

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

Estadísticas descriptivas

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

# Resumen rápido de las variables numéricas
datos |> select(gasto_educacion, acceso_internet, urbanizacion) |> summary()
 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  
# Medias por nivel de crecimiento (3 predictores ilustrativos)
datos |>
  group_by(crecimiento_alto) |>
  summarise(
    educacion_media     = mean(gasto_educacion),
    internet_medio      = mean(acceso_internet),
    urbanizacion_media  = mean(urbanizacion)
  )
# 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

Ejercicio 1: Exploración

Tarea: Exploren una de las variables que no mostramos en la diapositiva anterior.

  1. ¿Cuántas observaciones? ¿Hay valores faltantes?
  2. ¿Cómo se distribuye la variable objetivo crecimiento_alto?
  3. Elijan una de estas preguntas y respóndanla con una comparación numérica (el gráfico es opcional):
    • Elijan una de 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?
    • ¿Están altamente correlacionados 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)

Parte 2: Feature engineering

Preparar los datos

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:

# Frame listo para modelar (datos sigue intacto con pais y continente)
datos_modelo <- datos |>
  mutate(crecimiento_alto = factor(crecimiento_alto, levels = c("no", "si"))) |>
  select(-pais, -continente)

# Verificar
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…

Mantenemos el orden c("no", "si") para que "si" sea la clase positiva. Más adelante usaremos event_level = "second"

Crear nuevas variables

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          

Ejercicio 2: Crear un feature

Tarea: Elijan una variable derivada y créenla

Opciones sugeridas (elijan la que más les interese). Entre paréntesis, la variable base:

  • Acceso a internet por terciles: bajo, medio, alto (acceso_internet)
  • Gasto en educación alto: por encima de la mediana del dataset (gasto_educacion)
  • Inflación alta: tasa mayor a 15% (inflacion)
  • Apertura financiera: IED por encima de la mediana (inversion_extranjera)
  • Otro que se les ocurra (¡la creatividad es bienvenida!)
# Ejemplo: crear variable internet_grupos por terciles
datos_modelo <- datos_modelo |>
  mutate(
    internet_grupos = cut(acceso_internet,
                          breaks = quantile(acceso_internet, c(0, 1/3, 2/3, 1)),
                          labels = c("bajo", "medio", "alto"),
                          include.lowest = TRUE)
  )

¡Tómense 3-5 minutos!

Más ejemplos en el Apéndice 5

Dividir los datos

# 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/test
  • training() y testing(): Extraen los datasets de train y test
  • vfold_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

Parte 3: Comparación de modelos

Definir los modelos a comparar

Vamos a comparar dos modelos de naturaleza distinta: uno lineal y uno no lineal

# 1. Regresión logística (lineal)
modelo_logistico <- logistic_reg() |>
  set_engine("glm") |>
  set_mode("classification")

# 2. Árbol de decisión (no lineal, captura interacciones automáticamente)
modelo_arbol <- decision_tree() |>
  set_engine("rpart") |>
  set_mode("classification")

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/

Crear una fórmula

Decidamos qué variables usar:

# Fórmula con las 8 variables numéricas originales
formula_basica <- crecimiento_alto ~ gasto_educacion + acceso_internet +
                                     urbanizacion + gasto_salud + inflacion +
                                     desempleo + inversion_extranjera +
                                     indice_gobierno_digital

Más adelante añadiremos una variable derivada para ver si mejora el ajuste

Evaluar el modelo logístico

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 cruzada
  • metric_set() junta varias métricas en un solo objeto. Lo guardamos en mis_metricas y lo reusamos en cada evaluación
  • collect_metrics() devuelve la media y el error estándar de cada métrica entre folds
  • control = control_resamples(event_level = "second") indica que la clase positiva es "si"
  • Usamos precision, recall y roc_auc en lugar de accuracy: con clases desbalanceadas la accuracy puede ser engañosa
  • En el resultado, .metric guarda el nombre de cada métrica (precision, recall, roc_auc) y mean y std_err

Evaluar el árbol y comparar

# 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é?

¿Qué pasa si extendemos la fórmula?

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
  • En este ejemplo, alto_desempleo cambia las métricas del logístico apenas unas milésimas. No todo feature engineering mejora el modelo. Hay que medirlo, no asumirlo

Ejercicio 3: ¿Ayuda el feature engineering?

Tarea: 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)

  1. ¿Mejora la regresión logística? ¿Y el árbol?
  2. ¿Cuál de los dos modelos es más sensible al feature nuevo?
# 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!

Ir al Apéndice 6

Modelo final en datos de test

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)
  • El prefijo . es la convención de tidymodels para columnas que el paquete crea por ustedes
  • Reemplaza dos llamadas a predict() y dos bind_cols() por una sola línea
  • mis_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_auc

Tarea para casa

Hay 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 😉

Cierre del Día 1

Lo que cubrimos hoy

Sesión 1.1: ¿Qué es la IA?

  • Definición y tipos de IA
  • Historia: de Turing a GPT
  • Tipos de aprendizaje automático
  • Ética básica

Sesión 1.2: Fundamentos de ML

  • Flujo de trabajo de ML
  • División y validación
  • Sobreajuste y sesgo-varianza
  • Métricas de evaluación

Sesión 1.3: Laboratorio 1

  • Ecosistema tidymodels
  • Primer modelo de clasificación
  • Matriz de confusión y métricas

Sesión 1.4: Laboratorio 2

  • Exploración extendida con el mismo dataset
  • Feature engineering básico
  • Logístico vs. árbol con validación cruzada
  • Tarea 2 disponible en la página de laboratorios

Para mañana

Día 2: Aprendizaje supervisado

  • Sesión 2.1: Métodos de clasificación
    • Regresión logística en detalle, árboles de decisión, Random Forests
  • Sesión 2.2: Regresión y predicción
    • Regularización (LASSO, Ridge, Elastic Net) e ingeniería de variables
  • Sesiones 2.3 y 2.4: Laboratorios con datos de Latinobarómetro

Para profundizar (todo gratis): Tidy Modeling with RR for Data ScienceGet Started with tidymodels

¡Descansen y vengan con energía mañana! 😄

Fin del Día 1 🥳

Apéndices

Apéndice 1: Exploración inicial

# 1. Dimensiones
dim(datos)
[1] 179  11
# 2. Distribución de la variable objetivo
datos |>
  count(crecimiento_alto) |>
  mutate(prop = round(n / sum(n), 3))
# A tibble: 2 × 3
  crecimiento_alto     n  prop
  <chr>            <int> <dbl>
1 no                  93  0.52
2 si                  86  0.48
# 3. Valores faltantes por variable
colSums(is.na(datos))
                   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 
# 4. Resumen 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         

Volver al Ejercicio 1

Apéndice 2: Una variable a fondo

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:

# Estadísticas detalladas
summary(datos$inflacion)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   5.20    9.35   11.70   12.73   14.70   37.40 
# Histograma (muestra la cola larga)
ggplot(datos, aes(x = inflacion)) +
  geom_histogram(bins = 30, fill = "#2d4563") +
  labs(x = "Inflación anual (%)", y = "Países")

# Densidad por grupo (¿separa crecimiento alto de bajo?)
ggplot(datos, aes(x = inflacion, fill = crecimiento_alto)) +
  geom_density(alpha = 0.5) +
  scale_fill_manual(values = c("#e74c3c", "#27ae60")) +
  labs(x = "Inflación anual (%)")

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

Volver al Ejercicio 1

Apéndice 3: Predictores vs. objetivo

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:

# Medias por grupo de las 4 variables no mostradas en el cuerpo
datos |>
  group_by(crecimiento_alto) |>
  summarise(
    salud         = mean(gasto_salud),
    inflacion     = mean(inflacion),
    desempleo     = mean(desempleo),
    ied           = mean(inversion_extranjera),
    gob_digital   = mean(indice_gobierno_digital)
  )
# 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
# Correlación entre todos los predictores numéricos (¿hay pares redundantes?)
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
# Test formal para una variable concreta
t.test(indice_gobierno_digital ~ crecimiento_alto, data = datos)

    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 

Volver al Ejercicio 1

Apéndice 4: Boxplots de las 4 variables no mostradas

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")

Volver al Ejercicio 1

Apéndice 4 (cont.): Acceso a internet vs. urbanización

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

ggplot(datos, aes(x = urbanizacion, y = acceso_internet, color = crecimiento_alto)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", se = FALSE) +
  scale_color_manual(values = c("#e74c3c", "#27ae60")) +
  labs(x = "Urbanización (%)", y = "Acceso a internet (%)")

# La correlación numérica complementa el gráfico
cor(datos$urbanizacion, datos$acceso_internet)
[1] 0.1032229

Volver al Ejercicio 1

Apéndice 5: Crear más features

# 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
datos_modelo |> count(internet_grupos)
# A tibble: 3 × 2
  internet_grupos     n
  <fct>           <int>
1 bajo               60
2 medio              59
3 alto               60
datos_modelo |> count(inflacion_alta)
# A tibble: 2 × 2
  inflacion_alta     n
  <chr>          <int>
1 no               140
2 si                39
datos_modelo |> count(apertura_financiera)
# A tibble: 2 × 2
  apertura_financiera     n
  <chr>               <int>
1 alta                   83
2 baja                   96

Volver al Ejercicio 2

Apéndice 6: ¿Ayuda el feature engineering?

# 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

Volver al Ejercicio 3

Apéndice 7: Visualizar la comparación de modelos

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")

  • Las barras muestran la media de cada métrica; las líneas verticales son el error estándar (variabilidad entre folds)
  • Si las barras de error se superponen, la diferencia entre modelos no es robusta: probablemente no podamos decir cuál es “mejor” con confianza

Volver a la comparación numérica