Saltar a contenido

Capítulo 2: Regresión — Predecir Números

Módulo 02 — MACHINE LEARNING · AI Master Academy

"La regresión es el arte de convertir datos históricos en números futuros. Domínala y podrás ponerle precio a casi cualquier cosa."


Índice

  1. ¿Qué es la regresión y qué problemas de negocio resuelve?
  2. Regresión lineal simple y múltiple con sklearn
  3. Métricas de regresión a fondo
  4. Regresión polinómica: capturar curvas
  5. Regularización: Ridge, Lasso y ElasticNet
  6. Otros regresores que debes conocer
  7. Ejemplo integrador: predicción de precios de viviendas
  8. Caso empresarial: predicción de tiempos de entrega en logística
  9. Buenas prácticas
  10. Malas prácticas
  11. Errores comunes
  12. FAQ — Preguntas frecuentes
  13. Resumen del capítulo
  14. Bibliografía y recursos

1. ¿Qué es la regresión y qué problemas de negocio resuelve?

1.1 Definición

La regresión es la familia de problemas de aprendizaje supervisado en la que el objetivo (la variable y) es un número continuo. No una categoría, no una etiqueta: un número que puede tomar (en teoría) infinitos valores.

  • ¿Cuánto costará esta vivienda? → 312.450 €
  • ¿Cuántas unidades venderemos mañana? → 1.847 unidades
  • ¿Cuánto tardará este pedido en llegar? → 2,3 días
  • ¿Cuánto gastará este cliente en los próximos 12 meses? → 540 €

Compáralo con la clasificación (capítulo 3), donde la pregunta es "¿a qué clase pertenece?" (spam/no spam, moroso/no moroso). La frontera práctica es sencilla:

Si la respuesta a tu pregunta es un número con decimales que tiene sentido ordenar y promediar, es regresión. Si es una etiqueta, es clasificación.

Nota

El nombre "regresión" viene de Francis Galton (siglo XIX), que estudiaba cómo la estatura de los hijos "regresaba hacia la media" respecto a la de sus padres. El nombre se quedó, aunque hoy signifique simplemente "predecir valores continuos".

1.2 El mapa mental del problema

flowchart TD
    A["Pregunta de negocio"] --> B{"¿La respuesta es<br/>un número continuo?"}
    B -- "Sí"--> C["REGRESIÓN<br/>(este capítulo)"]
    B -- "No, es una categoría"--> D["CLASIFICACIÓN<br/>(capítulo 3)"]
    C --> E{"¿Relación<br/>aproximadamente lineal?"}
    E -- "Sí"--> F["Lineal / Ridge / Lasso<br/>+ interpretable"]
    E -- "No"--> G["Polinómica / Árboles /<br/>Gradient Boosting"]
    C --> H{"¿Necesitas explicar<br/>cada coeficiente a negocio?"}
    H -- "Sí"--> F
    H -- "No, prioriza precisión"--> G

1.3 Problemas de negocio que resuelve la regresión

La regresión está detrás de una cantidad enorme de decisiones empresariales diarias. Cada vez que una empresa necesita anticipar una cantidad, hay (o debería haber) un modelo de regresión trabajando.

Sector Problema de negocio Variable objetivo (y) Features típicas (X) Impacto económico
Inmobiliario Tasación automática de viviendas Precio (€) m², habitaciones, zona, antigüedad Tasar en segundos vs. días; detectar chollos
Retail Previsión de demanda Unidades vendidas/día Histórico, precio, promociones, festivos Menos stock muerto y menos roturas de stock
Logística Tiempo estimado de entrega (ETA) Horas hasta entrega Distancia, tráfico, carga del almacén SLA cumplidos, clientes informados
Marketing Lifetime Value (LTV) de cliente € gastados en 12 meses Frecuencia de compra, ticket medio, canal Decidir cuánto invertir en captar cada cliente
Energía Consumo energético de edificios kWh/día Temperatura, ocupación, día de la semana Compra de energía optimizada, ahorro directo
Banca Importe de pérdida esperada (LGD) € perdidos en impago Colateral, historial, importe del préstamo Provisiones regulatorias más precisas
RRHH Salario de mercado de un puesto Salario anual (€) Experiencia, ciudad, sector, skills Ofertas competitivas sin sobrepagar
Manufactura Vida útil restante de maquinaria Horas hasta fallo Vibración, temperatura, horas de uso Mantenimiento predictivo, menos paradas

Caso empresarial

Una cadena de supermercados española redujo un 23% el desperdicio de productos frescos al sustituir la previsión "a ojo" del encargado por un modelo de regresión que predecía la demanda diaria por producto y tienda. El modelo no era sofisticado (un gradient boosting con 30 features), pero el proceso sí lo era: reentrenamiento semanal, monitorización del error y un humano validando pedidos anómalos.

1.4 Anatomía de un problema de regresión

Todo problema de regresión bien planteado tiene estos ingredientes:

  1. Variable objetivo (y): el número a predecir. Debe estar disponible en los datos históricos.
  2. Features (X): variables que conoces en el momento de predecir. Esto es crucial: si predices el tiempo de entrega en el momento de comprar, no puedes usar como feature "hora de salida del repartidor" porque aún no existe (veremos este error real en la sección 8).
  3. Métrica de error: cómo mides la calidad (sección 3).
  4. Baseline: la predicción tonta contra la que compararte (normalmente la media o la mediana).
  5. Coste asimétrico del error (opcional pero frecuente): a veces pasarse cuesta distinto que quedarse corto. Prever demanda de más → stock sobrante; de menos → ventas perdidas.

Consejo profesional

Antes de escribir una línea de código, escribe en una frase: "Voy a predecir [y] usando [X] disponible en el momento [t], y el modelo será útil si el error típico baja de [umbral de negocio]". Si no puedes completar esa frase, no estás listo para modelar.

Ejercicio rápido 1

Clasifica estos problemas como regresión o clasificación:

  1. Predecir si un cliente cancelará su suscripción este mes.
  2. Predecir cuántos días tardará un cliente en cancelar.
  3. Predecir la nota (0-10) que un usuario dará a una película.
  4. Predecir el rango de precio de un piso: "barato", "medio", "caro".
Ver solución 1. **Clasificación** (sí/no — es una etiqueta binaria). 2. **Regresión** (número de días, continuo). Técnicamente es un problema de *supervivencia*, pero se puede abordar como regresión. 3. **Regresión** si tratas la nota como continua (predices 7,3). También podría ser clasificación ordinal, pero en la práctica se modela como regresión. 4. **Clasificación** (3 categorías). Ojo: alguien convirtió un problema de regresión (precio) en clasificación al discretizarlo. Suele ser mala idea salvo que negocio solo necesite el rango — pierdes información.

2. Regresión lineal simple y múltiple con sklearn

2.1 Repaso: la recta que ya conoces del módulo 01

En el módulo de fundamentos implementaste gradiente descendente para ajustar una recta. La regresión lineal simple modela:

ŷ = w·x + b
  • w (peso o coeficiente): cuánto cambia ŷ por cada unidad que aumenta x.
  • b (intercepto o sesgo): el valor de ŷ cuando x = 0.

Y la regresión lineal múltiple generaliza a varias features:

ŷ = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b

El modelo busca los w que minimizan el error cuadrático medio (MSE) sobre los datos de entrenamiento:

MSE = (1/m) · Σ (yᵢ - ŷᵢ)²

Nota

En el módulo 01 minimizaste el MSE con gradiente descendente iterativo. LinearRegression de sklearn no itera: usa la solución analítica (ecuaciones normales, resueltas vía descomposición SVD por estabilidad numérica): w = (XᵀX)⁻¹Xᵀy. Para datasets que caben en memoria es exacta e instantánea. El gradiente descendente (SGDRegressor) se reserva para datasets gigantes que no caben en RAM o para entrenamiento online.

¿Por qué existe? Porque es el modelo más simple que captura relaciones entre variables, es interpretable al 100%, entrena en milisegundos y, sorprendentemente a menudo, es difícil de batir cuando las relaciones son aproximadamente lineales y hay pocos datos.

2.2 Regresión lineal simple en sklearn, línea por línea

# ============================================================
# REGRESIÓN LINEAL SIMPLE: precio de vivienda vs. metros cuadrados
# ============================================================
import numpy as np                                    # Cálculo numérico: arrays, aleatoriedad
import matplotlib.pyplot as plt                       # Visualización (opcional pero recomendable)
from sklearn.linear_model import LinearRegression    # El estimador de regresión lineal (OLS)
from sklearn.model_selection import train_test_split # Para separar train/test (¡siempre!)
from sklearn.metrics import mean_absolute_error       # Métrica MAE (sección 3)

# ---- 1. Generamos datos sintéticos con relación lineal + ruido ----
rng = np.random.default_rng(42)                       # Generador con semilla fija → reproducible
m2 = rng.uniform(40, 200, size=300)                   # 300 viviendas entre 40 y 200 m²
# Precio real subyacente: 50.000€ base + 2.500€ por m² + ruido gaussiano de ±20.000€
precio = 50_000 + 2_500 * m2 + rng.normal(0, 20_000, size=300)

# ---- 2. sklearn espera X como matriz 2D (n_muestras, n_features) ----
X = m2.reshape(-1, 1)                                 # De vector (300,) a matriz (300, 1)
y = precio                                            # y sí puede ser vector 1D

# ---- 3. Separamos ANTES de tocar nada más (regla de oro del cap. 1) ----
X_train, X_test, y_train, y_test = train_test_split(
    X, y,                                             # Datos completos
    test_size=0.2,                                    # 20% reservado para evaluación honesta
    random_state=42                                   # Semilla → misma partición siempre
)

# ---- 4. Creamos y entrenamos el modelo ----
modelo = LinearRegression()                           # Instancia con hiperparámetros por defecto
modelo.fit(X_train, y_train)                          # fit() calcula w y b con la solución analítica

# ---- 5. Inspeccionamos lo aprendido ----
print(f"Coeficiente (w): {modelo.coef_[0]:,.0f} €/m²")   # coef_ es un array, uno por feature
print(f"Intercepto (b):  {modelo.intercept_:,.0f} €")    # intercept_ es un escalar
# Salida esperada: w ≈ 2.500 €/m², b ≈ 50.000 € (¡recuperó los valores reales!)

# ---- 6. Predecimos y evaluamos en test ----
y_pred = modelo.predict(X_test)                       # predict() aplica ŷ = w·x + b a cada fila
mae = mean_absolute_error(y_test, y_pred)             # Error absoluto medio
print(f"MAE en test: {mae:,.0f} €")                   # ≈ 16.000 € (coherente con el ruido inyectado)

# ---- 7. Predicción para un caso nuevo ----
piso_nuevo = np.array([[120]])                        # Piso de 120 m² (matriz 2D, ¡siempre!)
print(f"Precio estimado 120 m²: {modelo.predict(piso_nuevo)[0]:,.0f} €")

Puntos clave del código:

  • reshape(-1, 1): el error más común del principiante. sklearn exige X en 2D aunque haya una sola feature. El -1 significa "calcula tú esta dimensión".
  • fit solo con train: el test queda intacto hasta la evaluación final.
  • coef_ e intercept_ llevan guion bajo final: convención de sklearn para atributos aprendidos durante fit.

2.3 Regresión múltiple e interpretación de coeficientes (lo que negocio quiere oír)

# ============================================================
# REGRESIÓN MÚLTIPLE: varias features a la vez
# ============================================================
import pandas as pd                                   # DataFrames para features con nombre

rng = np.random.default_rng(7)                        # Nueva semilla para este ejemplo
n = 500                                               # 500 viviendas

df = pd.DataFrame({
    "m2":            rng.uniform(40, 200, n),         # Superficie
    "habitaciones":  rng.integers(1, 6, n),           # 1 a 5 habitaciones
    "antiguedad":    rng.uniform(0, 50, n),           # Años del edificio
    "dist_centro":   rng.uniform(0.5, 15, n),         # Km al centro de la ciudad
})

# Precio "real": cada feature contribuye con un peso conocido + ruido
df["precio"] = (
    60_000
    + 2_000 * df["m2"]                                # +2.000 € por m²
    + 8_000 * df["habitaciones"]                      # +8.000 € por habitación
    - 1_500 * df["antiguedad"]                        # -1.500 € por año de antigüedad
    - 4_000 * df["dist_centro"]                       # -4.000 € por km del centro
    + rng.normal(0, 25_000, n)                        # Ruido: lo que el modelo no puede explicar
)

X = df.drop(columns="precio")                         # Matriz de features (500, 4)
y = df["precio"]                                      # Vector objetivo

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

modelo = LinearRegression().fit(X_train, y_train)     # Entrenamos igual que antes

# ---- Interpretación de coeficientes: EL producto estrella para negocio ----
for nombre, coef in zip(X.columns, modelo.coef_):     # Emparejamos nombre de feature y su peso
    print(f"{nombre:>13}: {coef:>+10,.0f} € por unidad")
# Salida aproximada:
#            m2:     +2.003 € por unidad
#  habitaciones:     +7.612 € por unidad
#    antiguedad:     -1.488 € por unidad
#   dist_centro:     -3.947 € por unidad

La frase que puedes decir en una reunión de dirección:

"Manteniendo todo lo demás constante, cada metro cuadrado adicional añade unos 2.000 € al precio, cada kilómetro que te alejas del centro resta unos 4.000 €, y cada año de antigüedad resta unos 1.500 €."

Esa cláusula, "manteniendo todo lo demás constante" (ceteris paribus), es la esencia de la interpretación y también su trampa:

Advertencia

Los coeficientes se interpretan condicionados al resto de features del modelo. Si dos features están muy correlacionadas (p. ej. m2 y habitaciones), los coeficientes individuales se vuelven inestables y pueden hasta cambiar de signo entre reentrenamientos, aunque las predicciones sigan siendo buenas. Esto se llama multicolinealidad y es una de las razones por las que existe la regularización (sección 5). Y recuerda: un coeficiente indica asociación, no causalidad. Subir el precio de las casas no las hace más grandes.

Advertencia

Los coeficientes están en las unidades de cada feature. Un coeficiente grande no implica una feature importante: dist_centro en metros tendría un coeficiente 1.000 veces menor que en kilómetros siendo igual de relevante. Para comparar importancias, estandariza las features primero (o usa métodos específicos de importancia).

2.4 Supuestos del modelo lineal y qué pasa si los violas

La regresión lineal clásica (OLS) asume ciertas condiciones. Para predecir puedes relajarlas bastante; para interpretar coeficientes e intervalos de confianza importan mucho más.

Supuesto Qué significa Cómo detectar la violación Qué pasa si se viola Remedio típico
Linealidad La relación real entre X e y es lineal Gráfico de residuos vs. predicción: patrón curvo Sesgo sistemático: el modelo predice mal en rangos concretos Features polinómicas (sec. 4), transformaciones (log), modelos no lineales
Independencia de errores Los residuos no se correlacionan entre sí En series temporales: autocorrelación (Durbin-Watson) Métricas de test engañosas, intervalos falsos Features de lag, modelos de series temporales, split temporal
Homocedasticidad La varianza del error es constante Residuos en "embudo" (más dispersión a valores altos) Predicciones peores en un extremo; intervalos mal calibrados Transformar y con log, ponderar muestras
Normalidad de residuos Los errores siguen una gaussiana Histograma/QQ-plot de residuos Solo afecta a intervalos de confianza, no a la predicción puntual Transformaciones, más datos (TCL ayuda)
No multicolinealidad Las features no son combinación lineal unas de otras VIF > 5-10, matriz de correlación Coeficientes inestables y no interpretables Eliminar features redundantes, Ridge (sec. 5), PCA

Nota

El diagnóstico más rentable en la práctica es el gráfico de residuos: plt.scatter(y_pred, y_test - y_pred). Si ves una nube sin estructura alrededor de cero, bien. Si ves curvas, embudos o grupos, el modelo te está contando qué le falta.

2.5 Ventajas, desventajas y cuándo NO usar regresión lineal

Ventajas

  • Interpretabilidad total: cada coeficiente es una frase de negocio.
  • Entrena en milisegundos incluso con cientos de miles de filas.
  • Pocos datos necesarios: funciona razonablemente con decenas de muestras.
  • Sin hiperparámetros que ajustar (en su forma básica).
  • Es el baseline perfecto: si un modelo complejo no la bate claramente, no lo despliegues.

Desventajas y limitaciones

  • Solo captura relaciones lineales (salvo que ingenies features).
  • Muy sensible a outliers: el término cuadrático del MSE hace que un solo punto extremo arrastre la recta.
  • Sufre con multicolinealidad (coeficientes inestables).
  • No modela interacciones automáticamente (que el efecto de m2 dependa del barrio, por ejemplo).

Cuándo NO usarla

  • Relaciones claramente no lineales que no puedas linealizar con transformaciones.
  • Datos con outliers severos sin limpiar (considera HuberRegressor o RANSACRegressor, variantes robustas).
  • Cientos de features irrelevantes sin regularización (usa Lasso/Ridge, sección 5).
  • Cuando la precisión es crítica y hay datos de sobra: un gradient boosting casi siempre ganará (sección 6).

Ejercicio rápido 2

Entrenas una regresión lineal para predecir salario con features experiencia_años y experiencia_meses (¡la misma información en dos unidades!). ¿Qué problema esperas y qué harías?

Ver solución Es **multicolinealidad perfecta**: `experiencia_meses = 12 × experiencia_años`. La matriz XᵀX es singular (no invertible). sklearn no lanzará error (usa pseudo-inversa vía SVD), pero los coeficientes individuales serán arbitrarios: podría asignar +5.000 a años y -300 a meses, o -80.000 a años y +7.100 a meses — infinitas combinaciones dan las mismas predicciones. **Solución:** eliminar una de las dos features. En casos de correlación alta pero no perfecta, Ridge estabiliza los coeficientes.

3. Métricas de regresión a fondo

Elegir la métrica equivocada es fallar el proyecto aunque el modelo sea bueno. Esta sección es de las más importantes del capítulo: la métrica es el contrato entre el modelo y el negocio.

3.1 MAE — Error Absoluto Medio

Fórmula intuitiva:

MAE = (1/m) · Σ |yᵢ - ŷᵢ|

"Toma cada error, quítale el signo, y promedia." Es literalmente cuánto te equivocas en promedio, en las unidades originales.

from sklearn.metrics import mean_absolute_error

mae = mean_absolute_error(y_test, y_pred)          # Promedio de |real - predicho|
print(f"MAE: {mae:,.0f} €")                        # "Nos equivocamos, de media, en X €"
  • Cuándo usarla: cuando todos los errores pesan proporcionalmente a su tamaño (equivocarse en 20 es exactamente el doble de malo que en 10) y hay outliers que no quieres que dominen la métrica.
  • Cómo comunicarla a negocio: es la más fácil: "El modelo se equivoca de media en 14.500 € por vivienda". Todo el mundo lo entiende.
  • Trampas: trata igual pasarse que quedarse corto (si el coste es asimétrico, necesitas métricas ponderadas o quantile regression). Optimizar MAE lleva el modelo hacia la mediana condicional, no la media.

3.2 MSE — Error Cuadrático Medio

Fórmula intuitiva:

MSE = (1/m) · Σ (yᵢ - ŷᵢ)²

"Eleva cada error al cuadrado y promedia." Elevar al cuadrado hace dos cosas: elimina el signo y castiga desproporcionadamente los errores grandes (un error de 10 pesa 100; un error de 20 pesa 400 — el doble de error, el cuádruple de castigo).

from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y_test, y_pred)           # Promedio de (real - predicho)²
print(f"MSE: {mse:,.0f} €²")                       # ¡Unidades al cuadrado! Difícil de comunicar
  • Cuándo usarla: como función de pérdida para entrenar (es diferenciable y convexa — por eso el gradiente descendente la ama). Como métrica de reporte, casi nunca: sus unidades (€²) no significan nada para nadie.
  • Trampas: un solo outlier puede dominar el MSE completo. Si tu dataset tiene errores de captura (una casa registrada a 10 € en lugar de 100.000 €), el MSE explota.

3.3 RMSE — Raíz del Error Cuadrático Medio

Fórmula intuitiva:

RMSE = √MSE

"El MSE devuelto a las unidades originales." Conserva la propiedad de castigar más los errores grandes, pero se lee en euros, días o kWh.

from sklearn.metrics import root_mean_squared_error  # sklearn >= 1.4

rmse = root_mean_squared_error(y_test, y_pred)       # √(promedio de errores²)
print(f"RMSE: {rmse:,.0f} €")
# En versiones antiguas: rmse = mean_squared_error(y_test, y_pred) ** 0.5
  • Cuándo usarla: cuando los errores grandes son desproporcionadamente costosos (fallar por 10 días en una entrega es mucho peor que fallar por 1 día diez veces). Es la métrica estándar en muchas competiciones.
  • Cómo comunicarla: con cuidado. "Error típico de 18.000 €, con mayor penalización a los fallos grandes." Regla práctica: RMSE ≥ MAE siempre; cuanto mayor la brecha entre ambas, más errores extremos tiene tu modelo. Esa brecha es en sí misma un diagnóstico.
  • Trampas: hereda la sensibilidad a outliers del MSE. Un RMSE mucho mayor que el MAE te está gritando "mira tus outliers".

3.4 R² — Coeficiente de determinación

Fórmula intuitiva:

R² = 1 - (SSres / SStot) = 1 - (error de tu modelo / error del baseline "predecir la media")

"¿Qué fracción de la variabilidad de y explica tu modelo, comparado con simplemente predecir la media siempre?"

  • R² = 1 → predicciones perfectas.
  • R² = 0 → tu modelo es tan bueno como predecir la media (inútil).
  • R² < 0 → ¡tu modelo es peor que predecir la media! (sí, puede pasar en test).
from sklearn.metrics import r2_score

r2 = r2_score(y_test, y_pred)                       # 1 - SSres/SStot
print(f"R²: {r2:.3f}")                              # "El modelo explica el X% de la variabilidad"
# También: modelo.score(X_test, y_test) devuelve R² para regresores de sklearn
  • Cuándo usarla: para tener una noción adimensional de calidad, comparable entre problemas con distintas escalas. Útil en informes técnicos.
  • Cómo comunicarla a negocio: "El modelo explica el 87% de la variación de precios; el 13% restante depende de factores que no tenemos en los datos."
  • Trampas ( muchas):
  • R² sube siempre que añades features en train, aunque sean ruido puro. Evalúa siempre en test (o usa R² ajustado en contextos estadísticos).
  • Un R² alto no implica buen modelo: si la varianza de y es enorme, puedes tener R² = 0,9 y errores de 50.000 € inaceptables para negocio.
  • Un R² bajo no implica mal modelo: en problemas intrínsecamente ruidosos (retornos financieros, comportamiento humano), un R² = 0,1 puede valer millones.
  • Depende del rango de y en tu muestra: el mismo modelo evaluado solo en pisos de Madrid centro (rango estrecho) dará un R² menor que evaluado en toda España, con idéntico error absoluto.

3.5 MAPE — Error Porcentual Absoluto Medio

Fórmula intuitiva:

MAPE = (100/m) · Σ |yᵢ - ŷᵢ| / |yᵢ|

"El error medio en porcentaje sobre el valor real." Un MAPE del 8% significa "nos desviamos de media un 8%".

from sklearn.metrics import mean_absolute_percentage_error

mape = mean_absolute_percentage_error(y_test, y_pred)  # Devuelve fracción (0.08 = 8%)
print(f"MAPE: {mape:.1%}")                             # Formateamos como porcentaje
  • Cuándo usarla: cuando el error relativo es lo que importa y comparas entidades de escalas muy distintas (tiendas que venden 100 uds/día vs. 10.000 uds/día). Es la favorita de los equipos de forecasting de demanda.
  • Cómo comunicarla: perfecta para negocio: "Fallamos de media un 6% en la previsión de ventas."
  • Trampas ( la gran trampa):
  • Explota con valores reales cercanos a cero. Si una tienda vendió 1 unidad y predijiste 3, ese punto aporta un 200% al promedio. Si y = 0, la división es infinita. Nunca uses MAPE si tu objetivo puede valer cero o casi cero (demanda intermitente, por ejemplo).
  • Es asimétrica: penaliza más la sobreestimación que la subestimación (predecir 150 cuando es 100 → 50% de error; predecir 100 cuando es 150 → 33%). Un modelo puede "aprender" a quedarse corto sistemáticamente para minimizar el MAPE.
  • Alternativas cuando MAPE falla: sMAPE (simétrica), WAPE (Σ|error| / Σ|real|, robusta a ceros individuales y muy usada en retail), o MAE sobre log(y).

3.6 Tabla comparativa de decisión

Métrica Unidades Sensible a outliers Interpretable por negocio Úsala cuando... Evítala cuando...
MAE Las de y (€, días...) Baja Muy alta Todos los errores pesan linealmente; quieres robustez Los errores grandes son desproporcionadamente graves
MSE Unidades² Muy alta Nula Entrenas/optimizas (pérdida diferenciable) Reportas a humanos
RMSE Las de y Alta Media Errores grandes cuestan más que proporcionalmente Hay outliers de datos (no de fenómeno) sin limpiar
Adimensional (≤1) Alta Media Comparas entre problemas; informes técnicos La escala de y varía entre conjuntos evaluados
MAPE % Media Muy alta Escalas heterogéneas; el % es el lenguaje del negocio y puede ser 0 o cercano a 0

Consejo profesional

Reporta siempre al menos dos métricas: una robusta (MAE o WAPE) y una sensible a extremos (RMSE), más el baseline. La combinación cuenta la historia completa: "MAE 12.000 €, RMSE 19.000 € (hay colas), baseline-media 41.000 € → reducimos el error un 71%". Una métrica sola siempre miente un poco.

3.7 Toolkit: función de evaluación reutilizable

Para no repetir código en cada experimento, encapsula el informe de métricas en una función que uses en todos tus proyectos de regresión. Nota cómo incluye el baseline y la brecha RMSE/MAE, los dos diagnósticos que la gente olvida:

# ============================================================
# INFORME DE REGRESIÓN REUTILIZABLE — cópialo a tu toolkit
# ============================================================
import numpy as np
from sklearn.metrics import (mean_absolute_error,
                             root_mean_squared_error, r2_score)

def informe_regresion(y_real, y_pred, nombre="modelo"):
    """Imprime las métricas clave de regresión + diagnósticos de contexto.

    Parámetros
    ----------
    y_real : array — valores verdaderos del conjunto de evaluación
    y_pred : array — predicciones del modelo sobre ese mismo conjunto
    nombre : str   — etiqueta para el informe
    """
    y_real = np.asarray(y_real)                     # Asegura arrays NumPy
    y_pred = np.asarray(y_pred)

    mae  = mean_absolute_error(y_real, y_pred)      # Error medio absoluto
    rmse = root_mean_squared_error(y_real, y_pred)  # Error cuadrático (raíz)
    r2   = r2_score(y_real, y_pred)                 # Varianza explicada

    # Baseline implícito: predecir siempre la media de y_real
    mae_base = mean_absolute_error(y_real, np.full_like(y_real, y_real.mean()))
    mejora = 1 - mae / mae_base                     # % de reducción de error vs. baseline

    print(f"── Informe: {nombre} " + "─" * 30)
    print(f"  MAE : {mae:>12,.2f}   (baseline media: {mae_base:,.2f} → mejora {mejora:.0%})")
    print(f"  RMSE: {rmse:>12,.2f}   (ratio RMSE/MAE: {rmse/mae:.2f}"
          f"{' ⚠ posibles outliers' if rmse/mae > 1.6 else ' ✅ errores homogéneos'})")
    print(f"  R²  : {r2:>12.3f}")
    # Diagnóstico de sesgo: ¿el modelo se pasa o se queda corto sistemáticamente?
    sesgo = np.mean(y_pred - y_real)                # >0: sobreestima; <0: subestima
    print(f"  Sesgo medio: {sesgo:>+,.2f}  (ideal ≈ 0)")

La línea del sesgo medio merece atención: un modelo puede tener buen MAE y aun así sobreestimar sistemáticamente (+3.000 € en cada tasación, por ejemplo). Para negocio, un error sistemático es a la vez más peligroso (se acumula en agregados: 1.000 tasaciones × 3.000 € = 3 M€ de desviación) y más fácil de corregir (basta restar el sesgo). El error aleatorio, en cambio, se compensa al agregar pero no se puede corregir.

Ejercicio rápido 3

Tu modelo de demanda tiene MAE = 50 unidades y RMSE = 200 unidades. Tu jefa pregunta si el modelo es bueno. ¿Qué le dices y qué investigas?

Ver solución La brecha RMSE/MAE = 4 es enorme (si los errores fueran homogéneos, RMSE ≈ 1,25·MAE). Eso indica que **la mayoría de predicciones son buenas pero hay unos pocos fallos catastróficos**. Respuesta: "En el día a día fallamos ~50 unidades, pero hay días puntuales con errores de cientos. Antes de dar el visto bueno, voy a identificar esos días". Al investigar, casi siempre aparecen: promociones no registradas como feature, festivos, roturas de stock (demanda censurada) o errores de datos. Decidir sin mirar esa cola sería un error profesional.

4. Regresión polinómica: capturar curvas

4.1 Qué es y por qué existe

El mundo no siempre es una recta. El consumo energético vs. temperatura tiene forma de U (calefacción en invierno, aire acondicionado en verano). El efecto del precio sobre las ventas se satura. La solución más simple: seguir usando regresión lineal, pero sobre features transformadas.

La regresión polinómica de grado 2 con una feature x ajusta:

ŷ = w₁·x + w₂·x² + b

El truco conceptual clave: sigue siendo un modelo lineal... en los coeficientes. Solo hemos fabricado una feature nueva (). Toda la maquinaria de la sección 2 (solución analítica, interpretación, métricas) sigue funcionando.

flowchart LR
    A["Features originales<br/>x1, x2"] --> B["PolynomialFeatures<br/>(grado=2)"]
    B --> C["Features expandidas<br/>x1, x2, x1², x2², x1·x2"]
    C --> D["LinearRegression<br/>(o Ridge)"]
    D --> E["Modelo que captura<br/>curvas e interacciones"]
    style B fill:#e8f4fd,stroke:#2980b9
    style D fill:#eafaf1,stroke:#27ae60

Nota

PolynomialFeatures también genera términos de interacción (x1·x2), que capturan efectos conjuntos: "el m² extra vale más si el piso está en el centro". A veces las interacciones aportan más que los cuadrados; puedes generarlas solas con interaction_only=True.

4.2 Código: pipeline polinómico

# ============================================================
# REGRESIÓN POLINÓMICA con Pipeline (la forma correcta)
# ============================================================
from sklearn.preprocessing import PolynomialFeatures   # Genera x², x³, interacciones...
from sklearn.pipeline import make_pipeline             # Encadena pasos sin fugas de datos
from sklearn.linear_model import LinearRegression

# Datos con relación cuadrática real: consumo energético vs. temperatura
rng = np.random.default_rng(0)
temp = rng.uniform(-5, 38, 400)                        # Temperaturas de -5°C a 38°C
# Consumo real: parábola con mínimo hacia los 18°C + ruido
consumo = 15 + 0.09 * (temp - 18) ** 2 + rng.normal(0, 1.5, 400)

X = temp.reshape(-1, 1)                                # Matriz 2D, como siempre
X_train, X_test, y_train, y_test = train_test_split(X, consumo, test_size=0.2, random_state=0)

# Pipeline: primero expandir features, luego ajustar la lineal
modelo_poly = make_pipeline(
    PolynomialFeatures(degree=2, include_bias=False),  # Genera [x, x²]; sin columna de 1s
    LinearRegression()                                 # Ajusta w₁·x + w₂·x² + b
)
modelo_poly.fit(X_train, y_train)                      # El pipeline entrena ambos pasos en orden

print(f"R² lineal simple: {LinearRegression().fit(X_train, y_train).score(X_test, y_test):.3f}")
print(f"R² polinómico g2: {modelo_poly.score(X_test, y_test):.3f}")
# Salida típica: lineal ≈ 0.02 (¡inútil, la relación es simétrica!), polinómico ≈ 0.93

Advertencia

Usa siempre un Pipeline para encadenar PolynomialFeatures con el modelo. Si transformas fuera del pipeline y luego haces validación cruzada, cometerás fugas sutiles y te complicarás la vida en producción. El pipeline garantiza que la transformación se aprende/aplica correctamente en cada partición.

4.3 El experimento clave: grado vs. overfitting

El grado del polinomio es un dial de complejidad. Subirlo reduce el error de entrenamiento siempre, pero a partir de cierto punto el modelo memoriza el ruido: overfitting (concepto central del capítulo 1, aquí lo verás en números).

# ============================================================
# EXPERIMENTO: error train vs. test según el grado del polinomio
# ============================================================
from sklearn.metrics import root_mean_squared_error

# Pocos datos a propósito: el overfitting se ve mejor con muestras pequeñas
rng = np.random.default_rng(3)
x = rng.uniform(0, 10, 40)                             # Solo 40 puntos
y = 3 * np.sin(x) + x + rng.normal(0, 0.8, 40)         # Relación suave no lineal + ruido
X = x.reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=3)

print(f"{'Grado':>5} | {'RMSE train':>10} | {'RMSE test':>10}")
print("-" * 33)
for grado in [1, 2, 3, 5, 9, 15]:                      # Probamos complejidad creciente
    pipe = make_pipeline(                              # Nuevo pipeline por grado
        PolynomialFeatures(degree=grado, include_bias=False),
        LinearRegression()
    )
    pipe.fit(X_train, y_train)                         # Entrena solo con train
    rmse_tr = root_mean_squared_error(y_train, pipe.predict(X_train))  # Error donde "estudió"
    rmse_te = root_mean_squared_error(y_test, pipe.predict(X_test))    # Error donde importa
    print(f"{grado:>5} | {rmse_tr:>10.2f} | {rmse_te:>10.2f}")

# Salida típica:
# Grado | RMSE train |  RMSE test
# ---------------------------------
#     1 |       2.11 |       2.35   <- underfitting: ni train aprende
#     2 |       1.98 |       2.20
#     3 |       1.28 |       1.41
#     5 |       0.74 |       0.95   <- punto dulce ✅
#     9 |       0.68 |       1.80   <- empieza el memorizado
#    15 |       0.41 |      31.62   <- overfitting catastrófico

La curva que debes tener grabada a fuego:

 Error
 * │ *                                          . <- test (explota)
   │   *                                    .
   │     *                              .
   │        *  train baja SIEMPRE   .
   │           *                .
   │              * . _ .  ✅ .            <- mínimo de test = punto dulce
   │                    ° - ° - ° - ° - ° - °   <- train sigue bajando
   └──────────────────────────────────────────────► Grado del polinomio
      1     2     3     5     7     9     11    15

   [ UNDERFITTING ]   [ ZONA ✅ ]   [ OVERFITTING ]
   train alto          ambos bajos   train ≈ 0, test disparado
   test alto           y parecidos   brecha train-test enorme

4.4 Ventajas, limitaciones y cuándo NO

Ventajas: captura no-linealidad manteniendo toda la maquinaria lineal; interacciones explícitas e interpretables en grados bajos; con grado 2 sigue siendo razonablemente explicable.

Limitaciones y cuándo NO usarla:

  • Explosión combinatoria: con 10 features y grado 3 se generan 285 columnas; con 100 features, más de 176.000. Inviable e in-interpretable. Con muchas features, usa árboles o boosting.
  • Extrapolación desastrosa: los polinomios se disparan hacia ±∞ fuera del rango de entrenamiento. Un polinomio de grado 9 entrenado con pisos de 40-200 m² puede predecir precios negativos para 250 m². Los modelos de árboles, en cambio, extrapolan planos (constantes).
  • Grados > 3-4 casi nunca: son inestables numéricamente y overfittean. Si necesitas tanta flexibilidad, la herramienta correcta es otra (splines, boosting).

Consejo profesional

En la práctica profesional, la regresión polinómica se usa casi siempre con grado 2 + regularización (sección 5) — es la combinación "curvas sí, locura no". Y para una sola relación curva conocida, una transformación dirigida (log, raíz) suele batir a un polinomio genérico: log(dist_centro) modela mejor "los primeros km importan mucho, los últimos poco".

Ejercicio rápido 4

En el experimento anterior, ¿por qué el RMSE de train con grado 15 no llega exactamente a 0 si el polinomio tiene más parámetros (16) que restricciones aparentes?

Ver solución Con 24 puntos de train (60% de 40) y 16 parámetros, el polinomio aún no puede pasar exactamente por todos los puntos (necesitaría grado 23). Por eso train baja mucho pero no a 0. Si el grado igualara o superara n_train - 1, el RMSE de train sería ~0 (interpolación exacta)... y el de test, astronómico. Bonus: en la práctica también intervienen límites numéricos — x¹⁵ con x=10 es 10¹⁵, y esos rangos causan problemas de precisión en coma flotante. Otra razón para no usar grados altos jamás sin escalar.

5. Regularización: Ridge, Lasso y ElasticNet

5.1 Por qué existe la regularización

Dos enfermedades del modelo lineal motivan esta sección:

  1. Overfitting: acabas de verlo — cuando el modelo tiene demasiada libertad (muchas features, polinomios, pocas muestras), aprende el ruido. Síntoma delator: coeficientes gigantescos de signos opuestos que se compensan entre sí para pasar exactamente por los puntos de train.
  2. Multicolinealidad: features correlacionadas hacen que los coeficientes sean inestables (sección 2.4) — pequeños cambios en los datos producen coeficientes totalmente distintos.

La idea de la regularización es un pacto: "acepto un poco más de error en train a cambio de coeficientes pequeños y estables". Se implementa añadiendo un castigo por tamaño de coeficientes a la función de pérdida:

Pérdida regularizada = MSE + α · Penalización(w)

Ridge (L2):      Penalización = Σ wⱼ²          (suma de cuadrados)
Lasso (L1):      Penalización = Σ |wⱼ|         (suma de valores absolutos)
ElasticNet:      Penalización = mezcla de ambas

α (alpha) es el hiperparámetro que controla la dureza del pacto:

  • α = 0 → regresión lineal normal (sin castigo).
  • α pequeño → castigo suave, coeficientes casi iguales a OLS.
  • α enorme → coeficientes aplastados hacia 0; el modelo tiende a predecir la media (underfitting).

Nota

El intercepto b no se regulariza — castigarlo solo desplazaría todas las predicciones sin controlar complejidad.

5.2 Intuición geométrica: por qué L1 hace ceros y L2 no

Imagina el espacio de coeficientes (w₁, w₂). El MSE dibuja elipses concéntricas (curvas de nivel) alrededor de la solución OLS. La regularización impone quedarse dentro de una "región presupuestaria":

  • L2 (Ridge) → la región es un círculo (w₁² + w₂² ≤ presupuesto).
  • L1 (Lasso) → la región es un rombo (|w₁| + |w₂| ≤ presupuesto).
        w₂                                w₂
         │    ___                          │    ___
         │   /   \  <- elipses MSE         │   /   \
         │  ( OLS )                        │  ( OLS )
      ___│___\___/                         │  \___/
     /   │   \                            /│\
    │  ⊙ │    │  círculo L2              ◇ │ ◇   rombo L1
    │    │ ●<─┼── solución: toca         ● │  \  solución: toca
     \___│___/    en punto genérico     /  │   \ en el VÉRTICE
         │        (w₁≠0, w₂≠0)         ◇___│___◇  → ¡w₁ = 0!
    ─────┼─────── w₁               ────────┼────── w₁
         │                                 │
       RIDGE: encoge todos            LASSO: apaga features

La solución óptima es el punto donde la elipse del MSE toca la región. Con el círculo (L2), ese contacto ocurre casi siempre en un punto genérico: todos los coeficientes se encogen pero ninguno llega a 0 exactamente. Con el rombo (L1), las esquinas sobresalen — y las esquinas están sobre los ejes, donde algún coeficiente vale exactamente 0. La elipse tiende a tocar primero una esquina. Resultado: Lasso apaga features enteras; Ridge las atenúa todas.

La intuición práctica equivalente: la derivada del castigo L2 es 2w — cuando w ya es pequeño, el empuje hacia 0 es minúsculo, así que nunca llega. La "derivada" del castigo L1 es constante (±1) — sigue empujando con la misma fuerza hasta clavar el coeficiente en 0.

5.3 Ridge (L2) en código

# ============================================================
# RIDGE: regularización L2 — SIEMPRE escalar antes
# ============================================================
from sklearn.linear_model import Ridge                 # Regresión con penalización L2
from sklearn.preprocessing import StandardScaler       # Estandariza: media 0, desviación 1
from sklearn.pipeline import make_pipeline

# ¿Por qué escalar? La penalización Σw² trata a todos los coeficientes por igual,
# pero una feature en metros necesita un w 1.000 veces mayor que la misma en km.
# Sin escalar, Ridge castigaría injustamente a las features de unidades pequeñas.
modelo_ridge = make_pipeline(
    StandardScaler(),                                  # Paso 1: (x - media) / desviación
    Ridge(alpha=1.0)                                   # Paso 2: OLS + castigo α·Σw²
)
modelo_ridge.fit(X_train, y_train)                     # El scaler aprende media/std SOLO de train

# Elegir alpha con validación cruzada integrada (rápida y correcta):
from sklearn.linear_model import RidgeCV               # Ridge con búsqueda de alpha por CV

modelo_cv = make_pipeline(
    StandardScaler(),
    RidgeCV(alphas=np.logspace(-3, 3, 13))             # Prueba α de 0.001 a 1000 (escala log)
)
modelo_cv.fit(X_train, y_train)
print(f"Mejor alpha: {modelo_cv[-1].alpha_}")          # [-1] accede al último paso del pipeline

Advertencia (regla inviolable)

Escala las features antes de regularizar. Ridge, Lasso y ElasticNet penalizan la magnitud de los coeficientes, y esa magnitud depende de las unidades de cada feature. Sin escalar, la regularización castiga arbitrariamente según las unidades, no según la relevancia. Con LinearRegression pura el escalado es opcional; con regularización es obligatorio. Y siempre dentro de un Pipeline para que el scaler aprenda solo de train (evitando fuga de información del test).

Cuándo Ridge: es el "seguro a todo riesgo" por defecto. Crees que muchas/todas las features aportan algo; hay multicolinealidad; quieres estabilidad. Casi nunca empeora respecto a OLS y suele mejorar en test. Su solución es analítica (rápida y única).

Cuándo NO: si necesitas un modelo sparse (pocas features activas) para simplificar la captura de datos en producción — ahí Lasso.

5.4 Lasso (L1): el selector de features

# ============================================================
# LASSO: regularización L1 — coeficientes que se hacen CERO
# ============================================================
from sklearn.linear_model import Lasso

# Dataset con features útiles Y ruido a propósito:
rng = np.random.default_rng(42)
n = 300
X_util = rng.normal(size=(n, 3))                       # 3 features con señal real
X_ruido = rng.normal(size=(n, 7))                      # 7 features de RUIDO PURO
X = np.hstack([X_util, X_ruido])                       # Total: 10 features (300, 10)
y = 5*X[:, 0] - 3*X[:, 1] + 2*X[:, 2] + rng.normal(0, 1, n)  # Solo las 3 primeras importan

nombres = [f"util_{i}" for i in range(3)] + [f"ruido_{i}" for i in range(7)]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# Comparamos qué hace cada modelo con las features de ruido:
for nombre_m, m in [("OLS", LinearRegression()),
                    ("Ridge α=1", Ridge(alpha=1.0)),
                    ("Lasso α=0.1", Lasso(alpha=0.1))]:
    pipe = make_pipeline(StandardScaler(), m)          # Escalado obligatorio (ver 5.3)
    pipe.fit(X_train, y_train)
    coefs = pipe[-1].coef_                             # Coeficientes del modelo final
    n_ceros = np.sum(np.abs(coefs) < 1e-10)            # Cuenta coeficientes exactamente nulos
    print(f"{nombre_m:>12}: R²={pipe.score(X_test, y_test):.3f} | ceros={n_ceros}/10")
    print(f"{'':>14}coefs = {np.round(coefs, 2)}")

# Salida típica:
#          OLS: R²=0.986 | ceros=0/10
#               coefs = [ 4.97 -3.11  2.02  0.08 -0.05  0.11 -0.03  0.06 -0.09  0.04]
#    Ridge α=1: R²=0.986 | ceros=0/10
#               coefs = [ 4.95 -3.09  2.01  0.07 -0.05  0.10 -0.03  0.06 -0.08  0.04]
#  Lasso α=0.1: R²=0.985 | ceros=7/10   <- ¡Apagó exactamente las 7 de ruido!
#               coefs = [ 4.87 -3.00  1.92  0.    0.    0.    0.    0.    0.    0.  ]

Este es el superpoder de Lasso: selección automática de features. Las 7 columnas de ruido acaban con coeficiente exactamente 0. En un proyecto real esto significa: menos columnas que capturar, mantener y explicar en producción, y un modelo que cabe en una frase.

Limitaciones de Lasso:

  • Con features correlacionadas entre sí, Lasso elige una casi al azar y apaga las demás — inestable: con otra muestra elegiría otra. Si te importa cuál queda, cuidado.
  • Con más features que muestras (p > n), Lasso selecciona como máximo n features.
  • No tiene solución analítica (se resuelve por descenso por coordenadas) — algo más lento, y con alpha muy bajo puede avisar de convergencia (sube max_iter).

5.5 ElasticNet: lo mejor de ambos mundos

# ============================================================
# ELASTICNET: mezcla L1 + L2 controlada por l1_ratio
# ============================================================
from sklearn.linear_model import ElasticNetCV          # Con búsqueda de hiperparámetros por CV

modelo_en = make_pipeline(
    StandardScaler(),                                  # Obligatorio, como siempre
    ElasticNetCV(
        l1_ratio=[0.1, 0.5, 0.9, 1.0],                 # Proporción L1: 1.0 = Lasso puro, →0 = Ridge
        alphas=np.logspace(-3, 1, 20),                 # Malla de alphas a probar
        cv=5,                                          # Validación cruzada de 5 particiones
        max_iter=10_000                                # Margen para converger
    )
)
modelo_en.fit(X_train, y_train)
print(f"Mejor l1_ratio: {modelo_en[-1].l1_ratio_}, mejor alpha: {modelo_en[-1].alpha_:.4f}")

ElasticNet combina ambos castigos: α · (l1_ratio·Σ|w| + (1-l1_ratio)/2·Σw²). Su caso de uso estrella: grupos de features correlacionadas donde quieres sparsity. Donde Lasso elegiría una del grupo al azar, ElasticNet tiende a mantener el grupo entero con coeficientes repartidos ("grouping effect"), sin renunciar a poner ceros en el ruido.

5.6 Tabla de decisión: Ridge vs. Lasso vs. ElasticNet

Criterio Ridge (L2) Lasso (L1) ElasticNet (L1+L2)
Coeficientes a cero No (solo los encoge) Sí, exactamente 0
Selección de features No Automática Automática
Features correlacionadas Las reparte de forma estable Elige una al azar Mantiene el grupo
p > n (más features que filas) Funciona Máx. n features seleccionadas Funciona
Velocidad Solución analítica Iterativo Iterativo
Hiperparámetros alpha alpha alpha + l1_ratio
Úsalo cuando... Todas las features aportan; quieres estabilidad y es tu primera opción Sospechas que pocas features importan; quieres un modelo mínimo Muchas features, correlacionadas, y quieres sparsity
Variante CV en sklearn RidgeCV LassoCV ElasticNetCV

Consejo profesional

Flujo de decisión realista: empieza con Ridge (robusto, rápido, sin sorpresas). Si negocio pide "dime las 5 variables que importan" o tienes cientos de features, pasa a Lasso. Si Lasso se comporta de forma inestable entre reentrenamientos (síntoma de correlación), sube a ElasticNet. Y elige alpha siempre por validación cruzada en escala logarítmica — nunca a mano.

5.7 El camino de los coeficientes: visualizar qué hace alpha

Una forma poderosa de sentir la regularización es entrenar el mismo Lasso con una malla de alphas y observar cómo los coeficientes van muriendo uno a uno — el llamado camino de regularización (regularization path):

# ============================================================
# CAMINO DE REGULARIZACIÓN: coeficientes de Lasso según alpha
# ============================================================
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler

# Reutilizamos X_train/y_train de la sección 5.4 (3 features útiles + 7 de ruido)
scaler = StandardScaler().fit(X_train)                 # Escalado aprendido SOLO de train
X_tr_esc = scaler.transform(X_train)                   # Features estandarizadas

alphas = np.logspace(-3, 1, 30)                        # 30 alphas: de 0.001 a 10 (escala log)
caminos = []                                           # Aquí guardaremos coef_ por cada alpha
for a in alphas:
    lasso = Lasso(alpha=a, max_iter=10_000)            # Nuevo Lasso por cada alpha
    lasso.fit(X_tr_esc, y_train)                       # Entrena con features escaladas
    caminos.append(lasso.coef_)                        # Guarda los 10 coeficientes

caminos = np.array(caminos)                            # Matriz (30 alphas, 10 features)
# ¿Con qué alpha sobrevive cada feature? Cuenta de coeficientes no nulos:
for i, a in enumerate(alphas[::6]):                    # Muestreamos algunos alphas
    activos = np.sum(np.abs(caminos[::6][i]) > 1e-10)
    print(f"alpha={a:>8.4f} → features activas: {activos}/10")
# alpha=  0.0010 → features activas: 10/10   (casi OLS: todo vivo)
# alpha=  0.0075 → features activas:  9/10
# alpha=  0.0560 → features activas:  5/10   (el ruido va cayendo)
# alpha=  0.4175 → features activas:  3/10   (solo las útiles ✅)
# alpha=  3.1113 → features activas:  1/10   (empezamos a matar señal ⚠)

El camino, dibujado en ASCII (cada línea es el coeficiente de una feature al crecer α):

 |coef|
 5 ┤ util_0 ───────────────────╲
   │                             ╲___
 3 ┤ util_1 ────────────╲            ╲___
   │                      ╲___            ╲___
 2 ┤ util_2 ──────╲           ╲___            ╲___
   │                ╲___          ╲___            ╲______
   │ ruido_* ─╲__       ╲___          ╲___
 0 ┤═══════════╩═══════════╩══════════════╩══════════════════
   └────────────┬───────────┬─────────────┬──────────────────► α (log)
             0.001        0.05          0.4            10
                 ruido muere │  punto dulce ✅ │  la señal empieza a morir
                 primero ────┘                └── underfitting

Léelo así: al subir α, las features de ruido (coeficientes pequeños e inútiles) caen a cero primero; las útiles resisten más porque reducir su coeficiente encarece mucho el MSE. El alpha óptimo (el que elige LassoCV) vive en la franja donde el ruido ya murió y la señal aún no. Si sigues subiendo, empiezas a asesinar features útiles: underfitting. Este mismo gráfico con datos reales es una herramienta de auditoría estupenda: te dice el orden de importancia en que el modelo renunciaría a cada variable.

Ejercicio rápido 5

Un compañero entrena Lasso(alpha=1.0) sin escalar sobre features en euros (~10⁵), metros (~10²) y ratios (~10⁰). El modelo pone a cero los ratios y conserva los euros. ¿Puede concluir que los ratios no son informativos?

Ver solución **No.** Sin escalar, una feature de escala grande (euros ~10⁵) necesita coeficientes minúsculos para contribuir, así que su castigo L1 es casi gratis; una feature de escala pequeña (ratio ~1) necesita coeficientes grandes, carísimos para el castigo. Lasso apaga los ratios *por sus unidades, no por su relevancia*. Con `StandardScaler` delante, todas compiten en igualdad y los ceros sí reflejan (in)utilidad. Este es probablemente el error práctico más común con regularización.

6. Otros regresores que debes conocer

La regresión lineal (con o sin regularización) es la base, pero el catálogo de sklearn incluye familias completamente distintas. Aquí las presentamos para que sepas cuándo alcanzar cada herramienta; su mecánica interna se disecciona en el capítulo 6 (árboles y ensembles).

6.1 KNeighborsRegressor — "dime a qué te pareces"

from sklearn.neighbors import KNeighborsRegressor

# Predice el promedio de los k vecinos más cercanos en el espacio de features
knn = make_pipeline(
    StandardScaler(),                        # ¡Crítico! KNN usa distancias → escalas comparables
    KNeighborsRegressor(n_neighbors=5)       # k=5: promedia las 5 muestras más parecidas
)
knn.fit(X_train, y_train)

Idea: no aprende parámetros; memoriza el train y, para predecir, busca las k muestras más parecidas y promedia sus y. Elige KNN cuando: la relación es muy local e irregular, tienes pocas features (< ~10) y suficientes datos densos. Evítalo cuando: hay muchas features (la "maldición de la dimensionalidad" hace que todo esté lejos de todo), el dataset es grande (predicción lenta: busca vecinos cada vez), o necesitas interpretabilidad.

6.2 DecisionTreeRegressor — reglas si/entonces

from sklearn.tree import DecisionTreeRegressor

# Divide el espacio en regiones con preguntas tipo "¿m2 <= 95?"
# y predice la media de cada región
arbol = DecisionTreeRegressor(
    max_depth=4,                             # Limitar profundidad = control de overfitting
    random_state=42                          # Reproducibilidad en empates de división
)
arbol.fit(X_train, y_train)                  # No requiere escalado: usa umbrales, no distancias

Idea: particiona recursivamente los datos con preguntas binarias que reducen la varianza de y en cada rama. Elige árbol cuando: necesitas reglas literalmente explicables ("si m² > 95 y distrito = Centro → 320k€"), hay no-linealidad e interacciones, mezcla de tipos de features. Evítalo cuando: necesitas precisión (un árbol solo casi siempre pierde contra su versión en ensemble) o predicciones suaves — un árbol produce "escalones", y sin limitar la profundidad overfittea con violencia.

6.3 RandomForestRegressor — la sabiduría de muchos árboles

from sklearn.ensemble import RandomForestRegressor

# Cientos de árboles entrenados con muestras y features aleatorias; promedia sus predicciones
bosque = RandomForestRegressor(
    n_estimators=300,                        # Número de árboles (más = mejor, con rendimientos decrecientes)
    max_features="sqrt",                     # Cada división considera solo √n_features → diversidad
    n_jobs=-1,                               # Usa todos los núcleos de CPU
    random_state=42
)
bosque.fit(X_train, y_train)

Idea: el error de un árbol individual es alto pero diferente en cada árbol; al promediar cientos de árboles descorrelacionados, los errores se cancelan. Elige Random Forest cuando: quieres buen rendimiento sin apenas ajuste de hiperparámetros (es el modelo "difícil de estropear"), datos tabulares medianos, robustez a outliers y a features irrelevantes. Evítalo cuando: el modelo debe ser ligero/rápido en inferencia (cientos de árboles pesan), necesitas extrapolar fuera del rango de train (predice constantes fuera de él) o exigir la máxima precisión (boosting suele ganar).

6.4 HistGradientBoostingRegressor — el campeón tabular moderno

from sklearn.ensemble import HistGradientBoostingRegressor

# Árboles secuenciales: cada uno corrige los errores del anterior (boosting)
# "Hist": discretiza features en bins → muy rápido incluso con millones de filas
hgb = HistGradientBoostingRegressor(
    learning_rate=0.1,                       # Cuánto corrige cada árbol (bajo = más árboles, más suave)
    max_iter=300,                            # Número máximo de árboles secuenciales
    early_stopping=True,                     # Para de añadir árboles cuando la validación no mejora
    random_state=42
)
hgb.fit(X_train, y_train)                    # Bonus: maneja NaN de forma nativa, sin imputar

Idea: en lugar de árboles independientes en paralelo (bosque), entrena árboles en secuencia, cada uno especializado en los residuos (errores) del conjunto anterior. Es el equivalente sklearn de LightGBM/XGBoost. Elígelo cuando: buscas la máxima precisión en datos tabulares medianos/grandes y aceptas ajustar 2-3 hiperparámetros. Es el estándar de facto en la industria para regresión tabular en 2026. Evítalo cuando: tienes muy pocos datos (< ~1.000 filas: overfittea y la lineal regularizada suele ganar) o la interpretabilidad coeficiente-a-coeficiente es requisito regulatorio (aunque SHAP mitiga esto, lo verás en el capítulo 6).

6.5 Tabla comparativa de regresores

Modelo Interpretabilidad Captura no-linealidad Velocidad train / predicción Datos necesarios Requiere escalado Punto fuerte
LinearRegression (salvo features manuales) / Muy pocos (decenas) No (recomendable) Baseline + explicación a negocio
Ridge / Lasso / EN / Pocos Obligatorio Muchas features, estabilidad, selección
Polinómica (g2) + Ridge Suave / Pocos-medios Obligatorio Curvas e interacciones conocidas
KNeighborsRegressor Local / Medios y densos Obligatorio Patrones locales, pocas dimensiones
DecisionTreeRegressor (si es pequeño) / Medios No Reglas explicables si/entonces
RandomForestRegressor / Medios No Buen resultado sin esfuerzo
HistGradientBoosting / Medios-grandes (>1k) No Máxima precisión tabular

Consejo profesional

El flujo profesional en 2026 para regresión tabular: (1) baseline con la media, (2) Ridge con buenas features — a menudo suficiente y siempre informativo, (3) HistGradientBoosting si necesitas exprimir precisión, (4) compara honestamente en test y elige el más simple dentro del margen de empate. Saltar directo al boosting sin baseline lineal es de amateur: pierdes la referencia de cuánto aporta realmente la complejidad.


7. Ejemplo integrador: predicción de precios de viviendas

Ahora juntamos todo el capítulo en un proyecto completo y honesto: dataset sintético realista (con ruido y outliers, como la vida misma), EDA breve, baseline, modelos lineales, comparación de métricas e interpretación para negocio.

7.1 Generación del dataset (con las miserias de los datos reales)

# ============================================================
# PASO 0: DATASET SINTÉTICO REALISTA DE VIVIENDAS (~500 filas)
# ============================================================
import numpy as np
import pandas as pd

rng = np.random.default_rng(2026)                      # Semilla fija: resultados reproducibles
n = 500                                                # 500 viviendas

# --- Features con distribuciones plausibles ---
m2 = rng.gamma(shape=6, scale=18).round(0) if False else rng.gamma(6, 18, n).round(0)
m2 = np.clip(m2, 35, 320)                              # Superficie: gamma sesgada, entre 35 y 320 m²
habitaciones = np.clip((m2 / 35 + rng.normal(0, 0.7, n)).round(), 1, 6)  # Correlada con m² (¡realista!)
antiguedad = rng.uniform(0, 60, n).round(0)            # Años del edificio: 0 a 60
dist_centro = rng.exponential(4, n).clip(0.3, 20).round(1)  # Km al centro: muchas cerca, pocas lejos
planta = rng.integers(0, 9, n)                         # Planta 0 (bajo) a 8
ascensor = (rng.random(n) < 0.7).astype(int)           # 70% tienen ascensor (binaria 0/1)

# --- Precio real subyacente: efectos lineales + un efecto NO lineal + interacción ---
precio = (
    40_000
    + 2_200 * m2                                       # Efecto principal: el m² manda
    + 6_000 * habitaciones                             # Habitaciones suman (correlado con m²)
    - 1_200 * antiguedad                               # La antigüedad resta
    - 30_000 * np.log1p(dist_centro)                   # NO lineal: el 1er km resta mucho, el 15º poco
    + 3_000 * planta * ascensor                        # Interacción: la planta alta solo suma CON ascensor
    - 4_000 * planta * (1 - ascensor)                  # ...y RESTA sin ascensor (¡un 8º sin ascensor!)
    + rng.normal(0, 22_000, n)                         # Ruido irreducible: reformas, orientación, azar
)

# --- Outliers realistas: 2% de precios "raros" (herencias malvendidas, áticos de lujo) ---
idx_out = rng.choice(n, size=10, replace=False)        # 10 filas al azar
precio[idx_out] *= rng.choice([0.55, 1.9], size=10)    # Algunas a mitad de precio, otras casi doble

df = pd.DataFrame({
    "m2": m2, "habitaciones": habitaciones, "antiguedad": antiguedad,
    "dist_centro": dist_centro, "planta": planta, "ascensor": ascensor,
    "precio": precio.round(0)
})
print(df.head())
print(f"\nDimensiones: {df.shape}")                    # (500, 7)

7.2 EDA breve pero suficiente

# ============================================================
# PASO 1: EDA — conocer los datos antes de modelar
# ============================================================
print(df["precio"].describe().round(0))                # Estadísticos del objetivo
# count      500 | mean   ~344.000 | std  ~120.000
# min    ~90.000 | 50%    ~330.000 | max ~980.000  <- ojo a ese máximo (outliers)

print(df.isna().sum())                                 # ¿Nulos? En este sintético, ninguno.
                                                       # En datos reales, aquí decides imputación.

# Correlación de cada feature con el precio (primera intuición, solo captura lo LINEAL):
print(df.corr(numeric_only=True)["precio"].sort_values(ascending=False).round(2))
# precio        1.00
# m2            0.86   <- dominante, como esperábamos
# habitaciones  0.62   <- alta, pero ¡correlada con m2! (multicolinealidad suave)
# ascensor      0.15
# planta        0.05   <- parece irrelevante... porque su efecto DEPENDE de ascensor
# antiguedad   -0.27
# dist_centro  -0.36

# Detección rápida de outliers en el objetivo con la regla IQR:
q1, q3 = df["precio"].quantile([0.25, 0.75])           # Cuartiles 1 y 3
iqr = q3 - q1                                          # Rango intercuartílico
outliers = df[(df["precio"] < q1 - 1.5*iqr) | (df["precio"] > q3 + 1.5*iqr)]
print(f"\nPosibles outliers de precio: {len(outliers)} filas")  # Detecta ~los que inyectamos

Nota

Fíjate en planta: su correlación con el precio es casi nula (0,05) y sin embargo sí importa — su efecto es positivo con ascensor y negativo sin él, y ambos se cancelan en la correlación global. La correlación simple solo ve relaciones lineales marginales. Esta es la trampa clásica de descartar features "por baja correlación".

7.3 Baseline, lineal y Ridge: la escalera de modelos

# ============================================================
# PASO 2: SPLIT + BASELINE + MODELOS, comparados con las MISMAS métricas
# ============================================================
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyRegressor               # El baseline oficial de sklearn
from sklearn.linear_model import LinearRegression, RidgeCV
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.metrics import (mean_absolute_error,
                             root_mean_squared_error, r2_score)

X = df.drop(columns="precio")                          # 6 features
y = df["precio"]

# Un único split para TODO el experimento: comparación justa
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

def evaluar(nombre, modelo):
    """Entrena en train, evalúa en test, imprime las 3 métricas clave."""
    modelo.fit(X_train, y_train)                       # Aprende SOLO de train
    pred = modelo.predict(X_test)                      # Predice el test jamás visto
    mae  = mean_absolute_error(y_test, pred)           # Error medio en €
    rmse = root_mean_squared_error(y_test, pred)       # Error con castigo a fallos grandes
    r2   = r2_score(y_test, pred)                      # % de varianza explicada
    print(f"{nombre:<28} MAE={mae:>9,.0f} €  RMSE={rmse:>9,.0f} €  R²={r2:.3f}")
    return modelo

# --- Modelo 0: BASELINE — predecir siempre la media de train ---
base = evaluar("Baseline (media)", DummyRegressor(strategy="mean"))

# --- Modelo 1: regresión lineal pura ---
lineal = evaluar("LinearRegression", LinearRegression())

# --- Modelo 2: Ridge con escalado y alpha por CV ---
ridge = evaluar("Ridge (escalado + CV)", make_pipeline(
    StandardScaler(),                                  # Obligatorio antes de regularizar
    RidgeCV(alphas=np.logspace(-3, 3, 13))             # 13 alphas en escala log
))

# --- Modelo 3: polinómico grado 2 + Ridge (captura log(dist) e interacción planta×ascensor) ---
poly_ridge = evaluar("Poly(2) + Ridge", make_pipeline(
    PolynomialFeatures(degree=2, include_bias=False),  # Genera x², x·z: 27 features desde 6
    StandardScaler(),                                  # Escala DESPUÉS de expandir
    RidgeCV(alphas=np.logspace(-3, 3, 13))
))

# Salida típica (tus números variarán ligeramente):
# Baseline (media)             MAE=   93.000 €  RMSE=  121.000 €  R²=-0.003
# LinearRegression             MAE=   29.500 €  RMSE=   45.000 €  R²=0.861
# Ridge (escalado + CV)        MAE=   29.400 €  RMSE=   44.800 €  R²=0.862
# Poly(2) + Ridge              MAE=   24.100 €  RMSE=   40.500 €  R²=0.888

7.4 Lectura honesta de resultados

  1. El baseline pone la vara: fallar 93.000 € de media es lo que cuesta "no tener modelo". Todo se mide contra eso.
  2. La lineal captura lo grueso: reduce el MAE un 68%. Con 6 features y milisegundos de entrenamiento. Este es el valor del modelo simple.
  3. Ridge ≈ lineal aquí: con 500 filas y 6 features apenas hay overfitting que regularizar — la mejora es cosmética. Ridge brillaría con 50 features o 100 filas. No esperes magia de la regularización cuando no hay enfermedad que curar.
  4. Poly(2) + Ridge mejora de verdad (~5.000 € menos de MAE): captura parcialmente el efecto log de la distancia y, sobre todo, la interacción planta × ascensor que la lineal no puede ver. La regularización aquí sí trabaja: 27 features con 400 filas de train empieza a ser terreno de overfitting.
  5. La brecha RMSE−MAE (~16.000 €) delata los outliers inyectados: hay una minoría de viviendas "raras" que ningún modelo con estas features puede explicar. Decisión de negocio: ¿las filtramos (tasador humano para casos atípicos) o vivimos con la cola?
# ============================================================
# PASO 3: INTERPRETACIÓN PARA NEGOCIO (del modelo lineal simple)
# ============================================================
for nombre, coef in zip(X.columns, lineal.coef_):      # Coeficientes en unidades originales
    print(f"{nombre:>13}: {coef:>+10,.0f} €")
# m2:           +2.180 €  -> "cada m² añade ~2.200 €"
# habitaciones: +6.900 €  -> ojo: condicionado a m² fijo (multicolinealidad suave)
# antiguedad:   -1.230 €  -> "cada año de antigüedad resta ~1.200 €"
# dist_centro:  -6.100 €  -> promedio lineal de un efecto que es logarítmico (aproximación)
# planta:          +400 €  -> casi nulo: la lineal NO ve la interacción con ascensor
# ascensor:    +14.800 €  -> absorbe parte del efecto conjunto planta×ascensor

Consejo profesional

Presenta a negocio dos modelos: el lineal para explicar ("estos son los euros que aporta cada factor") y el mejor modelo para predecir. Es una práctica estándar y honesta: "El tasador automático usa el modelo B (error medio 24.000 €); las palancas de valor que ves en el informe salen del modelo A, más simple pero fiel en direcciones y órdenes de magnitud."

Ejercicio rápido 6

Añade al experimento un HistGradientBoostingRegressor (sin tocar features). ¿Qué esperas: que bata a Poly(2)+Ridge o no, y por qué? Piénsalo antes de mirar.

Ver solución
from sklearn.ensemble import HistGradientBoostingRegressor
evaluar("HistGradientBoosting", HistGradientBoostingRegressor(random_state=42))
# Típico: MAE ≈ 23.000-26.000 €, similar o levemente mejor que Poly+Ridge
Con solo 400 filas de train, el boosting **no arrasa**: captura la no-linealidad y la interacción por sí mismo (ventaja), pero tiene poco dato para exprimir y el ruido irreducible (σ=22.000 €) pone un suelo al error que ningún modelo puede perforar. Lección doble: (1) el boosting compensa la falta de ingeniería de features, (2) con pocos datos, un lineal con buenas features empata con el boosting — y es infinitamente más explicable. El "suelo de ruido" existe en todo problema real: saber estimarlo evita perseguir mejoras imposibles.

8. Caso empresarial: predicción de tiempos de entrega en logística

Basado en un patrón real que verás repetido en la industria. Nombres y números, ficticios.

8.1 El problema

LogisMed, operador logístico con 40.000 envíos/mes de farmacia y parafarmacia, promete entregas en 24-72 h. Su ETA ("entrega estimada") era una tabla estática por provincia hecha en 2019. Consecuencias: el 18% de las entregas llegaba fuera de la ventana prometida, el call center dedicaba un 30% de su tiempo a "¿dónde está mi pedido?", y operaciones sobredimensionaba rutas "por si acaso".

Objetivo del proyecto: predecir las horas hasta la entrega de cada pedido en el momento de la confirmación de compra. Es regresión pura: y = horas (continua), y la frase de contrato fue: "El modelo será útil si el MAE baja de 6 horas, porque la ventana de comunicación al cliente es de medio día."

8.2 Las features (y la disciplina del "momento de predicción")

Feature Disponible al confirmar compra Comentario
Distancia almacén→destino (km) Calculada por API de rutas
Provincia y tipo de zona (urbana/rural) Categórica codificada
Hora y día de semana del pedido Un pedido de viernes 19h "pierde" el finde
Carga del almacén (pedidos en cola) Foto del sistema en el instante de compra
Producto refrigerado (sí/no) Requiere ruta especial
Transportista asignado Solo si la asignación es automática e inmediata
Hora de salida del repartidor No existe aún al confirmar la compra

8.3 Qué salió mal la primera vez: leakage con una feature del futuro

El primer modelo (un gradient boosting) reportó un MAE de 1,9 horas en test. Celebración. Despliegue. Y en producción... MAE real de 11 horas. Peor que la tabla de 2019.

La autopsia encontró al culpable: la feature hora_salida_reparto — la hora a la que el paquete salió del almacén en el último tramo. En el histórico esa columna existe para todos los pedidos (todos salieron en algún momento), y es oro puro para predecir la entrega: si sabes que salió a las 8:00, la entrega es casi determinista. Pero en el momento de predecir (confirmación de compra), esa hora está en el futuro: no existe. En producción el campo llegaba nulo y el pipeline lo imputaba con la mediana, destrozando las predicciones.

Esto es data leakage temporal, el error más caro del ML aplicado:

sequenceDiagram
    participant C as Cliente confirma compra (t0)
    participant M as Modelo predice ETA
    participant A as Almacén procesa (t0+h)
    participant R as Reparto sale (t0+h+k) 
    participant E as Entrega (y)
    Note over C,M: Momento de predicción:<br/>solo existen datos de t ≤ t0
    Note over R: hora_salida_reparto se genera DESPUÉS<br/>de predecir → usarla en train = LEAKAGE
    M--xR: Feature del futuro
    C->>M: distancia, hora pedido, carga almacén, refrigerado

La regla que LogisMed institucionalizó después: para cada feature candidata, responder por escrito "¿qué valor exacto tiene este campo en el milisegundo en que se ejecuta predict()?". Si la respuesta es "aún no existe" o "podría cambiar después", fuera. Además montaron un test automático: reconstruir el dataset de entrenamiento usando snapshots de la base de datos en el momento t0 de cada pedido (no el estado final), y comparar métricas. El MAE honesto del boosting sin la feature tramposa: 4,8 horas.

8.4 El modelo elegido y por qué

Compararon con el protocolo del capítulo (split temporal — entrenar con enero-abril, testear con mayo — porque los datos tienen orden temporal y un split aleatorio también sería una fuga suave):

Modelo MAE test (h) ¿Por qué sí / no?
Baseline (mediana por provincia) 9,6 La "tabla de 2019" mejorada. La vara a batir.
Ridge con features escaladas 6,1 Bien, pero no capta interacciones (viernes×rural)
Random Forest 5,2 Sólido, pero pesado en inferencia
HistGradientBoosting 4,8 Elegido: mejor MAE, rápido en predicción, maneja nulos nativamente

Eligieron HistGradientBoosting no solo por el MAE: predicción en <1 ms (crítico para el checkout), tolerancia nativa a nulos (los datos de carga del almacén fallaban a veces) y reentrenamiento semanal barato. El Ridge se mantuvo vivo como modelo de contraste: si el boosting y el Ridge divergían mucho en un pedido, se marcaba para revisión — un patrón barato de detección de anomalías.

8.5 Cómo se comunicó el error a operaciones

El equipo NO dijo "MAE 4,8 y R² 0,81". Dijo:

"En la mitad de los pedidos acertamos con un margen de ±3 horas. En 9 de cada 10, con ±10 horas. Al cliente le mostraremos una ventana, no una hora exacta: 'entrega estimada el martes por la tarde'. Y para el 4% de pedidos donde el modelo tiene poca confianza (zonas rurales con datos escasos), mostraremos la ventana ancha antigua."

Tres decisiones de comunicación que salvaron el proyecto:

  1. Percentiles, no promedios: operaciones entiende "el 90% llega dentro de X" mejor que cualquier métrica. (Técnicamente: entrenaron regresión de cuantiles, loss="quantile", para las ventanas.)
  2. El error se tradujo a decisión: ventana estrecha si el modelo es fiable, ancha si no.
  3. Se reportó el error por segmento, no solo el global: urbano MAE 3,1 h, rural 8,9 h. El error global de 4,8 h escondía que en rural el modelo era mediocre — y negocio decidió con esa información, no a pesar de ella.

Resultados a los 6 meses: entregas fuera de ventana del 18% → 6%; llamadas "¿dónde está mi pedido?" −41%; y el hallazgo lateral que más dinero dio: el modelo reveló que la carga del almacén al confirmar era el segundo factor del retraso — operaciones añadió un turno parcial los lunes, atacando la causa, no el síntoma.

Consejo profesional

El patrón de este caso es un checklist reutilizable: (1) contrato de métrica con negocio antes de modelar, (2) auditoría de "momento de predicción" por feature, (3) split temporal si hay tiempo de por medio, (4) baseline vergonzosamente simple, (5) comunicar en percentiles y por segmento, (6) modelo simple de contraste en producción. Memorízalo: vale más que cualquier hiperparámetro.

Ejercicio rápido 7

Audita estas cuatro features candidatas para el modelo de LogisMed (predicción al confirmar la compra). ¿Cuáles son seguras, cuáles son leakage y cuáles son dudosas?

  1. num_pedidos_cliente_ultimo_mes — pedidos del cliente en los 30 días previos a la compra.
  2. tiempo_medio_entrega_provincia — media histórica calculada sobre todo el dataset, incluidos pedidos posteriores.
  3. transportista_final — el transportista que efectivamente entregó el paquete.
  4. prevision_meteo_destino — previsión meteorológica para el destino consultada al confirmar la compra.
Ver solución 1. **Segura** : solo usa el pasado relativo al momento de predicción. Cuidado únicamente con calcularla con datos hasta t0 exacto, no hasta "fin del día". 2. **Leakage sutil** : la media incluye entregas *posteriores* al pedido que se está prediciendo — información del futuro filtrada por agregación. Corrección: calcular la media *expanding* solo con pedidos anteriores a t0 (y así es como debe computarse también en producción). 3. **Leakage casi seguro** : si la asignación de transportista ocurre horas después de la compra, en el momento de predecir no existe. Solo sería válida si la asignación es automática e instantánea en el checkout — hay que verificarlo con el equipo de operaciones, no asumirlo. 4. **Dudosa pero utilizable** : la previsión existe en t0 (válida), pero ojo al construir el histórico de entrenamiento: hay que usar la previsión *que existía en t0*, no el tiempo que finalmente hizo. Si el histórico guarda el clima real observado, entrenar con él es leakage (el modelo aprendería con información más precisa de la que tendrá en producción).

9. Buenas prácticas

  1. Empieza siempre con un baseline (DummyRegressor): sin él, "MAE 24.000 €" no significa nada.
  2. Congela el test set al principio y tócalo una sola vez, al final. Para iterar, usa validación cruzada sobre train.
  3. Usa Pipeline para todo preprocesado (escalado, polinomios): elimina fugas y hace el modelo desplegable tal cual.
  4. Escala antes de regularizar (Ridge/Lasso/ElasticNet) y antes de KNN. Sin excepciones.
  5. Reporta al menos dos métricas + baseline (MAE y RMSE como pareja mínima) y el error por segmento, no solo global.
  6. Audita cada feature contra el momento de predicción: ¿existe este dato cuando se ejecuta predict()? Escríbelo en la documentación del proyecto.
  7. Split temporal si los datos tienen orden temporal. Un split aleatorio en series temporales es una fuga silenciosa.
  8. Mira los residuos (y_test - y_pred): un histograma y un scatter contra la predicción revelan más que cualquier métrica agregada.
  9. Elige alpha por validación cruzada en escala logarítmica (np.logspace), nunca a ojo.
  10. Prefiere el modelo más simple dentro del margen de empate: menos mantenimiento, más explicabilidad, menos sorpresas.
  11. Fija random_state en splits y modelos: la reproducibilidad no es opcional en un entorno profesional.
  12. Documenta la interpretación de coeficientes con sus unidades y su condición ceteris paribus — es lo que negocio va a citar (y malcitar).

10. Malas prácticas

  1. Optimizar contra el test set: probar 20 modelos mirando el test cada vez convierte el test en un segundo train. El resultado final será optimista y producción te lo cobrará.
  2. Reportar solo R²: es la métrica más fácil de inflar y la más difícil de accionar por negocio.
  3. Usar MAPE con objetivos que pueden valer ~0: un divisor diminuto fabrica errores porcentuales infinitos.
  4. Interpretar coeficientes como causalidad: "cada m² añade 2.200 €" no significa que reformar para ganar m² revalorice exactamente eso.
  5. Regularizar sin escalar: los ceros de Lasso reflejarán unidades, no relevancia.
  6. Grados polinómicos altos "porque baja el error": baja el de train; el de test te espera en la esquina.
  7. Eliminar outliers automáticamente sin investigarlos: a veces son errores de datos (elimínalos), a veces son el fenómeno más valioso (¡un segmento de lujo sin modelar!).
  8. Descartar features por baja correlación simple con el objetivo: las interacciones y efectos no lineales son invisibles para la correlación de Pearson (recuerda planta en la sección 7).
  9. Desplegar sin monitorizar el error en producción: el mundo cambia (inflación, hábitos, rutas); todo modelo de regresión caduca.
  10. Presentar el MAE global escondiendo segmentos donde el modelo es malo: te descubrirán, y con razón.

11. Errores comunes

# Error Síntoma Causa raíz Solución
1 X en 1D ValueError: Expected 2D array sklearn exige matriz (n, features) X.reshape(-1, 1) o df[["col"]]
2 Escalar antes del split Métricas de test ligeramente infladas El scaler "vio" el test (fuga) Pipeline con scaler dentro + fit solo en train
3 R² espectacular, producción desastrosa Test 0,95 → real 0,4 Leakage: feature del futuro o duplicados train/test Auditoría de momento de predicción; split temporal
4 Lasso apaga las features "equivocadas" Ceros en features que sabes relevantes Sin escalar, o features correlacionadas StandardScaler + considerar ElasticNet
5 RMSE >> MAE y nadie investiga Modelo "bueno de media" con fallos graves Outliers o segmento sin modelar Analizar la cola de errores caso a caso
6 MAPE infinito o absurdo Errores porcentuales de miles de % Valores reales ≈ 0 en el objetivo WAPE, sMAPE o MAE sobre log(y)
7 Coeficientes que cambian de signo entre reentrenos Interpretación imposible Multicolinealidad Eliminar redundantes o Ridge
8 Predicciones absurdas fuera de rango Precio negativo para un piso de 300 m² Extrapolación (sobre todo polinómica) Acotar el dominio de aplicación; validar entradas
9 ConvergenceWarning en Lasso/ElasticNet Warning al entrenar alpha bajo, datos sin escalar, pocas iteraciones Escalar + max_iter=10_000
10 Comparar modelos con splits distintos Ranking de modelos irreproducible Cada modelo evaluado en particiones diferentes Mismo split (mismo random_state) o misma CV para todos

12. FAQ — Preguntas frecuentes

1. ¿Cuándo uso regresión y cuándo clasificación si puedo plantear el problema de ambas formas? Modela en la forma más rica en información, que casi siempre es regresión (predice el precio, no "caro/barato"). Discretiza después si negocio necesita categorías: un umbral sobre la predicción continua es gratis; recuperar el número desde la categoría es imposible.

2. ¿LinearRegression usa gradiente descendente como vimos en el módulo 01? No: usa la solución analítica por mínimos cuadrados (vía SVD). El resultado es el mismo que alcanzaría el gradiente descendente bien convergido, pero exacto y sin iterar. Para datasets que no caben en memoria o entrenamiento incremental, existe SGDRegressor, que sí es el gradiente descendente del módulo 01 (con regularización opcional incluida).

3. ¿Qué métrica elijo si solo puedo reportar una? MAE si todos los errores pesan proporcionalmente; RMSE si los errores grandes son desproporcionadamente costosos; WAPE si negocio habla en porcentajes y hay valores pequeños. Pero de verdad: reporta dos, más el baseline.

4. ¿Un R² de 0,6 es bueno o malo? Depende del ruido intrínseco del problema. Prediciendo consumo energético de edificios, 0,6 es flojo (fenómeno bastante físico y predecible). Prediciendo cuánto gastará un cliente el año que viene, 0,6 es excelente. Compara siempre contra el baseline y contra el "suelo de ruido" estimado, no contra un número mágico universal.

5. ¿Ridge o Lasso por defecto? Ridge por defecto: estable, rápido, casi nunca empeora. Lasso cuando necesitas selección de features (muchas columnas, coste de captura de datos, exigencia de simplicidad). ElasticNet cuando Lasso resulta inestable por correlaciones. Y el alpha, siempre por CV.

6. ¿Por qué mi modelo empeora en producción si el test daba bien? Sospechosos por orden: (1) leakage — alguna feature no existe o vale distinto en el momento real de predicción; (2) drift — los datos de producción ya no se parecen a los de entrenamiento (precios de 2024 prediciendo 2026); (3) diferencias de preprocesado entre notebook y producción (otra razón para Pipeline); (4) el test era demasiado pequeño y la métrica tenía mucha varianza.

7. ¿Cómo manejo los outliers en regresión? Primero investígalos: ¿error de datos o fenómeno real? Errores → corregir o eliminar documentando. Fenómeno real → decide con negocio: modelarlos (más datos de ese segmento), tratarlos aparte (regla especial o modelo dedicado) o usar métodos robustos (HuberRegressor, optimizar MAE, transformar y con log). Nunca los borres en silencio para maquillar la métrica.

8. ¿Puedo predecir valores fuera del rango de entrenamiento (extrapolar)? Con muchísima cautela. Los lineales extrapolan con la misma pendiente (a veces razonable, a veces absurdo: precios negativos); los polinómicos se disparan; los árboles y boosting predicen constantes (planos) fuera del rango. Buenas prácticas: validar las entradas en producción y alertar/derivar a humano cuando caen fuera del dominio de entrenamiento.

9. ¿Cuántos datos necesito para regresión? Regla de andar por casa: al menos 10-20 filas por feature para un lineal, y a partir de ~1.000 filas para que el boosting empiece a rendir. Pero manda el ruido del problema: con relación limpia bastan decenas de filas; con relación ruidosa, ni un millón garantizan R² alto.

10. ¿Cómo doy intervalos ("entre X e Y") en lugar de un solo número? Tres vías prácticas: regresión de cuantiles (QuantileRegressor, o HistGradientBoostingRegressor(loss="quantile") — entrenas el percentil 10 y el 90 y ya tienes la ventana, como hizo LogisMed), conformal prediction (con la librería MAPIE, agnóstica al modelo y con garantías estadísticas) o, en contexto estadístico clásico, los intervalos de statsmodels. Para comunicar a negocio, las ventanas casi siempre superan al número puntual.

13. Resumen del capítulo

  • La regresión predice números continuos y está detrás de decisiones de negocio de alto impacto: precios, demanda, tiempos, LTV, consumo. El planteamiento (objetivo, features disponibles en el momento de predicción, métrica pactada, baseline) importa más que el algoritmo.
  • La regresión lineal es el punto de partida universal: interpretable ("cada m² añade 2.200 €"), instantánea y difícil de batir con pocos datos. Sus supuestos importan sobre todo para interpretar; sus enemigos son la no-linealidad, los outliers y la multicolinealidad.
  • Las métricas son el contrato con negocio: MAE (error medio, robusto y comunicable), RMSE (castiga fallos grandes; su brecha con MAE diagnostica outliers), R² (adimensional y traicionero), MAPE (porcentual, letal cerca de cero). Reporta siempre dos + baseline + por segmento.
  • La regresión polinómica añade curvas e interacciones fabricando features; el grado es un dial de complejidad y el experimento train-vs-test por grado es la demostración canónica del overfitting. Grado 2 + regularización, casi siempre; más de 3, casi nunca.
  • La regularización cambia un poco de sesgo por mucha estabilidad: Ridge (L2) encoge todos los coeficientes (tu primera opción), Lasso (L1) apaga features exactas (selección automática), ElasticNet combina ambos para features correlacionadas. Escalar antes de regularizar es obligatorio, y alpha se elige por CV.
  • El catálogo no lineal — KNN (localidad), árbol (reglas), Random Forest (robustez sin esfuerzo), HistGradientBoosting (precisión tabular estándar en la industria) — se elige por el trade-off interpretabilidad/precisión/datos; lo diseccionaremos en el capítulo 6.
  • El ejemplo integrador y el caso LogisMed condensan la disciplina profesional: baseline → escalera de modelos → comparación honesta en un test intocado → interpretación para negocio → auditoría anti-leakage del momento de predicción → comunicación en ventanas y por segmento.

En el capítulo 3 cambiamos de pregunta: de "¿cuánto?" a "¿cuál?" — clasificación, con su propio arsenal de modelos y, sobre todo, de métricas (precision, recall y el eterno malentendido de la accuracy).

14. Bibliografía y recursos

  • Scikit-learn — Linear Models (guía oficial): https://scikit-learn.org/stable/modules/linear_model.html
  • Scikit-learn — Metrics and scoring (regresión): https://scikit-learn.org/stable/modules/model_evaluation.html#regression-metrics
  • Scikit-learn — Ensembles (HistGradientBoosting, Random Forest): https://scikit-learn.org/stable/modules/ensemble.html
  • James, Witten, Hastie, Tibshirani — An Introduction to Statistical Learning (ISL, gratuito): https://www.statlearning.com/ — capítulos 3 (regresión lineal) y 6 (regularización) son la referencia pedagógica canónica.
  • Hastie, Tibshirani, Friedman — The Elements of Statistical Learning (gratuito): https://hastie.su.domains/ElemStatLearn/ — la versión avanzada y matemática.
  • Aurélien Géron — Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow (3ª ed., O'Reilly): https://www.oreilly.com/library/view/hands-on-machine-learning/9781098125967/ — capítulo 4 cubre regresión y regularización con código.
  • Molnar — Interpretable Machine Learning (gratuito): https://christophm.github.io/interpretable-ml-book/ — interpretación de modelos lineales y más allá.
  • MAPIE (conformal prediction en Python): https://mapie.readthedocs.io/ — para intervalos de predicción con garantías.
  • Kaggle Learn — Intermediate Machine Learning (gratuito, práctico): https://www.kaggle.com/learn/intermediate-machine-learning
  • StatQuest (Josh Starmer) — Regularization (vídeos): https://www.youtube.com/watch?v=Q81RR3yKn30 — la mejor intuición visual de Ridge/Lasso en YouTube.

Anterior Módulo Siguiente
Capítulo 1: Introducción al ML 02-MACHINE-LEARNING Capítulo 3: Clasificación