---
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
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
[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)
:::
:::
[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}
:::
:::
[¿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"}