Vuit mòduls amb MercaFresh t'han ensenyat el camí; ara toca recórrer-lo tot sol. En aquest primer projecte abandonem temporalment el supermercat per resoldre un problema clàssic de regressió de cap a cap: predir el preu mitjà de l'habitatge en districtes de Califòrnia a partir de dades del cens. Practicaràs el flux complet de les 7 fases que vas veure a 01-05 "El flux de treball d'un projecte de Machine Learning": definició, dades, exploració, preprocessament, modelatge, avaluació i conclusions. Farem servir el dataset California Housing, inclòs a scikit-learn (fetch_california_housing), de manera que no necessites comptes externs ni baixades de Kaggle: tot funciona amb el teu entorn habitual de Python.

Contingut

  1. Definició del problema i mètrica objectiu
  2. Obtenció i càrrega de les dades
  3. Anàlisi exploratòria (EDA)
  4. Preprocessament amb Pipeline
  5. Modelatge: del baseline als ensembles
  6. Comparació honesta amb validació creuada
  7. Anàlisi d'errors
  8. Avaluació final en test
  9. Conclusions del projecte

Definició del problema i mètrica objectiu

Abans d'escriure ni una línia de codi, definim el problema tal com vas aprendre a 01-05:

  • Pregunta de negoci: donat un districte de Califòrnia descrit per 8 variables del cens, estimar el valor medià dels seus habitatges.
  • Tipus de problema: regressió supervisada (la variable objectiu és contínua), com a 04-01 "Regressió lineal".
  • Mètrica principal: RMSE (arrel de l'error quadràtic mitjà), perquè penalitza més els errors grans i s'expressa en les mateixes unitats que el preu. L'acompanyarem amb el MAE (més robust als atípics) i el (proporció de variància explicada), les tres presentades a 06-02 "Mètriques d'avaluació".
  • Criteri d'èxit: batre amb claredat un baseline trivial i aconseguir un R² competitiu (per a aquest dataset, per sobre de 0,80 és un bon resultat).

El preu objectiu està expressat en centenars de milers de dòlars (un valor de 2,5 vol dir 250.000 $). Un detall important que descobrirem durant l'EDA: els preus estan censurats a 5,0 (tot districte per sobre de 500.000 $ apareix com a 5,00001).

Obtenció i càrrega de les dades

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_california_housing

dades = fetch_california_housing(as_frame=True)
df = dades.frame  # DataFrame amb features + target
print(df.shape)   # (20640, 9)
df.head()

Amb as_frame=True obtenim directament un DataFrame de pandas. El diccionari de dades:

Variable Significat Unitat
MedInc Ingrés medià del districte Desenes de milers de $
HouseAge Antiguitat mediana dels habitatges Anys
AveRooms Habitacions mitjanes per llar Habitacions
AveBedrms Dormitoris mitjans per llar Dormitoris
Population Població del districte Persones
AveOccup Ocupants mitjans per llar Persones
Latitude Latitud del districte Graus
Longitude Longitud del districte Graus
MedHouseVal Objectiu: preu medià Centenars de milers de $

La primera cosa, com sempre des de 06-01 "Divisió de dades: entrenament, validació i prova", és apartar el conjunt de test abans d'explorar res, perquè cap decisió no es contamini amb informació que no hauríem de veure:

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 / 4128

A partir d'aquí, tota l'EDA es fa només amb el conjunt d'entrenament. El test queda sota clau fins al final.

Anàlisi exploratòria (EDA)

Distribucions

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("Nuls per columna:\n", train.isna().sum())

Observacions clau (compara-les amb el que vegis a la pantalla):

  • No hi ha valors nuls: ens estalviem les estratègies de 03-02 "Gestió de dades mancants" — un luxe poc habitual en dades reals.
  • MedHouseVal té un pic artificial a 5,0: és la censura esmentada. Uns 800 districtes del train valen "500.000 $ o més"; el model no podrà distingir dins d'aquest grup.
  • MedInc i l'objectiu tenen asimetria positiva (cua a la dreta), el patró que vas aprendre a tractar amb logaritmes a 03-03 "Transformació de dades". Aquí l'asimetria del preu és moderada, així que provarem amb i sense transformació als reptes; en el cos del projecte treballarem amb el preu en la seva escala original per interpretar els errors en dòlars.
  • AveRooms, AveBedrms, Population i AveOccup tenen atípics extrems (districtes amb residències col·lectives, hotels, etc.): un AveOccup de 1.200 persones per llar no és una llar. Això afectarà els models lineals més que no pas els d'arbres, com vas raonar a 04-03 "Arbres de decisió".

Correlacions

Com a 02-03 "Correlació i covariància", mesurem la relació lineal de cada feature amb l'objectiu:

corr = train.corr(numeric_only=True)["MedHouseVal"].sort_values(ascending=False)
print(corr.round(3))

Resultat típic: MedInc domina (≈ 0,69), seguida molt de lluny per AveRooms (≈ 0,15); Latitude i Longitude tenen correlacions lineals petites i de signe negatiu, però això no vol dir que no importin: la seva relació amb el preu no és lineal, és geogràfica.

El mapa: latitud i longitud acolorides per preu

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="Preu media (x100.000 USD)")
plt.xlabel("Longitud")
plt.ylabel("Latitud")
plt.title("California: el preu es geografia")
plt.show()

El dibuix que apareix és un mapa de Califòrnia: els preus alts es concentren a la costa (badia de San Francisco, Los Angeles, San Diego) i cauen cap a l'interior. Aquesta estructura espacial és exactament la mena de patró no lineal i interactiu (importa la combinació latitud-longitud, no cadascuna per separat) on els arbres i ensembles del mòdul 7 haurien de superar la regressió lineal. Acabem de formular una hipòtesi comprovable — això és fer EDA amb propòsit.

Preprocessament amb Pipeline

Totes les variables són numèriques, així que el preprocessament és senzill: estandarditzar per als models sensibles a l'escala (lineal, Ridge, Lasso — recorda 03-05 "Normalització i estandardització") i deixar les dades tal com són per als d'arbres, que són invariants a transformacions monòtones. Ho encapsulem en un Pipeline com vas aprendre al final del mòdul 3, perquè l'escalat s'ajusti només amb el train de cada partició i evitar les fuites de dades 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

models = {
    "Dummy (mitjana)": 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),
}

Fixa't que el diccionari ja explica la història del curs: el baseline trivial, la regressió lineal de 04-01, les versions regularitzades de 07-01 "Regularització: Ridge, Lasso i Elastic Net", el Random Forest de 07-02 "Ensemble Learning" i el gradient boosting per histogrames de 07-03 "Gradient Boosting".

Modelatge: del baseline als ensembles

Per què començar amb DummyRegressor

El baseline respon una pregunta imprescindible: fins a quin punt és bo "no saber res"? DummyRegressor(strategy="mean") prediu sempre el preu mitjà. El seu RMSE és, essencialment, la desviació estàndard de l'objectiu (≈ 1,15, és a dir, ±115.000 $). Qualsevol model que no el bati amb folgança no ha après res; i la seva xifra dona escala a totes les altres.

Comparació honesta amb validació creuada

Comparem tots els models amb validació creuada de 5 particions sobre el train, com vas aprendre a 06-03 "Validació creuada": cap model no veu el test, i cada xifra ve amb la seva variabilitat.

from sklearn.model_selection import cross_validate, KFold

cv = KFold(n_splits=5, shuffle=True, random_state=42)
metriques = {
    "RMSE": "neg_root_mean_squared_error",
    "MAE": "neg_mean_absolute_error",
    "R2": "r2",
}

files = []
for nom, model in models.items():
    res = cross_validate(model, X_train, y_train, cv=cv, scoring=metriques)
    files.append({
        "Model": nom,
        "RMSE": -res["test_RMSE"].mean(),
        "RMSE_std": res["test_RMSE"].std(),
        "MAE": -res["test_MAE"].mean(),
        "R2": res["test_R2"].mean(),
    })

taula = pd.DataFrame(files).sort_values("RMSE").round(3)
print(taula.to_string(index=False))

Resultats orientatius (els teus variaran lleugerament en els decimals):

Model RMSE (CV) MAE (CV) R² (CV) Lectura
HistGradientBoosting ≈ 0,47 ≈ 0,31 ≈ 0,83 El millor: capta la no linealitat geogràfica
Random Forest ≈ 0,51 ≈ 0,33 ≈ 0,80 Molt a prop, més lent i pesant
Lineal ≈ 0,72 ≈ 0,53 ≈ 0,61 Digne, però cec a les interaccions
Ridge ≈ 0,72 ≈ 0,53 ≈ 0,61 Regularitzar no ajuda: no hi havia overfitting lineal
Lasso ≈ 0,72 ≈ 0,53 ≈ 0,61 Ídem; amb un alpha alt comença a anul·lar coeficients
Dummy (mitjana) ≈ 1,15 ≈ 0,91 ≈ 0,00 El llistó mínim

Tres lectures honestes, en l'esperit de 06-05 "Overfitting i underfitting":

  1. Els models lineals pateixen underfitting: amb 8 features i 16.500 files, el problema no és la variància sinó el biaix — la relació preu-geografia no és lineal. Per això Ridge i Lasso, que combaten l'overfitting, aquí no aporten res.
  2. La hipòtesi de l'EDA es confirma: els models d'arbres, capaços de particionar l'espai latitud-longitud en regions, redueixen el RMSE un 35 %.
  3. Les desviacions estàndard importen: si dos models difereixen menys que la variabilitat entre particions, declarar un "guanyador" és soroll.

Anàlisi d'errors

Abans de donar per bo el millor model, mirem on falla. Entrenem el HistGradientBoosting sobre tot el train i n'analitzem els residus amb una validació interna:

from sklearn.model_selection import cross_val_predict

millor = HistGradientBoostingRegressor(random_state=42)
y_pred_cv = cross_val_predict(millor, X_train, y_train, cv=cv)
residus = y_train - y_pred_cv

fig, eixos = plt.subplots(1, 2, figsize=(13, 5))
eixos[0].scatter(y_pred_cv, residus, s=5, alpha=0.3)
eixos[0].axhline(0, color="red", lw=1)
eixos[0].set_xlabel("Prediccio"); eixos[0].set_ylabel("Residu")
eixos[0].set_title("Residus vs. prediccio")

eixos[1].scatter(X_train["Longitude"], X_train["Latitude"],
                 c=residus.abs(), cmap="Reds", s=8, alpha=0.4)
eixos[1].set_title("Error absolut sobre el mapa")
plt.show()

Què cal buscar-hi i què hi sol aparèixer:

  • Una franja diagonal de residus positius a la dreta del primer gràfic: són els districtes censurats a 5,0. El model prediu 3,5–4,5 per a districtes que "valen 5+", i no pot fer-ho millor perquè l'etiqueta està retallada. És un límit de la dada, no del model.
  • Al mapa d'errors, les errades grans es concentren en zones costaneres cares i en districtes atípics (pocs habitatges, usos no residencials). Saber on falla el model val més que una dècima de RMSE: orienta quines dades addicionals cal demanar.

Avaluació final en test

Només ara, amb el model triat i sense més decisions pendents, obrim el test. Una única vegada, tal com vas jurar a 06-01:

from sklearn.metrics import root_mean_squared_error, mean_absolute_error, r2_score

millor.fit(X_train, y_train)
y_pred = millor.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("R2   test:", round(r2_score(y_test, y_pred), 3))

Hauries d'obtenir xifres molt semblants a les de la validació creuada (RMSE ≈ 0,46–0,48, R² ≈ 0,83). Aquesta coincidència és la notícia: vol dir que el protocol va ser net i que l'estimació de CV era fiable. Si el test hagués sortit clarament pitjor, la sospita seria una fuita de dades o decisions sobreajustades a la validació.

Traducció a negoci: un MAE de ≈ 0,31 significa que el model s'equivoca, en mediana de districte, uns 31.000 $ — davant dels 91.000 $ del baseline.

Errors Comuns i Consells

  • Explorar abans de dividir. Si calcules correlacions o dibuixes el mapa amb el dataset complet, les teves decisions ja han "vist" el test. Divideix primer, sempre.
  • Escalar fora del Pipeline. Ajustar StandardScaler sobre totes les dades i després fer CV és la fuita clàssica de 06-01. El Pipeline ho fa bé automàticament.
  • Ignorar la censura de l'objectiu. El pic a 5,0 no és cap clúster de mercat: és una retallada del dataset. En un informe real caldria declarar-ho (i valorar excloure aquests districtes o modelar-los a part).
  • Triar model per una sola xifra. Compara RMSE, MAE i R² amb la seva desviació entre particions; dos models "diferents" en la tercera decimal són el mateix model.
  • Interpretar el R² com un percentatge d'encerts. R² = 0,83 no vol dir "encerta el 83 %": vol dir que explica el 83 % de la variància del preu. La xifra en dòlars (MAE/RMSE) és la que entén negoci.

Reptes per ampliar

  1. Features d'interacció. Crea variables noves a l'estil de 03-06 "Enginyeria de característiques": habitacions_per_ocupant = AveRooms / AveOccup, dormitoris_ratio = AveBedrms / AveRooms, o la distància de cada districte a les coordenades de San Francisco i Los Angeles. Pista: mesura'n l'efecte sobre la regressió lineal (on més haurien d'ajudar) i sobre el boosting (que potser ja les descobria tot sol).
  2. XGBoost contra HistGradientBoosting. Instal·la xgboost i enfronta'l al guanyador amb els hiperparàmetres per defecte; després afina'ls tots dos amb RandomizedSearchCV com a 07-05 "Optimització d'hiperparàmetres". Pista: comença per n_estimators, learning_rate i max_depth.
  3. Intervals de predicció. Un preu puntual sense incertesa és mitja resposta. Investiga HistGradientBoostingRegressor(loss="quantile", quantile=0.05) i el seu bessó amb 0,95 per construir un interval del 90 %. Pista: comprova quina fracció de preus reals del test cau dins de l'interval — hauria de rondar el 90 %.

Conclusió

Primer projecte completat: has recorregut tot sol el flux sencer que MercaFresh t'havia ensenyat per fases — definir el problema i la seva mètrica, apartar el test abans de mirar res, explorar amb hipòtesis (la geografia mana), preprocessar dins d'un Pipeline, escalar des d'un baseline trivial fins al gradient boosting, comparar amb validació creuada honesta, analitzar residus per saber on i per què falla el model, i confirmar en test que res no s'havia contaminat. La lliçó de fons: el millor model no va guanyar per art de màgia, sinó perquè l'EDA ja anunciava una estructura no lineal que els arbres capturen i una recta no. En el pròxim projecte canviem completament de terreny: de taules de números a imatges, on comprovaràs en primera persona per què els mètodes clàssics es queden curts i les xarxes convolucionals de 07-04 agafen el relleu.

Curs de Machine Learning

Mòdul 1: Introducció al Machine Learning

Mòdul 2: Fonaments d'Estadística i Probabilitat

Mòdul 3: Preprocessament de Dades

Mòdul 4: Algorismes de Machine Learning Supervisat

Mòdul 5: Algorismes de Machine Learning No Supervisat

Mòdul 6: Avaluació i Validació de Models

Mòdul 7: Tècniques Avançades i Optimització

Mòdul 8: Implementació i Desplegament de Models

Mòdul 9: Projectes Pràctics

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats