AI Hints
Home›Machine Learning Tutorial›Capstone: HomeWorth
Machine Learning · Lesson 69 of 85

Capstone: The Contest

A fair bake-off.

⏱ 2 min read▶ Video at 2:24:22🎓 Free
Watch this lesson in the Machine Learning courseWatch on YouTube ↗Full Machine Learning course

Now we find the champion. Remember our rule. Start with a baseline, because without one, you don't know whether any of this is working. Then give each model exactly the same fair test.

Capstone: The Contest
From the video: Capstone: The Contest
  • Find the champion
  • Baseline first
  • Every model gets the same test

Step three: the contest

Step three. We set up four contenders. A dummy baseline, which always guesses the average. Ridge. A random forest. And boosting. Each one is a pipeline, using our feature step, so nothing can leak. They all use the same five folds. Run it. The baseline's error is huge, which shows how much our models are adding. Ridge improves a lot. The forest is much stronger. And boosting takes first place. On tables of data, that's what we expected.

step3_contest.pypython
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import train_test_split

CITIES = {"SF": (37.77, -122.42), "LA": (34.05, -118.24),
          "SD": (32.72, -117.16), "SJ": (37.34, -121.89)}

class HouseFeatures(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        X = X.copy()
        X["BedroomShare"] = X["AveBedrms"] / X["AveRooms"]
        X["PeoplePerRoom"] = X["AveOccup"] / X["AveRooms"]
        for name, (lat, lon) in CITIES.items():
            X[name] = np.hypot(X["Latitude"] - lat, X["Longitude"] - lon)
        return X

df = fetch_california_housing(as_frame=True).frame
df = df[df["MedHouseVal"] < 5].reset_index(drop=True)
train, test = train_test_split(df, test_size=0.2, random_state=42)
X_train, y_train = train.drop(columns="MedHouseVal"), train["MedHouseVal"]
X_test, y_test = test.drop(columns="MedHouseVal"), test["MedHouseVal"]

from sklearn.dummy import DummyRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

models = {
    "Baseline": DummyRegressor(),
    "Ridge": make_pipeline(HouseFeatures(), StandardScaler(), Ridge()),
    "Forest": make_pipeline(HouseFeatures(),
                            RandomForestRegressor(60, n_jobs=-1, random_state=0)),
    "Boosting": make_pipeline(HouseFeatures(),
                              HistGradientBoostingRegressor(random_state=0)),
}
folds = KFold(5, shuffle=True, random_state=0)
for name, m in models.items():
    s = -cross_val_score(m, X_train, y_train, cv=folds,
                         scoring="neg_mean_absolute_error").mean()
    print(f"{name:<9} MAE ${s * 100000:,.0f}")
Output
Baseline  MAE $77,743
Ridge     MAE $46,143
Forest    MAE $29,575
Boosting  MAE $28,448

Step four: tune the champion

Step four. Now that we know the winner, let's tune it. Optuna searches over the learning rate, tree size, leaf size, and regularisation. We use three folds, ten trials, and a third of the rows, to keep this quick. Notice that we're still only using the training data. Run it. It reports the best settings that it found. The gain from tuning is usually modest, and that's how it goes. Most of our improvement came from cleaning, features, and picking the right kind of model.

step4_tune.pypython
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import train_test_split

CITIES = {"SF": (37.77, -122.42), "LA": (34.05, -118.24),
          "SD": (32.72, -117.16), "SJ": (37.34, -121.89)}

class HouseFeatures(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        X = X.copy()
        X["BedroomShare"] = X["AveBedrms"] / X["AveRooms"]
        X["PeoplePerRoom"] = X["AveOccup"] / X["AveRooms"]
        for name, (lat, lon) in CITIES.items():
            X[name] = np.hypot(X["Latitude"] - lat, X["Longitude"] - lon)
        return X

df = fetch_california_housing(as_frame=True).frame
df = df[df["MedHouseVal"] < 5].reset_index(drop=True)
train, test = train_test_split(df, test_size=0.2, random_state=42)
X_train, y_train = train.drop(columns="MedHouseVal"), train["MedHouseVal"]
X_test, y_test = test.drop(columns="MedHouseVal"), test["MedHouseVal"]

import optuna
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import make_pipeline

optuna.logging.set_verbosity(optuna.logging.WARNING)
folds = KFold(3, shuffle=True, random_state=0)

def objective(trial):
    m = make_pipeline(HouseFeatures(), HistGradientBoostingRegressor(
        learning_rate=trial.suggest_float("lr", 0.02, 0.2, log=True),
        max_leaf_nodes=trial.suggest_int("leaves", 15, 90),
        min_samples_leaf=trial.suggest_int("leaf", 5, 50),
        l2_regularization=trial.suggest_float("l2", 0.0, 5.0),
        max_iter=400, random_state=0))
    return -cross_val_score(m, X_train[::3], y_train[::3], cv=folds,
                            scoring="neg_mean_absolute_error").mean()

study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=0))
study.optimize(objective, n_trials=10)

print(f"Best MAE: ${study.best_value * 100000:,.0f}")
print({k: round(v, 3) for k, v in study.best_params.items()})
Output
Best MAE: $30,169
{'lr': 0.057, 'leaves': 58, 'leaf': 5, 'l2': 3.088}

Summary

Our champion is chosen and tuned, using nothing but training data. Now for the moment of truth. It's time to break the seal, and open the test set for the very first time.

Key takeaways
  • Baseline shows the value added
  • Boosting wins on tables
  • Tuning adds a modest gain
Next up: Capstone: The Moment of Truth — opening the sealed test set