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
El modelo memoriza el training set pero falla en datos nuevos.
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}%")
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?
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?
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
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
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
)
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
)
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
)
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 |
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}")
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)
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:
• Analizar coeficientes inestables
• Curvas de aprendizaje
• Complejidad del modelo
• Análisis de residuales
• 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.