D
Practica aplicada M8 avanzado 35 min

Capitulo 08

Aprendizaje Supervisado profundo: regresion, metricas, cross-validation

El modulo mas aplicado de la carrera: del laboratorio al diagnostico riguroso

Regresion Lineal (supuestos, Ridge/Lasso, diagnostico de residuos), Arboles + Random Forest (max_depth, feature importance), el ABC de las metricas (accuracy, F1, ROC-AUC, PR-AUC, MAE, RMSE, R²) y como elegir el threshold optimo por costo de negocio.

8.1 Regresion Lineal: el modelo mas viejo sigue siendo util

La regresion lineal no es “anticuada”. Es el modelo mas interpretable que existe. Cuando un CFO te pregunta “¿que feature importa mas?”, un Random Forest te da una distribucion de importancias. Una regresion lineal te da: “cada metro cuadrado adicional aumenta el precio en $1.800, manteniendo todo lo demas constante”. Esa es la diferencia entre interpretar y explicar.

Los 4 supuestos que importan

No son opcionales

La regresion lineal NO es solo “ajustar una recta”. Tiene 4 supuestos que DEBES verificar antes de confiar en sus resultados. Si no se cumplen, los p-valores, los intervalos de confianza y las predicciones son invalidas.

ConceptoDescripcion
Linealidad La relacion entre X e y es aproximadamente lineal. Verificar con scatter plot. Si no, polynomial features o modelo no lineal.
Homocedast Varianza de residuos CONSTANTE en todo el rango de prediccion. Verificar con plot de residuos vs predicciones. Si forma embudo, log(y) o modelo robusto.
Normalidad Los residuos siguen una distribucion normal. Importa para p-valores e ICs. Q-Q plot o test de Shapiro-Wilk. Para predicciones puntuales, no es critico.
No multicol Features no estan altamente correlacionadas entre si (|r| < 0.9). VIF > 10 es senal de problema. Solucion: PCA, eliminar features, regularizar.
python Regresion Lineal + diagnostico completo
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from scipy import stats

# 1) Dataset sintetico: precio de viviendas
np.random.seed(42)
N = 300
superficie       = np.random.normal(80, 25, N).clip(30, 200)
antiguedad       = np.random.uniform(0, 50, N)
distancia_centro = np.random.exponential(8, N)
precio = (50000 + superficie*1800 - antiguedad*800 - distancia_centro*1200
          + np.random.normal(0, 15000, N))
df = pd.DataFrame({'superficie': superficie, 'antiguedad': antiguedad,
                   'distancia_centro': distancia_centro, 'precio': precio})

X = df.drop('precio', axis=1)
y = df['precio']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 2) Modelo: Pipeline con escalado + regresion
pipe = Pipeline([('scaler', StandardScaler()), ('model', LinearRegression())])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)

print(f"R² en test: {r2_score(y_test, y_pred):.3f}")
print(f"MAE en test: {mean_absolute_error(y_test, y_pred):.0f}")

# 3) DIAGNOSTICO de los 4 supuestos
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# a) Linealidad: y_pred vs y_test
axes[0,0].scatter(y_test, y_pred, alpha=0.6, color='#06B6D4')
axes[0,0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
axes[0,0].set_xlabel('Real')
axes[0,0].set_ylabel('Predicho')
axes[0,0].set_title('Linealidad: nube deberia estar en la diagonal')

# b) Homocedasticidad: residuos vs predicciones
residuos = y_test - y_pred
axes[0,1].scatter(y_pred, residuos, alpha=0.6, color='#8B5CF6')
axes[0,1].axhline(0, color='red', linestyle='--')
axes[0,1].set_xlabel('Predicciones')
axes[0,1].set_ylabel('Residuos')
axes[0,1].set_title('Homocedasticidad: dispersion CONSTANTE alrededor de 0')

# c) Normalidad: Q-Q plot
stats.probplot(residuos, dist='norm', plot=axes[1,0])
axes[1,0].set_title('Normalidad: puntos deberian caer en la linea')

# d) Multicolinealidad: heatmap de correlaciones
corr = X.corr()
sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdBu_r', center=0, ax=axes[1,1], square=True)
axes[1,1].set_title('Multicolinealidad: |r| > 0.9 es senal de problema')

for ax in axes.flatten():
    sns.despine(ax=ax)
    if ax.get_xlabel() and 'Correlacion' not in ax.get_title():
        ax.grid(alpha=0.3, linestyle='--')
plt.tight_layout()
plt.show()

Regularizacion: Ridge y Lasso

Ridge (L2)

M8

Agrega una penalidad lambda * sum(w^2) a la funcion de costo. Encoge los coeficientes hacia cero sin llegar a cero. Mejora el modelo en presencia de multicolinealidad. Util cuando TODOS los features son relevantes.

#ml #regularizacion #regresion

Lasso (L1)

M8

Agrega una penalidad lambda * sum(|w|) a la funcion de costo. Puede llevar coeficientes EXACTAMENTE a cero, haciendo feature selection automatica. Util cuando sospechas que solo algunos features son relevantes.

#ml #regularizacion #regresion
python Ridge vs Lasso: cuando cada uno
from sklearn.linear_model import Ridge, Lasso
import numpy as np

# Supongamos 50 features, pero solo 5 son realmente predictivos
np.random.seed(42)
N, n_features = 200, 50
X = np.random.randn(N, n_features)
w_real = np.zeros(n_features)
w_real[:5] = [3, -2, 1.5, 0.8, -1]  # solo 5 features importan
y = X @ w_real + np.random.normal(0, 0.5, N)

# Ridge: los 5 features se mantienen, los otros se acercan a 0
ridge = Ridge(alpha=1.0).fit(X, y)
print("Ridge - features 'realmente' importantes:", np.argsort(np.abs(ridge.coef_))[-5:][::-1].tolist())
print("Ridge - coeficientes en los 5 reales:", ridge.coef_[:5].round(2).tolist())
print("Ridge - coeficientes en ruido:", ridge.coef_[10:].mean().round(3))

# Lasso: lleva los irrelevantes EXACTAMENTE a 0 (feature selection)
lasso = Lasso(alpha=0.1).fit(X, y)
n_zeros = (lasso.coef_ == 0).sum()
print(f"\nLasso - {n_zeros}/{n_features} coeficientes en cero (feature selection)")
print("Lasso - coeficientes no-cero:", lasso.coef_[lasso.coef_ != 0].round(2).tolist())
Ridge vs Lasso vs ElasticNet
  • Ridge (L2): cuando todos los features aportan algo. No hace feature selection.
  • Lasso (L1): cuando sospechas que solo algunos features importan. Hace feature selection.
  • ElasticNet (L1 + L2): combinacion. Util cuando hay features correlacionados (Lasso tiende a elegir uno solo).

La eleccion de alpha (fuerza de regularizacion) se hace con cross-validation.

8.2 Arboles: el algoritmo que mejor se interpreta

Decision Tree: una secuencia de preguntas

El arbol particiona el espacio de features usando preguntas binarias. Cada nodo interno pregunta “¿feature X >= umbral?”. Las hojas son las predicciones. Para clasificar una instancia nueva: arrancas en la raiz y bajas por las ramas segun las respuestas. El camino es la “explicacion” de la prediccion.

ConceptoDescripcion
max_depth Profundidad maxima del arbol. Default=None (crece hasta que las hojas sean puras) = overfit casi seguro. Tipico: 3-10.
min_samples_leaf Minimo de muestras en cada hoja. Mas alto = mas regularizacion. Tipico: 1-50. Es el hiperparametro mas efectivo.
min_samples_split Minimo de muestras para dividir un nodo. Si < esto, no divide. Tipico: 2-20.
max_features Cantidad de features a considerar en cada split. "sqrt" o "log2" para Random Forest. None para Decision Tree.
criterion Funcion de impureza: "gini" (rapido) o "entropy" / "log_loss" (mas preciso).
python Tuning de max_depth: donde empieza el overfitting
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import cross_val_score
import seaborn as sns

# Comparar train R² vs CV R² para distintos max_depth
depths = range(1, 21)
train_r2, cv_r2 = [], []

for d in depths:
    tree = DecisionTreeRegressor(max_depth=d, random_state=42)
    # Train R² (riesgo de overfitting se ve aqui)
    tree.fit(X_train, y_train)
    train_r2.append(tree.score(X_train, y_train))
    # CV R² (honesto)
    cv_r2.append(cross_val_score(tree, X_train, y_train, cv=5, scoring='r2').mean())

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(depths, train_r2, 'o-', label='Train R²', color='#06B6D4', linewidth=2)
ax.plot(depths, cv_r2, 'o-', label='CV R² (5-fold)', color='#8B5CF6', linewidth=2)
ax.axvline(x=4, color='red', linestyle='--', alpha=0.5, label='Punto optimo (depth=4)')
ax.fill_between(depths, cv_r2, train_r2, alpha=0.15, color='red', label='Gap (overfitting)')
ax.set_xlabel('max_depth')
ax.set_ylabel('R²')
ax.set_title('Tuning de max_depth: train vs CV')
ax.legend()
ax.grid(alpha=0.3, linestyle='--')
sns.despine(ax=ax)
plt.tight_layout()
plt.show()

# Lectura:
# - Train R² siempre crece (mas profundidad = mas capacidad de memorizar)
# - CV R² crece hasta un punto, luego BAJA (overfitting)
# - El gap train-test es la "varianza" del modelo
# - El optimo es donde CV R² es maximo: depth=4 aca

8.3 Random Forest: el ensamblador que siempre anda bien

Random Forest: el modelo de batalla

Cuando no sabes que modelo usar, usa Random Forest. No es el mas fancy, no es el mas exacto en todos los datasets, pero es:

  1. Raramente overfittea (gracias al bagging + feature subsampling)
  2. Robusto a outliers y features irrelevantes
  3. No requiere escalado
  4. Da feature importance gratis
  5. Tiene pocos hiperparametros que tuning
python Random Forest: feature importance
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
import seaborn as sns

# Random Forest nos da feature importance por "reduccion de impureza"
rf = RandomForestRegressor(n_estimators=200, max_depth=8, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)

importancias = pd.Series(rf.feature_importances_, index=X.columns).sort_values()

fig, ax = plt.subplots(figsize=(8, 4))
importancias.plot(kind='barh', color='#8B5CF6', ax=ax)
ax.set_title('Feature Importance - Random Forest')
ax.set_xlabel('Importancia (reduccion de impureza)')
sns.despine(ax=ax)
plt.tight_layout()
plt.show()

# ⚠️ CUIDADO: feature importance de RF es SESGADA hacia features con
# alta cardinalidad (muchas categorias) o muchos splits posibles.
# Para interpretacion seria, usar SHAP (lo vemos en M10).

8.4 Metricas: el idioma para hablar de modelos

Clasificacion

ConceptoDescripcion
Accuracy Proporcion de predicciones correctas. Enganosa en clases desbalanceadas. Ej: 99% accuracy prediciendo siempre 0 en dataset 99/1.
Precision De los que predije positivos, cuantos eran. Util cuando FP es caro (spam: no marcar mail bueno como spam).
Recall De los positivos reales, cuantos encontre. Util cuando FN es caro (cancer: no dejar pasar un caso).
F1 Media armonica de P y R. Balanceada. Util en desbalance. Variante F2 pesa mas el recall.
ROC-AUC Area bajo la curva ROC. Independiente del threshold. Bueno en clases balanceadas. NO recomendado en desbalance extremo.
PR-AUC Area bajo la curva Precision-Recall. MEJOR que ROC-AUC en desbalance extremo (fraude, enfermedades raras).

Regresion

ConceptoDescripcion
MAE Promedio de |error|. En la misma unidad que y. Robusto a outliers. Error "promedio" que veras en produccion.
RMSE Raiz del MSE. Penaliza errores grandes al cuadrado. Si un error grande es inaceptable, usar RMSE.
Proporcion de varianza en y explicada por el modelo. Rango (-inf, 1]. 1 = perfecto, 0 = tan bueno como la media, < 0 = peor que la media.
MAPE MAE en porcentaje. Util para presentar a negocio: "el modelo se equivoca en promedio 5%". Indefinido si y=0.

8.5 Umbral optimo: cuando el negocio importa

El threshold de 0.5 es casi siempre arbitrario

sklearn por defecto usa 0.5 para predecir clases. Pero ese numero es arbitrario: no tiene nada que ver con tu problema. La eleccion optima del threshold depende del COSTO de cada tipo de error.

python Elegir threshold por costo de negocio
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import precision_recall_curve, f1_score
import seaborn as sns

# Asumimos: modelo ya entrenado, tenemos y_proba para test
# y_proba = modelo.predict_proba(X_test)[:, 1]

# Costos del negocio
COSTO_FP = 50    # contactamos un cliente que NO se va
COSTO_FN = 10000 # NO contactamos un cliente que SI se va

thresholds = np.linspace(0.0, 1.0, 101)
ganancias = []

for t in thresholds:
    y_pred_t = (y_proba >= t).astype(int)
    fp = ((y_pred_t == 1) & (y_test == 0)).sum()
    fn = ((y_pred_t == 0) & (y_test == 1)).sum()
    # Ganancia negativa (costos)
    ganancia = -(fp * COSTO_FP + fn * COSTO_FN)
    ganancias.append(ganancia)

ganancias = np.array(ganancias)
t_optimo = thresholds[np.argmax(ganancias)]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(thresholds, ganancias, color='#06B6D4', linewidth=2)
ax.axvline(t_optimo, color='red', linestyle='--', label=f'Threshold optimo = {t_optimo:.2f}')
ax.set_xlabel('Threshold')
ax.set_ylabel('Costo total (menor = mejor)')
ax.set_title(f'Costo de negocio vs threshold (FP=${COSTO_FP}, FN=${COSTO_FN:,})')
ax.legend()
ax.grid(alpha=0.3, linestyle='--')
sns.despine(ax=ax)
plt.tight_layout()
plt.show()

print(f"Threshold optimo: {t_optimo:.2f}")
print(f"Ahorro vs threshold=0.5: ${(ganancias[50] - ganancias.max()):,.0f}")
Regla practica de threshold
  • Si FN es MUCHO mas caro que FP (fraude, cancer): threshold BAJO (0.1-0.3). Maximiza recall.
  • Si FP es mas caro que FN (spam, marketing): threshold ALTO (0.7-0.9). Maximiza precision.
  • Si son similares: threshold que maximiza F1 (~0.5 si el modelo esta bien calibrado).
  • NUNCA elegir threshold mirando el test. Usar CV o un set de validacion separado.

8.6 Errores comunes

Reportar R² sin chequear que sea positivo
✗ 'Mi modelo tiene R² = -0.3' (peor que predecir la media, pero lo reportas igual)
✓ Si R² < 0 en test, el modelo es inutil. Volve a la mesa de diseno. No lo reportes como logro.
Confundir precision y accuracy en clases desbalanceadas
✗ 'Tengo 99% accuracy'. En dataset 99/1, eso es predecir siempre 0.
✓ Reporta F1, ROC-AUC, PR-AUC. Accuracy no cuenta la historia en desbalance.
Interpretar feature importance de Random Forest como causalidad
✗ 'La feature X es la mas importante, entonces es lo que causa el fenomeno.'
✓ Importance mide prediccion, no causalidad. Para causalidad, hace falta diseno experimental o modelos causales.
No verificar los supuestos de la regresion lineal
✗ Ajuste lineal + reporte de coeficientes sin verificar linealidad, homocedasticidad, normalidad, multicolinealidad.
✓ Hacer el diagnostico de 4 plots ANTES de interpretar. Si no se cumplen, los coeficientes son invalidos.
Threshold = 0.5 como default sin cuestionarlo
✗ sklearn usa 0.5, debe ser lo correcto.
✓ El threshold optimo depende de los costos de FP y FN. Calculalo con el codigo de la seccion 8.5.
Libro: "An Introduction to Statistical Learning" (ISLR) - James et al. Cap 3 (regresion), 8 (arboles), 9 (SVM). Gratuito online.
Libro: "Hands-On Machine Learning" (3ra ed) - Aurélien Géron. Cap 4 (regresion), 6 (arboles, RF), 7 (boosting), 8 (pipelines).
Paper: Breiman: "Random Forests" (Machine Learning, 2001, el paper seminal)
Paper: Tibshirani: "Regression Shrinkage and Selection via the Lasso" (JRSS B, 1996, la base teorica de Lasso)
Video: StatQuest: "Ridge, Lasso and Elastic-Net Regression" (YouTube, 12 min, visual)
Video: StatQuest: "Random Forests" (YouTube, ~20 min, 3 videos)
Articulo: "Accuracy, Precision, Recall or F1?" - Koo Ping Shun (Towards Data Science)
Mini-proyecto 8: diagnostico completo de un modelo supervisado avanzado
  1. Cargá un dataset de regresion (House Prices, California Housing, Bike Sharing). Documenta el problema en 3 lineas.
  2. Entrena regresion lineal + Ridge + Lasso + Random Forest con Pipeline (todo el preprocessing adentro). Evalua con CV, reportando MAE, RMSE y R².
  3. Para el modelo de regresion lineal, hace el diagnostico de 4 plots (linealidad, homocedasticidad, normalidad, multicolinealidad). Comenta que supuestos se cumplen y cuales no.
  4. Tunea max_depth y n_estimators de Random Forest. Grafica train vs CV para detectar el punto optimo.
  5. Bonus: convertí el problema a clasificacion (precio > mediana = 1, sino 0). Compara accuracy vs F1. Si el dataset esta desbalanceado, reporta PR-AUC.
  6. Bonus 2: elegí un threshold optimo segun un costo de negocio ficticio (ej: FP = $100, FN = $5000). Compara con threshold=0.5.