---
title: "IA para Científicos Sociales"
subtitle: "Sesión 1.3: Laboratorio - Primer flujo de trabajo con tidymodels"
author:
- name: Danilo Freire
orcid: 0000-0002-4712-6810
email: danilofreire@gmail.com
affiliations: "Departament of Data and Decision Sciences
Emory University"
format:
clean-revealjs:
self-contained: true
footer: "[Sesión 1.3](https://danilofreire.github.io/introduccion-ia-ucu/clases/dia-01/03-laboratorio-01.html)"
transition: slide
transition-speed: default
scrollable: true
revealjs-plugins:
- multimodal
engine: knitr
editor:
render-on-save: true
lang: es
---
```{r setup, include=FALSE}
options(htmltools.dir.version = FALSE)
library(knitr)
opts_chunk$set(
prompt = T,
fig.align = "center",
dpi = 300,
cache = T,
engine.opts = list(bash = "-l")
)
knit_hooks$set(
prompt = function(before, options, envir) {
options(
prompt = if (options$engine %in% c("sh", "bash", "zsh")) "$ " else "R> ",
continue = if (options$engine %in% c("sh", "bash", "zsh")) "$ " else "+ "
)
}
)
options(repos = c(CRAN = "https://cran.rstudio.com/"))
if (!require("fontawesome", character.only = TRUE)) {
install.packages("fontawesome", dependencies = TRUE)
library(fontawesome, character.only = TRUE)
}
```
# Laboratorio 1: Primer flujo de trabajo con tidymodels {background-color="#2d4563"}
## Objetivos del laboratorio
:::{style="margin-top: 30px; font-size: 28px;"}
:::{.columns}
:::{.column width=50%}
**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
:::
:::{.column width=50%}
**Lo que vamos a aprender:**
- El ecosistema [tidymodels]{.alert}
- 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.]{.alert}
:::
:::
:::
# Parte 1: Configuración y exploración {background-color="#2d4563"}
## Configuración del entorno
:::{style="margin-top: 30px; font-size: 24px;"}
Necesitamos instalar y cargar los paquetes. Ejecuten este código en R:
```{r lab-setup, eval=FALSE, prompt=FALSE, echo=TRUE}
# Instalar paquetes (solo la primera vez)
install.packages(c("tidymodels", "tidyverse"))
# Cargar paquetes
library(tidymodels)
library(tidyverse)
```
```{r lab-load, echo=FALSE, message=FALSE, warning=FALSE}
library(tidymodels)
library(tidyverse)
```
- Si ya tienen tidyverse instalado, solo necesitan instalar [tidymodels]{.alert}
- tidymodels carga automáticamente: rsample, parsnip, yardstick, y otros paquetes que necesitaremos
- Si la instalación falla, intenten:
```{r lab-setup-02, eval=FALSE, prompt=FALSE, echo=TRUE}
install.packages("tidymodels", dependencies = TRUE)
```
:::
## Recapitulando: el ecosistema tidy
:::{style="margin-top: 30px; font-size: 28px;"}
[tidymodels](https://www.tidymodels.org/) es un conjunto de paquetes que comparten la filosofía del [tidyverse]{.alert}. Los que usaremos hoy:
- [rsample:]{.alert} división de datos
- [parsnip:]{.alert} especificación de modelos
- [yardstick:]{.alert} métricas de evaluación
Ventaja: [misma sintaxis]{.alert} para cualquier modelo, sea regresión logística, random forest o redes neuronales. La [lista completa](https://www.tidymodels.org/packages/) queda en la documentación.
:::
## Cargar los datos
:::{style="margin-top: 30px; font-size: 22px;"}
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]{.alert} (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)
```{r lab-cargar, message=FALSE, warning=FALSE, prompt=FALSE, echo=TRUE}
# Cargar los datos
datos <- read_csv("datos/indicadores_mundiales.csv")
# Ver las primeras filas
glimpse(datos)
```
:::
## Exploración inicial
:::{style="margin-top: 30px; font-size: 22px;"}
Antes de modelar, siempre hay que [explorar los datos]{.alert}:
```{r lab-explorar, prompt=FALSE, echo=TRUE}
# Resumen estadístico
summary(datos)
```
:::
## Explorar la variable de resultado
:::{style="margin-top: 30px; font-size: 26px;"}
:::{.columns}
:::{.column width=55%}
- **Problema**: predecir si un país tendrá [crecimiento alto del PIB]{.alert} (>= 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]{.alert}
- [Nota:]{.alert} |> es el operador de pipe nativo de R 4.1+ (la fuente de estas diapositivas tiene ligaduras tipográficas, por eso se ve diferente)
:::
:::{.column width=45%}
:::{style="margin-top: 20px; font-size: 26px;"}
```{r lab-outcome, prompt=FALSE, echo=TRUE}
# Verificar la distribución
# del outcome
datos |>
count(crecimiento_alto) |>
mutate(prop = n / sum(n))
```
:::
:::
:::
:::
## Ejercicio 1: Exploración {#sec:exercise01}
:::{style="margin-top: 30px; font-size: 24px;"}
**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?
```{r ej1-solucion, eval=FALSE, prompt=FALSE}
# Sugerencias de código:
dim(datos) # Dimensiones
sum(is.na(datos)) # Total de NAs
cor(datos |> select(where(is.numeric))) # Correlaciones
```
[Tómense 5 minutos para explorar.]{.alert}
[[Apéndice 1: Solución]{.button}](#sec:appendix01)
:::
# Parte 2: Preprocesamiento y división {background-color="#2d4563"}
## Preparar los datos
:::{style="margin-top: 30px; font-size: 24px;"}
Convertimos la variable de resultado a factor (necesario para clasificación) y seleccionamos las variables:
```{r lab-preparar, prompt=FALSE, echo=TRUE}
# 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)
```
:::
## ¿Por qué convertir a factor?
:::{style="margin-top: 30px; font-size: 24px;"}
:::{.columns}
:::{.column width=55%}
- En R, los modelos de [clasificación]{.alert} esperan que la variable objetivo sea un [factor]{.alert}
- Un factor es un tipo de dato para variables categóricas
- Tiene [niveles]{.alert} 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]{.alert} para interpretar métricas como precisión y recall
:::
:::{.column width=45%}
:::{style="margin-top: 30px; font-size: 24px;"}
```{r factor-ejemplo, prompt=FALSE, echo=TRUE}
# Ver los niveles del factor
levels(datos_modelo$crecimiento_alto)
```
:::
:::
:::
:::
## Dividir los datos
:::{style="margin-top: 30px; font-size: 24px;"}
Usamos `initial_split()` de rsample para crear la división train/test:
```{r lab-dividir, prompt=FALSE, echo=TRUE}
# 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")
cat("Prueba:", nrow(datos_test), "filas\n")
```
- `strata = crecimiento_alto` asegura que la proporción de sí/no sea similar en ambos conjuntos
- [La estratificación es buena práctica]{.alert}, especialmente con clases desbalanceadas
:::
## Verificar la estratificación
:::{style="margin-top: 30px; font-size: 24px;"}
Comprobemos que las proporciones son similares en ambos conjuntos:
```{r verificar-strata, prompt=FALSE, echo=TRUE}
# Proporciones en entrenamiento
datos_train |>
count(crecimiento_alto) |>
mutate(prop = round(n / sum(n), 3), conjunto = "train")
# Proporciones en prueba
datos_test |>
count(crecimiento_alto) |>
mutate(prop = round(n / sum(n), 3), conjunto = "test")
```
[Las proporciones deberían ser muy similares (¡y realmente lo son en este caso!)]{.alert}
:::
## Variaciones de la división (demo) {#sec:exercise02}
:::{style="margin-top: 30px; font-size: 22px;"}
Tres parámetros moldean la partición. Vean el efecto:
```{r variaciones-split, prompt=FALSE, echo=TRUE}
# (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")
# (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))
```
- `prop` baja: menos datos para aprender, peor ajuste
- sin `strata`: proporciones de clase pueden diferir, [sesga las métricas]{.alert}
- otra semilla: resultados levemente distintos, la estratificación los mantiene comparables
- [[Más ejemplos en el Apéndice 2]{.button}](#sec:appendix02)
:::
# Parte 3: Entrenamiento y evaluación {background-color="#2d4563"}
## Especificar el modelo
:::{style="margin-top: 30px; font-size: 24px;"}
Usamos `logistic_reg()` de parsnip para definir una regresión logística:
```{r lab-modelo, prompt=FALSE, echo=TRUE}
# Especificar el modelo
modelo_log <- logistic_reg() |>
set_engine("glm") |>
set_mode("classification")
modelo_log
```
- `logistic_reg()` define [qué tipo de modelo]{.alert} queremos
- `set_engine("glm")` define [qué implementación]{.alert} 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]{.alert}: solo definimos la especificación
- Otros modelos disponibles en parsnip: [lista completa](https://www.tidymodels.org/find/parsnip/)
:::
## Ajustar el modelo
:::{style="margin-top: 30px; font-size: 22px;"}
Ahora ajustamos el modelo a los datos de entrenamiento:
```{r lab-ajustar, prompt=FALSE, echo=TRUE}
# Ajustar el modelo
ajuste <- modelo_log |>
fit(crecimiento_alto ~ ., data = datos_train)
# Ver los coeficientes
tidy(ajuste)
```
- `crecimiento_alto ~ .` significa: predecir `crecimiento_alto` usando [todas las demás variables]{.alert}
- `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
:::{style="margin-top: 30px; font-size: 22px;"}
:::{.columns}
:::{.column width=45%}
**Recordatorio: regresión logística**
- Los coeficientes están en escala de [log-odds]{.alert}
- 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?
:::
:::{.column width=55%}
:::{style="margin-top: 30px; font-size: 24px;"}
```{r coef-interpretacion, prompt=FALSE, echo=TRUE}
# Ver los odds ratios
tidy(ajuste) |>
mutate(odds_ratio = exp(estimate)) |>
select(term, estimate, odds_ratio, p.value)
```
:::
:::
:::
:::
## Generar predicciones
:::{style="margin-top: 30px; font-size: 22px;"}
- Generamos predicciones en los datos de prueba
- `.pred_class` es la clase predicha por el modelo
- Comparamos con `crecimiento_alto` (la verdad)
```{r lab-predecir, prompt=FALSE, echo=TRUE}
# 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)
```
:::
## Matriz de confusión
:::{style="margin-top: 30px; font-size: 24px;"}
La matriz de confusión muestra todos los resultados posibles:
```{r lab-evaluar, prompt=FALSE, echo=TRUE}
# Matriz de confusión (conf_mat = confusion matrix)
conf_mat(predicciones, truth = crecimiento_alto, estimate = .pred_class)
```
:::{.columns}
:::{.column width=50%}
- **Verdaderos Negativos (VN):** predijo "no" y era "no"
- **Falsos Positivos (FP):** predijo "sí" pero era "no"
:::
:::{.column width=50%}
- **Falsos Negativos (FN):** predijo "no" pero era "sí"
- **Verdaderos Positivos (VP):** predijo "sí" y era "sí"
:::
:::
:::
## Métricas de evaluación
:::{style="margin-top: 30px; font-size: 24px;"}
```{r lab-metricas-adicionales, prompt=FALSE, echo=TRUE}
# Conjunto completo de métricas
predicciones |>
conf_mat(truth = crecimiento_alto, estimate = .pred_class) |>
summary()
```
[Observen las diferentes métricas y piensen cuál es más relevante para este problema.]{.alert}
:::
## Ejercicio 3: Interpretación {#sec:exercise03}
:::{style="margin-top: 30px; font-size: 26px;"}
**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.]{.alert}
[[Apéndice 3: Reflexión]{.button}](#sec:appendix03)
:::
# Parte 4: Más allá de la clasificación binaria {background-color="#2d4563"}
## Probabilidades y umbral (demo) {#sec:exercise04}
:::{style="margin-top: 30px; font-size: 20px;"}
El modelo no solo predice clases: también da [probabilidades]{.alert}. Por defecto, `predict()` usa un umbral de 0.5, pero podemos cambiarlo.
```{r probs-umbral, prompt=FALSE, echo=TRUE}
# 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")
)
})
```
- Umbral bajo (0.3): predice "sí" con facilidad, [alta recall, baja precisión]{.alert}
- Umbral alto (0.7): predice "sí" con cautela, [alta precisión, baja recall]{.alert}
- Detalle de observaciones inciertas: [[Apéndice 4]{.button}](#sec:appendix04)
:::
## Curva ROC y AUC (demo) {#sec:exercise07}
:::{style="margin-top: 30px; font-size: 22px;"}
La curva ROC resume el rendimiento [en todos los umbrales a la vez]{.alert}:
```{r roc-demo, prompt=FALSE, fig.width=6, fig.height=3.5, echo=TRUE}
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")
```
- `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
:::{style="margin-top: 30px; font-size: 28px;"}
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]{.button}](#sec:appendix05)
- **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]{.alert}. Ejemplo en [[Apéndice 6]{.button}](#sec:appendix06)
[Material opcional: carguen el script [`laboratorio-01.R`](https://danilofreire.github.io/introduccion-ia-ucu/clases/dia-01/laboratorio-01.R) para ejecutar todos los ejemplos.]{.alert}
:::
# Resumen y cierre {background-color="#2d4563"}
## Lo que aprendimos
:::{style="margin-top: 30px; font-size: 26px;"}
:::{.columns}
:::{.column width=50%}
**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)
:::
:::{.column width=50%}
**Conceptos clave:**
- [tidymodels]{.alert} unifica el modelado en R
- La [estratificación]{.alert} mantiene proporciones de clases
- Las [probabilidades]{.alert} dan más información que las clases
- El [umbral]{.alert} afecta el balance precision/recall
- La [validación cruzada]{.alert} da estimaciones más robustas
:::
:::
:::
## Próximo: Laboratorio 2
:::{style="margin-top: 40px; font-size: 28px;"}
En el [Laboratorio 2 (Sesión 1.4)]{.alert} 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!]{.alert} 😉
:::
# Apéndice: Soluciones {background-color="#2d4563"}
## Apéndice 1: Exploración {#sec:appendix01}
:::{style="margin-top: 20px; font-size: 20px;"}
:::{.columns}
:::{.column width=50%}
**Comandos para cada pregunta:**
```{r ap1-sol, prompt=FALSE, echo=TRUE, eval=TRUE}
# 1. Dimensiones del dataset
dim(datos) # filas y columnas
nrow(datos) # solo filas
ncol(datos) # solo columnas
# 2. Valores faltantes
sum(is.na(datos)) # total de NAs
colSums(is.na(datos)) # NAs por columna
# 3. Distribución de variables numéricas
datos |>
select(where(is.numeric)) |>
summary()
# 4. Correlaciones entre variables
datos |>
select(where(is.numeric)) |>
cor() |>
round(2)
```
:::
:::{.column width=50%}
**Respuestas esperadas:**
- [179 observaciones]{.alert} (un país por fila), 11 variables: `pais`, `continente`, 8 predictores numéricos y `crecimiento_alto`
- [0 valores faltantes]{.alert}: 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:]{.alert} `ggplot2` permite visualizar distribuciones con `geom_histogram()` y correlaciones con el paquete `corrplot` o `ggcorrplot`.
:::
:::
[[Volver al ejercicio]{.button}](#sec:exercise01)
:::
## Apéndice 2: División de datos {#sec:appendix02}
:::{style="margin-top: 20px; font-size: 20px;"}
```{r ap2-sol, prompt=FALSE, echo=TRUE, eval=TRUE}
# 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)))
# 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))
testing(split_sin) |> count(crecimiento_alto) |> mutate(prop = round(n / sum(n), 3))
# 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))
# Los resultados del modelo varían, pero la distribución de clases
# se mantiene gracias a la estratificación
```
- Con [menos datos de entrenamiento]{.alert} (`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]{.alert}
- Cambiar la semilla produce una partición diferente; los resultados varían algo, pero no deberían ser muy distintos
[[Volver al ejercicio]{.button}](#sec:exercise02)
:::
## Apéndice 3: Interpretación {#sec:appendix03}
:::{style="margin-top: 20px; font-size: 20px;"}
:::{.columns}
:::{.column width=50%}
**Calcular las métricas:**
```{r ap3-metricas, prompt=FALSE, echo=TRUE, eval=TRUE}
predicciones |>
precision(truth = crecimiento_alto,
estimate = .pred_class,
event_level = "second")
predicciones |>
recall(truth = crecimiento_alto,
estimate = .pred_class,
event_level = "second")
# O ambas a la vez:
predicciones |>
conf_mat(truth = crecimiento_alto,
estimate = .pred_class) |>
summary(event_level = "second")
```
:::
:::{.column width=50%}
**Cómo interpretar la diferencia:**
- [Precisión alta, recall bajo]{.alert}: el modelo predice "crecimiento alto" con cautela. Pocos falsos positivos, pero se pierden casos reales
- [Recall alto, precisión baja]{.alert}: predice "sí" con frecuencia. No pierde casos, pero genera más falsas alarmas
**¿Qué métrica priorizar?**
- Si el [costo de perder un caso]{.alert} es alto (p. ej., decisiones de inversión): prioricen [recall]{.alert}
- Si el [costo de una falsa alarma]{.alert} es alto (p. ej., asignar recursos mal): prioricen [precisión]{.alert}
- Sin preferencia clara: usen [F1]{.alert}, el promedio armónico de ambas
[No hay respuesta universal.]{.alert} El contexto define la métrica correcta.
:::
:::
[[Volver al ejercicio]{.button}](#sec:exercise03)
:::
## Apéndice 4: Probabilidades {#sec:appendix04}
:::{style="margin-top: 20px; font-size: 21px;"}
**¿Cómo identificar predicciones inciertas?**
```{r ap4-sol, prompt=FALSE, echo=TRUE, eval=TRUE}
# 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)
```
- Las observaciones con `.pred_si` cercano a 0.5 son las más [difíciles de clasificar]{.alert}: pequeños cambios en los predictores pueden cambiar la clase predicha
- En contextos reales, estas observaciones merecen [análisis adicional]{.alert} 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]{.button}](#sec:exercise04)
:::
## Apéndice 5: Cambio de umbral {#sec:appendix05}
:::{style="margin-top: 20px; font-size: 21px;"}
```{r ap5-sol, prompt=FALSE, echo=TRUE, eval=TRUE}
# 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")
)
})
```
| Umbral | Precisión | Recall | Interpretación |
|--------|-----------|--------|----------------|
| [0.3]{.alert} | baja | alta | predice "sí" fácilmente, más falsos positivos |
| [0.5]{.alert} | media | media | punto de equilibrio (por defecto) |
| [0.7]{.alert} | alta | baja | predice "sí" con cautela, más falsos negativos |
Este es el [compromiso precisión-recall]{.alert} en acción. El umbral óptimo depende del problema.
[[Volver al ejercicio]{.button}](#sec:exercise05)
:::
## Apéndice 6: Validación cruzada {#sec:appendix06}
:::{style="margin-top: 20px; font-size: 21px;"}
```{r ap6-interpretacion, prompt=FALSE, echo=TRUE, eval=TRUE}
# collect_metrics() devuelve media y error estándar de cada métrica
collect_metrics(cv_results)
# Comparar con las métricas de la división única
predicciones |>
metrics(truth = crecimiento_alto, estimate = .pred_class)
```
- `collect_metrics()` devuelve la [media]{.alert} de cada métrica sobre los 5 folds junto con su error estándar (`std_err`)
- Un [error estándar bajo]{.alert} indica resultados consistentes entre folds (buena señal)
- Si las métricas de CV son [peores que las de la división única]{.alert}, la partición original fue demasiado favorable al modelo
- La validación cruzada es más [confiable]{.alert} porque cada observación aparece exactamente una vez en validación
[[Volver al ejercicio]{.button}](#sec:exercise06)
:::
## Apéndice 7: Curva ROC {#sec:appendix07}
:::{style="margin-top: 20px; font-size: 26px;"}
**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]{.alert} indica que el modelo identifica los positivos más claros primero
- El AUC es [independiente del umbral]{.alert}: evalúa el rendimiento en todos los posibles umbrales a la vez
- Es especialmente útil para [comparar modelos]{.alert} sin tener que elegir un umbral
[[Volver al ejercicio]{.button}](#sec:exercise07)
:::