Ocho módulos con MercaFresh te han enseñado el camino; ahora toca recorrerlo solo. En este primer proyecto abandonamos temporalmente el supermercado para resolver un problema clásico de regresión de punta a punta: predecir el precio medio de la vivienda en distritos de California a partir de datos del censo. Practicarás el flujo completo de las 7 fases que viste en 01-05 "El flujo de trabajo de un proyecto de Machine Learning": definición, datos, exploración, preprocesamiento, modelado, evaluación y conclusiones. Usaremos el dataset California Housing, incluido en scikit-learn (fetch_california_housing), de modo que no necesitas cuentas externas ni descargas de Kaggle: todo funciona con tu entorno habitual de Python.
Contenido
- Definición del problema y métrica objetivo
- Obtención y carga de los datos
- Análisis exploratorio (EDA)
- Preprocesamiento con Pipeline
- Modelado: del baseline a los ensembles
- Comparación honesta con validación cruzada
- Análisis de errores
- Evaluación final en test
- Conclusiones del proyecto
Definición del problema y métrica objetivo
Antes de escribir una línea de código, definimos el problema como aprendiste en 01-05:
- Pregunta de negocio: dado un distrito de California descrito por 8 variables del censo, estimar el valor mediano de sus viviendas.
- Tipo de problema: regresión supervisada (la variable objetivo es continua), como en 04-01 "Regresión lineal".
- Métrica principal: RMSE (raíz del error cuadrático medio), porque penaliza más los errores grandes y se expresa en las mismas unidades que el precio. Acompañaremos con MAE (más robusto a atípicos) y R² (proporción de varianza explicada), las tres presentadas en 06-02 "Métricas de evaluación".
- Criterio de éxito: batir con claridad a un baseline trivial y conseguir un R² competitivo (para este dataset, por encima de 0,80 es un buen resultado).
El precio objetivo está expresado en centenas de miles de dólares (un valor de 2,5 significa 250.000 $). Un detalle importante que descubriremos en el EDA: los precios están censurados en 5,0 (todo distrito por encima de 500.000 $ aparece como 5,00001).
Obtención y carga de los datos
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_california_housing
datos = fetch_california_housing(as_frame=True)
df = datos.frame # DataFrame con features + target
print(df.shape) # (20640, 9)
df.head()Con as_frame=True obtenemos directamente un DataFrame de pandas. El diccionario de datos:
| Variable | Significado | Unidad |
|---|---|---|
MedInc |
Ingreso mediano del distrito | Decenas de miles de $ |
HouseAge |
Antigüedad mediana de las viviendas | Años |
AveRooms |
Habitaciones medias por hogar | Habitaciones |
AveBedrms |
Dormitorios medios por hogar | Dormitorios |
Population |
Población del distrito | Personas |
AveOccup |
Ocupantes medios por hogar | Personas |
Latitude |
Latitud del distrito | Grados |
Longitude |
Longitud del distrito | Grados |
MedHouseVal |
Objetivo: precio mediano | Centenas de miles de $ |
Lo primero, como siempre desde 06-01 "División de datos: entrenamiento, validación y prueba", es apartar el conjunto de test antes de explorar nada, para que ninguna decisión se contamine con información que no deberíamos ver:
from sklearn.model_selection import train_test_split
X = df.drop(columns="MedHouseVal")
y = df["MedHouseVal"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print(len(X_train), len(X_test)) # 16512 / 4128A partir de aquí, todo el EDA se hace solo con el conjunto de entrenamiento. El test queda bajo llave hasta el final.
Análisis exploratorio (EDA)
Distribuciones
train = X_train.copy()
train["MedHouseVal"] = y_train
train.hist(bins=50, figsize=(14, 9))
plt.tight_layout()
plt.show()
print(train.describe().round(2))
print("Nulos por columna:\n", train.isna().sum())Observaciones clave (compáralas con lo que veas en pantalla):
- No hay valores nulos: nos ahorramos las estrategias de 03-02 "Manejo de datos faltantes" — un lujo poco habitual en datos reales.
MedHouseValtiene un pico artificial en 5,0: es la censura mencionada. Unos 800 distritos del train valen "500.000 $ o más"; el modelo no podrá distinguir dentro de ese grupo.MedIncy el objetivo tienen asimetría positiva (cola a la derecha), el patrón que aprendiste a tratar con logaritmos en 03-03 "Transformación de datos". Aquí la asimetría del precio es moderada, así que probaremos con y sin transformación en los retos; en el cuerpo del proyecto trabajaremos con el precio en su escala original para interpretar los errores en dólares.AveRooms,AveBedrms,PopulationyAveOccuptienen atípicos extremos (distritos con residencias colectivas, hoteles, etc.): unAveOccupde 1.200 personas por hogar no es un hogar. Esto afectará a los modelos lineales más que a los de árboles, como razonaste en 04-03 "Árboles de decisión".
Correlaciones
Como en 02-03 "Correlación y covarianza", medimos la relación lineal de cada feature con el objetivo:
corr = train.corr(numeric_only=True)["MedHouseVal"].sort_values(ascending=False)
print(corr.round(3))Resultado típico: MedInc domina (≈ 0,69), seguida muy de lejos por AveRooms (≈ 0,15); Latitude y Longitude tienen correlaciones lineales pequeñas y de signo negativo, pero eso no significa que no importen: su relación con el precio no es lineal, es geográfica.
El mapa: latitud y longitud coloreadas por precio
plt.figure(figsize=(9, 7))
sc = plt.scatter(
train["Longitude"], train["Latitude"],
c=train["MedHouseVal"], cmap="viridis",
s=8, alpha=0.4
)
plt.colorbar(sc, label="Precio mediano (x100.000 $)")
plt.xlabel("Longitud")
plt.ylabel("Latitud")
plt.title("California: el precio es geografía")
plt.show()El dibujo que aparece es un mapa de California: los precios altos se concentran en la costa (bahía de San Francisco, Los Ángeles, San Diego) y caen hacia el interior. Esta estructura espacial es exactamente el tipo de patrón no lineal e interactivo (importa la combinación latitud-longitud, no cada una por separado) donde los árboles y ensembles del módulo 7 deberían superar a la regresión lineal. Acabamos de formular una hipótesis comprobable — eso es hacer EDA con propósito.
Preprocesamiento con Pipeline
Todas las variables son numéricas, así que el preprocesamiento es sencillo: estandarizar para los modelos sensibles a escala (lineal, Ridge, Lasso — recuerda 03-05 "Normalización y estandarización") y dejar los datos tal cual para los de árboles, que son invariantes a transformaciones monótonas. Lo encapsulamos en Pipeline como aprendiste al final del módulo 3, para que el escalado se ajuste solo con el train de cada partición y evitar las fugas de datos de 06-01:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.ensemble import RandomForestRegressor, HistGradientBoostingRegressor
from sklearn.dummy import DummyRegressor
modelos = {
"Dummy (media)": DummyRegressor(strategy="mean"),
"Lineal": Pipeline([("esc", StandardScaler()), ("m", LinearRegression())]),
"Ridge": Pipeline([("esc", StandardScaler()), ("m", Ridge(alpha=1.0))]),
"Lasso": Pipeline([("esc", StandardScaler()), ("m", Lasso(alpha=0.001))]),
"Random Forest": RandomForestRegressor(
n_estimators=200, n_jobs=-1, random_state=42
),
"HistGradientBoosting": HistGradientBoostingRegressor(random_state=42),
}Fíjate en que el diccionario ya cuenta la historia del curso: el baseline trivial, la regresión lineal de 04-01, las versiones regularizadas de 07-01 "Regularización: Ridge, Lasso y Elastic Net", el Random Forest de 07-02 "Ensemble Learning" y el gradient boosting por histogramas de 07-03 "Gradient Boosting".
Modelado: del baseline a los ensembles
Por qué empezar con DummyRegressor
El baseline responde una pregunta imprescindible: ¿cuánto de bueno es "no saber nada"? DummyRegressor(strategy="mean") predice siempre el precio medio. Su RMSE es, esencialmente, la desviación típica del objetivo (≈ 1,15, es decir, ±115.000 $). Cualquier modelo que no lo bata con holgura no ha aprendido nada; y su cifra da escala a todas las demás.
Comparación honesta con validación cruzada
Comparamos todos los modelos con validación cruzada de 5 particiones sobre el train, como aprendiste en 06-03 "Validación cruzada": ningún modelo ve el test, y cada cifra viene con su variabilidad.
from sklearn.model_selection import cross_validate, KFold
cv = KFold(n_splits=5, shuffle=True, random_state=42)
metricas = {
"RMSE": "neg_root_mean_squared_error",
"MAE": "neg_mean_absolute_error",
"R2": "r2",
}
filas = []
for nombre, modelo in modelos.items():
res = cross_validate(modelo, X_train, y_train, cv=cv, scoring=metricas)
filas.append({
"Modelo": nombre,
"RMSE": -res["test_RMSE"].mean(),
"RMSE_std": res["test_RMSE"].std(),
"MAE": -res["test_MAE"].mean(),
"R2": res["test_R2"].mean(),
})
tabla = pd.DataFrame(filas).sort_values("RMSE").round(3)
print(tabla.to_string(index=False))Resultados orientativos (los tuyos variarán ligeramente en los decimales):
| Modelo | RMSE (CV) | MAE (CV) | R² (CV) | Lectura |
|---|---|---|---|---|
| HistGradientBoosting | ≈ 0,47 | ≈ 0,31 | ≈ 0,83 | El mejor: capta la no linealidad geográfica |
| Random Forest | ≈ 0,51 | ≈ 0,33 | ≈ 0,80 | Muy cerca, más lento y pesado |
| Lineal | ≈ 0,72 | ≈ 0,53 | ≈ 0,61 | Digno, pero ciego a las interacciones |
| Ridge | ≈ 0,72 | ≈ 0,53 | ≈ 0,61 | Regularizar no ayuda: no había overfitting lineal |
| Lasso | ≈ 0,72 | ≈ 0,53 | ≈ 0,61 | Ídem; con alpha alto empieza a anular coeficientes |
| Dummy (media) | ≈ 1,15 | ≈ 0,91 | ≈ 0,00 | El listón mínimo |
Tres lecturas honestas, en el espíritu de 06-05 "Overfitting y underfitting":
- Los modelos lineales sufren underfitting: con 8 features y 16.500 filas, el problema no es la varianza sino el sesgo — la relación precio-geografía no es lineal. Por eso Ridge y Lasso, que combaten el overfitting, no aportan nada aquí.
- La hipótesis del EDA se confirma: los modelos de árboles, capaces de particionar el espacio latitud-longitud en regiones, reducen el RMSE un 35 %.
- Las desviaciones típicas importan: si dos modelos difieren menos que la variabilidad entre particiones, declarar un "ganador" es ruido.
Análisis de errores
Antes de dar por bueno el mejor modelo, miramos dónde falla. Entrenamos el HistGradientBoosting sobre todo el train y analizamos sus residuos con una validación interna:
from sklearn.model_selection import cross_val_predict
mejor = HistGradientBoostingRegressor(random_state=42)
y_pred_cv = cross_val_predict(mejor, X_train, y_train, cv=cv)
residuos = y_train - y_pred_cv
fig, ejes = plt.subplots(1, 2, figsize=(13, 5))
ejes[0].scatter(y_pred_cv, residuos, s=5, alpha=0.3)
ejes[0].axhline(0, color="red", lw=1)
ejes[0].set_xlabel("Predicción"); ejes[0].set_ylabel("Residuo")
ejes[0].set_title("Residuos vs. predicción")
ejes[1].scatter(X_train["Longitude"], X_train["Latitude"],
c=residuos.abs(), cmap="Reds", s=8, alpha=0.4)
ejes[1].set_title("Error absoluto sobre el mapa")
plt.show()Qué buscar y qué suele aparecer:
- Una franja diagonal de residuos positivos en la derecha del primer gráfico: son los distritos censurados en 5,0. El modelo predice 3,5–4,5 para distritos que "valen 5+", y no puede hacerlo mejor porque la etiqueta está recortada. Es un límite del dato, no del modelo.
- En el mapa de errores, los fallos grandes se concentran en zonas costeras caras y en distritos atípicos (pocas viviendas, usos no residenciales). Saber dónde falla el modelo vale más que una décima de RMSE: orienta qué datos adicionales pedir.
Evaluación final en test
Solo ahora, con el modelo elegido y sin más decisiones pendientes, abrimos el test. Una única vez, como juraste en 06-01:
from sklearn.metrics import root_mean_squared_error, mean_absolute_error, r2_score
mejor.fit(X_train, y_train)
y_pred = mejor.predict(X_test)
print("RMSE test:", round(root_mean_squared_error(y_test, y_pred), 3))
print("MAE test:", round(mean_absolute_error(y_test, y_pred), 3))
print("R² test:", round(r2_score(y_test, y_pred), 3))Deberías obtener cifras muy parecidas a las de la validación cruzada (RMSE ≈ 0,46–0,48, R² ≈ 0,83). Esa coincidencia es la noticia: significa que el protocolo fue limpio y la estimación de CV era fiable. Si el test hubiera salido claramente peor, la sospecha sería una fuga de datos o decisiones sobreajustadas a la validación.
Traducción a negocio: un MAE de ≈ 0,31 significa que el modelo se equivoca, en mediana de distrito, unos 31.000 $ — frente a los 91.000 $ del baseline.
Errores Comunes y Consejos
- Explorar antes de dividir. Si calculas correlaciones o dibujas el mapa con el dataset completo, tus decisiones ya han "visto" el test. Divide primero, siempre.
- Escalar fuera del Pipeline. Ajustar
StandardScalersobre todos los datos y luego hacer CV es la fuga clásica de 06-01. ElPipelinelo hace bien automáticamente. - Ignorar la censura del objetivo. El pico en 5,0 no es un cluster de mercado: es un recorte del dataset. En un informe real habría que declararlo (y valorar excluir esos distritos o modelarlos aparte).
- Elegir modelo por una sola cifra. Compara RMSE, MAE y R² con su desviación entre particiones; dos modelos "distintos" en la tercera decimal son el mismo modelo.
- Interpretar R² como porcentaje de aciertos. R² = 0,83 no significa "acierta el 83 %": significa que explica el 83 % de la varianza del precio. La cifra en dólares (MAE/RMSE) es la que entiende negocio.
Retos para ampliar
- Features de interacción. Crea variables nuevas al estilo de 03-06 "Ingeniería de características":
habitaciones_por_ocupante = AveRooms / AveOccup,dormitorios_ratio = AveBedrms / AveRooms, o la distancia de cada distrito a las coordenadas de San Francisco y Los Ángeles. Pista: mide su efecto sobre la regresión lineal (donde más deberían ayudar) y sobre el boosting (que quizá ya las descubría solo). - XGBoost contra HistGradientBoosting. Instala
xgboosty enfréntalo al ganador con los hiperparámetros por defecto; después afina ambos conRandomizedSearchCVcomo en 07-05 "Optimización de hiperparámetros". Pista: empieza porn_estimators,learning_rateymax_depth. - Intervalos de predicción. Un precio puntual sin incertidumbre es media respuesta. Investiga
HistGradientBoostingRegressor(loss="quantile", quantile=0.05)y su gemelo con 0,95 para construir un intervalo del 90 %. Pista: comprueba qué fracción de precios reales del test cae dentro del intervalo — debería rondar el 90 %.
Conclusión
Primer proyecto completado: has recorrido tú solo el flujo entero que MercaFresh te enseñó por fases — definir el problema y su métrica, apartar el test antes de mirar nada, explorar con hipótesis (la geografía manda), preprocesar dentro de un Pipeline, escalar desde un baseline trivial hasta el gradient boosting, comparar con validación cruzada honesta, analizar residuos para saber dónde y por qué falla el modelo, y confirmar en test que nada se había contaminado. La lección de fondo: el mejor modelo no ganó por magia, sino porque el EDA ya anunciaba una estructura no lineal que los árboles capturan y una recta no. En el siguiente proyecto cambiamos por completo de terreno: de tablas de números a imágenes, donde comprobarás en carne propia por qué los métodos clásicos se quedan cortos y las redes convolucionales de 07-04 toman el relevo.
Curso de Machine Learning
Módulo 1: Introducción al Machine Learning
- ¿Qué es el Machine Learning?
- Historia y evolución del Machine Learning
- Tipos de Machine Learning
- Aplicaciones del Machine Learning
- El flujo de trabajo de un proyecto de Machine Learning
Módulo 2: Fundamentos de Estadística y Probabilidad
- Conceptos básicos de estadística
- Distribuciones de probabilidad
- Correlación y covarianza
- Inferencia estadística
- Teorema de Bayes
Módulo 3: Preprocesamiento de Datos
- Limpieza de datos
- Manejo de datos faltantes
- Transformación de datos
- Codificación de variables categóricas
- Normalización y estandarización
- Ingeniería de características
Módulo 4: Algoritmos de Machine Learning Supervisado
- Regresión lineal
- Regresión logística
- Árboles de decisión
- Máquinas de soporte vectorial (SVM)
- K-Vecinos más cercanos (K-NN)
- Naive Bayes
- Redes neuronales
Módulo 5: Algoritmos de Machine Learning No Supervisado
- Clustering: K-means
- Clustering jerárquico
- Análisis de componentes principales (PCA)
- Análisis de agrupamiento DBSCAN
- Visualización de datos con t-SNE y UMAP
Módulo 6: Evaluación y Validación de Modelos
- División de datos: entrenamiento, validación y prueba
- Métricas de evaluación
- Validación cruzada
- Curva ROC y AUC
- Overfitting y underfitting
Módulo 7: Técnicas Avanzadas y Optimización
- Regularización: Ridge, Lasso y Elastic Net
- Ensemble Learning
- Gradient Boosting
- Redes neuronales profundas (Deep Learning)
- Optimización de hiperparámetros
Módulo 8: Implementación y Despliegue de Modelos
- Frameworks y bibliotecas populares
- Implementación de modelos en producción
- Mantenimiento y monitoreo de modelos
- Consideraciones éticas y de privacidad
Módulo 9: Proyectos Prácticos
- Proyecto 1: Predicción de precios de viviendas
- Proyecto 2: Clasificación de imágenes
- Proyecto 3: Análisis de sentimientos en redes sociales
- Proyecto 4: Detección de fraudes
- Proyecto 5: Segmentación de clientes
