DataLens

Overfitting, underfitting, and generalization

See how a model can be too simple or too flexible, measure it with training vs test error, and learn the habits that keep models honest.

Level: Foundations · 7 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

The goal of a model isn't to fit the training data. It's to generalise: to make good predictions on new data it has never seen. Two things can go wrong:

Analogy

Studying for an exam. Underfitting is skimming the summary and missing the ideas. Overfitting is memorising last year's answers word for word: perfect on those exact questions, lost on new ones. Generalising is understanding the material.

An experiment

Take a small sample of days: ad spend vs coffee units, which has a real but noisy upward relationship. Fit polynomials of increasing degree (a degree-1 polynomial is a straight line; higher degrees can bend more) and compare training and test error.

import numpy as np
import pandas as pd

demand = pd.read_csv("daily_demand.csv")
rng = np.random.default_rng(19)
sample = demand.iloc[rng.choice(len(demand), size=30, replace=False)]
train, test = sample.iloc[:20], sample.iloc[20:]


def fit_and_score(degree):
    coefs = np.polyfit(train["ad_spend"], train["units"], degree)
    train_mse = np.mean((np.polyval(coefs, train["ad_spend"]) - train["units"]) ** 2)
    test_mse = np.mean((np.polyval(coefs, test["ad_spend"]) - test["units"]) ** 2)
    return float(round(train_mse, 1)), float(round(test_mse, 1))


for degree in [0, 1, 3, 9, 15]:
    print(degree, fit_and_score(degree))
0 (138.8, 170.5)
1 (85.7, 94.2)
3 (84.0, 110.8)
9 (31.0, 531.1)
15 (3.8, 9636.3)

(You may also see a RankWarning: NumPy warning that a degree-15 polynomial on 20 points is numerically shaky. That's the point.)

Read the table from top to bottom:

With 20 training points, a degree-15 polynomial has enough flexibility to almost pass through every point. It "learned" the noise.

Key idea

Training error always improves with more flexibility; it's a useless guide to model quality on its own. Judge models by their error on data they didn't train on.

The two symptoms

Training errorTest errorFix
Underfittinghighhighmore flexible model, better features
Good fitlowlow, close to trainingkeep it
Overfittingvery lowmuch highersimpler model, more data, regularisation

The gap between training and test error is the signature of overfitting.

Memorising: the extreme case

A "model" that just stores the training data in a dictionary has zero training error:

lookup = dict(zip(train["date"], train["units"]))
train_errors = [lookup[d] - u for d, u in zip(train["date"], train["units"])]
print("training MSE:", np.mean(np.square(train_errors)))
print("can predict a new date?", test["date"].iloc[0] in lookup)
training MSE: 0.0
can predict a new date? False

Perfect on the past, useless for the future. Every overfit model is a softer version of this lookup table.

What helps generalisation

A quick check you can always do

How do you know whether your own model is overfitting? Compare how wrong it is on the data it trained on with how wrong it is on data it has never seen. Here's that check for a model that predicts daily units sold from the weather, the day of the week, promotions and ad spend:

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

X = demand[["temperature", "weekend", "promo", "ad_spend"]]
y = demand["units"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
model = LinearRegression().fit(X_train, y_train)
train_mse = np.mean((model.predict(X_train) - y_train) ** 2)
test_mse = np.mean((model.predict(X_test) - y_test) ** 2)
print(round(train_mse, 1), round(test_mse, 1))
23.4 29.4

Training and test errors are close: this simple linear model generalises well. If the test error were several times the training error, you'd have an overfitting problem to fix before trusting it.

Why this matters for clean code

Always reporting both training and test performance is the ML equivalent of writing tests: it catches the bug (overfitting) that looks like success. Build the comparison into your workflow (a small evaluate(model) function that prints both), and you'll never ship a model that only works on yesterday's data.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Linear regression (the math intuition) →