--- title: "IA para Científicos Sociales" subtitle: "Sesión 2.4: Laboratorio - Regresión y regularización" 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 2.4](https://danilofreire.github.io/introduccion-ia-ucu/clases/dia-02/06-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: 28px;"} :::{.columns} :::{.column width=50%} **Lo que vamos a hacer:** 1. Predecir satisfacción con la vida 2. Baseline con OLS 3. Ajustar LASSO con validación cruzada 4. Interpretar coeficientes y selección 5. Contrastar con Ridge en una slide 6. Cerrar con una tabla comparativa final ::: :::{.column width=50%} **Lo que vamos a aprender:** - Cuándo usar [regularización]{.alert} - Diferencias prácticas entre LASSO y Ridge - [Tuning]{.alert} de penalty con CV - Trade-off interpretabilidad vs. rendimiento
[Usaremos el mismo dataset de Latinobarómetro (datos simulados).]{.alert} ::: ::: ::: # Parte 1: Preparación y baseline {background-color="#2d4563"} ## Cargar los paquetes necesarios :::{style="margin-top: 30px; font-size: 22px;"} ```{r} #| label: cargar-paquetes #| message: false #| warning: false #| eval: true # Paquetes necesarios paquetes <- c("tidyverse", "tidymodels", "glmnet", "ranger", "vip") for (pkg in paquetes) { if (!require(pkg, character.only = TRUE)) { install.packages(pkg, dependencies = TRUE) library(pkg, character.only = TRUE) } } library(tidymodels) 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) glimpse(datos) ``` ::: ## Cargar y explorar los datos :::{style="margin-top: 30px; font-size: 20px;"} ```{r} #| label: cargar-datos #| eval: true # Convertir variables categóricas datos <- datos |> mutate( pais = factor(pais), zona = factor(zona), genero = factor(genero), uso_internet = factor(uso_internet, levels = c("nunca", "semanal", "diario")) ) # Nuestra variable objetivo summary(datos$satisfaccion_vida) # Histograma ggplot(datos, aes(x = satisfaccion_vida)) + geom_histogram(binwidth = 1, fill = "#2d4563", color = "white") + labs(title = "Distribución de satisfacción con la vida", x = "Satisfacción (1-10)", y = "Frecuencia") + theme_minimal() ``` ::: ## Dividir los datos :::{style="margin-top: 30px; font-size: 22px;"} ```{r} #| label: dividir-datos #| eval: true # División train/test division <- initial_split(datos, prop = 0.75) datos_train <- training(division) datos_test <- testing(division) cat("Observaciones en train:", nrow(datos_train), "\n") cat("Observaciones en test:", nrow(datos_test), "\n") # Media de la variable objetivo cat("\nMedia satisfacción (train):", mean(datos_train$satisfaccion_vida)) cat("\nMedia satisfacción (test):", mean(datos_test$satisfaccion_vida)) ``` ::: ## Receta de preprocesamiento :::{style="margin-top: 30px; font-size: 20px;"} ```{r} #| label: crear-receta #| eval: true # Definir predictores (excluimos voto y pais) receta <- recipe(satisfaccion_vida ~ edad + educacion_anios + ingreso_hogar + zona + genero + confianza_gobierno + confianza_justicia + satisfaccion_democracia + percepcion_economia + uso_internet + interes_politica, data = datos_train) |> # Dummies para categóricas step_dummy(all_nominal_predictors()) |> # Normalizar (importante para regularización) step_normalize(all_numeric_predictors()) |> # Eliminar varianza cero step_zv(all_predictors()) # Verificar receta |> prep() |> juice() |> glimpse() ``` ::: ## Modelo baseline: OLS :::{style="margin-top: 30px; font-size: 20px;"} ```{r} #| label: modelo-ols #| eval: true # Regresión lineal simple modelo_ols <- linear_reg() |> set_engine("lm") |> set_mode("regression") # Workflow wf_ols <- workflow() |> add_recipe(receta) |> add_model(modelo_ols) # Ajustar ajuste_ols <- fit(wf_ols, data = datos_train) # Coeficientes tidy(ajuste_ols) |> arrange(desc(abs(estimate))) ``` ::: ## Evaluar OLS en test :::{style="margin-top: 30px; font-size: 22px;"} ```{r} #| label: evaluar-ols #| eval: true # Predicciones en test pred_ols <- predict(ajuste_ols, datos_test) |> bind_cols(datos_test |> select(satisfaccion_vida)) # Métricas metricas_ols <- pred_ols |> metrics(truth = satisfaccion_vida, estimate = .pred) metricas_ols # Visualizar predicciones vs. reales ggplot(pred_ols, aes(x = satisfaccion_vida, y = .pred)) + geom_point(alpha = 0.5) + geom_abline(color = "red", linetype = "dashed") + labs(title = "Predicciones OLS vs. valores reales", x = "Satisfacción real", y = "Satisfacción predicha") + theme_minimal() ``` ::: # Parte 2: LASSO con tuning {background-color="#2d4563"} ## ¿Por qué regularizar? :::{style="margin-top: 30px; font-size: 24px;"} :::{.columns} :::{.column width=55%} **Nuestro caso:** - 11 predictores, ~375 observaciones en train - La ratio n/p no es tan extrema - Pero algunas variables podrían ser irrelevantes **¿Cuándo regularizar?** - Muchos predictores relativos a las observaciones - Sospecha de multicolinealidad - Queremos [selección automática]{.alert} de variables - Prevenir sobreajuste ::: :::{.column width=45%} :::{style="margin-top: 30px;"} :::{.callout-note} **En este ejemplo** Usamos regularización principalmente para: 1. [Demostrar el método]{.alert} 2. Identificar variables que no aportan 3. Comparar con OLS En la práctica, con p = 11 y n = 375, OLS probablemente está bien. ::: ::: ::: ::: ::: ## Definir LASSO con penalty a ajustar :::{style="margin-top: 30px; font-size: 20px;"} ```{r} #| label: lasso-spec #| eval: true # LASSO: mixture = 1 modelo_lasso <- linear_reg( penalty = tune(), # λ a ajustar mixture = 1 # 1 = LASSO puro ) |> set_engine("glmnet") |> set_mode("regression") # Workflow wf_lasso <- workflow() |> add_recipe(receta) |> add_model(modelo_lasso) # Grilla de valores de penalty (escala logarítmica) grilla_lambda <- grid_regular( penalty(range = c(-4, 0)), # 10^-4 a 10^0 = 0.0001 a 1 levels = 30 ) head(grilla_lambda) ``` ::: ## Validación cruzada para LASSO :::{style="margin-top: 30px; font-size: 20px;"} ```{r} #| label: lasso-cv #| eval: true #| cache: true # 10-fold CV folds <- vfold_cv(datos_train, v = 10) # Ajustar todas las lambdas resultados_lasso <- tune_grid( wf_lasso, resamples = folds, grid = grilla_lambda, metrics = metric_set(rmse, rsq, mae) ) # Ver resultados resultados_lasso |> collect_metrics() |> filter(.metric == "rmse") |> arrange(mean) |> head(10) ``` ::: ## Visualizar el tuning de LASSO :::{style="margin-top: 30px; font-size: 22px;"} ```{r} #| label: lasso-viz #| eval: true #| fig-width: 10 #| fig-height: 5 # Gráfico de RMSE vs. penalty autoplot(resultados_lasso) + scale_x_log10() + theme_minimal() + labs(title = "Tuning de LASSO: RMSE vs. penalty (λ)") ``` ::: ## Ejercicio 1: Efecto del λ en la selección {#sec:exercise01} :::{style="margin-top: 30px; font-size: 22px;"} **Instrucciones:** 1. Ajustar LASSO con varios valores de λ: 0.001, 0.01, 0.1, 0.5 y 1 2. Contar cuántas variables se eliminan en cada caso 3. ¿Qué pasa con un λ muy grande? ¿Y con uno muy pequeño? *Pista:* usen `map_df()` con una función que ajuste el workflow para cada lambda y cuente los coeficientes iguales a cero con `tidy()`. [Tómense 5 minutos para experimentar.]{.alert} [[Apéndice 1: Solución]{.button}](#sec:appendix01) ::: ## Seleccionar λ y ajustar LASSO final :::{style="margin-top: 30px; font-size: 18px;"} ```{r} #| label: lasso-final #| eval: true # λ mínimo vs. λ 1SE (modelo más simple dentro de 1 error estándar) lambda_min <- select_best(resultados_lasso, metric = "rmse") lambda_1se <- select_by_one_std_err(resultados_lasso, metric = "rmse", desc(penalty)) cat("Lambda mínimo:", lambda_min$penalty, " | Lambda 1SE:", lambda_1se$penalty, "\n") # Usamos lambda mínimo y ajustamos con todo el training wf_lasso_final <- finalize_workflow(wf_lasso, lambda_min) ajuste_lasso <- fit(wf_lasso_final, data = datos_train) # Coeficientes y variables eliminadas coef_lasso <- tidy(ajuste_lasso) |> filter(term != "(Intercept)") |> arrange(desc(abs(estimate))) coef_lasso cat("\nVariables eliminadas:", sum(coef_lasso$estimate == 0), "de", nrow(coef_lasso)) ``` ::: ## Comparar coeficientes OLS vs. LASSO :::{style="margin-top: 30px; font-size: 18px;"} ```{r} #| label: comparar-coef #| eval: true #| fig-width: 10 #| fig-height: 5 # Extraer coeficientes de ambos coef_ols <- tidy(ajuste_ols) |> filter(term != "(Intercept)") |> select(term, estimate_ols = estimate) coef_lasso_comp <- tidy(ajuste_lasso) |> filter(term != "(Intercept)") |> select(term, estimate_lasso = estimate) # Combinar comparacion <- left_join(coef_ols, coef_lasso_comp, by = "term") |> pivot_longer(cols = starts_with("estimate"), names_to = "modelo", values_to = "coef") |> mutate(modelo = ifelse(modelo == "estimate_ols", "OLS", "LASSO")) # Visualizar ggplot(comparacion, 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 = "Comparación de coeficientes: OLS vs. LASSO", x = "Variable", y = "Coeficiente (normalizado)") + theme_minimal() ``` ::: ## Evaluar LASSO en test :::{style="margin-top: 30px; font-size: 22px;"} ```{r} #| label: evaluar-lasso #| eval: true # Predicciones pred_lasso <- predict(ajuste_lasso, datos_test) |> bind_cols(datos_test |> select(satisfaccion_vida)) # Métricas metricas_lasso <- pred_lasso |> metrics(truth = satisfaccion_vida, estimate = .pred) # Comparar con OLS cat("OLS:\n") print(metricas_ols) cat("\nLASSO:\n") print(metricas_lasso) ``` ::: # Parte 3: Ridge y cierre {background-color="#2d4563"} ## Ridge regression :::{style="margin-top: 30px; font-size: 18px;"} ```{r} #| label: ridge #| eval: true #| cache: true # Ridge: mixture = 0 (LASSO usa mixture = 1) modelo_ridge <- linear_reg(penalty = tune(), mixture = 0) |> set_engine("glmnet") |> set_mode("regression") wf_ridge <- workflow() |> add_recipe(receta) |> add_model(modelo_ridge) # Reutilizamos los folds y la grilla de lambda de LASSO 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) # Predicciones y métricas en test pred_ridge <- predict(ajuste_ridge, datos_test) |> bind_cols(datos_test |> select(satisfaccion_vida)) metricas_ridge <- pred_ridge |> metrics(truth = satisfaccion_vida, estimate = .pred) # Ridge nunca elimina variables (coefs se achican, no se anulan) cat("Variables con coef = 0 en Ridge:", sum(tidy(ajuste_ridge)$estimate[-1] == 0), "\n\n") metricas_ridge ``` - LASSO [selecciona]{.alert} (achica y anula); Ridge [encoge]{.alert} todos los coeficientes - Para una versión intermedia (Elastic Net), ver [Apéndice 4](#sec:appendix04) ::: # Parte 4: Comparación final {background-color="#2d4563"} ## Tabla comparativa en test :::{style="margin-top: 30px; font-size: 20px;"} ```{r} #| label: tabla-final #| eval: true # Combinar métricas de los tres modelos lineales tabla_comparacion <- 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_comparacion ```
:::{.callout-note} Random Forest y Elastic Net están en los apéndices con código listo para correr en casa. En nuestras pruebas, RF reduce el RMSE ~0.15 puntos; Elastic Net se parece mucho a LASSO. ::: ::: ## Visualización de predicciones :::{style="margin-top: 30px; font-size: 18px;"} ```{r} #| label: viz-pred #| eval: true #| fig-width: 12 #| fig-height: 4.5 # Combinar predicciones de los tres modelos lineales todas_pred <- bind_rows( pred_ols |> mutate(modelo = "OLS"), pred_lasso |> mutate(modelo = "LASSO"), pred_ridge |> mutate(modelo = "Ridge") ) ggplot(todas_pred, aes(x = satisfaccion_vida, y = .pred)) + geom_point(alpha = 0.4, color = "#2d4563") + geom_abline(color = "red", linetype = "dashed") + facet_wrap(~modelo, nrow = 1) + labs(title = "Predicciones vs. valores reales", x = "Satisfacción real", y = "Satisfacción predicha") + theme_minimal() ``` ::: ## Discusión: ¿Qué modelo elegir? :::{style="margin-top: 30px; font-size: 24px;"} :::{.columns} :::{.column width=50%} **Observaciones:** - Los tres modelos lineales dan resultados muy similares - LASSO eliminó algunas variables sin perder RMSE - La mejora de regularización sobre OLS es [marginal]{.alert} **¿Por qué?** - El dataset tiene relaciones aproximadamente lineales - No hay muchas variables irrelevantes - n/p no es extremo ::: :::{.column width=50%} **Recomendación:** - Si quiero [interpretabilidad]{.alert}: OLS o LASSO - Si quiero [selección de variables]{.alert}: LASSO - Si quiero [mejor predicción]{.alert}: ver Random Forest en el [Apéndice 5](#sec:appendix05)
[En este caso, OLS y LASSO dan casi el mismo error. La regularización brilla cuando p/n es grande o hay colinealidad fuerte.]{.alert} ::: ::: ::: ## Resumen del laboratorio :::{style="margin-top: 30px; font-size: 24px;"} :::{.columns} :::{.column width=50%} **Lo que practicamos:** - Regularización con LASSO y Ridge - Tuning de λ con CV - Comparación con OLS en test - Interpretación de coeficientes ::: :::{.column width=50%} **Conceptos clave:** - [mixture = 1]{.alert}: LASSO (selecciona) - [mixture = 0]{.alert}: Ridge (reduce) - [Validación cruzada]{.alert} para elegir λ - [λ 1SE]{.alert} para modelos más simples - Apéndices: Elastic Net, Random Forest, ejercicios extra ::: :::
[En el próximo día veremos aprendizaje no supervisado y análisis de texto.]{.alert} ::: # Apéndice: Soluciones {background-color="#2d4563"} ## Apéndice 1: Efecto del λ {#sec:appendix01} :::{style="margin-top: 20px; font-size: 20px;"} ```{r ap1-sol, prompt=FALSE, echo=TRUE, eval=TRUE} # Probar varios valores de lambda lambdas <- c(0.001, 0.01, 0.1, 0.5, 1) # Para cada lambda, ajustar y contar variables eliminadas resultados_lambda <- map_df(lambdas, function(l) { modelo <- linear_reg(penalty = l, mixture = 1) |> set_engine("glmnet") |> set_mode("regression") ajuste <- workflow() |> add_recipe(receta) |> add_model(modelo) |> fit(data = datos_train) coefs <- tidy(ajuste) |> filter(term != "(Intercept)") n_eliminadas <- sum(coefs$estimate == 0) tibble(lambda = l, vars_eliminadas = n_eliminadas, vars_total = nrow(coefs)) }) resultados_lambda ``` - Con [λ muy pequeño]{.alert} (0.001), LASSO se comporta casi como OLS: no elimina variables - Con [λ muy grande]{.alert} (1), LASSO elimina la mayoría o todas las variables - El λ óptimo balancea entre sesgo (simplificación) y varianza (sobreajuste) [[Volver al ejercicio]{.button}](#sec:exercise01) ::: ## Apéndice 2: Interacciones {#sec:appendix02} :::{style="margin-top: 20px; font-size: 20px;"} ```{r ap2-sol, prompt=FALSE, echo=TRUE, eval=TRUE} # Receta con interacciones receta_interact <- recipe(satisfaccion_vida ~ edad + educacion_anios + ingreso_hogar + zona + genero + confianza_gobierno + confianza_justicia + satisfaccion_democracia + percepcion_economia + uso_internet + interes_politica, data = datos_train) |> step_dummy(all_nominal_predictors()) |> step_interact(terms = ~ edad:educacion_anios) |> step_normalize(all_numeric_predictors()) |> step_zv(all_predictors()) # Ajustar LASSO con la receta de interacciones wf_lasso_interact <- workflow() |> add_recipe(receta_interact) |> add_model(modelo_lasso) resultados_interact <- tune_grid( wf_lasso_interact, resamples = folds, grid = grilla_lambda, metrics = metric_set(rmse) ) # Mejor lambda y modelo final mejor_interact <- select_best(resultados_interact, metric = "rmse") ajuste_interact <- finalize_workflow(wf_lasso_interact, mejor_interact) |> fit(data = datos_train) # ¿La interaccion sobrevive? tidy(ajuste_interact) |> filter(term != "(Intercept)") |> arrange(desc(abs(estimate))) ``` - Si la interacción tiene [coeficiente distinto de cero]{.alert}, LASSO la considera útil - La mejora en RMSE suele ser pequeña porque la relación edad-educación ya se captura parcialmente por separado - Agregar interacciones aumenta el número de predictores, lo que da más trabajo al regularizador ::: ## Apéndice 3: Países {#sec:appendix03} :::{style="margin-top: 20px; font-size: 20px;"} ```{r ap3-sol, prompt=FALSE, echo=TRUE, eval=TRUE} # Receta que incluye pais receta_pais <- recipe(satisfaccion_vida ~ ., data = datos_train) |> step_rm(voto) |> step_dummy(all_nominal_predictors()) |> step_normalize(all_numeric_predictors()) |> step_zv(all_predictors()) # Ajustar LASSO wf_lasso_pais <- workflow() |> add_recipe(receta_pais) |> add_model(modelo_lasso) resultados_pais <- tune_grid( wf_lasso_pais, resamples = folds, grid = grilla_lambda, metrics = metric_set(rmse) ) mejor_pais <- select_best(resultados_pais, metric = "rmse") ajuste_pais <- finalize_workflow(wf_lasso_pais, mejor_pais) |> fit(data = datos_train) # ¿Qué países sobreviven? coef_pais <- tidy(ajuste_pais) |> filter(str_detect(term, "pais"), estimate != 0) |> arrange(desc(abs(estimate))) cat("Países con coeficiente distinto de cero:", nrow(coef_pais), "\n") coef_pais ``` - LASSO [selecciona automáticamente]{.alert} los países que aportan información predictiva - Con muchas dummies de país, la regularización es más útil que en el modelo base - Los países con coeficientes positivos tienen mayor satisfacción (controlando por las otras variables) ::: ## Apéndice 4: Elastic Net {#sec:appendix04} :::{style="margin-top: 20px; font-size: 18px;"} ```{r ap4-enet, prompt=FALSE, echo=TRUE, eval=TRUE, cache=TRUE} # Elastic Net: combina L1 (LASSO) y L2 (Ridge) # Ajustamos penalty y mixture simultáneamente 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: 15 lambdas x 5 valores de mixture grilla_enet <- grid_regular( penalty(range = c(-4, 0)), mixture(range = c(0, 1)), levels = c(15, 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 # Ajustar modelo final ajuste_enet <- finalize_workflow(wf_enet, mejor_enet) |> fit(data = datos_train) pred_enet <- predict(ajuste_enet, datos_test) |> bind_cols(datos_test |> select(satisfaccion_vida)) pred_enet |> metrics(truth = satisfaccion_vida, estimate = .pred) ``` - Elastic Net se sitúa entre LASSO (mixture = 1) y Ridge (mixture = 0) - Útil cuando hay [grupos de predictores correlacionados]{.alert}: LASSO tiende a elegir solo uno, Ridge los encoge a todos, Elastic Net captura un balance - En este dataset el RMSE queda casi idéntico al de LASSO; el método brilla con dimensiones mucho más altas ::: ## Apéndice 5: Random Forest para regresión {#sec:appendix05} :::{style="margin-top: 20px; font-size: 18px;"} ```{r ap5-rf, prompt=FALSE, echo=TRUE, eval=TRUE, cache=TRUE} # Random Forest para regresión (mode = "regression") modelo_rf <- rand_forest( trees = 500, mtry = tune(), min_n = tune() ) |> set_engine("ranger") |> set_mode("regression") wf_rf <- workflow() |> add_recipe(receta) |> add_model(modelo_rf) grilla_rf <- grid_regular( mtry(range = c(2, 8)), min_n(range = c(5, 20)), levels = c(4, 4) ) resultados_rf <- tune_grid( wf_rf, resamples = folds, grid = grilla_rf, metrics = metric_set(rmse) ) mejor_rf <- select_best(resultados_rf, metric = "rmse") ajuste_rf <- finalize_workflow(wf_rf, mejor_rf) |> fit(data = datos_train) pred_rf <- predict(ajuste_rf, datos_test) |> bind_cols(datos_test |> select(satisfaccion_vida)) metricas_rf <- pred_rf |> metrics(truth = satisfaccion_vida, estimate = .pred) metricas_rf ``` - RF [reduce el RMSE]{.alert} unas décimas frente a los modelos lineales - El costo es interpretabilidad: sin coeficientes, hay que usar VIP y PDP - En ciencias sociales, pesa la pregunta: ¿importa entender los coeficientes o predecir lo mejor posible? ::: # Fin del día 2! 🤓 {background-color="#2d4563"}