A model has to be judged on data it hasn't learned from. That's why ML projects split their data into separate sets:
- Training set: used to fit the model.
- Validation set: used to compare models and choose settings.
- Test set: used once, at the end, to estimate real-world performance.
from sklearn.model_selection import train_test_split
X = [[n] for n in range(12)]
y = [n * 2 for n in range(12)]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=7)
print(len(X_train), len(X_test)) # 9 3This lesson covers the role of each set, choosing a split strategy that matches how the data was collected (random, stratified, time-based or grouped), fitting preprocessing without leakage, and making splits reproducible. The examples predict passenger demand for a city bus route.
Data roles
Each set has one job, and mixing them up quietly inflates results:
| Set | Used for | Typical share |
|---|---|---|
| training | fitting model parameters | 60-80% |
| validation | choosing models, features, hyperparameters, thresholds | 10-20% |
| test | the final, one-time performance estimate | 10-20% |
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=0)
# 60% train, 20% validation, 20% testWith small datasets, cross-validation replaces the single validation set by rotating through several training/validation splits.
The rule that matters most: once you look at test results and then change the model, the test set has influenced your choices and has effectively become validation data. Its score will be optimistic. Keep the test set locked away until you've finished deciding, and if you must iterate further, collect new test data.
Split strategy
How you split must reflect how the model will be used:
- Random split: rows are shuffled and divided. Suitable when rows are independent and the future looks like the past.
- Stratified split (
stratify=y): keeps class proportions the same in every set, important when one class is rare. - Time-based split: train on earlier periods, test on later ones. Essential for forecasting, such as tomorrow's bus demand, where a random split would let the model learn from the future.
- Grouped split: keep all rows from the same group, such as the same passenger, school or bus stop, in one set, using
GroupShuffleSplitorGroupKFold, so the model is tested on groups it has never seen.
cutoff = int(len(days) * 0.8)
train_days, test_days = days[:cutoff], days[cutoff:] # data sorted by dateA random split of related rows leaks information: if Monday's and Tuesday's counts for the same stop are nearly identical and one lands in training while the other is in testing, the model gets credit for recognising a near-duplicate rather than predicting.
Preprocessing
Anything learned from data, such as the mean and standard deviation for scaling, the vocabulary for text features, the median used to fill gaps or the categories for one-hot encoding, must be learned from the training set only and then applied unchanged to validation and test data:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(X_train) # learn from training rows
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test) # apply, don't refitFitting the scaler on all the data before splitting lets test-set statistics influence training. The effect is often small but it's real, and with steps like feature selection or target encoding it can be large.
The reliable way to avoid this is a pipeline: make_pipeline(StandardScaler(), model). Fitting the pipeline fits every step on the training data, and it can be passed straight to cross-validation, which refits it correctly inside each fold.
Reproducibility
Random splits depend on a random number generator. Set random_state to an integer so the same rows land in the same sets every time you run the code; otherwise results change between runs and comparisons become unreliable.
train_test_split(X, y, test_size=0.25, random_state=42) # same split every runRecord everything needed to recreate the split: the method, the random_state, the date cut-off or grouping column, and the data version. Saving the row IDs of each set is even safer when the data may change.
A fixed seed makes a split repeatable, not appropriate. A reproducible random split of time-series data is still wrong for forecasting. And results can vary noticeably between different seeds on small datasets, which is another reason to prefer cross-validation, where you see the spread of scores instead of one lucky or unlucky split.
Independent practice and a worked check
Task: choose and justify a bus demand split, recording training, validation and final test roles. Write the plan as code comments next to the split:
# 52 weeks of daily passenger counts, sorted by date (one row per day)
weeks = list(range(52 * 7))
# Forecasting next week's demand -> time-based split, never shuffled.
train = weeks[: 40 * 7] # weeks 1-40: fit models
validation = weeks[40 * 7 : 46 * 7] # weeks 41-46: compare models and settings
test = weeks[46 * 7 :] # weeks 47-52: touched once, for the final report
print(len(train), len(validation), len(test)) # 280 42 42
assert max(train) < min(validation) and max(validation) < min(test)The assertion proves no future day leaks into an earlier set. Scalers and other learned preprocessing are fitted on train only. For the worked check, the observations that should fit a preprocessing transformation are the training observations.
Reference and next practice
scikit-learn documents train_test_split and its cross-validation splitters, and its common pitfalls page covers leakage. Continue with cross-validation and evaluation. Practise with the Python exercises and Python quiz.
Training and Testing Datasets Summary
- Data roles: Training data fits a model while validation guides choices and final test data assesses the chosen procedure.
- Split strategy: Random, temporal and grouped splits suit different observation relationships.
- Preprocessing: Learn transformations on training observations and apply the learned transformation to held out data.
- Reproducibility: Record split rules and random seeds where randomization is appropriate.
Training and Testing Datasets Examples
Each example shows the code, the output it produces and what each part does.
Training and Testing Datasets in a small program
from sklearn.model_selection import train_test_split
X = [[n] for n in range(12)]
y = [n * 2 for n in range(12)]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=7)
print(len(X_train), len(X_test))9 3Line-by-line explanation
- Lines 2 to 3Twelve examples with one feature each.
- Line 4A quarter of the rows, 3 of 12, go to the test set;
random_state=7makes the choice repeatable. - Line 5Nine rows remain for training.
Random versus time-based splits
- The random split reports a lower error, 2.5 passengers against 4.1, because filling gaps between known days is easier than predicting days after the training period. For forecasting, the time-based figure is the honest one.
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split
# daily passengers grow over 20 days as a new estate opens; feature: day number
days = [[d] for d in range(20)]
passengers = [100 + 6 * d + (8 if d % 7 in (5, 6) else 0) for d in range(20)]
X_tr, X_te, y_tr, y_te = train_test_split(days, passengers, test_size=0.25, random_state=3)
random_model = LinearRegression().fit(X_tr, y_tr)
print("random split test days:", sorted(d[0] for d in X_te))
print("random split MAE:", round(mean_absolute_error(y_te, random_model.predict(X_te)), 1))
time_model = LinearRegression().fit(days[:15], passengers[:15])
print("time split test days: ", [d[0] for d in days[15:]])
print("time split MAE: ", round(mean_absolute_error(passengers[15:], time_model.predict(days[15:])), 1))random split test days: [1, 2, 4, 14, 17]
random split MAE: 2.5
time split test days: [15, 16, 17, 18, 19]
time split MAE: 4.1Line-by-line explanation
- Line 9The random split scatters test days through the period, so every test day has training days on both sides.
- Line 14The time split trains on the first 15 days and tests on the last 5, the situation the model will actually face.
Common Training and Testing Datasets Mistakes
Fitting preprocessing before splitting
X_scaled = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y)X_train, X_test, y_train, y_test = train_test_split(X, y)
model = make_pipeline(StandardScaler(), LinearRegression()).fit(X_train, y_train)Randomly splitting time-ordered data for a forecast
train_test_split(daily_counts, targets, shuffle=True) # trains on future dayscut = int(len(daily_counts) * 0.8)
X_train, X_test = daily_counts[:cut], daily_counts[cut:]
y_train, y_test = targets[:cut], targets[cut:]Training and Testing Datasets in Real Projects
Splitting data in real projects
Transport operators forecast demand with time-based splits, often "train on everything before last month, test on last month". Medical studies split by patient so the same person never appears in both sets. Recommendation systems split by time so models are tested on interactions that happened after training. Competitions such as Kaggle keep a hidden test set precisely so participants can't tune against it.
Many surprising drops in performance after deployment trace back to a split that didn't match reality: random splits on time-ordered data, related rows across sets, or preprocessing fitted before splitting.
Training and Testing Datasets FAQ
What is the difference between validation and test sets?
The validation set is used repeatedly while building the model, to compare options and tune settings. The test set is used once at the end to estimate performance on new data. Tuning against the test set makes its score optimistic.
What split ratio should I use?
Common choices are 80/20 or 70/30 for train/test, or 60/20/20 with a validation set. With lots of data, smaller test shares are fine; with little data, use cross-validation so every example contributes to both training and evaluation.
What does random_state do in train_test_split?
It seeds the random shuffling, so the same rows are assigned to each set every time the code runs. Any integer works; the value itself doesn't matter, only that it stays fixed.
When should I use stratify in train_test_split?
For classification, especially with imbalanced classes. stratify=y keeps the share of each class equal across training and test sets, so a rare class isn't missing from the test set by chance.
What is data leakage?
When information that wouldn't be available at prediction time influences training, for example preprocessing fitted on test data, future rows in training for a forecast, or features recorded after the outcome. It produces optimistic test scores that real use doesn't match.
Training and Testing Datasets Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which observations should fit a preprocessing transformation?
Which review concern is valid for training and testing datasets?
Training and Testing Datasets Quiz
2 questions to check what you learned.
Which observations should fit a preprocessing transformation?
- The full dataset including test observations
- Training observations
- Python ignores the distinction
Which statement correctly describes preprocessing?
- Learn transformations on training observations and apply the learned transformation to held out data.
- Only the example output matters, not the rule
- Every operation has the same behavior