Train, validation and test sets
Hold data back so you can trust your results.
Create a free account Try part of it here
Why it matters
A model can learn its training data by heart. Then it scores perfectly on that data and is useless on new customers. The only honest way to judge a model is on data it has never seen. So you hold some data back and keep it out of training. This is the same idea as the eval set for prompts in Phase 1. The same mistake ruins both: tuning on the data you use for the final score.
What you'll be able to do
- Explain why a model must be scored on data it never trained on
- Give train, validation and test sets their separate jobs
- Split randomly with a seed, stratify by label, or split by time
- Recognize the ways test data leaks into training
What it covers
- Scoring on training data lies
- Three sets, three jobs
- How to split
- Worked example: 20 customers, 4 churners
- How test data leaks into training
Scoring on training data lies
Picture a student who gets a copy of the exam answers the night before. They memorize every answer and score 100%. Does that prove they understand the subject? No. Give them a fresh exam with new questions and you'll find out what they really learned.
A model can do the same thing. It can memorize the examples it trained on, quirks and all. So a score on those same examples tells you how good its memory is. It doesn't tell you how well it will predict.
The fix is to keep some data back. You train on one part, then score on the part the model never saw. That kept-back part is called held-out data.
The code below loads a table of customers. Each row says whether that customer churned, which means they stopped buying. It trains a decision tree, a model that learns a flowchart of yes/no questions. This tree is allowed to grow as deep as it likes.
The code then prints three numbers. Each one is an accuracy: the share of customers the model labels correctly. The first is on the customers the tree learned from. The second is on customers it didn't see. The third is a baseline, a dumb rule to compare against: predict "stays" for everyone.
import pandas as pd import requests from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"]) features = ["tenure_months", "orders_last_year", "avg_order_value", "days_since_last_order", "support_tickets", "trail_club"] X_train, X_test, y_train, y_test = train_test_split(df[features], df["churned"], test_size=0.25, random_state=0, stratify=df["churned"]) tree = DecisionTreeClassifier(random_state=0).fit(X_train, y_train) print("accuracy on training data:", round(tree.score(X_train, y_train), 3)) print("accuracy on held-out data:", round(tree.score(X_test, y_test), 3)) print("always predicting 'stays': ", round(1 - y_test.mean(), 3))
The tree is perfect on its training data. On new customers it does worse than the baseline. Saying "stays" for everyone beats it. The tree memorized noise, meaning random quirks of those particular customers that don't carry over to anyone else. Without the held-out data, you'd have believed the perfect score and shipped it.
Show the answer
70%, the held-out score, and it's worse than the 81% baseline
Only the held-out score measures prediction on unseen customers. Here it falls below the simple baseline, so the model memorized instead of learning.
Three sets, three jobs
Think of a school course. You learn from the textbook and homework. You check your progress on practice tests, and you change how you study based on them. At the end there's one final exam that you've never seen.
Data gets split the same way, into three parts:
| Set | Typical share | Used for |
|---|---|---|
| Training | 60–80% | fitting the model's parameters |
| Validation | 10–20% | choosing between models and settings (tree depth, features, learning rate) |
| Test | 10–20% | one final, honest estimate, used once at the end |
- The training set is the homework. The model learns its parameters from it. Parameters are the numbers inside the model that training adjusts.
- The validation set is the practice tests. You use it to compare choices you make yourself. Examples are how deep the tree may grow or which columns to use. These choices are called settings (or hyperparameters).
- The test set is the final exam. You use it once, at the very end, for the score you report.
Why you need a validation set
Why not compare settings on the test set? Say you try some settings, score them on a set of data, and keep the winner. Each time you do that, you fit a little to that data. Part of the winner's score is luck. It happened to suit those particular rows.
Do that fifty times on the test set and its score is no longer honest. It has become a practice test. So you tune on validation and touch the test set once.
When you have little data, holding back a fixed validation set wastes too much of it. Cross-validation takes its place. It gets its own lesson later.
Go deeper Why the winner's score is too high
Say 40 settings all have the same true accuracy, 80%. On a test set of a few hundred rows, each one's score wobbles a little by chance. Some land at 78%, some at 82%. If you pick the highest, you pick the setting that got the luckiest rows. Its score overstates what it will do on new data. The more settings you try, the bigger that luck can be. A fresh test set, untouched during the choosing, has no such bias.
Each set answers one question. Mixing their jobs is how inflated scores happen.
Show the answer
The test set was used for choosing, so its score is optimistic; tune on validation, test once
Choosing by a set makes its score biased upward. The winner partly won by luck on those rows.
How to split
There are four common ways to cut your data. Which one you use depends on what the data looks like.
- Random, with a seed. Shuffle the rows and cut. A seed is a starting number for the shuffle. The same seed always gives the same shuffle. In scikit-learn you set it with
random_state, so the split is repeatable. - Stratified. Here about 19% of customers churn and 81% stay. A random cut can leave a test set with too few churners. A stratified split keeps the share of each label the same in every part.
- By time. Say you'll use the model to predict the future. Then train on the past and test on the most recent period. A random split lets the model peek at future patterns. It can't do that once it's in real use.
- By group. Sometimes one customer has many rows, one per order. Keep all of a customer's rows in the same part. Otherwise the model can recognize the customer instead of learning the pattern.
The code below makes small test sets, 10% of the customers each time, five times over. It prints the churn rate in each test set. The first line uses plain random splits. The second uses stratified splits.
import numpy as np import pandas as pd import requests from sklearn.model_selection import train_test_split df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"]) y = df["churned"] for strat in [None, y]: rates = [] for seed in range(5): _, _, _, y_test = train_test_split(df, y, test_size=0.1, random_state=seed, stratify=strat) rates.append(round(float(y_test.mean()), 3)) print("stratified" if strat is not None else "random ", rates)
With small test sets, random splits make the churn rate swing around from seed to seed. Stratified splits hold it steady.
rows = list(range(10)) split = int(len(rows) * 0.8) train, test = rows[:split], rows[split:] print(len(train), len(test))
Show the answer
8 2
An 80/20 split keeps most data for training and holds back some for an honest score.
Show the answer
The model can score well on her test rows by recognizing Ana, not by learning a pattern
Rows from the same customer look alike. Keep each customer's rows together in one part, so the test checks the model on people it has never met.
Worked example: 20 customers, 4 churners
You have 20 customers. 4 churned and 16 stayed, so 20% churned. You want a 25% test set, which is 5 customers. A fair test set would hold 1 churner, the same 20%. Run 10 random splits and count how many churners land in the test set each time. Then work out the stratified split by hand.
import numpy as np labels = np.array([1] * 4 + [0] * 16) # 20 customers: 4 churned (1), 16 stayed (0) counts = [] for seed in range(10): order = np.random.default_rng(seed).permutation(20) # shuffle the positions 0 to 19 test = order[:5] # the first 5: a 25% test set counts.append(int(labels[test].sum())) # churners that landed in test print("random split, churners in test:", counts) # Stratified: take 25% of each group separately print("stratified: churners", round(4 * 0.25), "+ stayers", round(16 * 0.25))
The random splits give [1, 1, 1, 1, 1, 2, 2, 1, 0, 1]. Most hold 1 churner. Two hold 2, which makes the test set 40% churners. Seed 8 gives 0 churners, so you couldn't measure churn predictions at all. A stratified split takes 25% of each group: 25% of 4 churners is 1, and 25% of 16 stayers is 4. That's 5 customers with exactly 1 churner, every time.
How test data leaks into training
Data leakage means information from the test data sneaks into training. The model then looks better than it really is on new data. Leaks are easy to miss, because nothing crashes. The score looks good, and that's all you see.
Watch for these:
- Tuning on the test set. Every look that changes a choice leaks a little.
- A random split on time-ordered data. The model trains on days that come after some of the days it's tested on.
- The same customer in both parts. The model recognizes the person instead of the pattern.
- Preparing data before you split. Say you fill missing values with the average of the whole table. That average includes the test rows, so a little of the test set has reached training. Split first. Then work out averages and other fill-in values from the training rows only.
A good habit is to split first, before you explore or clean anything. Then put the test set away until the end.
Show the answer
The average used the test rows, so information from the test set reached training
Split first, then compute the average from the training rows and use it to fill both parts.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
What is the validation set for?