library(tidymodels)
library(tidyverse)
library(glmnet)
datos <- read_csv("datos/desempeno_escuelas.csv", show_col_types = FALSE)
datos <- datos |>
mutate(
departamento = factor(departamento),
sector = factor(sector),
zona = factor(zona)
)
set.seed(2026)Tarea 4: Regresión y regularización – Respuestas
IA para Científicos Sociales - UCU
1 Instrucciones
Esta es la clave de respuestas de la Tarea 4. Cada pregunta incluye el código R completo y una respuesta escrita.
1.1 Configuración
2 Exploración
2.1 Pregunta 1: Resumen del dataset
Calculen la media y desviación estándar de puntaje_prueba por departamento. ¿Las diferencias entre departamentos son grandes comparadas con la variación dentro de cada uno?
datos |>
group_by(departamento) |>
summarise(
escuelas = n(),
media = round(mean(puntaje_prueba), 1),
sd = round(sd(puntaje_prueba), 1)
) |>
arrange(desc(media))# A tibble: 8 × 4
departamento escuelas media sd
<fct> <int> <dbl> <dbl>
1 Colonia 50 529. 42.4
2 Montevideo 50 529. 43.6
3 Paysandú 62 523. 48.8
4 Maldonado 45 522. 42.3
5 Canelones 53 520. 38.7
6 Rivera 50 518. 48.4
7 Salto 55 514. 46.1
8 Tacuarembó 35 510. 38.2
Respuesta: Colonia y Montevideo encabezan el ranking (~529 puntos) y Tacuarembó queda último (~510). Pero la diferencia entre el mejor y el peor departamento (~19 puntos) es menos de la mitad de la desviación estándar dentro de cualquier departamento (~38-49 puntos). La variación importante está dentro de los departamentos, entre escuelas, no entre departamentos. Esto anticipa que las dummies de departamento van a aportar poco al modelo (y de hecho LASSO las elimina casi todas en la Pregunta 9).
2.2 Pregunta 2: Correlaciones con la variable objetivo
Calculen la correlación entre puntaje_prueba y todas las variables numéricas. ¿Cuáles dos tienen la mayor correlación? ¿Algún signo sorprende?
datos |>
select(where(is.numeric)) |>
cor() |>
as.data.frame() |>
select(cor_con_puntaje = puntaje_prueba) |>
rownames_to_column("variable") |>
filter(variable != "puntaje_prueba") |>
arrange(desc(abs(cor_con_puntaje))) |>
mutate(cor_con_puntaje = round(cor_con_puntaje, 3)) variable cor_con_puntaje
1 nivel_socioeconomico 0.718
2 presupuesto_alumno 0.549
3 asistencia_pct 0.472
4 computadoras_alumno 0.285
5 tamano_clase -0.242
6 experiencia_docente 0.171
7 formacion_docente_pct 0.027
8 horas_semanales -0.026
Respuesta: Las dos correlaciones más fuertes son nivel_socioeconomico (~0,72) y presupuesto_alumno (~0,55), seguidas de asistencia_pct (~0,47). Ningún signo sorprende demasiado: tamano_clase es negativa (clases más grandes, peores puntajes) y horas_semanales es prácticamente cero, un primer indicio de que esa variable no aporta nada. Vale la pena notar que computadoras_alumno correlaciona ~0,29 con el puntaje: parece un predictor razonable, y sin embargo LASSO la va a eliminar (Pregunta 9). La correlación bivariada y la utilidad dentro de un modelo multivariado son cosas distintas.
2.3 Pregunta 3: Distribución por sector
Creen un boxplot de puntaje_prueba por sector. ¿La diferencia bruta prueba que las privadas “enseñan mejor”?
ggplot(datos, aes(x = sector, y = puntaje_prueba, fill = sector)) +
geom_boxplot(show.legend = FALSE) +
labs(title = "Puntaje en la prueba por sector",
x = "Sector", y = "Puntaje promedio de la escuela") +
theme_minimal()
datos |>
group_by(sector) |>
summarise(media = round(mean(puntaje_prueba), 1))# A tibble: 2 × 2
sector media
<fct> <dbl>
1 privado 545
2 publico 512.
Respuesta: Las escuelas privadas promedian ~545 puntos y las públicas ~512: una brecha bruta de ~33 puntos. Pero esa diferencia no prueba que las privadas enseñen mejor: las privadas también tienen alumnos de mayor nivel socioeconómico, más presupuesto por alumno y clases más chicas. La comparación bruta mezcla el “efecto escuela” con el efecto de a quién recibe cada escuela (sesgo de selección). De hecho, cuando el modelo multivariado controla por esas variables (Pregunta 5), la dummy de sector pierde casi toda su importancia. Volvemos sobre esto en la Pregunta 13.
3 Modelo baseline
3.1 Pregunta 4: División de datos y receta
Dividan 80/20 con set.seed(2026) y creen la receta (sin escuela, dummies, normalización, varianza cero). ¿Cuántos predictores quedan?
set.seed(2026)
datos_split <- initial_split(datos, prop = 0.8)
datos_train <- training(datos_split)
datos_test <- testing(datos_split)
receta <- recipe(puntaje_prueba ~ ., data = datos_train) |>
step_rm(escuela) |>
step_dummy(all_nominal_predictors()) |>
step_normalize(all_numeric_predictors()) |>
step_zv(all_predictors())
cat("Predictores en la receta preparada:",
ncol(juice(prep(receta))) - 1, "\n")Predictores en la receta preparada: 17
cat("Entrenamiento:", nrow(datos_train), "| Prueba:", nrow(datos_test), "\n")Entrenamiento: 320 | Prueba: 80
Respuesta: Quedan 17 predictores: los 8 numéricos originales más las dummies de departamento (7), sector (1) y zona (1). El identificador escuela sale con step_rm(): dejarlo entrar como predictor sería un error clásico (400 valores únicos sin ninguna información generalizable). La normalización es necesaria porque LASSO y Ridge penalizan el tamaño de los coeficientes: sin escalar, una variable medida en pesos (presupuesto) recibiría una penalización injustamente distinta que una medida en porcentaje. Una nota: acá no estratificamos la división; con un outcome continuo también se puede, como hicimos en el laboratorio (strata = puntaje_prueba lo divide por cuartiles).
3.2 Pregunta 5: Modelo OLS
Ajusten OLS con la receta de la pregunta 4. ¿Cuál es el predictor más importante? ¿El signo de tamano_clase va en la dirección esperada?
modelo_ols <- linear_reg() |>
set_engine("lm")
ajuste_ols <- workflow() |>
add_recipe(receta) |>
add_model(modelo_ols) |>
fit(data = datos_train)
tidy(ajuste_ols) |>
filter(term != "(Intercept)") |>
arrange(desc(abs(estimate))) |>
mutate(across(where(is.numeric), \(x) round(x, 2)))# A tibble: 17 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 nivel_socioeconomico 20.6 2.02 10.2 0
2 presupuesto_alumno 12.3 2.46 4.98 0
3 asistencia_pct 11.2 1.62 6.94 0
4 experiencia_docente 7.69 1.51 5.1 0
5 tamano_clase -7.51 1.59 -4.72 0
6 departamento_Colonia 2.81 1.91 1.47 0.14
7 sector_publico 2.77 1.85 1.5 0.14
8 computadoras_alumno -2.46 1.95 -1.26 0.21
9 departamento_Montevideo 2.01 1.89 1.06 0.29
10 zona_urbana -1.13 1.56 -0.73 0.47
11 departamento_Salto -0.84 1.93 -0.43 0.67
12 departamento_Tacuarembó 0.67 1.81 0.37 0.71
13 departamento_Paysandú -0.61 1.94 -0.32 0.75
14 departamento_Maldonado 0.59 1.89 0.31 0.76
15 departamento_Rivera -0.51 1.93 -0.27 0.79
16 horas_semanales 0.17 1.52 0.11 0.91
17 formacion_docente_pct 0.07 1.52 0.05 0.96
Respuesta: Como los predictores están normalizados, los coeficientes son comparables: el más importante por lejos es nivel_socioeconomico (~+21 puntos por desviación estándar), seguido por presupuesto_alumno (~+12) y asistencia_pct (~+11). tamano_clase tiene coeficiente negativo (~-7,5), exactamente la dirección esperada: clases más grandes se asocian con peores puntajes, manteniendo lo demás constante. Noten que las dummies de departamento y la de sector quedan al fondo de la tabla, con coeficientes chicos y no significativos: una vez que controlamos por la composición de la escuela, ni el departamento ni el sector agregan mucho.
3.3 Pregunta 6: Evaluar OLS
Calculen RMSE, R² y MAE en el conjunto de prueba. ¿El RMSE es grande o chico en la escala de la prueba?
pred_ols <- augment(ajuste_ols, datos_test)
metricas_ols <- pred_ols |>
metrics(truth = puntaje_prueba, estimate = .pred)
metricas_ols |>
mutate(.estimate = round(.estimate, 3))# A tibble: 3 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 26.7
2 rsq standard 0.681
3 mae standard 21.7
Respuesta: El modelo explica ~68% de la variación en los puntajes (R² ≈ 0,68), con un RMSE de ~27 puntos. En una escala tipo PISA (media ~520, desviación estándar ~43), errar por 27 puntos en promedio es un error moderado: suficiente para distinguir escuelas muy buenas de muy malas, pero no para rankear con precisión escuelas parecidas. Es una buena referencia para juzgar los modelos regularizados que vienen: ya sabemos que el techo predictivo de estos datos anda cerca de R² ≈ 0,7.
4 LASSO
4.1 Pregunta 7: Tuning de LASSO
Definan LASSO con penalty = tune(), grilla de 40 lambdas entre 10^-3 y 10^0.5, y CV con 10 folds. ¿Cuál es el mejor lambda?
modelo_lasso <- linear_reg(penalty = tune(), mixture = 1) |>
set_engine("glmnet")
wf_lasso <- workflow() |>
add_recipe(receta) |>
add_model(modelo_lasso)
grilla_lambda <- grid_regular(penalty(range = c(-3, 0.5)), levels = 40)
set.seed(2026)
folds <- vfold_cv(datos_train, v = 10)
resultados_lasso <- tune_grid(
wf_lasso,
resamples = folds,
grid = grilla_lambda
)
show_best(resultados_lasso, metric = "rmse", n = 5) |>
mutate(across(where(is.numeric), \(x) round(x, 4)))# A tibble: 5 × 7
penalty .metric .estimator mean n std_err .config
<dbl> <chr> <chr> <dbl> <dbl> <dbl> <chr>
1 1.38 rmse standard 26.6 10 1.46 pre0_mod36_post0
2 1.13 rmse standard 26.6 10 1.47 pre0_mod35_post0
3 1.70 rmse standard 26.6 10 1.44 pre0_mod37_post0
4 0.915 rmse standard 26.6 10 1.47 pre0_mod34_post0
5 0.744 rmse standard 26.7 10 1.47 pre0_mod33_post0
Respuesta: El mejor lambda según RMSE es ~1,4. Vale la pena mirar la tabla: hay varios lambdas vecinos con RMSE casi idéntico, lo que es típico. La curva de RMSE contra lambda suele ser plana cerca del mínimo, así que la elección exacta importa menos que el orden de magnitud. Lo que sí castiga fuerte el RMSE son los lambdas muy grandes, que aplastan todos los coeficientes hacia cero.
4.2 Pregunta 8: Lambda mínimo vs. 1SE
Comparen select_best() con select_by_one_std_err(). ¿Cuál lambda es más grande? ¿Qué implica para la complejidad del modelo?
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:", round(lambda_min$penalty, 4), "\n")Lambda mínimo: 1.3836
cat("Lambda 1SE:", round(lambda_1se$penalty, 4), "\n")Lambda 1SE: 3.1623
cat("Razón 1SE / mínimo:",
round(lambda_1se$penalty / lambda_min$penalty, 1), "\n")Razón 1SE / mínimo: 2.3
Respuesta: El lambda 1SE (~3,2) es algo más del doble que el lambda mínimo (~1,4). La lógica de la regla 1SE: si varios lambdas rinden estadísticamente igual (dentro de un error estándar del mínimo), conviene elegir el más grande, porque penaliza más y produce un modelo más simple, con más coeficientes en cero. Es la versión regularizada del principio de parsimonia: ante igual desempeño, el modelo con menos variables. El costo es aceptar un RMSE nominalmente un poco peor a cambio de un modelo más interpretable y robusto.
4.3 Pregunta 9: Variables eliminadas por LASSO
Ajusten el LASSO final con el lambda mínimo. ¿Cuántos predictores quedan en cero? ¿Por qué caen computadoras_alumno y horas_semanales?
ajuste_lasso <- finalize_workflow(wf_lasso, lambda_min) |>
fit(data = datos_train)
coefs_lasso <- tidy(ajuste_lasso) |>
filter(term != "(Intercept)")
# Variables eliminadas (coeficiente exactamente cero)
coefs_lasso |>
filter(estimate == 0) |>
pull(term) [1] "computadoras_alumno" "horas_semanales"
[3] "formacion_docente_pct" "departamento_Maldonado"
[5] "departamento_Paysandú" "departamento_Rivera"
[7] "departamento_Salto" "departamento_Tacuarembó"
[9] "sector_publico" "zona_urbana"
cat("\nEliminadas:", sum(coefs_lasso$estimate == 0), "de",
nrow(coefs_lasso), "predictores\n")
Eliminadas: 10 de 17 predictores
Respuesta: Con el lambda mínimo, LASSO elimina 10 de los 17 predictores: computadoras_alumno, horas_semanales, formacion_docente_pct y casi todas las dummies de departamento, sector y zona. El caso de computadoras_alumno es el más instructivo: correlaciona ~0,29 con el puntaje (Pregunta 2), pero esa correlación existe solo porque las computadoras van donde va el presupuesto. Una vez que presupuesto_alumno está en el modelo, las computadoras no agregan información independiente y LASSO las pone en cero. horas_semanales cae por la razón opuesta: nunca tuvo relación con el puntaje. LASSO no distingue entre “redundante” e “irrelevante”: descarta lo que no ayuda a predecir, por dos caminos distintos.
5 Ridge y Elastic Net
5.1 Pregunta 10: Comparar coeficientes Ridge vs. LASSO
Ajusten Ridge con la misma grilla y folds. ¿Cuántos coeficientes deja en cero cada método?
modelo_ridge <- linear_reg(penalty = tune(), mixture = 0) |>
set_engine("glmnet")
wf_ridge <- workflow() |>
add_recipe(receta) |>
add_model(modelo_ridge)
resultados_ridge <- tune_grid(
wf_ridge,
resamples = folds,
grid = grilla_lambda
)
ajuste_ridge <- finalize_workflow(
wf_ridge,
select_best(resultados_ridge, metric = "rmse")
) |>
fit(data = datos_train)
coefs_ridge <- tidy(ajuste_ridge) |>
filter(term != "(Intercept)")
cat("Coeficientes en cero - LASSO:", sum(coefs_lasso$estimate == 0), "\n")Coeficientes en cero - LASSO: 10
cat("Coeficientes en cero - Ridge:", sum(coefs_ridge$estimate == 0), "\n")Coeficientes en cero - Ridge: 0
# Qué hace Ridge con las variables que LASSO eliminó
coefs_lasso |>
select(term, lasso = estimate) |>
left_join(coefs_ridge |> select(term, ridge = estimate), by = "term") |>
filter(lasso == 0) |>
mutate(across(where(is.numeric), \(x) round(x, 2)))# A tibble: 10 × 3
term lasso ridge
<chr> <dbl> <dbl>
1 computadoras_alumno 0 -1.41
2 horas_semanales 0 0.08
3 formacion_docente_pct 0 0.09
4 departamento_Maldonado 0 0.4
5 departamento_Paysandú 0 -0.68
6 departamento_Rivera 0 -0.67
7 departamento_Salto 0 -0.91
8 departamento_Tacuarembó 0 0.35
9 sector_publico 0 1.68
10 zona_urbana 0 -0.53
Respuesta: LASSO deja 10 coeficientes exactamente en cero; Ridge, ninguno. Es la diferencia de diseño entre las dos penalizaciones: la L1 de LASSO puede llevar coeficientes hasta el cero exacto (selección de variables), mientras que la L2 de Ridge los encoge hacia cero pero nunca los anula. En la tabla se ve bien: las variables que LASSO eliminó sobreviven en Ridge con coeficientes chicos (en general menores que ±2). Ridge “reparte” la señal entre predictores correlacionados; LASSO elige uno y descarta el resto.
5.2 Pregunta 11: Elastic Net con dos hiperparámetros
Tuneen penalty y mixture (15 × 5). ¿La mejor combinación se acerca a LASSO o a Ridge?
modelo_en <- linear_reg(penalty = tune(), mixture = tune()) |>
set_engine("glmnet")
wf_en <- workflow() |>
add_recipe(receta) |>
add_model(modelo_en)
grilla_en <- grid_regular(
penalty(range = c(-3, 0.5)),
mixture(),
levels = c(penalty = 15, mixture = 5)
)
resultados_en <- tune_grid(
wf_en,
resamples = folds,
grid = grilla_en
)
select_best(resultados_en, metric = "rmse") |>
mutate(across(where(is.numeric), \(x) round(x, 4)))# A tibble: 1 × 3
penalty mixture .config
<dbl> <dbl> <chr>
1 1.78 0.75 pre0_mod69_post0
Respuesta: La mejor combinación tiene mixture = 0,75: una mezcla que se inclina fuerte hacia LASSO (L1) pero conserva algo de Ridge (L2). Elastic Net tenía permitido elegir cualquier punto entre Ridge (0) y LASSO (1), y la validación cruzada se quedó cerca del extremo LASSO. Tiene sentido dada la estructura del dataset: hay un grupo claro de predictores irrelevantes o redundantes (los de la Pregunta 9) que conviene encoger con fuerza, y el componente L1 dominante hace casi todo el trabajo de selección. La lección general: Elastic Net es un buen punto de partida porque contiene a Ridge y LASSO como casos particulares (mixture 0 y 1) y deja que los datos elijan la mezcla.
6 Comparación
6.1 Pregunta 12: Tabla comparativa
Comparen OLS, LASSO, Ridge y Elastic Net en el conjunto de prueba.
ajuste_en <- finalize_workflow(
wf_en,
select_best(resultados_en, metric = "rmse")
) |>
fit(data = datos_train)
bind_rows(
augment(ajuste_ols, datos_test) |>
metrics(truth = puntaje_prueba, estimate = .pred) |>
mutate(modelo = "OLS"),
augment(ajuste_lasso, datos_test) |>
metrics(truth = puntaje_prueba, estimate = .pred) |>
mutate(modelo = "LASSO"),
augment(ajuste_ridge, datos_test) |>
metrics(truth = puntaje_prueba, estimate = .pred) |>
mutate(modelo = "Ridge"),
augment(ajuste_en, datos_test) |>
metrics(truth = puntaje_prueba, estimate = .pred) |>
mutate(modelo = "Elastic Net")
) |>
select(modelo, .metric, .estimate) |>
pivot_wider(names_from = .metric, values_from = .estimate) |>
arrange(rmse) |>
mutate(across(where(is.numeric), \(x) round(x, 2)))# A tibble: 4 × 4
modelo rmse rsq mae
<chr> <dbl> <dbl> <dbl>
1 OLS 26.7 0.68 21.8
2 LASSO 27.0 0.68 22.0
3 Elastic Net 27.1 0.68 22.0
4 Ridge 27.2 0.68 22.1
Respuesta: La tabla queda apretadísima: el mejor RMSE es el de OLS (~26,7), con LASSO, Elastic Net y Ridge a apenas ~0,3-0,5 puntos. En términos prácticos, la diferencia es irrelevante: una fracción de punto de RMSE, en una escala donde el error ronda los 27 puntos, no cambia ninguna decisión. ¿Por qué no gana nadie? Porque con 320 escuelas de entrenamiento y solo 17 predictores (n mucho mayor que p) y relaciones aproximadamente lineales, OLS no sufre el sobreajuste que la regularización viene a curar. La regularización brilla con muchos predictores, pocos datos o fuerte colinealidad; acá su valor no es predecir mejor sino producir un modelo más simple (LASSO rinde igual que OLS usando 7 variables en vez de 17).
6.2 Pregunta 13: Reflexión
Respuesta:
- No, en este dataset la regularización casi no mejora a OLS (Pregunta 12), porque hay muchas más observaciones que predictores y la relación es básicamente lineal. Lo que sí aporta LASSO es parsimonia: el mismo desempeño con menos de la mitad de las variables. La regularización no es magia predictiva, es un seguro contra el sobreajuste, y acá había poco sobreajuste que asegurar
- LASSO conviene cuando se sospecha que muchos predictores son irrelevantes y se quiere un modelo interpretable con selección automática de variables (como acá). Ridge conviene cuando hay muchos predictores correlacionados que aportan señal de a poco (por ejemplo, miles de píxeles o de genes), donde anular variables de a una destruiría información; Ridge la reparte en vez de elegir
- El problema es confundir predicción con causalidad. El modelo dice qué escuelas tendrán puntajes bajos, no qué pasaría si les mandamos presupuesto. La Pregunta 3 lo ilustra: el sector privado “predice” buen puntaje, pero no porque enseñe mejor sino por a quién recibe. Si el ministerio asigna presupuesto según las predicciones, premia o castiga a las escuelas por la composición de su alumnado, no por su trabajo. Para decidir intervenciones hace falta un diseño causal (experimentos, regresión discontinua), no un buen R²