Data Science & Machine Learning
ConceptBeginner 25 min· Machine learning · 9 of 20

Train, validation, test and overfitting

Learn how to measure honestly whether a model will work on new data: train, validation and test sets, cross-validation, overfitting and underfitting, and data leakage, with runnable examples.

Imagine a student who memorises last year's question paper word for word. They score 100% on that paper and fail the real exam, because the questions are different. Machine learning models can do the same thing. A model that only memorises its training data is useless on new data, and the whole point of ML is to work on data it has not seen.

This lesson explains how to measure honestly whether a model will work on new data, and what to do when it will not.

Train, validation and test sets

You split your data into parts, each with one job:

The three sets

SetTypical shareJobAnalogy
Training60% to 80%The model learns from itPractice questions
Validation10% to 20%You compare models and settings on itMock exams
Test10% to 20%One final score, used once at the endThe real exam

Why not just two sets? Every time you look at a score and then change something (a different model, a different setting), you slightly tune your choices to that data. After many rounds, the score on it is too optimistic. The test set stays untouched so that its score is an honest estimate. Use it once, after all decisions are made.

Three-way split and an overfitting experiment
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

# A noisy dataset: 15% of the labels are deliberately wrong, like real-life data.
X, y = make_classification(
    n_samples=400, n_features=10, n_informative=3, n_redundant=0,
    flip_y=0.15, class_sep=1.0, random_state=1,
)
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=1)
print(len(X_train), len(X_val), len(X_test))   # 240 80 80

for depth in [1, 2, 3, 5, 8, 12, None]:
    model = DecisionTreeClassifier(max_depth=depth, random_state=0)
    model.fit(X_train, y_train)
    print(depth, "train:", round(model.score(X_train, y_train), 3),
          "validation:", round(model.score(X_val, y_val), 3))

Splits 400 rows into 240 train, 80 validation and 80 test. Then trains decision trees of increasing depth and prints train and validation accuracy.

Output
240 80 80
1 train: 0.833 validation: 0.8
2 train: 0.846 validation: 0.8
3 train: 0.846 validation: 0.775
5 train: 0.929 validation: 0.838
8 train: 0.988 validation: 0.825
12 train: 1.0 validation: 0.825
None train: 1.0 validation: 0.825

A decision tree with no depth limit memorises the training set: 100% accuracy there, but only about 82% on validation. The deeper the tree, the higher the training score climbs, while the validation score stops improving. That growing gap is overfitting. (Your numbers may differ a little with other library versions.)

Overfitting and underfitting

Reading the gap

ProblemWhat it looks likeCauseFixes
Underfitting (high bias)Low score on training AND validationModel too simple, or features too weakMore expressive model, better features, less regularisation
Good fitGood scores, small gap between training and validationRight amount of complexityKeep it
Overfitting (high variance)High training score, much lower validation scoreModel too complex, too little data, noisy labelsMore data, simpler model, regularisation, early stopping, fewer features

Think of fitting a curve through dots on a graph. A straight line through dots that clearly curve underfits. A wiggly line that touches every dot, including the odd ones caused by measurement noise, overfits. The best line follows the trend and ignores the noise.

The tool is simple: always compare the training score with the validation score. A big gap means overfitting. Two low scores mean underfitting.

Cross-validation: a steadier estimate

With a small dataset, one validation split is a gamble: a lucky or unlucky split changes the score a lot (with 80 rows, one extra mistake moves accuracy by more than one percentage point). K-fold cross-validation solves this. Split the training data into k parts (folds). Train k times, each time holding out a different fold for validation, then average the k scores.

In 5-fold cross-validation each row is used for validation exactly once and for training four times.

Cross-validation to choose max_depth
from sklearn.model_selection import cross_val_score

for depth in [2, 3, 5, 8, None]:
    model = DecisionTreeClassifier(max_depth=depth, random_state=0)
    scores = cross_val_score(model, X_temp, y_temp, cv=5)
    print(depth, scores.round(2), "mean:", round(scores.mean(), 3))

final = DecisionTreeClassifier(max_depth=3, random_state=0)
final.fit(X_temp, y_temp)                        # refit on train + validation
print("Test accuracy:", round(final.score(X_test, y_test), 3))   # look at this once

Run this after the previous code cell: it reuses X_temp, y_temp (train plus validation, 320 rows), X_test, y_test and DecisionTreeClassifier.

Output
2 [0.8  0.77 0.81 0.84 0.89] mean: 0.822
3 [0.77 0.81 0.78 0.84 0.88] mean: 0.816
5 [0.78 0.78 0.78 0.88 0.83] mean: 0.809
8 [0.78 0.78 0.67 0.83 0.8 ] mean: 0.772
None [0.78 0.72 0.66 0.83 0.81] mean: 0.759
Test accuracy: 0.825

Cross-validation shows the trend clearly: shallow trees score about 0.82 and the unlimited tree scores about 0.76. The five scores per setting also show the spread. A model that scores 0.82 plus or minus 0.04 is not clearly better than one at 0.81.

The workflow: use cross-validation on the training data to choose a model and settings, then refit on all training data, then score once on the test set. The test score (0.825 here) is the number you report.

Time series is different

For classification, scikit-learn's cross_val_score uses stratified folds by default, which keeps class shares equal. For time-ordered data (sales by date, stock prices) never shuffle: train on the past and validate on the future, for example with TimeSeriesSplit.

Data leakage: the silent score inflator

Data leakage means information from outside the training data sneaks into training, so your validation score looks great but the model fails in real life. It is the most common reason a project works in the notebook and fails in production.

Typical causes:

- Fitting a scaler, imputer or feature selector on all the data before splitting. - A feature that contains the answer or is only known after the event (for example "cancellation_date" when predicting churn, or "loan_defaulted_flag" when predicting default). - The same customer, patient or image appearing in both train and test sets. - Shuffling time-ordered data so the model sees the future.

Leakage in action
import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

rng = np.random.default_rng(0)
X = rng.normal(size=(100, 5000))   # 5000 columns of pure noise
y = rng.integers(0, 2, size=100)   # random labels: there is nothing to learn

# Wrong: choose the 20 "best" columns using ALL rows, then cross-validate.
X_leaky = SelectKBest(f_classif, k=20).fit_transform(X, y)
print("Leaky  :", round(cross_val_score(LogisticRegression(max_iter=1000), X_leaky, y, cv=5).mean(), 2))

# Right: the selection happens inside the pipeline, so it only sees training folds.
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=1000))
print("Correct:", round(cross_val_score(pipe, X, y, cv=5).mean(), 2))

The labels here are random, so no model can honestly do better than 50% accuracy.

Output
Leaky  : 0.85
Correct: 0.48

Both versions have random labels, so the honest answer is about 50%. The leaky version picked the 20 columns that happened to match the labels using all rows, so cross-validation reported 85%. The correct version does the selection inside each training fold and reports about 48%, which is the truth. The fix is always the same: put every step that learns from data inside a Pipeline, and cross-validate the pipeline.

Too good to be true

If your model is suspiciously good (99% on a hard problem), suspect leakage before celebrating. Check which features matter most and ask whether each one would really be known at prediction time.

What to do about overfitting

A checklist

  1. Get more data, or more varied data, if you can.
  2. Use a simpler model or limit its complexity (for trees: max_depth, min_samples_leaf).
  3. Use regularisation: Ridge and Lasso for linear models, the C parameter in logistic regression (smaller C means stronger regularisation).
  4. Remove noisy or useless features.
  5. Use early stopping for boosting and neural networks: stop training when the validation score stops improving.
  6. Use ensembles such as random forests, which average many models.

How this comes up in interviews

Very common questions: "What is overfitting and how do you detect it?" (large gap between training and validation scores), "Why do we need a test set as well as a validation set?", "Explain k-fold cross-validation", "What is data leakage? Give an example" and "Your model has 99% accuracy; what do you check first?". Answer with a concrete example like the ones above.

Prompt · Review my evaluation setup for leakage
I am building a model to predict [target] from [describe your table and columns]. Here is my code:

[paste your code]

Review it for data leakage and for mistakes in how I split and evaluate. Check specifically: preprocessing fitted before splitting, features that would not be known at prediction time, duplicate or related rows across the split, and time order. List each problem and the fix.

What to expect: A good answer points to exact lines, explains why each is leakage, and suggests moving preprocessing into a Pipeline.

Try it: See the bias and variance trade-off yourself

Using the data from the first code cell, train RandomForestClassifier(max_depth=d, random_state=0) for d in 1, 2, 3, 5, 8, 12, None and print train and validation accuracy for each. Where does the gap start to widen? Then try min_samples_leaf=10 with no depth limit.

Key takeaways

  1. Training data teaches the model, validation data picks between options, test data gives one final honest score.
  2. Overfitting: high training score, much lower validation score. Underfitting: both scores low.
  3. Cross-validation averages k validation scores and shows how much the score varies.
  4. Data leakage makes scores look better than reality. Keep all learning steps inside a Pipeline.
  5. Use the test set once, at the very end. For time-ordered data, split by time, never by shuffling.
  6. Fix overfitting with more data, simpler models, regularisation, fewer features or early stopping.

Check yourself

Finishing the quiz marks the lesson as done.

1. A model scores 99% on training data and 70% on validation data. What is the most likely problem?

2. Why keep a test set that you use only once?

3. You scale all rows with StandardScaler and then run cross-validation. What is wrong?

4. You are predicting next month's sales from past data. How should you split it?

5. Which of these helps reduce overfitting?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free