Overfitting and Cross-Validation: Find What Really Makes a Better Cup
Hands-on lab · IDE in your browser

Overfitting and Cross-Validation: Find What Really Makes a Better Cup

Watch a model memorise 150 training rows, see how much a single validation split can lie, choose models by cross-validation, tame 60 noise columns with Ridge and Lasso, prove on pure noise that feature selection outside the pipeline fakes a good score, and finish with a grid search and one honest look at the test set.

Time
45 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit45 min · 5 stepsSession running
4 / 5 steps passingChoose by cross-validation, test once · step 5 of 5
roast.py▶ Run✓ Check
# ---------- Step 5: choose by CV, test once ----------ALPHAS = [0.01, 0.03, 0.1, 0.2, 0.3, 0.5, 1.0]  def final_model(X_train, y_train):    """A GridSearchCV over Lasso alphas (ALPHAS) with cv=FOLDS, fitted on the training set only."""   def setting_effects(search):    """The roast settings' coefficients in the best model (cup-score points per standard deviation), largest    effect first: [(name, coefficient rounded to 2), ...], settings only."""    
TerminalOutput

The job

Brewline Roasters has 200 roasts, each with six settings, sixty probe readings and a cup score from the tasting panel. The head roaster wants to know which settings make a better cup. With more columns than sense and few rows, every model you try will look better than it is, until you learn to measure it honestly.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Watch it overfit

    Brewline Roasters logged 200 roasts.

    You writedepth_curve()
  2. 2

    One split lies

    Step 1 picked a depth by looking at the test set, which turns the test set into training data: the next time you look, it can't give an honest answer.

    You writesplit_spread()cv_r2()
  3. 3

    Regularisation

    A linear model is simpler than a deep tree, but with 66 columns and 150 rows it still has room to fit noise: - On the training data it scores 0.90.

    You writelasso_path()
  4. 4

    Leakage inside cross-validation

    Cross-validation is only honest if every fold's model is built without its test rows.

    You writeleaky_cv()honest_cv()
  5. 5

    Choose by cross-validation, test once

    Put it together: 1.

    You writefinal_model()setting_effects()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Watch it overfit

Brewline Roasters logged 200 roasts. Each has six roast settings, 60 readings from a bank of temperature probes, and the cup score the tasting panel gave it. The head roaster wants to know which settings make a better cup. With 66 columns and only 150 training rows, a model has plenty of room to fool you.

A decision tree can keep splitting until every training roast sits in its own leaf. The question is what that does to roasts it has not seen.

Do this

1. Write depth_curve(): for each depth, fit a tree on the training set and record R² on train and test.

2. Run it. Find the depth where test R² peaks, and watch what the training score does after it.

roast.py, the file you edit91 lines
"""Which roast settings make a better cup? Brewline Roasters' 200 roasts, 66 measurements each."""
import warnings

import numpy as np
import pandas as pd
from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.linear_model import Lasso, LinearRegression, Ridge
from sklearn.model_selection import GridSearchCV, KFold, cross_val_score, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeRegressor

warnings.filterwarnings("ignore", category=UserWarning)
TARGET = "cup_score"
SETTINGS = ["charge_temp", "first_crack_min", "development_ratio", "bean_moisture", "drop_temp", "airflow_pct"]
FOLDS = KFold(n_splits=5, shuffle=True, random_state=0)


def load():
    """(X_train, X_test, y_train, y_test): 75/25 split, random_state=0."""
    df = pd.read_csv("roasts.csv")
    X, y = df.drop(columns=["roast_id", TARGET]), df[TARGET]
    return train_test_split(X, y, test_size=0.25, random_state=0)


# ---------- Step 1: watch it overfit ----------
def depth_curve(X_train, y_train, X_test, y_test, depths):
    """For each max_depth, a DecisionTreeRegressor(max_depth=d, random_state=0) fitted on the training set:
    [{"depth": d, "train": R² on train, "test": R² on test}, ...], scores rounded to 3."""
    # TODO (Step 1): for each d, fit DecisionTreeRegressor(max_depth=d, random_state=0) on the training set and
    # record .score() (R²) on train and on test, rounded to 3.
    raise NotImplementedError("Step 1: write depth_curve()")


# ---------- Step 2: one split lies, cross-validation averages ----------
def split_spread(model, X, y, n=20):
    """Hold out 25% of (X, y) with random_state 0..n-1, fit a fresh clone of model on the rest, score R² on the
    held-out part. Returns {"min", "max"} of the n scores, rounded to 3."""
    raise NotImplementedError("split_spread() arrives in Step 2")


def cv_r2(model, X, y):
    """Mean R² over FOLDS, rounded to 3."""
    raise NotImplementedError("cv_r2() arrives in Step 2")


# ---------- Step 3: regularisation ----------
def linear(alpha=None, kind="ridge"):
    """Scaled features into LinearRegression (alpha None), Ridge(alpha) or Lasso(alpha, max_iter=50000)."""
    if alpha is None:
        return make_pipeline(StandardScaler(), LinearRegression())
    reg = Ridge(alpha=alpha) if kind == "ridge" else Lasso(alpha=alpha, max_iter=50000)
    return make_pipeline(StandardScaler(), reg)


def lasso_path(X, y, alphas):
    """For each alpha: {"alpha", "cv": cv_r2 of the Lasso, "features": names with a non-zero coefficient after
    fitting on all of (X, y), in column order}."""
    raise NotImplementedError("lasso_path() arrives in Step 3")


# ---------- Step 4: leakage inside cross-validation ----------
def noise_data(n=150, p=1000, seed=0):
    """A dataset with no signal at all: n rows of p random columns and a random target."""
    rng = np.random.default_rng(seed)
    return pd.DataFrame(rng.normal(size=(n, p))).add_prefix("x"), pd.Series(rng.normal(size=n))


def leaky_cv(X, y, k=20):
    """Pick the k columns most correlated with y using ALL rows, then cross-validate a LinearRegression on them."""
    raise NotImplementedError("leaky_cv() arrives in Step 4")


def honest_cv(X, y, k=20):
    """The same selection and model, as one pipeline, so each fold picks its columns from its own training rows."""
    raise NotImplementedError("honest_cv() arrives in Step 4")


# ---------- Step 5: choose by CV, test once ----------
ALPHAS = [0.01, 0.03, 0.1, 0.2, 0.3, 0.5, 1.0]


def final_model(X_train, y_train):
    """A GridSearchCV over Lasso alphas (ALPHAS) with cv=FOLDS, fitted on the training set only."""
    raise NotImplementedError("final_model() arrives in Step 5")


def setting_effects(search):
    """The roast settings' coefficients in the best model (cup-score points per standard deviation), largest
    effect first: [(name, coefficient rounded to 2), ...], settings only."""
    raise NotImplementedError("setting_effects() arrives in Step 5")
Provided for you:roasts.csvtry_it.py

Frequently asked questions

How do I know if my model is overfitting?

Compare its score on the training data with its score on data it has not seen, such as cross-validation folds. A large gap, with training near perfect, means it has learned noise.

Why use cross-validation instead of one validation split?

A single split's score depends on which rows landed in it, and on small datasets it can vary widely. K-fold cross-validation averages over several splits and uses every row for both training and validation.

Can feature selection cause data leakage?

Yes. Choosing features on the full dataset before cross-validation lets every fold's test rows influence the model. On pure noise this can report a clearly positive score. Put selection inside a pipeline so each fold selects from its own training rows.

Overfitting, cross-validation and regularisation in scikit-learn

Overfitting is the default when you have many columns and few rows. The model fits the noise, the training score looks great, and new data disappoints. The tools that catch it are simple, but easy to use in a way that fools you. In this lab you watch a decision tree overfit, measure how much one validation split varies, compare models by k-fold cross-validation, regularise a linear model with Ridge and Lasso, demonstrate feature selection leakage on pure noise, and pick a final model by grid search before looking at the test set once.