Análisis Avanzado de Datos — 1. Regresión y regularización (Ridge y Lasso)

estadistica
regularizacion
ridge-lasso
Por qué la regresión lineal se queda corta ante el sobreajuste, y cómo Ridge (L2) y Lasso (L1) equilibran sesgo y varianza, con comparación visual entre los tres enfoques.
Author

Wilder Ramírez Delgado

Published

August 26, 2026

Análisis Avanzado de Datos — 1. Regresión y regularización (Ridge y Lasso)

Open in Colab

👋 Sobre el autor

Wilder Ramírez Delgado es Científico de Datos, Arquitecto de IA, Ingeniero Electrónico y Magíster en Analítica de Datos. CEO y fundador de Business Innovation Technology (BIT), consultor y docente universitario, trabaja en la intersección entre Data Science, Inteligencia Artificial, Big Data e IoT, transformando problemas reales en soluciones aplicadas.

De la teoría a la práctica, un problema a la vez.

¿Por qué la regresión lineal se queda corta?

La regresión lineal es, probablemente, el primer modelo que aprendiste en ciencia de datos. Es simple, interpretable y funciona sorprendentemente bien en muchos problemas reales. Pero tiene puntos débiles que se vuelven críticos en la práctica:

  • Cuando hay muchas variables predictoras, algunas correlacionadas entre sí (multicolinealidad), los coeficientes se vuelven inestables: pequeños cambios en los datos producen coeficientes muy distintos.
  • Cuando el modelo tiene más flexibilidad de la necesaria (muchas variables, o términos polinomiales), tiende a memorizar el ruido del conjunto de entrenamiento en lugar de aprender el patrón real — esto se llama sobreajuste (overfitting).
  • No tienes forma de decirle al modelo qué variables no aportan, para que las ignore.

La solución a estos tres problemas tiene un nombre: regularización. En este artículo vas a construir la intuición completa, con código ejecutable de principio a fin, sobre las dos formas de regularización más usadas en regresión: Ridge y Lasso.

Repaso: regresión lineal múltiple

La regresión lineal múltiple modela una variable de respuesta \(y\) como una combinación lineal de \(p\) variables predictoras:

\[\hat{y} = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \dots + \beta_p x_p\]

Los coeficientes \(\beta_0, \beta_1, \dots, \beta_p\) se estiman minimizando la suma de errores al cuadrado (mínimos cuadrados ordinarios, OLS):

\[\min_{\beta} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2\]

Para repasarla, vas a usar un dataset real: precios de vivienda en California, disponible directamente en scikit-learn. Cada fila es un bloque censal, con variables como ingreso medio, antigüedad de las viviendas, número de habitaciones, población y ubicación geográfica; el objetivo es predecir el precio medio de la vivienda.

import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing

# Dataset real: precios de vivienda en California (incluido en scikit-learn)
housing = fetch_california_housing(as_frame=True)
X = housing.data
y = housing.target  # precio medio de vivienda, en cientos de miles de USD

print(f"Observaciones: {X.shape[0]}")
print(f"Variables predictoras: {list(X.columns)}")
X.head()
Observaciones: 20640
Variables predictoras: ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude']
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude
0 8.3252 41.0 6.984127 1.023810 322.0 2.555556 37.88 -122.23
1 8.3014 21.0 6.238137 0.971880 2401.0 2.109842 37.86 -122.22
2 7.2574 52.0 8.288136 1.073446 496.0 2.802260 37.85 -122.24
3 5.6431 52.0 5.817352 1.073059 558.0 2.547945 37.85 -122.25
4 3.8462 52.0 6.281853 1.081081 565.0 2.181467 37.85 -122.25

Para evaluar el modelo de forma honesta, separa los datos en un conjunto de entrenamiento y uno de prueba: ajusta el modelo solo con el primero y evalúalo con el segundo, que nunca vio durante el entrenamiento.

import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

housing = fetch_california_housing(as_frame=True)
X = housing.data
y = housing.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

modelo_lineal = LinearRegression()
modelo_lineal.fit(X_train, y_train)
y_pred = modelo_lineal.predict(X_test)

print(f"R² en test:   {r2_score(y_test, y_pred):.3f}")
print(f"RMSE en test: {np.sqrt(mean_squared_error(y_test, y_pred)):.3f}")

coeficientes = pd.Series(modelo_lineal.coef_, index=X.columns).sort_values()
print("\nCoeficientes del modelo:")
print(coeficientes)

print(f"\nCorrelación AveRooms-AveBedrms: {X['AveRooms'].corr(X['AveBedrms']):.2f}")
R² en test:   0.576
RMSE en test: 0.746

Coeficientes del modelo:
Longitude    -0.433708
Latitude     -0.419792
AveRooms     -0.123323
AveOccup     -0.003526
Population   -0.000002
HouseAge      0.009724
MedInc        0.448675
AveBedrms     0.783145
dtype: float64

Correlación AveRooms-AveBedrms: 0.85

Lectura del modelo sin regularizar

  • El modelo explica una parte razonable de la varianza del precio: R²=0.576 (RMSE=0.746, en cientos de miles de USD) — nada mal para un puñado de variables socioeconómicas y geográficas simples.
  • AveRooms y AveBedrms están correlacionadas 0.85 entre sí, y eso se nota directamente en los coeficientes impresos arriba: AveRooms sale con -0.12 (¿más habitaciones promedio, precio más bajo?) mientras AveBedrms sale con +0.78 — contraintuitivo si se leen por separado.
  • Ese par de signos opuestos no refleja una relación causal real: es el síntoma clásico de multicolinealidad, donde el modelo “reparte” de forma inestable el mismo efecto real entre dos variables que se mueven casi juntas.

El problema del sobreajuste y el compromiso sesgo-varianza

Para ver el sobreajuste con claridad, aléjate por un momento del dataset de 8 variables y mira un ejemplo con una sola variable, donde puedas graficar el ajuste directamente.

Imagina que la relación real entre \(x\) y \(y\) es una curva suave, pero solo tienes observaciones con ruido. Si ajustas:

  • Un modelo demasiado simple (una recta) va a subajustar (underfitting): no captura la curva real. Esto es alto sesgo.
  • Un modelo demasiado flexible (un polinomio de grado muy alto) va a memorizar el ruido de los puntos de entrenamiento. Esto es alta varianza: el modelo cambia mucho si cambian ligeramente los datos.

El objetivo es encontrar el punto intermedio donde el error en datos nuevos (no vistos durante el entrenamiento) es mínimo.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.pipeline import make_pipeline

# Datos sintéticos: una curva real + ruido
rng = np.random.default_rng(42)
x = np.sort(rng.uniform(0, 10, 60))
y_real = np.sin(x) * 3 + 0.3 * x
y = y_real + rng.normal(scale=1.0, size=x.shape)

X = x.reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

grados = range(1, 16)
error_train, error_test = [], []

for grado in grados:
    # Escalar x antes de generar las potencias evita que las columnas de alto grado
    # exploten en magnitud y hagan inestable la solución de mínimos cuadrados.
    modelo = make_pipeline(StandardScaler(), PolynomialFeatures(grado), LinearRegression())
    modelo.fit(X_train, y_train)
    error_train.append(mean_squared_error(y_train, modelo.predict(X_train)))
    error_test.append(mean_squared_error(y_test, modelo.predict(X_test)))

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.plot(list(grados), error_train, marker="o", label="Error de entrenamiento")
ax.plot(list(grados), error_test, marker="o", label="Error de prueba")
ax.set_xlabel("Grado del polinomio (complejidad del modelo)")
ax.set_ylabel("Error cuadrático medio")
ax.set_title("Compromiso sesgo-varianza: complejidad vs error")
ax.legend()
ax.grid(alpha=0.3)
plt.show()

grado_optimo = list(grados)[int(np.argmin(error_test))]
print(f"Grado con menor error de prueba: {grado_optimo} (MSE_test={min(error_test):.2f}, MSE_train={error_train[np.argmin(error_test)]:.2f})")
print(f"MSE de prueba en grado 12: {error_test[11]:.2f}")
print(f"MSE de entrenamiento en grado 15: {error_train[-1]:.2f} (el más bajo de toda la curva)")

Grado con menor error de prueba: 4 (MSE_test=0.50, MSE_train=0.90)
MSE de prueba en grado 12: 73.09
MSE de entrenamiento en grado 15: 0.32 (el más bajo de toda la curva)

Lectura de la curva sesgo-varianza

  • El error de entrenamiento baja de forma monótona con la complejidad: pasa de 5.25 en grado 1 hasta 0.32 en grado 15 (el punto más bajo de toda la curva) — un polinomio de grado 15 tiene tanta flexibilidad que prácticamente memoriza los 42 puntos de entrenamiento.
  • El error de prueba cuenta otra historia: toca su mínimo en grado 4 (MSE=0.50, con MSE de entrenamiento de apenas 0.90 en ese mismo punto) y después empieza a subir. En grado 12 llega a 73.09 — casi 150 veces el mínimo alcanzado en grado 4.
  • Esa brecha entre “el entrenamiento sigue mejorando” y “la prueba se dispara” es la definición operativa de sobreajuste: pasado el grado 4, cada grado adicional solo ayuda a memorizar ruido del conjunto de entrenamiento, no a predecir mejor sobre datos nuevos.

Regularización Ridge (L2)

Ridge modifica la función de costo de la regresión lineal agregando una penalización proporcional a la suma de los coeficientes al cuadrado:

\[\min_{\beta} \sum_{i=1}^n (y_i - \hat y_i)^2 + \alpha \sum_{j=1}^p \beta_j^2\]

El hiperparámetro \(\alpha\) (alpha) controla la fuerza de la penalización:

  • \(\alpha = 0\) → Ridge es idéntico a la regresión lineal ordinaria.
  • \(\alpha \to \infty\) → todos los coeficientes se acercan a cero (el modelo se vuelve casi constante).

La intuición: Ridge encoge (shrinks) los coeficientes hacia cero, especialmente los de variables correlacionadas o poco informativas, sin llegar a eliminarlos por completo. Esto reduce la varianza del modelo a cambio de un poco de sesgo — justo el compromiso que viste arriba.

Para ver el efecto con claridad, genera un dataset sintético con variables informativas y variables que son puro ruido, y observa cómo cambian los coeficientes a medida que aumenta \(\alpha\).

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

# Dataset sintético: 10 variables, solo 4 realmente informativas
X, y, coef_real = make_regression(
    n_samples=200,
    n_features=10,
    n_informative=4,
    noise=10.0,
    coef=True,
    random_state=42,
)
X = StandardScaler().fit_transform(X)  # estandarizar es clave para regularizar

alphas = np.logspace(-2, 4, 50)
coeficientes_ridge = []

for alpha in alphas:
    modelo = Ridge(alpha=alpha)
    modelo.fit(X, y)
    coeficientes_ridge.append(modelo.coef_)

coeficientes_ridge = np.array(coeficientes_ridge)

fig, ax = plt.subplots(figsize=(7, 4.5))
for i in range(X.shape[1]):
    ax.plot(alphas, coeficientes_ridge[:, i], label=f"x{i+1}")
ax.set_xscale("log")
ax.set_xlabel("alpha (fuerza de regularización)")
ax.set_ylabel("Valor del coeficiente")
ax.set_title("Ridge: los coeficientes se encogen hacia cero, pero no lo alcanzan")
ax.legend(loc="upper right", fontsize=8, ncol=2)
ax.grid(alpha=0.3)
plt.show()

print(f"Variables realmente informativas (coeficiente ≠ 0 en la generación): {[f'x{i+1}' for i, c in enumerate(coef_real) if abs(c) > 0]}")
print(f"\nSuma de |coeficientes| en alpha={alphas[0]:.2f}: {np.sum(np.abs(coeficientes_ridge[0])):.1f}")
print(f"Suma de |coeficientes| en alpha={alphas[-1]:.0f}: {np.sum(np.abs(coeficientes_ridge[-1])):.1f}")
print(f"Coeficiente de x7 (el más grande, informativo) — alpha bajo: {coeficientes_ridge[0][6]:.2f}, alpha alto: {coeficientes_ridge[-1][6]:.2f}")

Variables realmente informativas (coeficiente ≠ 0 en la generación): ['x2', 'x5', 'x7', 'x10']

Suma de |coeficientes| en alpha=0.01: 91.7
Suma de |coeficientes| en alpha=10000: 2.1
Coeficiente de x7 (el más grande, informativo) — alpha bajo: 36.97, alpha alto: 0.80

Lectura del camino de regularización Ridge

  • Las variables realmente informativas (x2, x5, x7 y x10) son justo las que parten con los coeficientes más grandes en alpha bajo — el modelo sí está capturando la señal real antes de regularizar.
  • A medida que alpha crece de 0.01 a 10000, la suma de |coeficientes| cae de 91.7 a 2.1 — un encogimiento de más de 40 veces, y afecta a todas las variables, no solo a las irrelevantes.
  • x7 (la variable con más peso real) pasa de 36.97 a apenas 0.80: se encoge muchísimo, pero en ningún punto del camino llega a cero exacto — la firma que distingue a Ridge de Lasso.

Regularización Lasso (L1)

Lasso (Least Absolute Shrinkage and Selection Operator) usa una penalización distinta: la suma de los valores absolutos de los coeficientes.

\[\min_{\beta} \sum_{i=1}^n (y_i - \hat y_i)^2 + \alpha \sum_{j=1}^p |\beta_j|\]

La diferencia con Ridge parece sutil (cuadrado vs. valor absoluto), pero tiene una consecuencia geométrica importante: la región de penalización de Lasso tiene esquinas (sobre los ejes, donde algún coeficiente es exactamente cero), mientras que la de Ridge es una esfera suave. Eso hace que la solución óptima de Lasso “aterrice” con frecuencia exactamente sobre uno de esos ejes.

En la práctica, esto significa que Lasso no solo encoge coeficientes: los apaga por completo. Es, de facto, un método de selección automática de variables.

Usa el mismo dataset sintético de antes (10 variables, solo 4 informativas) para comprobarlo.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Lasso

X, y, coef_real = make_regression(
    n_samples=200,
    n_features=10,
    n_informative=4,
    noise=10.0,
    coef=True,
    random_state=42,
)
X = StandardScaler().fit_transform(X)

alphas = np.logspace(-2, 1.5, 50)
coeficientes_lasso = []

for alpha in alphas:
    modelo = Lasso(alpha=alpha, max_iter=10000)
    modelo.fit(X, y)
    coeficientes_lasso.append(modelo.coef_)

coeficientes_lasso = np.array(coeficientes_lasso)

fig, ax = plt.subplots(figsize=(7, 4.5))
for i in range(X.shape[1]):
    ax.plot(alphas, coeficientes_lasso[:, i], label=f"x{i+1}")
ax.set_xscale("log")
ax.set_xlabel("alpha (fuerza de regularización)")
ax.set_ylabel("Valor del coeficiente")
ax.set_title("Lasso: los coeficientes menos relevantes llegan exactamente a cero")
ax.legend(loc="upper right", fontsize=8, ncol=2)
ax.grid(alpha=0.3)
plt.show()

print("Variables informativas reales (coeficiente ≠ 0 en la generación):")
print([f"x{i+1}" for i, c in enumerate(coef_real) if abs(c) > 0])

Variables informativas reales (coeficiente ≠ 0 en la generación):
['x2', 'x5', 'x7', 'x10']
import numpy as np
from sklearn.datasets import make_regression
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Lasso

X, y, coef_real = make_regression(
    n_samples=200,
    n_features=10,
    n_informative=4,
    noise=10.0,
    coef=True,
    random_state=42,
)
X = StandardScaler().fit_transform(X)

modelo_lasso = Lasso(alpha=1.0, max_iter=10000)
modelo_lasso.fit(X, y)

no_cero = np.sum(modelo_lasso.coef_ != 0)
print(f"Variables con coeficiente distinto de cero: {no_cero} de {X.shape[1]}")
print(f"Variables realmente informativas: {np.sum(np.abs(coef_real) > 0)}")
Variables con coeficiente distinto de cero: 4 de 10
Variables realmente informativas: 4

Lectura de la selección de variables con Lasso

  • Con alpha=1.0, Lasso dejó exactamente 4 coeficientes distintos de cero de las 10 variables — y esas 4 son precisamente x2, x5, x7 y x10: las mismas 4 que realmente generaron la señal al construir el dataset sintético. Coincidencia perfecta entre lo que el modelo “eligió” y lo que era verdad.
  • Las 6 variables sin efecto real (x1, x3, x4, x6, x8, x9) quedaron en exactamente cero, no solo “chiquitas”: Lasso las apagó del todo.
  • En un problema real nunca vas a tener esta certeza (no conoces el coef_real de antemano), pero el mecanismo es el mismo: a un alpha suficientemente alto, Lasso deja activas solo las variables que más aportan y apaga el resto — de facto, selección automática de variables.

Comparación visual: sin regularizar vs. Ridge vs. Lasso

Ahora compara, lado a lado, los coeficientes que produce cada enfoque sobre el mismo dataset sintético, con un valor de \(\alpha\) fijo y razonable para cada uno.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso

X, y, coef_real = make_regression(
    n_samples=200,
    n_features=10,
    n_informative=4,
    noise=10.0,
    coef=True,
    random_state=42,
)
X = StandardScaler().fit_transform(X)

modelo_ols = LinearRegression().fit(X, y)
modelo_ridge = Ridge(alpha=10.0).fit(X, y)
modelo_lasso = Lasso(alpha=1.0, max_iter=10000).fit(X, y)

variables = [f"x{i+1}" for i in range(X.shape[1])]
ancho = 0.25
posiciones = np.arange(len(variables))

fig, ax = plt.subplots(figsize=(9, 5))
ax.bar(posiciones - ancho, modelo_ols.coef_, width=ancho, label="Sin regularizar (OLS)")
ax.bar(posiciones, modelo_ridge.coef_, width=ancho, label="Ridge (L2)")
ax.bar(posiciones + ancho, modelo_lasso.coef_, width=ancho, label="Lasso (L1)")
ax.set_xticks(posiciones)
ax.set_xticklabels(variables)
ax.set_ylabel("Valor del coeficiente")
ax.set_title("Coeficientes: OLS vs Ridge vs Lasso")
ax.axhline(0, color="black", linewidth=0.8)
ax.legend()
ax.grid(alpha=0.3, axis="y")
plt.show()

print(f"Coeficientes con |valor|>0.01 — OLS: {np.sum(np.abs(modelo_ols.coef_)>0.01)}, Ridge: {np.sum(np.abs(modelo_ridge.coef_)>0.01)}, Lasso: {np.sum(modelo_lasso.coef_ != 0)}")
print(f"Suma de |coeficientes|      — OLS: {np.sum(np.abs(modelo_ols.coef_)):.1f}, Ridge: {np.sum(np.abs(modelo_ridge.coef_)):.1f}, Lasso: {np.sum(np.abs(modelo_lasso.coef_)):.1f}")
print(f"Coeficiente de x4 (irrelevante, coef_real=0) — OLS: {modelo_ols.coef_[3]:.2f}, Ridge: {modelo_ridge.coef_[3]:.2f}, Lasso: {modelo_lasso.coef_[3]:.2f}")

Coeficientes con |valor|>0.01 — OLS: 10, Ridge: 10, Lasso: 4
Suma de |coeficientes|      — OLS: 91.7, Ridge: 87.6, Lasso: 83.9
Coeficiente de x4 (irrelevante, coef_real=0) — OLS: 1.10, Ridge: 1.05, Lasso: 0.00

Lectura de la comparación OLS vs. Ridge vs. Lasso

  • OLS asigna coeficiente distinto de cero a las 10 variables, incluidas las 6 que no tienen ningún efecto real. x4, por ejemplo, sale con +1.10 pese a que su coeficiente real es 0 — puro ruido que el modelo interpreta como señal.
  • Ridge (alpha=10) reduce apenas un poco la magnitud total (91.7 a 87.6 en suma de |coeficientes|) y sigue dejando las 10 variables activas: a x4 lo baja de 1.10 a 1.05, un recorte mínimo.
  • Lasso (alpha=1.0) es el único que produce un modelo esparso: solo 4 de 10 coeficientes quedan distintos de cero, y x4 queda en 0.00 exacto — apagado por completo, no solo reducido.

Cierre técnico: ¿cuándo usar cada una?

Situación Recomendación
Muchas variables correlacionadas entre sí (multicolinealidad) Ridge — distribuye el “peso” entre variables correlacionadas en vez de elegir una de forma arbitraria
Sospechas que muchas variables son irrelevantes y quieres un modelo más simple e interpretable Lasso — hace selección de variables automáticamente
Quieres lo mejor de ambos mundos (selección + manejo de correlación) Elastic Net — combina las penalizaciones L1 y L2 (no cubierto en este artículo, disponible en sklearn.linear_model.ElasticNet)
No hay señales de sobreajuste y el modelo lineal simple ya generaliza bien Regresión lineal ordinaria — no toda situación necesita regularización

En la práctica, el valor de \(\alpha\) no se elige a mano: se selecciona con validación cruzada (RidgeCV, LassoCV en scikit-learn), probando un rango de valores y quedándote con el que minimiza el error en datos de validación. Ese será tema de un próximo artículo de la serie, dedicado a evaluación de modelos y bootstrap.

Un último punto práctico: tanto Ridge como Lasso son sensibles a la escala de las variables — por eso, en todos los ejemplos de este artículo, se estandarizaron los datos con StandardScaler antes de ajustar el modelo. Si te saltas ese paso, las variables con escalas más grandes quedan penalizadas de forma desproporcionada respecto a las demás.

💬 ¿Te sirvió?

Deja en los comentarios una duda o un caso donde aplicarías esto — respondo todos. Sígueme para no perderte el próximo artículo de la serie y comparte con alguien que esté aprendiendo análisis de datos.

👉 El código completo está disponible para ejecutar directamente.