---
title: "IA para Científicos Sociales"
subtitle: "Sesión 2.4: Laboratorio 4 - Regresión y regularización"
author:
  - name: Danilo Freire
    orcid: 0000-0002-4712-6810
    email: danilofreire@gmail.com
    affiliations: "Department of Data and Decision Sciences <br> Emory University"
format:
  clean-revealjs:
    self-contained: true
    footer: "[Sesión 2.4](https://danilofreire.github.io/introduccion-ia-ucu/clases/dia-02/08-laboratorio-04.html)"
    transition: slide
    transition-speed: default
    scrollable: true
execute:
  echo: 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 = FALSE,
  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 4: Regresión y regularización {background-color="#2d4563"}

## Objetivos del laboratorio

:::{style="margin-top: 30px; font-size: 24px;"}
:::{.columns}
:::{.column width=50%}
**Retomamos la clase de regresión.** Hoy ejecutamos paso a paso la misma receta que vimos en la teoría:

1. Preparar los datos con una `receta`
2. Ajustar un baseline [OLS]{.alert}
3. Tunear [LASSO]{.alert} con validación cruzada
4. Implementar [Ridge]{.alert} por analogía (**Ejercicio 1**)
5. Comparar los tres modelos
6. Decidir: ¿predicción o explicación? (**Ejercicio 2**)
:::

:::{.column width=50%}
**Lo que vamos a aprender:**

- Cuándo y cómo usar [regularización]{.alert}
- Diferencias prácticas entre [LASSO]{.alert} y [Ridge]{.alert}
- Tuning de `penalty` con validación cruzada
- Trade-off entre interpretabilidad y rendimiento

<br>

[La mayoría de los slides son demostraciones: corremos el código juntos. Solo hay 2 ejercicios, ambos con solución en el apéndice]{.alert} 🤓
:::
:::
:::

# Parte 1: Preparación {background-color="#2d4563"}

## Cargar paquetes y datos

:::{style="margin-top: 30px; font-size: 20px;"}

```{r}
#| label: setup-paquetes
#| message: false
#| warning: false
#| eval: true

# Si algún paquete falta: install.packages(c("tidymodels", "tidyverse", "glmnet"))
library(tidymodels)  # rsample, parsnip, recipes, workflows, tune, yardstick
library(tidyverse)   # incluye readr (read_csv), dplyr, ggplot2, etc.
library(glmnet)      # motor para LASSO, Ridge, Elastic Net

set.seed(2026)

# Cargar el dataset
datos <- read_csv("datos/latinobarometro_sim.csv", show_col_types = FALSE)
# Lean el archivo directo desde la web:
# datos <- read_csv("https://raw.githubusercontent.com/danilofreire/introduccion-ia-ucu/main/clases/dia-02/datos/latinobarometro_sim.csv", show_col_types = FALSE)

# Convertir categóricas a factor
datos <- datos |>
  mutate(
    pais         = factor(pais),
    zona         = factor(zona),
    genero       = factor(genero),
    uso_internet = factor(uso_internet, levels = c("nunca", "semanal", "diario"))
  )

glimpse(datos)
```
:::

## Dividir los datos

:::{style="margin-top: 30px; font-size: 24px;"}

```{r}
#| label: split-datos
#| eval: true

# División train/test (75/25), estratificada por el outcome
# (con una variable numérica, rsample estratifica por cuartiles)
datos_split <- initial_split(datos, prop = 0.75, strata = satisfaccion_vida)
datos_train <- training(datos_split)
datos_test  <- testing(datos_split)

cat("Train:", nrow(datos_train), "obs | Test:", nrow(datos_test), "obs\n")
```

- La variable objetivo es `satisfaccion_vida` (1-10, continua)
- Train tendrá ~375 obs, test ~125
:::

## Receta de preprocesamiento

:::{style="margin-top: 30px; font-size: 20px;"}

```{r}
#| label: receta
#| eval: true

# Predecir con todas las variables menos voto (outcome del Lab 3) y pais (alta cardinalidad)
receta <- recipe(satisfaccion_vida ~ ., data = datos_train) |>
  step_rm(pais, voto) |>                       # eliminar variables no predictivas o con demasiados niveles
  step_dummy(all_nominal_predictors()) |>      # categóricas → dummies
  step_normalize(all_numeric_predictors()) |>  # normalizar (importante para regularización)
  step_zv(all_predictors())                    # eliminar varianza cero

# Verificar
# prep() = preparar la receta con los datos de train; juice() = extraer el dataset preprocesado
receta |> prep() |> juice() |> glimpse()
```
:::

# Parte 2: OLS baseline {background-color="#2d4563"}

## OLS: spec y ajuste

:::{style="margin-top: 30px; font-size: 22px;"}

```{r}
#| label: ols-ajuste
#| eval: true

# Regresión lineal con motor lm
modelo_ols <- linear_reg() |>
  set_engine("lm") |>
  set_mode("regression")

# Workflow + ajuste (combinados en una pipeline)
ajuste_ols <- workflow() |>
  add_recipe(receta) |>
  add_model(modelo_ols) |>
  fit(data = datos_train)

# Coeficientes ordenados por magnitud (top 5)
tidy(ajuste_ols) |>
  arrange(desc(abs(estimate))) |>
  head(5)
```

- `ajuste_ols` ya tiene el modelo ajustado a `datos_train`
- En el próximo slide lo evaluamos en test
:::

## Evaluar OLS en test

:::{style="margin-top: 30px; font-size: 24px;"}

```{r}
#| label: ols-eval
#| eval: true

# Generar predicciones (augment() añade .pred al test set)
pred_ols <- augment(ajuste_ols, datos_test)

# Calcular métricas en test
metricas_ols <- pred_ols |>
  metrics(truth = satisfaccion_vida, estimate = .pred)

metricas_ols |> knitr::kable(digits = 3)
```

:::{style="margin-top:30px;"}
- RMSE ≈ 1.3: en promedio la predicción se aleja de la realidad en ~1.3 puntos (escala 1-10)
- R² ≈ 0.23: el modelo explica ~23% de la varianza de la satisfacción
:::
:::

# Parte 3: LASSO con tuning {background-color="#2d4563"}

## Definir LASSO + grilla de búsqueda

:::{style="margin-top: 30px; font-size: 22px;"}

```{r}
#| label: lasso-spec
#| eval: true

# LASSO: linear_reg con mixture = 1 (L1 puro)
# tune() = "este valor se busca con CV"
modelo_lasso <- linear_reg(penalty = tune(), mixture = 1) |>
  set_engine("glmnet") |>
  set_mode("regression")

wf_lasso <- workflow() |> add_recipe(receta) |> add_model(modelo_lasso)

# Grilla de 30 valores de λ en escala logarítmica (10^-4 = 0.0001 a 10^0 = 1)
grilla_lambda <- grid_regular(penalty(range = c(-4, 0)), levels = 30)

head(grilla_lambda)
```

- `mixture = 1` → LASSO puro. Si fuera `0`, sería Ridge (lo ven en el Ejercicio 1)
- `tune()` deja `penalty` (λ) pendiente: lo elige la validación cruzada
:::

## Tuning de LASSO con CV

:::{style="margin-top: 30px; font-size: 19px;"}

```{r}
#| label: lasso-cv
#| eval: true

# Crear folds (10-fold CV)
folds <- vfold_cv(datos_train, v = 10)

# Tuning: probar cada valor de la grilla con CV
resultados_lasso <- tune_grid(
  wf_lasso,
  resamples = folds,
  grid      = grilla_lambda,
  metrics   = metric_set(rmse)
)

# Top 5 mejores λ por RMSE
resultados_lasso |>
  collect_metrics() |>
  filter(.metric == "rmse") |>
  arrange(mean) |>
  head(5) |>
  knitr::kable(digits = 4)
```

- `mean` es el RMSE promedio sobre los 10 folds; `std_err` indica cuánto varía entre folds
:::

## Visualizar el tuning de LASSO

:::{style="margin-top: 30px; font-size: 22px;"}

```{r}
#| label: lasso-viz
#| eval: true
#| fig-width: 9
#| fig-height: 4

autoplot(resultados_lasso) +
  scale_x_log10() +
  theme_minimal() +
  labs(title = "RMSE vs. λ (escala log)")
```

- La curva suele tener forma de **U**: λ muy bajo → poca regularización (sobreajuste); λ muy alto → demasiada (subajuste)
- El mínimo de la curva es el λ óptimo según CV
:::

## Seleccionar λ y ajuste final

:::{style="margin-top: 30px; font-size: 18px;"}

```{r}
#| label: lasso-final
#| eval: true

# 1. Seleccionar el λ con menor RMSE
lambda_min <- select_best(resultados_lasso, metric = "rmse")

# 2. Finalizar el workflow con ese λ y ajustar a todo el train
ajuste_lasso <- wf_lasso |>
  finalize_workflow(lambda_min) |>
  fit(data = datos_train)

# Métricas en test (las reutilizamos en la tabla comparativa)
pred_lasso     <- augment(ajuste_lasso, datos_test)
metricas_lasso <- pred_lasso |>
  metrics(truth = satisfaccion_vida, estimate = .pred)

# 3. Coeficientes y conteo de variables eliminadas
coef_lasso <- tidy(ajuste_lasso) |>
  filter(term != "(Intercept)") |>
  arrange(desc(abs(estimate)))

cat("Variables eliminadas (coef = 0):",
    sum(coef_lasso$estimate == 0), "de", nrow(coef_lasso), "\n")
coef_lasso |> head(8) |> knitr::kable(digits = 3)
```
:::

## Comparar coeficientes OLS vs. LASSO

:::{style="margin-top: 30px; font-size: 19px;"}

```{r}
#| label: comparar-coef
#| eval: true
#| fig-width: 10
#| fig-height: 4

coef_ols  <- tidy(ajuste_ols)   |> filter(term != "(Intercept)") |> select(term, OLS   = estimate)
coef_lasc <- tidy(ajuste_lasso) |> filter(term != "(Intercept)") |> select(term, LASSO = estimate)

left_join(coef_ols, coef_lasc, by = "term") |>
  pivot_longer(c(OLS, LASSO), names_to = "modelo", values_to = "coef") |>
  ggplot(aes(x = reorder(term, abs(coef)), y = coef, fill = modelo)) +
  geom_col(position = "dodge") + coord_flip() +
  scale_fill_manual(values = c("OLS" = "#3498DB", "LASSO" = "#E74C3C")) +
  labs(title = "Coeficientes: OLS vs. LASSO",
       x = NULL, y = "Coeficiente (normalizado)") +
  theme_minimal()
```

- Es la versión práctica de "interpretar los coeficientes de LASSO" que vimos en la clase: LASSO encoge coeficientes hacia cero (algunos exactamente cero); OLS los deja todos sin restricción
:::

# Parte 4: Ridge por analogía {background-color="#2d4563"}

## Ejercicio 1: Implementar Ridge por analogía {#sec:exercise01}

:::{style="margin-top: 30px; font-size: 18px;"}

**Tarea (5 min):** adapten el pipeline de LASSO que acabamos de ver. Cambien solo lo necesario para Ridge: completen los tres huecos

```{r}
#| label: ej1-skeleton
#| eval: false

# 1. Modelo Ridge: igual que LASSO pero mixture = 0
modelo_ridge <- linear_reg(penalty = tune(), mixture = ___) |>  # TODO: ¿qué valor?
  set_engine("glmnet") |>
  set_mode("regression")

# 2. Workflow + tuning (reutilizamos folds y grilla_lambda)
wf_ridge <- workflow() |> add_recipe(receta) |> add_model(modelo_ridge)

resultados_ridge <- tune_grid(
  ___, resamples = folds, grid = grilla_lambda,    # TODO: ¿qué workflow?
  metrics = metric_set(rmse)
)

# 3. Seleccionar mejor λ, finalizar, ajustar
lambda_ridge <- select_best(___, metric = "rmse")  # TODO: ¿qué resultados?
ajuste_ridge <- finalize_workflow(wf_ridge, lambda_ridge) |>
  fit(data = datos_train)

# 4. Evaluar en test
pred_ridge     <- augment(ajuste_ridge, datos_test)
metricas_ridge <- pred_ridge |>
  metrics(truth = satisfaccion_vida, estimate = .pred)

# 5. Contar variables eliminadas (debería ser 0!)
cat("Variables eliminadas en Ridge:",
    sum(tidy(ajuste_ridge)$estimate[-1] == 0), "\n")
metricas_ridge
```

```{r}
#| label: ej1-hidden
#| include: false

modelo_ridge <- linear_reg(penalty = tune(), mixture = 0) |>
  set_engine("glmnet") |>
  set_mode("regression")

wf_ridge <- workflow() |> add_recipe(receta) |> add_model(modelo_ridge)

resultados_ridge <- tune_grid(
  wf_ridge, resamples = folds, grid = grilla_lambda,
  metrics = metric_set(rmse)
)

lambda_ridge <- select_best(resultados_ridge, metric = "rmse")
ajuste_ridge <- finalize_workflow(wf_ridge, lambda_ridge) |>
  fit(data = datos_train)

pred_ridge     <- augment(ajuste_ridge, datos_test)
metricas_ridge <- pred_ridge |>
  metrics(truth = satisfaccion_vida, estimate = .pred)
```

:::{.callout-note}
**Pista**: el único cambio conceptual respecto a LASSO es `mixture = 0`. Los objetos `folds` y `grilla_lambda` ya están definidos; reutilícenlos tal cual
:::

:::{.callout-tip}
**Checkpoint**: Ridge debería eliminar **0 variables** (puede mostrar valores como `1e-10`, prácticamente cero pero no exactos). RMSE similar a LASSO (~1.1-1.3)
:::

[[Apéndice 1: Solución]{.button}](#sec:appendix01)
:::

## LASSO selecciona, Ridge encoge {#sec:lasso-vs-ridge}

:::{style="margin-top: 30px; font-size: 22px;"}
:::{.columns}
:::{.column width=50%}
**LASSO (L1)**

- Penaliza la suma de [valores absolutos]{.alert}
- Reduce algunos coeficientes a [exactamente cero]{.alert}
- Hace [selección automática]{.alert} de variables
- Útil cuando solo algunas variables importan
:::

:::{.column width=50%}
**Ridge (L2)**

- Penaliza la suma de [cuadrados]{.alert}
- Reduce todos los coeficientes pero ninguno a cero exacto
- [Mantiene todas las variables]{.alert}
- Útil con variables correlacionadas (multicolinealidad)
:::
:::

<br>

[Geométricamente: la "bola" de L1 tiene esquinas que tocan los ejes (coeficiente = 0); la "bola" de L2 es lisa y nunca toca exactamente un eje.]{.alert}

Para una versión intermedia (Elastic Net), ver [[Apéndice 5]{.button}](#sec:appendix05)
:::

# Parte 5: Comparación final {background-color="#2d4563"}

## Tabla comparativa: OLS, LASSO y Ridge

:::{style="margin-top: 30px; font-size: 24px;"}

```{r}
#| label: tabla-comparativa
#| eval: true

# Combinar las métricas de los 3 modelos (ya creadas en los slides anteriores)
tabla_final <- bind_rows(
  metricas_ols   |> mutate(modelo = "OLS"),
  metricas_lasso |> mutate(modelo = "LASSO"),
  metricas_ridge |> mutate(modelo = "Ridge")
) |>
  select(modelo, .metric, .estimate) |>
  pivot_wider(names_from = .metric, values_from = .estimate) |>
  arrange(rmse)

tabla_final |> knitr::kable(digits = 3)
```

- Los 3 modelos producen RMSE muy similares en este dataset
- LASSO selecciona variables; Ridge las encoge; OLS no penaliza
:::

## Ejercicio 2: ¿Predicción o explicación? {#sec:exercise02}

:::{style="margin-top: 30px; font-size: 24px;"}

**Tarea (5 min):** miren la tabla comparativa y el gráfico de coeficientes OLS vs. LASSO, y discutan en grupo

1. ¿Qué variables redujo LASSO a cero? ¿Tiene sentido sustantivo que esas pesen menos?
2. Si el objetivo es [explicar]{.alert} qué determina la satisfacción con la vida, ¿qué modelo prefieren? ¿Y si solo quieren [predecirla]{.alert} para casos nuevos?
3. ¿Por qué las diferencias de RMSE entre los tres modelos son tan pequeñas con estos datos?

:::{.callout-note}
**Pista**: vuelvan a la distinción de la clase entre [predicción]{.alert} y [explicación]{.alert}. Un modelo más simple (menos variables) es más fácil de explicar, aunque prediga casi igual
:::

[[Apéndice 2: Discusión]{.button}](#sec:appendix02)
:::

# Cierre {background-color="#2d4563"}

## Discusión y resumen {#sec:discussion}

:::{style="margin-top: 30px; font-size: 22px;"}
:::{.columns}
:::{.column width=50%}
**Lo que vimos hoy:**

1. Ejecutar la receta completa: `receta` → modelo → `workflow`
2. Tunear LASSO con CV y seleccionar λ
3. Implementar Ridge por analogía (Ejercicio 1)
4. Comparar OLS, LASSO y Ridge
5. Decidir entre predicción y explicación (Ejercicio 2)
:::

:::{.column width=50%}
**Conceptos clave:**

- `mixture = 1` → [LASSO]{.alert} (selecciona)
- `mixture = 0` → [Ridge]{.alert} (encoge)
- `tune()` + `tune_grid()` para CV
- `select_best()` + `finalize_workflow()` para fijar λ
- En este dataset las diferencias entre OLS/LASSO/Ridge son [marginales]{.alert}
:::
:::

<br>

[¿Por qué tan marginales? Porque las relaciones son aproximadamente lineales y el ratio n/p no es extremo. La regularización brilla cuando p ≫ n o hay colinealidad fuerte.]{.alert}

Para llevarse a casa: en el [[Apéndice 4]{.button}](#sec:appendix04) apliquen todo el pipeline a un outcome diferente (`satisfaccion_democracia`)
:::

# Apéndice: Soluciones {background-color="#2d4563"}

## Apéndice 1: Solución Ejercicio 1 {#sec:appendix01}

:::{style="margin-top: 20px; font-size: 18px;"}

```{r}
#| label: ap1-sol
#| eval: false

# Cambio único: mixture = 0 (Ridge en vez de LASSO)
modelo_ridge <- linear_reg(penalty = tune(), mixture = 0) |>
  set_engine("glmnet") |>
  set_mode("regression")

wf_ridge <- workflow() |> add_recipe(receta) |> add_model(modelo_ridge)

resultados_ridge <- tune_grid(
  wf_ridge, resamples = folds, grid = grilla_lambda,
  metrics = metric_set(rmse)
)

lambda_ridge <- select_best(resultados_ridge, metric = "rmse")
ajuste_ridge <- finalize_workflow(wf_ridge, lambda_ridge) |>
  fit(data = datos_train)

pred_ridge     <- augment(ajuste_ridge, datos_test)
metricas_ridge <- pred_ridge |>
  metrics(truth = satisfaccion_vida, estimate = .pred)

cat("Variables eliminadas en Ridge:",
    sum(tidy(ajuste_ridge)$estimate[-1] == 0), "\n")
metricas_ridge
```

**Métricas de Ridge en test:**

```{r}
#| label: ap1-display
#| echo: false
#| eval: true

metricas_ridge |> knitr::kable(digits = 3)
```

- Ridge nunca elimina variables, solo las encoge; el conteo de eliminadas es 0
- RMSE similar al de LASSO en este dataset

[[Volver al ejercicio]{.button}](#sec:exercise01)
:::

## Apéndice 2: Discusión Ejercicio 2 {#sec:appendix02}

:::{style="margin-top: 20px; font-size: 20px;"}

**No hay una única respuesta correcta. Algunas ideas:**

- **Variables eliminadas:** LASSO suele llevar a cero las de menor señal en estos datos (por ejemplo algunas dummies de `zona` o `genero`). Que pesen poco es plausible: no son los grandes determinantes de la satisfacción
- **¿Explicar o predecir?** Si el objetivo es [explicar]{.alert}, conviene un modelo simple e interpretable: OLS o LASSO (LASSO además deja una lista corta de variables). Si el objetivo es [predecir]{.alert} casos nuevos, elegimos por RMSE en test, sin importar cuántas variables queden
- **Diferencias pequeñas de RMSE:** con relaciones casi lineales y `n` mayor que el número de predictores, hay poco sobreajuste que corregir. La regularización aporta más cuando hay muchas variables o colinealidad fuerte

[[Volver al ejercicio]{.button}](#sec:exercise02)
:::

## Apéndice 3: Dos criterios para elegir λ (opcional) {#sec:appendix03}

:::{style="margin-top: 20px; font-size: 19px;"}

En la clase vimos dos formas de elegir λ. Hasta ahora usamos `select_best` (mínimo RMSE). La regla [1-SE]{.alert} elige el λ más grande (modelo más simple) que queda a menos de un error estándar del mínimo:

```{r}
#| label: ap3-onese
#| eval: false

# Criterio 1: mínimo RMSE (el que usamos en el lab)
lambda_min  <- select_best(resultados_lasso, metric = "rmse")

# Criterio 2: regla de un error estándar (modelo más parsimonioso)
lambda_1se  <- select_by_one_std_err(resultados_lasso, metric = "rmse", desc(penalty))

bind_rows(
  lambda_min |> mutate(criterio = "min"),
  lambda_1se |> mutate(criterio = "1-SE")
)
```

- `lambda_1se` suele ser mayor, así que regulariza más y deja menos variables
- Sacrifica un poco de RMSE a cambio de un modelo más simple de explicar

[[Volver a la sección de discusión]{.button}](#sec:discussion)
:::

## Apéndice 4: Capstone: outcome diferente (opcional) {#sec:appendix04}

:::{style="margin-top: 20px; font-size: 17px;"}

**Para llevarse a casa:** apliquen todo el pipeline a `satisfaccion_democracia` (escala 1-5). ¿Cómo cambian los resultados?

```{r}
#| label: ap4-sol
#| eval: false

# 1. Nueva receta para predecir satisfaccion_democracia
receta_dem <- recipe(satisfaccion_democracia ~ ., data = datos_train) |>
  step_rm(pais, voto, satisfaccion_vida) |>      # excluir outcomes ajenos
  step_dummy(all_nominal_predictors()) |>
  step_normalize(all_numeric_predictors()) |>
  step_zv(all_predictors())

# 2. LASSO con la nueva receta (mismo patrón)
wf_lasso_dem <- workflow() |> add_recipe(receta_dem) |> add_model(modelo_lasso)

resultados_dem <- tune_grid(
  wf_lasso_dem, resamples = folds, grid = grilla_lambda,
  metrics = metric_set(rmse)
)

lambda_dem  <- select_best(resultados_dem, metric = "rmse")
ajuste_dem  <- finalize_workflow(wf_lasso_dem, lambda_dem) |>
  fit(data = datos_train)

# 3. Evaluación
pred_dem <- augment(ajuste_dem, datos_test)

pred_dem |> metrics(truth = satisfaccion_democracia, estimate = .pred)
```

Preguntas para reflexionar:

- ¿Las mismas variables son predictivas, o cambian?
- ¿El RMSE es mayor o menor que para `satisfaccion_vida`?
- Pueden repetir con `percepcion_economia` como tercer outcome

[[Volver a la sección de discusión]{.button}](#sec:discussion)
:::

## Apéndice 5: Elastic Net (opcional) {#sec:appendix05}

:::{style="margin-top: 20px; font-size: 18px;"}

Elastic Net combina LASSO y Ridge: `mixture` entre 0 y 1

```{r}
#| label: ap5-enet
#| eval: false

# Ajustar AMBOS hiperparámetros: penalty (λ) y mixture (α)
modelo_enet <- linear_reg(penalty = tune(), mixture = tune()) |>
  set_engine("glmnet") |>
  set_mode("regression")

wf_enet <- workflow() |> add_recipe(receta) |> add_model(modelo_enet)

# Grilla 2D: 20 valores de λ × 5 de mixture (como en la clase)
grilla_enet <- grid_regular(
  penalty(range = c(-4, 0)),
  mixture(range = c(0, 1)),
  levels = c(20, 5)
)

resultados_enet <- tune_grid(
  wf_enet, resamples = folds, grid = grilla_enet,
  metrics = metric_set(rmse)
)

mejor_enet  <- select_best(resultados_enet, metric = "rmse")
mejor_enet  # vean el mixture óptimo

ajuste_enet <- finalize_workflow(wf_enet, mejor_enet) |>
  fit(data = datos_train)

predict(ajuste_enet, datos_test) |>
  bind_cols(datos_test |> select(satisfaccion_vida)) |>
  metrics(truth = satisfaccion_vida, estimate = .pred)
```

- Si `mixture` óptimo está cerca de 1 → LASSO ganó
- Si está cerca de 0 → Ridge ganó
- Si intermedio → Elastic Net mejora sobre ambos

[[Volver a la sección de LASSO vs. Ridge]{.button}](#sec:lasso-vs-ridge)
:::

# ¡Fin del Día 2! 🤓 {background-color="#2d4563"}
