Train / Validation / Test Split
Why three splits not two. Holdout sets, stratified splits, data leakage across splits, and the time-series exception where random splits break everything.
Your model scored 99% accuracy. Then you deployed it and it was wrong on half the real orders. What went wrong?
You trained a delivery time model on 10,000 DoorDash orders and measured its accuracy on the same 10,000 orders it trained on. It scored incredibly well. You deployed it. It was terrible on real incoming orders. The problem: the model had already seen every order it was evaluated on. It did not learn to predict — it learned to memorise.
This is why you always split your data before training. You hold back a portion of your data that the model never sees during training. You evaluate on this held-out portion only. If the model performs well on data it has never seen, you have evidence it has actually learned something generalisable — not just memorised the training set.
But a two-way split — train and test — has a subtle flaw. When you tune hyperparameters (how deep should the tree be? what is the best regularisation strength?) using the test set score to decide, you are indirectly letting the test set influence your training decisions. Over many experiments, you overfit to the test set without realising it. This is why you need three splits — not two.
Think of preparing for a competitive exam like GATE or CAT. Your textbook problems are the training set — you practice on these, make mistakes, learn from them. Practice mock tests are the validation set — you use your score to decide which topics to study more. The actual exam on exam day is the test set — you sit it exactly once, at the very end, to get your true performance.
If you kept using the exam paper to decide what to study, your exam score would look great — but you would have cheated yourself out of knowing your real ability. Same with the test set in ML.
Training, validation, and test — what each one is for
Each split has a specific job. Confusing the jobs leads to either overly optimistic performance estimates or models that do not generalise.
Stratified splits — preserve class balance across all three sets
A random split might put 90% of the rare class into training and leave only 10% in test — making evaluation noisy and unreliable. Stratified splitting ensures each split has the same class proportion as the original dataset. This is especially important for imbalanced classification problems — fraud detection, churn prediction, medical diagnosis — where the minority class is what you actually care about.
Data leakage across splits — five ways your evaluation lies to you
Data leakage across splits is when information from the test or validation set influences the training process — even indirectly. The result is an evaluation metric that looks excellent in development but collapses in production. It is the most common and most costly mistake in applied ML.
Why: Test set mean and std contaminate the scaler used for training.
Fix: Always split first, then fit preprocessors on X_train only. Use Pipeline.
Why: Target means include test set labels. Each test row's target leaks into its own feature.
Fix: Compute target encoding inside cross-validation folds or use sklearn TargetEncoder.
Why: Feature selection uses test set labels to choose features. Overfits to test.
Fix: Perform feature selection inside the training fold only. Wrap in Pipeline.
Why: Model memorises training samples and scores them perfectly in test.
Fix: Deduplicate BEFORE splitting. Check: assert len(set(train_ids) & set(test_ids)) == 0
Why: Model learns from the future to predict the past. Impossible in production.
Fix: Always use time-based split for time-series: train on past, validate on future.
Time-series splits — when random splitting destroys your model
For time-series data — stock prices, daily orders, sensor readings, anything measured over time — random splitting is not just suboptimal, it is fundamentally wrong. A random split puts future data in the training set and past data in the test set. The model learns from information that would not exist at prediction time. You are training on the future to predict the past — the opposite of what you need.
For any dataset ordered by time, your split must respect chronological order. Training data must come entirely before validation data. Validation data must come entirely before test data. There must be no temporal overlap between any two splits.
This simulates what actually happens in production: your model was trained on historical data and is now predicting future events it has never seen.
Holdout split vs cross-validation — when each is appropriate
A single holdout split is fast but noisy — performance depends on which samples ended up in test. Cross-validation runs multiple splits and averages the result, giving a more reliable estimate. But it is k times slower and requires that all preprocessing fits inside each fold (a Pipeline).
How much data to put in each split — rules of thumb
There is no universally correct split ratio. The right ratio depends on how much data you have and what you need from each split. Here are the rules practitioners actually use:
Every common split error — explained and fixed
How splitting actually goes wrong in production — beyond the textbook case
The leakage table earlier in this module covers the mechanics. In practice, the two failure modes that show up over and over on real teams are group leakage and splitting after feature engineering — both are easy to miss because the code runs without any error, the metrics look great, and the problem only surfaces weeks later when production performance does not match what development promised.
Group leakage happens whenever more than one row in your dataset comes from the same underlying entity. A churn model built on customer order history has many rows per customer — one per order. Split those rows randomly and a customer's orders from January land in training while that same customer's orders from March land in test. The model does not have to generalise to a new customer to score well on that test row — it can partially recognise the customer from other orders it already saw in training, which is a much easier and much less useful thing to have learned. The same problem shows up with patients across multiple hospital visits, devices across multiple sensor readings, and users across multiple sessions — anywhere a single entity contributes more than one row.
Splitting after feature engineering is the second recurring failure, and it is subtler than the "scaler fit before split" case this module already covers. A team computes a rolling 30-day average or a cumulative count directly on the full, chronologically sorted dataframe — before ever calling a split function — and only then splits train and test out of the result. Every "past" feature for a row near the train/test boundary was computed using a window that quietly extends into what is now the test period, or a cumulative count includes rows that have not happened yet relative to that row's own timestamp. The split function itself is completely correct; the leakage was already baked into the features before the split ever ran.
The practical habit that prevents both of these: decide your split — by time, by group, or both — before writing a single line of feature engineering code, and treat the split boundary as a hard constraint that every subsequent computation has to respect, rather than something you bolt on at the end once the features already exist.
Five things people get wrong about train/val/test splitting
Random splitting assumes every row is independent and exchangeable with every other row — swap any two rows between train and test and nothing about the underlying problem changes. That assumption breaks for time-ordered data, where a row's position in time matters, and it breaks for grouped data, where multiple rows share an underlying entity like a customer or patient. In both cases a random split can leak information across the train/test boundary even though nothing in the code looks wrong. "Split randomly" is the right default only once you have checked that your rows really are independent of each other.
A single split gives you one sample of "how well did this model do on unseen data," and that sample has its own randomness — a slightly different split could easily rank two similar models the other way around, especially on a small dataset. Using that one score to choose between several hyperparameter settings or model families is effectively tuning to the noise in that particular split, not to genuine generalisation ability. Cross-validation, which averages performance across several different splits, gives a materially more reliable basis for model selection than any single holdout can.
Hyperparameter tuning is its most common use, but far from its only one. Validation performance is also how you decide when to stop training a neural network (early stopping), how you compare entirely different model families against each other, and how you catch overfitting during development before it ever reaches the test set. Treating it purely as a tuning knob undersells how much of routine model development — architecture choices, feature additions, preprocessing changes — is actually validated against this set, not the training set and not the sealed test set.
A good test score is consistent with a correct split, but it is equally consistent with several kinds of leakage that make the score look better than it should. Preprocessing fit on the full dataset, a target-encoded feature computed before cross-validation, or the same customer appearing in both train and test can all inflate test performance without producing any error or warning. A high test score tells you the evaluation is optimistic or accurate — it cannot by itself tell you which one, which is exactly why auditing the pipeline for leakage has to happen regardless of how good the numbers look.
Every leakage example in this module — a scaler fit before splitting, a rolling feature computed across the split boundary, a customer's rows scattered across train and test — happens without anyone including the label anywhere in X. Leakage is usually a statistical dependency introduced by the order operations happen in, not a deliberate shortcut. That is exactly why it is dangerous: it survives a visual code review that only checks "is the target column present in the features," and requires actually tracing when each preprocessing step was fit relative to when the split happened.
Train/val/test splitting — 5 questions interviewers actually ask
A random split scatters test samples throughout the timeline, which means some training rows come from dates after some test rows — the model ends up learning from the future to predict the past, a situation that can never occur at actual prediction time. I would instead use a chronological split, where every training row comes strictly before every validation and test row, or TimeSeriesSplit for cross-validation, which performs walk-forward validation: train on an expanding window of the past, validate on the period immediately following it, and repeat. This simulates exactly what the model faces in production — predicting a future it has never seen.
Group leakage happens when a single entity contributes multiple rows to the dataset and a random split scatters that entity's rows across both train and test. The model can then partially recognise the entity from rows it already saw in training, which inflates test performance without the model having learned anything that generalises to a genuinely new entity. I would use GroupShuffleSplit or GroupKFold, passing the entity ID as the groups parameter, which guarantees every row belonging to the same entity ends up entirely in one split or entirely in the other, never split across both.
The test set's entire value is that it gives an honest, unbiased estimate of production performance — but that only holds if no decision during development was ever influenced by its score. The moment you check the test score and use it to choose a hyperparameter, pick a model, or decide whether to keep a feature, the test set has effectively become a second validation set, and every subsequent "improvement" is partly tuned to that specific holdout rather than to the underlying problem. Repeated peeking accumulates: each individual check feels harmless, but across many experiments it silently overfits your final reported number to that one test set.
I would trace the order of operations from raw data to final metric. First, I would check whether any preprocessing — scaling, imputation, encoding, feature selection — was fit before the train/test split rather than after. Second, I would check whether any feature was engineered using the full dataset's chronology or full-dataset aggregates before the split existed, such as a rolling window or a groupby computed pre-split. Third, I would check whether the data has a natural grouping (customers, sessions, devices) and whether the split respected it. Finally I would check the project history for any hyperparameter decision that was made by directly consulting the test score rather than a validation score.
It mostly comes down to dataset size. Under a few thousand rows, a single holdout test set is too small to trust — I would use cross-validation for tuning and evaluation, since it uses the data far more efficiently. In the tens of thousands of rows range, a three-way 70/15/15 split becomes reliable enough to use directly. Above roughly a hundred thousand rows, I would shrink the validation and test proportions further, since even five or ten percent of a very large dataset is statistically enough to give a stable estimate, and it leaves more data available for training. Time-series or grouped data changes the method, not this general sizing logic.
Data is collected, cleaned, scaled, encoded, and split. You are ready to build models.
This completes Section 4 — Data Engineering for ML. You can now take any raw dataset, clean it, engineer features, encode categoricals, scale numerics, and split it correctly without leaking information. These five modules are the foundation every ML model you will ever build sits on.
Section 5 — Classical Machine Learning — begins next. Module 21 answers the question you have been building toward: what actually is machine learning, and how does training work mechanically? Every algorithm in the section — linear regression, logistic regression, decision trees, random forests — builds on the data engineering foundation you have just completed.
Not the Wikipedia definition. The actual idea — what training means mechanically, the 3 types of ML, the 7-step workflow, and 12 key terms defined once and for all.
🎯 Key Takeaways
- ✓You need three splits — not two — because using the test set to make tuning decisions turns it into a second validation set. Over many experiments you silently overfit to it. The test set must be touched exactly once, at the very end, to get an honest performance estimate.
- ✓Training set: the model fits on this. Validation set: you use the score to make tuning decisions — the model never trains on it. Test set: the final honest evaluation — touched once, never used for decisions.
- ✓Always split before any preprocessing. Fitting a scaler, encoder, or imputer on the full dataset (before splitting) leaks test set statistics into training. Use sklearn Pipeline to make this structurally impossible.
- ✓Use stratify=y for classification problems. Without stratification, a random split can put most of the minority class into one split — making evaluation unreliable and hyperparameter tuning misleading.
- ✓For time-series data, random splits are fundamentally wrong. They put future data in the training set and past data in test — leaking information that would not exist at prediction time. Always use chronological splits: train on past, evaluate on future. Use TimeSeriesSplit for cross-validation.
- ✓Split size depends on dataset size. Under 1,000 rows: use cross-validation, no holdout. 1k–10k: 80/20 with CV for tuning. 10k–100k: 70/15/15 three-way split. Over 100k: 80/10/10 is reliable.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.