Building Pipelines for Model Evaluation
The evaluation is no longer a clean test of how the model handles unseen data.
Try it: K-Fold Cross-Validation and Model Selection
How hold-out and k-fold cross-validation choose among candidate models: each fold takes a turn as the validation set, the fold errors are averaged, the lowest average picks the model, and only then is the untouched test set used once — while tuning on the test set would report an optimistic number.
How it works
- Set 10 test points aside; they take no part in any choice.
- Optionally shuffle the 20 development points (seeded), then cut them into k folds (hold-out: one chunk validates, the rest trains).
- For every round, train each candidate polynomial on the other folds and measure its validation MSE on the held-out fold.
- Average each candidate's fold errors (with repeated shuffling: average the p run scores; p × k fits per candidate).
- Select the candidate with the lowest average, retrain it on all development points, and evaluate it once on the test set.
- For contrast, show which candidate the test set itself would have picked — that figure is leaked and optimistic.
Default run (12 steps): 20 development points (sorted by x as they arrived) and 10 test points set aside. Candidates: polynomial degree 0, 1, 2, 3, 4, 5, 6. Plan: 5-fold cross-validation, shuffled with seed 1 → 5 fits per candidate (p × k = 1 × 5), 35 in total. … 5-fold cross-validation picks degree 3 (score 0.0768); its honest test MSE is 0.0889. Picking by test error would report degree 4 at 0.0868 — a leaked, optimistic figure.
Simplified: One fixed toy dataset (20 development + 10 test points from sin(2πx) with noise) and polynomial least squares of degree 0–6 as the candidate models. Shuffling uses a seeded mulberry32 Fisher–Yates; folds are contiguous chunks like scikit-learn's KFold. Hold-out uses the first of k chunks as the validation set.
Loading the simulation…
When a Strong Score Is a Warning Sign
You build a model, run cross-validation, and get a score that looks excellent. The natural reaction is relief, and the natural next step is to trust the number and move on. This article is about a case where that trust is misplaced. A score can be high not because the model has learned anything real, but because information from the held-out data found its way into the training process. When that happens, the estimate of how well the model will perform on new data is inflated.
What do you think happens?
Suppose the features and the target were generated completely independently, so no real relationship exists between them. Could a carefully run evaluation still report a high score?
Reveal answer
Yes, if information from the held-out rows shapes the training process.
With no real signal, an honest evaluation should find nothing to learn. But if feature selection has already seen the test rows, the features it keeps can look correlated with the target on both the training and test folds, and the reported score then reflects the selection rather than generalization.
A Deliberately Empty Dataset
The source builds its argument on a deliberately constructed example: a synthetic regression task with 100 samples and 1,000 features. Every feature is drawn independently from a Gaussian distribution, and the response is also drawn from a Gaussian distribution. Because the features and the target are generated independently, there is no real relationship between them. Keep that fact in mind throughout the article. Any honest evaluation of a model trained on this data should find nothing to learn.
Where the Leak Enters the Workflow
The misconception this article targets is the assumption that feature selection is a harmless preprocessing step that can be done once, on the full dataset, before cross-validation begins. It feels reasonable. Feature selection is simply choosing a subset of relevant features, which seems like neutral housekeeping. Many people would not think twice about running it first and evaluating the model afterward.
The problem is that feature selection is not neutral with respect to the target. It looks at the relationship between each feature and the response in order to decide which features to keep. If that decision is made using every row, including the rows that cross-validation later treats as test data, then the test rows have already influenced which features the model sees. The evaluation is no longer a clean test of how the model handles unseen data.
The leak is a matter of ordering. In the flawed version, feature selection is fit on the full feature matrix and target before cross-validation splits the data into folds. The selected features are then passed to cross-validation, which trains a Ridge regressor on some folds and tests it on the remaining fold, repeating across the splits. Nothing in the code looks wrong. Each call does what it says. The flaw sits in the sequence.
Why Selection Outside the Loop Inflates the Estimate
Because the selection step has already seen the rows that will later serve as the test fold, the features it chose can be correlated with the target on both the training and test folds. Cross-validation then measures the model on data whose structure was partly shaped by the test rows. The result is an estimate that reflects the selection process rather than the model's ability to generalize.
This is why the effect appears even on random data. With 1,000 independent features and only 100 samples, some features will look related to the response by chance. Selection that uses all rows will keep those chance relationships, and because the test rows were part of that decision, the same chance relationships show up again on the held-out fold. The scores look like learning, but they are a record of the selection step.
The source's comparison with a pipeline gives a very different picture. Once selection is confined to the training folds, the estimate changes sharply. The reason is that features chosen using only training rows can look correlated with the target there, yet fail to be correlated on the test rows, because the data is random. The practical lesson is that the difference between a fully leaky evaluation and a clean one can be the difference between concluding that a model works very well and concluding that it does not work at all.
How a Pipeline Bundles the Steps
A pipeline is a sequence of data processing steps, including preprocessing steps and a final classifier, that can be treated as one object. A pipeline chains the selection step and the regressor into one object. When cross-validation is run on that pipeline, the selection step is refit for each training fold, so it can only see training rows. The held-out fold plays no role in choosing features.
- The list of (name, step) pairs defines the order: the scaler comes first, the SVM last.
- fit is called once with training data only.
- score is called with the held-out test data.
The first step in this example is a scaler and the last step is an SVM. When you call fit on the pipeline, it does not simply hand the data to the SVM. It passes the training data through each step in order, so that every step learns from exactly the data it will later process. Start with the code, then follow what happens to the data at each call. The distinction between which object receives training data and which receives test data is what the rest of the article depends on.
What fit Does to Each Step
The source describes three actions inside a single call to fit. First, the pipeline calls fit on the scaler. Second, it transforms the training data with that scaler, producing scaled training data. Third, it fits the SVM using the scaled data. Each action depends on the one before it, so the order is part of the meaning of the pipeline.
The sequence matters because the SVM never sees raw training values. It sees the output of the scaler, which is exactly the representation that will be produced for test rows later. Keeping that chain together inside one object is what prevents the training and evaluation steps from drifting apart.
What score Does to Test Rows
After fitting, you evaluate the pipeline by calling score on the test data. The source explains that calling score first transforms the test data using the scaler and then calls score on the SVM with that scaled test data. The pipeline does not refit the scaler on the test data. It applies the transformation it already learned during fit.
Tracing One score Call
A pipeline with a scaler and an SVM has already been fitted on training rows. Trace what happens to the test rows when score is called.
- 1Receive test rows
score gets X_test and
y_test. No training happens during this call. - 2Transform with the scaler
The scaler applies the transformation it learned during fit. It is not refit on the test rows, so the test data cannot change what the scaler learned.
- 3Score the SVM
The SVM receives the scaled test data and is scored against
y_test. Its parameters are whatever fit produced. - 4Compare with the manual route
The source notes that this result matches doing the transformations by hand. A mismatch with a manual rebuild would point to a changed step, not to the pipeline itself.
The only step that touches test rows is the transform. The classifier only scores them. Nothing about the test rows reaches the fitted parameters.
Reusing One Pipeline in Cross-Validation
The source states that using a pipeline reduces the code needed for the preprocessing plus classification process. The bigger advantage is that the pipeline behaves as one estimator. Because of this, a single pipeline object can be passed directly to cross_val_score or GridSearchCV, rather than requiring you to coordinate a scaler and an SVM separately inside each evaluation loop.
In practice, the same pipeline object is reused across folds. For each fold, the selection or scaling step is fit on that fold's training rows, and the held-out rows are only transformed and scored. This is the mechanism that keeps the held-out fold out of the choice of features, which is exactly what the flawed workflow lacked.
Mistakes When Reading Pipeline Results
Common mistake Running feature selection once on the full dataset before cross-validation
What it looks likeSelecting features on all 100 rows, then passing them to cross_val_score with a Ridge regressor.
Why it failsThe selection step has already seen the rows that later serve as test folds, so the estimate reflects the selection rather than generalization.
Do this insteadPlace the selection step inside a pipeline so it is refit on each training fold.
Common mistake Assuming a correct-looking score means the code is correct
Why it failsThe leak requires no error message. The code runs and each score is computed correctly for the code as written.
Do this insteadCheck the order of operations, not only whether the code runs.
Common mistake Refitting the scaler on test data during score
Why it failsscore only transforms test rows with the already-fitted steps. Refitting on test rows would let test information change the transformation.
Do this insteadFit only on training data and let score apply the learned transformation.
Common mistake Treating a mismatch with a manual rebuild as a pipeline bug rather than a changed step
Tracing an Unexpected Score
Suppose a colleague reports a test score that is noticeably different from 0.95 for the same model. Rather than guessing, trace the pipeline stage by stage. The pipeline gives you a fixed order of events, so you can ask at each stage whether the data is what you expect. Work through the stages in this order, stopping at the first one that does not match what you expect.
- 1Confirm that fit received only training rows, and score received only test rows.
- 2Check whether any feature selection or scaling was performed outside the pipeline before the split.
- 3Check whether the scaler or other preprocessing step was refit during score.
- 4Compare the pipeline result with a manual rebuild of the same transformations, which the source notes should match.
- 5If the scores still disagree, identify which step changed between the two runs.
A teammate fits a feature selector on the full dataset, then runs cross_val_score on a Ridge regressor using the selected features. Explain where test-fold information enters training, and describe how wrapping the selector and Ridge in one pipeline changes what each fold can see.
Hints
- Ask which rows the selector looked at before the folds were created.
- In a pipeline, consider when the selection step is refit and which rows it receives.
Key Takeaways
- A high cross-validation score on data with no real signal can come from information leakage, not from learning, so a strong score deserves a check on the workflow.
- Feature selection is not neutral with respect to the target; if it is fit on all rows before the split, the test rows shape the features the model sees.
- A pipeline bundles the selection or preprocessing steps with the final estimator, so each training fold refits those steps on training rows only.
- During fit, each step is fitted and transformed in order before the final estimator is fitted; during score, test rows are only transformed with fitted parameters and then scored.
- When a result is unexpected, trace the pipeline stage by stage and check which data each call received, since one pipeline object can be passed to
cross_val_scoreor GridSearchCV.