Splitting the data
Lesson 3 of 8
3Splitting the data
The big pictureA number measured on data the model has seen says nothing about the future. A correct split is what makes your numbers an honest promise, and leakage is what turns them into a convincing lie.
What the picture shows: Data passes through three parts in a fixed order: the model learns from training, you choose on validation, and the test set is opened once at the end. Choose on the test set and its number becomes optimistic, no longer an honest promise.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Give a student the exact exam questions to practise and they score high, yet you will not know whether they understood. Models are the same. So we split the data: one part to train on, one to tune settings with, and one hidden until the end and used for a single exam.
train, validation and test splits, or k-fold cross-validation. A stratified split keeps class ratios, and a time-based split stops you seeing the future. data leakage (like fitting a scaler on all data before splitting) makes every number after it a lie.
Before You Start
🎯The baseline
You know accuracy and the naive baseline from the framing lesson.
📏Logistic regression
You train a model with fit and measure it with score, as in the regression lesson.
The Core Idea
A number measured on data the model has seen says nothing about the future. Here you build a three-part split, see how the answer leaks, measure safely with cross-validation, and split by time.
1Three parts, one job each
Training learns the weights, validation picks the model and settings, and test gives one final number. Look at the picture: the row count of each part of the breast cancer data.
Training is what the model learns from. Validation is where you choose between models and settings. Test is opened once at the end to get the number you will report. We split with train_test_split twice, with stratify so the class balance stays the same in all three parts.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
X, y = load_breast_cancer(return_X_y=True)
X_rest, X_test, y_rest, y_test = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y)
X_train, X_val, y_train, y_val = train_test_split(X_rest, y_rest, test_size=0.25, random_state=0, stratify=y_rest)
for name, part in [("train", y_train), ("val", y_val), ("test", y_test)]:
print(f"{name:5} rows={len(part):3} positive={part.mean():.2f}")
60%, 20% and 20%, with the positive share at 0.62 to 0.63 in every part. Without stratify a small part can land with a different balance by chance, and you compare models on data unlike reality.
2Why two parts are not enough
Choosing is a kind of training too. Look at the picture: you try many settings and pick the best on validation, and only then reach the test set, once.
If you try twenty settings and pick the best on the test data, you have "trained" your choice on it. The winning number is optimistic because you selected it from twenty attempts. Validation absorbs that selection, and the test set stays clean.
3Leakage: a model out of noise
Leakage is information from evaluation data reaching training. Its most dangerous form is processing done before the split.
Completely random data: two thousand columns of random numbers and a random output. There is no real pattern, so any honest model should land near 50%. Watch what happens when we pick the "best" ten columns before splitting.
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
rng = np.random.default_rng(0)
X = rng.normal(size=(100, 2000))
y = rng.integers(0, 2, size=100)
def top_features(X, y, k=10):
corr = np.abs([np.corrcoef(X[:, j], y)[0, 1] for j in range(X.shape[1])])
return np.argsort(corr)[-k:]
best = top_features(X, y)
X_tr, X_te, y_tr, y_te = train_test_split(X[:, best], y, test_size=0.5, random_state=0)
leaky = LogisticRegression().fit(X_tr, y_tr).score(X_te, y_te)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.5, random_state=0)
best = top_features(X_tr, y_tr)
honest = LogisticRegression().fit(X_tr[:, best], y_tr).score(X_te[:, best], y_te)
print(f"leaky accuracy on pure noise: {leaky:.2f}")
print(f"honest accuracy on pure noise: {honest:.2f}")
74% on pure noise. The reason: feature selection saw the test set, so it chose columns that happen to correlate with the test outputs. The honest way, selecting from the training half only, gives 46%, chance, as it should. Ship the first model and it collapses immediately.
Look at the picture again: the code runs and the number is great, and nothing says there is a bug. So you hunt for leakage on purpose, and the rule is simple: split first, then do everything else on training only.
4Cross-validation
One number from one split may be luck. Cross-validation cuts the data into five folds and makes each fold the validation set once.
make_pipeline ties preprocessing to the model, so scaling is fitted inside each training fold separately. It is the only safe way to cross-validate with any preprocessing.
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_breast_cancer(return_X_y=True)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
scores = cross_val_score(model, X, y, cv=5)
print("folds:", [round(float(s), 3) for s in scores])
print(f"mean={scores.mean():.3f} std={scores.std():.3f}")
Five results between 0.974 and 0.991. The 0.007 spread says a gap under about 0.01 between two models may be split luck rather than real improvement. Always report the mean and the spread together.
5Splitting by time
If the model will be used on the future, like sales or fraud, a random split shows it tomorrow to predict yesterday. Train on the past and evaluate on what follows.
import pandas as pd
days = pd.date_range("2026-01-01", periods=10, freq="D")
df = pd.DataFrame({"day": days, "sales": [12, 15, 14, 18, 20, 19, 23, 25, 24, 28]})
cut = int(len(df) * 0.7)
train, test = df.iloc[:cut], df.iloc[cut:]
print("train:", train.day.min().date(), "->", train.day.max().date())
print("test: ", test.day.min().date(), "->", test.day.max().date())
print("no future row in train:", train.day.max() < test.day.min())
Sort by time, then cut, no shuffling. For time-aware cross-validation scikit-learn offers TimeSeriesSplit, which always trains on the past and evaluates on what follows.
How It Works, Step by Step: how to split data without leakage
Hold out the test set first, 20% with train_test_split and stratify, and do not look at it again.
Split the rest into training and validation, giving 60%, 20% and 20%.
Fit every preprocessing step, from scaling to feature selection, on training only, ideally inside make_pipeline.
Choose the model and settings on validation, then open the test set once, for the winner only.
Worked example
Picture a student who saw the exam questions beforehand. A perfect score tells you nothing about understanding. A model is the same: you must hold back data it never saw to measure it on.
GroupShuffleSplit.
Try it without code
Myth vs Reality
| Myth | Reality |
|---|---|
| Two parts, training and test, are enough. | If you choose among twenty settings on the test set, the winner's number becomes optimistic. Validation absorbs that selection and keeps the test set clean. |
| Leakage shows up as a bug in the code. | The code runs and the numbers look great, then the model collapses in production. Selecting features before the split gave 74% on pure noise. |
| A random split is fine for any data. | On time-ordered data the model sees tomorrow to predict yesterday. Train on the past and measure on what follows. |
Try it yourself
You tried 30 settings on the test set and picked the best at 91%. What does that number honestly describe?
Picking the best of thirty attempts on the same data selects some luck along with quality, so the winning number leans high. The fix: choose on validation, then open the test set once, for the winner only.
You are predicting next week's restaurant demand from two years of data. Which split is correct?
The model will run on the future, so it must be evaluated on a future it has not seen. A random split lets it learn patterns from days after the test day, like a holiday or a season, so the numbers look better than they will be.
Quick review cards
Check Your Understanding
You scaled all the data with StandardScaler, then split it. Why is this leakage, and why is its effect usually small anyway?
Because the mean and spread used for scaling were computed from test rows too, so training "knows" something about the test distribution. The effect is usually small because the mean of thousands of rows barely moves when the test rows are added. But the rule matters, since the same pattern with feature selection or target-based filling produces huge leakage, as the noise example showed.
Why do we use stratify when splitting?
So the class balance stays the same in every part, as positives stayed at 0.62 to 0.63 across the three parts. Without it a small part can land with a different balance by chance.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs2
Done with this lesson?
Sign in to save your progress.