---
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 <br> 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

<br>

[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

<br>

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