California Housing

Median house values for 20,640 California census block groups from the 1990 census. The modern default for “show me a real regression problem”: big enough to be non-trivial, small enough to fit anywhere, and full of instructive pathologies — capped targets, aggregate features, and spatial structure.

Provenance

Constructed by R. Kelley Pace and Ronald Barry for Sparse Spatial Autoregressions, Statistics and Probability Letters 33(3), 1997 — the point of the paper was spatial statistics, not machine learning. The data derive from the 1990 US census at the block group level (the smallest census unit, typically 600 to 3,000 people). It was long distributed via CMU’s StatLib archive; sklearn’s fetch_california_housing mirrors that original. A cosmetically extended variant (with an ocean_proximity categorical) is the running example in Geron’s Hands-On Machine Learning, chapter 2 — numbers from the two variants are not interchangeable.

Schema

  • instances: 20,640 block groups. No missing values.
  • features (8, all numeric): MedInc (median income, in $10k), HouseAge, AveRooms, AveBedrms, Population, AveOccup, Latitude, Longitude.
  • target: median house value of the block group, in units of $100,000.
  • quirks worth knowing:
    • the target is censored: values were capped at $500,000 — 965 block groups (4.7%) sit exactly on the ceiling, which any honest error analysis will notice as a horizontal stripe in the residuals.
    • features are block-group aggregates, not houses: AveRooms and AveOccup have wild outliers where a block group is dominated by non-residential quirks (resorts, institutions).
    • rows are ordered geographically, which matters for cross-validation (see below).

Benchmarks

Two baselines, 5-fold cross-validated, target in $100k units:

import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score

X, y = fetch_california_housing(return_X_y=True)
shuffled = KFold(5, shuffle=True, random_state=0)  # rows are ordered geographically!
for name, reg in [("ols", LinearRegression()),
                  ("hist gradient boosting", HistGradientBoostingRegressor(random_state=0))]:
    for label, cv in [("contiguous folds", 5), ("shuffled folds", shuffled)]:
        mse = -cross_val_score(reg, X, y, cv=cv, scoring="neg_mean_squared_error").mean()
        print(f"{name:<23} {label:<17} rmse {np.sqrt(mse):.3f} (~${np.sqrt(mse)*100_000:,.0f})")

Reference points: plain OLS sits around 0.73 RMSE (about $73k of error on a median-value target), and tuned tree ensembles reach roughly 0.45 to 0.47 with shuffled splits. The gap between the two boosting rows is the interesting bit — because rows are ordered by geography, unshuffled k-fold asks the model to extrapolate into unseen regions of the state, and latitude/longitude splits stop paying off. Flexible models lose far more than linear ones. Which number is “right” depends on the question: shuffled folds estimate interpolation error, contiguous folds are closer to a spatial generalisation test. Say which one you are reporting.

On this site

  • ordinary least squares — the natural first model here, and the source of the linear baseline above.
  • regularised regression — ridge/lasso on the correlated aggregate features (AveRooms vs AveBedrms) is the standard follow-up.
  • the homl3 notebooks in the 10khrs repo work the extended Geron variant end-to-end (chapter 2’s project).

Loading

First call downloads about 400 KB from StatLib mirrors and caches under ~/scikit_learn_data:

from sklearn.datasets import fetch_california_housing

cal = fetch_california_housing()
X, y = cal.data, cal.target
print(X.shape)
print(cal.feature_names)
print(f"target: median house value in $100k, min {y.min():.5f}, max {y.max():.5f}")
print(f"capped at $500k: {(y == y.max()).sum()} block groups sit on the ceiling")