SECCIÓN 14

RETO FINAL.BiasVariance()

DIAGNOSTICANDO Y CORRIGIENDO OVERFITTING

ENGINEERING CHALLENGE

Reto: Diagnosticando Overfitting

Detecta, diagnostica y corrige overfitting en predicción de precios de viviendas.
Usa curvas de aprendizaje, regularización L1/L2, y análisis de residuales.

01. Contexto del Problema

📊 Dataset: Precios de Viviendas

Tienes un dataset con 10,000 viviendas y 100 features (área, habitaciones, ubicación, edad, etc.). Tu objetivo es predecir el precio de venta.

Estadísticas del Dataset
Total filas 10,000
Features 100
Train/Test split 80% / 20%
Precio medio $250,000
⚠️ Problema Detectado
Training RMSE: $5,000 (R² = 0.99)
Test RMSE: $50,000 (R² = 0.45)
Gap enorme → OVERFITTING
El modelo memoriza el training set pero falla en datos nuevos.
🎯 Tu Misión: Reducir el gap training-test de 90% a menos de 20%, manteniendo un test RMSE < $15,000.

02. Fase 1: Entrenar Modelo Overfitted (Intencionalmente)

Crear el Problema

Primero, vamos a crear un modelo intencionalmente overfitted para entender el problema.

from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler, StandardScaler, PolynomialExpansion
from pyspark.ml.regression import LinearRegression
from pyspark.ml.evaluation import RegressionEvaluator

# Cargar datos
df = spark.read.csv("housing_prices.csv", header=True, inferSchema=True)

# Split 80/20
train_df, test_df = df.randomSplit([0.8, 0.2], seed=42)

# ======================================
# ESTRATEGIA DE OVERFITTING:
# 1. Muchas features polinómicas (grado 5!)
# 2. Sin regularización (regParam = 0)
# ======================================

assembler = VectorAssembler(
    inputCols=[# todas las 100 features],
    outputCol="features_raw"
)

# Expansión polinómica grado 5 (explosión de features)
poly = PolynomialExpansion(
    degree=5,  # ¡Demasiado!
    inputCol="features_raw",
    outputCol="features_poly"
)

scaler = StandardScaler(
    inputCol="features_poly",
    outputCol="features"
)

# Sin regularización
lr_overfit = LinearRegression(
    featuresCol="features",
    labelCol="precio",
    regParam=0.0,  # ❌ Sin penalización
    maxIter=100
)

pipeline = Pipeline(stages=[assembler, poly, scaler, lr_overfit])

# Entrenar
model_overfit = pipeline.fit(train_df)

# Evaluar
evaluator = RegressionEvaluator(labelCol="precio", predictionCol="prediction")

train_pred = model_overfit.transform(train_df)
test_pred = model_overfit.transform(test_df)

train_rmse = evaluator.evaluate(train_pred, {evaluator.metricName: "rmse"})
test_rmse = evaluator.evaluate(test_pred, {evaluator.metricName: "rmse"})

print(f"Training RMSE: ${train_rmse:.2f}")
print(f"Test RMSE: ${test_rmse:.2f}")
print(f"Gap: {((test_rmse - train_rmse) / train_rmse * 100):.1f}%")
Resultado Esperado (Overfitting):
Training RMSE: $5,234.12
Test RMSE: $52,891.45
Gap: 910.6% ← ¡Modelo inútil en producción!

Tarea 1: ¿Por qué está overfitting?

Analiza los coeficientes:
lr_model = model_overfit.stages[3]  # LinearRegression es stage 3
coefs = lr_model.coefficients

# TODO: Calcular estadísticas de coeficientes
# - Media: np.mean(coefs)
# - Desviación estándar: np.std(coefs)
# - Max absoluto: np.max(np.abs(coefs))

# ¿Son muy grandes? ¿Muy variables?
Cuenta features: Con 100 features originales y expansión polinómica grado 5, ¿cuántas features finales tienes?
Fórmula: \(\binom{n+d}{d}\) donde \(n=100\), \(d=5\)

03. Fase 2: Diagnosticar con Curvas de Aprendizaje

Learning Curves: Training vs Validation Error

Concepto:
$$ \begin{aligned} \text{Training Error}(m) &= \frac{1}{m} \sum_{i=1}^{m} (y_i - \hat{y}_i)^2 \\[0.5em] \text{Validation Error}(m) &= \text{Error en datos no vistos} \end{aligned} $$
Graficar ambos errores vs tamaño del training set \(m\). Permite diagnosticar bias vs variance.
import matplotlib.pyplot as plt
import numpy as np

# Tamaños de training set a probar
train_sizes = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]

train_errors = []
val_errors = []

for size in train_sizes:
    # Subset del training set
    subset = train_df.sample(fraction=size, seed=42)

    # Entrenar modelo en subset
    model = pipeline.fit(subset)

    # Evaluar en subset (training error)
    train_pred = model.transform(subset)
    train_error = evaluator.evaluate(train_pred, {evaluator.metricName: "rmse"})
    train_errors.append(train_error)

    # Evaluar en test set (validation error)
    test_pred = model.transform(test_df)
    val_error = evaluator.evaluate(test_pred, {evaluator.metricName: "rmse"})
    val_errors.append(val_error)

    print(f"Size: {size:.1f} | Train RMSE: ${train_error:.0f} | Val RMSE: ${val_error:.0f}")

# Graficar
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_errors, 'o-', label='Training Error', linewidth=2)
plt.plot(train_sizes, val_errors, 's--', label='Validation Error', linewidth=2)
plt.xlabel('Training Set Size (fraction)')
plt.ylabel('RMSE ($)')
plt.title('Learning Curves: Diagnosing Overfitting')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
Gráfico Esperado (Overfitting)
    RMSE ($)
      ↑
  60k │                 ● ● ● ● ● ● ● ● ●  Validation Error
      │                                    (alto y constante)
  50k │
      │           Gap grande
  40k │
      │
  20k │ ○ ○ ○ ○ ○ ○ ○ ○ ○  Training Error
      │                      (bajo y mejorando)
   0k └──────────────────────────────────→ Training Size
       0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  1.0

       ○ Training Error
       ● Validation Error
                        
Síntomas de Overfitting:
✓ Training error disminuye con más datos
✓ Validation error alto y casi constante
✓ Gap grande entre ambas curvas
✓ Agregar más datos NO ayuda
Solución:
✓ Reducir complejidad del modelo
✓ Aplicar regularización (Ridge/Lasso)
✓ Eliminar features polinómicas
✓ Feature selection

04. Fase 3: Aplicar Regularización L1/L2

Estrategias a Probar

Estrategia 1: Ridge Regression
lr_ridge = LinearRegression(
    featuresCol="features",
    labelCol="precio",
    regParam=1.0,  # Probar: [0.1, 1.0, 10.0]
    elasticNetParam=0.0  # Ridge puro
)
Objetivo: Reducir magnitud de coeficientes sin eliminarlos. Útil si crees que todas las features son relevantes.
Estrategia 2: Lasso Regression
lr_lasso = LinearRegression(
    featuresCol="features",
    labelCol="precio",
    regParam=1.0,  # Probar: [0.1, 1.0, 10.0]
    elasticNetParam=1.0  # Lasso puro
)
Objetivo: Selección automática de features. Elimina features polinómicas irrelevantes (→ 0).
Estrategia 3: Elastic Net + Sin Polinómicas
# Eliminar PolynomialExpansion del pipeline
pipeline_simple = Pipeline(stages=[assembler, scaler, lr_elastic])

lr_elastic = LinearRegression(
    featuresCol="features",
    labelCol="precio",
    regParam=0.5,
    elasticNetParam=0.5  # 50% L1 + 50% L2
)
Objetivo: Reducir complejidad estructural (sin polinómicas) + regularización balanceada.

Tarea 2: Comparación Sistemática

Crea una tabla comparando todas las estrategias:

Modelo Train RMSE Test RMSE Gap (%) # Features ≠ 0
Sin Reg (Poly 5) $5,234 $52,891 910% ~10,000
Ridge λ=1.0 ??? ??? ??? ~10,000
Lasso λ=1.0 ??? ??? ??? ???
EN sin Poly $12,500 $14,200 13.6% 85
Meta: Gap < 20%, Test RMSE < $15,000

05. Fase 4: Validación Final

Análisis de Residuales

Los residuales \(e_i = y_i - \hat{y}_i\) deben ser aleatorios (ruido blanco). Si hay patrones, el modelo no captura toda la información.

import matplotlib.pyplot as plt
from scipy import stats

# Calcular residuales
predictions_pd = test_pred.select("precio", "prediction").toPandas()
residuals = predictions_pd["precio"] - predictions_pd["prediction"]

# ======================================
# Gráfico 1: Residuales vs Predicción
# ======================================
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.scatter(predictions_pd["prediction"], residuals, alpha=0.5)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('Predicted Price ($)')
plt.ylabel('Residuals ($)')
plt.title('Residual Plot')
plt.grid(True, alpha=0.3)

# ======================================
# Gráfico 2: Q-Q Plot (normalidad)
# ======================================
plt.subplot(1, 2, 2)
stats.probplot(residuals, dist="norm", plot=plt)
plt.title('Q-Q Plot')

plt.tight_layout()
plt.show()

# Estadísticas de residuales
print(f"Media residuales: ${residuals.mean():.2f} (debe ser ~0)")
print(f"Std residuales: ${residuals.std():.2f}")
✅ Residuales Saludables:
• Distribuidos aleatoriamente alrededor de 0
• Sin patrones (bandas, curvas)
• Varianza constante (homocedasticidad)
• Q-Q plot lineal (normalidad)
❌ Problemas Comunes:
• Patrón en U → Relación no lineal faltante
• Varianza creciente → Necesita transformación log(y)
• Q-Q desviado → Outliers o distribución no normal
• Clusters → Features categóricas mal codificadas

Interpretación de Coeficientes

# Modelo final (mejor performance)
best_model = # tu mejor modelo aquí
lr_model = best_model.stages[-1]  # LinearRegression es última etapa

coefs = lr_model.coefficients
feature_names = ["area", "habitaciones", "baños", # ...]

# Crear DataFrame con coeficientes
coef_df = pd.DataFrame({
    'feature': feature_names,
    'coefficient': coefs
}).sort_values('coefficient', key=lambda x: abs(x), ascending=False)

print(coef_df.head(10))  # Top 10 features más importantes

# Ejemplo de interpretación:
# Si coef["area"] = 150.5:
# → Cada m² adicional incrementa el precio en $150.50 (ceteris paribus)
💡 Insight: Con Lasso, muchos coeficientes serán exactamente 0. Las features con coef ≠ 0 son las que el modelo considera más importantes.

06. Criterios de Evaluación

Requisitos Obligatorios

  • Gap < 20%: Cerrar brecha training-test
  • Test RMSE < $15,000: Error aceptable
  • Curvas de aprendizaje: Graficar y analizar
  • Comparación Ridge/Lasso: Probar ambos
  • Análisis residuales: Gráfico + interpretación

Puntos Bonus

  • Cross-Validation: Grid search λ y α (+10 pts)
  • Feature importance: Ranking de features (+10 pts)
  • Regularization path: Graficar β vs λ (+15 pts)
  • Deployment: Guardar y cargar modelo (+10 pts)

🎯 Objetivo de Aprendizaje

Al completar este reto, habrás dominado el ciclo completo de diagnóstico y corrección de overfitting:

1. Detectar
• Identificar gap training-test
• Analizar coeficientes inestables
• Curvas de aprendizaje
2. Diagnosticar
• Bias vs Variance tradeoff
• Complejidad del modelo
• Análisis de residuales
3. Corregir
• Aplicar regularización L1/L2
• Selección de λ con CV
• Validar mejora en test set

💡 Aplicación real: Este flujo es idéntico al que usarás en producción. El 80% de proyectos de ML fallan por overfitting. Saber diagnosticarlo y corregirlo te hace un ingeniero de datos valioso.