Proyecto 1 · Predicción de precios de vivienda
Este primer proyecto recorre el ciclo completo de un problema de regresión: entender el dataset, construir una baseline, detectar fuga de información, comparar modelos y comunicar resultados con incertidumbre. Es deliberadamente pequeño para que puedas repetirlo entero en una tarde.
Al terminar sabrás
El problema
Predecir el valor mediano de una vivienda en un distrito de California a partir de ocho variables censales. El dataset California Housing está disponible directamente en Scikit-learn con fetch_california_housing. Contiene 20 640 registros y la variable objetivo está expresada en cientos de miles de dólares (1990).
| Variable | Descripción | Cuidado |
|---|---|---|
| MedInc | Ingreso mediano del bloque | Unidad censal, no hogar individual |
| HouseAge | Edad mediana de las viviendas | Relación no lineal con el precio |
| AveRooms / AveBedrms | Habitaciones y dormitorios promedio | Valores extremos por bloques pequeños |
| Population | Población del bloque | Puede correlacionar con ruido |
| AveOccup | Ocupantes por hogar | Outliers que distorsionan escaladores |
| Latitude / Longitude | Coordenadas del bloque | Fuga si se parte aleatoriamente por proximidad |
| MedHouseVal | Valor mediano (objetivo) | Está truncado en 5.00001 |
Preguntas antes de modelar
- ¿Qué decisión depende de la predicción? Si se usa para priorizar tasaciones, el coste de sobreestimar y subestimar puede ser distinto.
- ¿Cuál es la baseline? Predecir la media o la mediana del entrenamiento. Si tu modelo no la supera, algo está mal.
- ¿La partición refleja el uso real? Si el modelo se aplicará a distritos nuevos, una partición aleatoria por filas es demasiado optimista porque distritos vecinos comparten información.
- ¿Qué error es aceptable? Un MAE de 0.5 equivale a 50 000 dólares; juzga en unidades del problema, no solo en la métrica normalizada.
Pipeline y baseline
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, KFold, cross_validate
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
datos = fetch_california_housing(as_frame=True)
df = datos.frame
print(df.shape)
print(df.describe().T[["mean", "std", "min", "max"]])
print("tope del objetivo:", (df["MedHouseVal"] >= 5.0).mean())
X = df.drop(columns="MedHouseVal")
y = df["MedHouseVal"]
# Partición estratificada por precio para que el test cubra todo el rango
bins = pd.qcut(y, q=5, labels=False)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=bins
)
baseline = DummyRegressor(strategy="median")
baseline.fit(X_train, y_train)
pred_base = baseline.predict(X_test)
print("MAE baseline:", mean_absolute_error(y_test, pred_base))
modelos = {
"ridge": Pipeline([
("imputar", SimpleImputer(strategy="median")),
("escalar", StandardScaler()),
("modelo", Ridge(alpha=1.0))
]),
"hist_gb": HistGradientBoostingRegressor(
max_iter=400, learning_rate=0.05, max_leaf_nodes=31,
l2_regularization=1.0, random_state=42
)
}
cv = KFold(n_splits=5, shuffle=True, random_state=42)
for nombre, modelo in modelos.items():
scores = cross_validate(
modelo, X_train, y_train, cv=cv,
scoring=("neg_mean_absolute_error", "neg_root_mean_squared_error", "r2"),
n_jobs=-1
)
print(nombre)
print(" MAE :", -scores["test_neg_mean_absolute_error"].mean())
print(" RMSE:", -scores["test_neg_root_mean_squared_error"].mean())
print(" R2 :", scores["test_r2"].mean())
# Ajuste final y evaluación única sobre test
final = modelos["hist_gb"].fit(X_train, y_train)
pred = final.predict(X_test)
mae = mean_absolute_error(y_test, pred)
rmse = np.sqrt(mean_squared_error(y_test, pred))
r2 = r2_score(y_test, pred)
print(f"test MAE={mae:.3f} RMSE={rmse:.3f} R2={r2:.3f}")
# Error por rango de precio: un promedio global puede ocultar fallos
cortes = [0, 1.5, 2.5, 3.5, 5.1]
etiquetas = ["bajo", "medio-bajo", "medio-alto", "alto"]
grupo = pd.cut(y_test, bins=cortes, labels=etiquetas)
analisis = pd.DataFrame({"real": y_test, "pred": pred, "grupo": grupo})
print(analisis.groupby("grupo", observed=True).apply(
lambda d: pd.Series({
"n": len(d),
"MAE": np.mean(np.abs(d.real - d.pred)),
"sesgo": np.mean(d.pred - d.real)
})
))Fuga de información geográfica
La partición aleatoria mezcla distritos vecinos entre train y test. Como latitude y longitude están en las features y los precios son espacialmente suaves, el modelo puede «reconocer» la zona en lugar de generalizar. Si el objetivo es predecir distritos nuevos, usa una partición por región:
from sklearn.cluster import KMeans
from sklearn.model_selection import GroupShuffleSplit
# Agrupa coordenadas en 20 regiones approx. y separa regiones completas
coords = X[["Latitude", "Longitude"]]
regiones = KMeans(n_clusters=20, n_init=10, random_state=42).fit_predict(coords)
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=regiones))
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
# Ninguna región debe aparecer en ambos lados
print(set(regiones[train_idx]) & set(regiones[test_idx])) # set()La segunda versión suele dar un error mayor y más honesto. Esa diferencia es exactamente la fuga que la partición aleatoria ocultaba.
Criterios de evaluación
| Criterio | Insuficiente | Aceptable | Excelente |
|---|---|---|---|
| Formulación | Sin métrica ni baseline | Métrica definida y baseline | Métrica ligada al coste y análisis de errores |
| Datos | Sin EDA | EDA básico y outliers | EDA, censura del target y análisis espacial |
| Validación | Un solo split | K-fold | K-fold + partición espacial comparada |
| Modelo | Solo un modelo sin justificar | Baseline + 2 modelos | Selección justificada con dispersión entre folds |
| Comunicación | Solo un número | Tabla de métricas | Error por subgrupo, sesgo e interpretación |
Entregables
- Notebook o script que reproduzca el pipeline de principio a fin con semilla fija.
- Tabla comparativa de baseline, Ridge y gradient boosting con media y desviación entre folds.
- Gráfico de residuos y análisis por rango de precio.
- Un párrafo con la decisión recomendada y sus limitaciones.
- Archivo
requirements.txtcon versiones fijadas.
Ejercicio 11.1 · Mejorar con criterio
Prueba tres mejoras: variables derivadas (habitaciones por persona, ratio de ocupación), transformación logarítmica del target y un modelo con pérdida robusta. Para cada una, reporta el cambio en MAE y en el error del decil más caro. ¿Alguna mejora reduce el error global pero empeora el grupo caro?
Orientación: el logaritmo del target puede ayudar si la distribución es asimétrica, pero complica la interpretación y el error en dólares. Las variables derivadas pueden capturar hacinamiento. Un modelo robusto puede mejorar la mediana del error y empeorar la cola. La decisión depende de qué error importa.