← All Posts
Deep Learning · Data Preparation and Preprocessing · Pipelines and Reproducibility

Avoiding Leakage with Pipelines

A pipeline protects one particular boundary: the rows supplied to its fit call determine what its steps learn. Cross-validation can then fit a fresh pipeline on each training fold and transform that fold’s held-out rows using the saved state.

That protection depends on passing raw features into the evaluation procedure and choosing the correct split. A pipeline cannot remove future information already written into a feature, or turn a split of repeated observations into an evaluation on unseen people. This chapter focuses on checking those boundaries rather than repeating the random-label leakage experiment.

A held-out value should not change a fitted training mean

Fix one split: training inputs are [100, 102, 104, 106], while held-out inputs are [0, 2, 4, 6]. A scaler fitted on the training rows learns mean 103. A scaler fitted on all eight rows learns mean 53.

Now change only the held-out inputs. The training data has not changed, so its fitted mean should stay 103. A mean that moves in response to this edit reveals that held-out values entered the fitting calculation.

Change held-out data; inspect learned state
Training · unchanged[100, 102, 104, 106]
Held out · adjustable[0, 2, 4, 6]
Rows used by fitLearned mean
Training rows only103
All rows53
Only the all-rows mean depends on the held-out values. This inspects fitting, not a model’s prediction score.
The split stays fixed. The example is intentionally small and separated to make the dependency visible; it is not a recommendation for how to split a real dataset.

Place the complete recipe inside cross-validation

The leaking order is: fit preprocessing on the full dataset, transform it, and only then cross-validate an estimator on that transformed table. Each validation fold has already influenced the representation before the evaluation starts.

01Split raw rowsChoose training and validation indices for this evaluation.
02Fit on trainingLearn the scaler and estimator using only the training indices.
03Predict held outReuse the fitted scaler; compare predictions with validation targets.
Each fold starts with a fresh fit. Repeat using new fitted objects for the next fold. The validation labels are available to the scoring step, not to the fit for their own fold.

Pass Pipeline([("scale", StandardScaler()), ("model", Ridge())]) as the estimator to the CV function. Scikit-learn clones the estimator for each split. Passing a previously transformed feature table instead does not recover the original fitting boundary.

The same principle applies to imputers, category vocabularies, feature selection, dimensionality reduction, and learned feature engineering. Label-free statistics can leak too: the all-rows mean above never used a target.

Inspect the actual fitted objects

A score cannot tell us which rows a scaler saw. cross_validate can return both the fitted pipelines and the indices for each split. The following audit uses the eight inputs above in two folds and checks the learned means directly.

Run the fold-by-fold fitted-state audit
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold, cross_validate

X = np.array([[0.], [2.], [4.], [6.], [100.], [102.], [104.], [106.]])
y = np.arange(8, dtype=float)
recipe = Pipeline([("scale", StandardScaler()), ("model", Ridge())])
result = cross_validate(
    recipe, X, y, cv=KFold(n_splits=2, shuffle=False),
    scoring="neg_mean_absolute_error", return_estimator=True, return_indices=True,
)
for fitted, train, valid in zip(
    result["estimator"], result["indices"]["train"], result["indices"]["test"]
):
    learned = fitted.named_steps["scale"].mean_[0]
    assert np.isclose(learned, X[train].mean())
    print("train", train.tolist(), "valid", valid.tolist(), "mean", learned)
train [4, 5, 6, 7] valid [0, 1, 2, 3] mean 103.0
train [0, 1, 2, 3] valid [4, 5, 6, 7] mean 3.0

Although this call also computes a prediction metric, the printed evidence concerns fitted state. The global mean, 53, appears in neither fitted scaler. Each learned mean matches that fold’s training inputs.

As a diagnostic, refit the first-fold recipe after changing only its validation features. With the same training rows and controlled randomness, the training state should remain unchanged. Checking one mean is a useful probe, not proof that every feature and component is safe; inspect learned vocabularies, selected columns, and feature construction as appropriate.

Three boundaries a pipeline does not choose for you

BoundaryConcrete failureWhat establishes it
Who or what is unseen?Visits from the same person occur in both training and validation when the claim concerns new people.A group split based on the intended unit of generalization.
What was known then?A feature for a prediction made on day 1 includes an outcome recorded on day 30.Feature values assembled using information available at the prediction time, plus a suitable time split.
Which score selected the recipe?The best score from many tuning attempts is reported as an independent final evaluation.An untouched final test or nested evaluation around the selection procedure.

These boundaries are complementary. A correctly fitted pipeline can still be evaluated on the wrong population or receive information unavailable in deployment. Choose the prediction task first, then construct features and splits that reproduce it.

Supervised transforms need a suitable training representation

A target encoder may use labels to produce numerical features. Even within a training fold, directly encoding every row using its own label can make the training representation unusually easy to fit. A future row will not have that label available.

Scikit-learn’s TargetEncoder addresses this through internal cross-fitting in fit_transform. The enclosing pipeline preserves that interface, while later validation rows use statistics fitted on the training fold. The target-encoding chapter explains the two levels without conflating them.

Custom transformers need the same care. An object that reads a global table containing validation labels during its fit method can violate the boundary even though it sits inside a Pipeline. Composition controls the arguments it passes; it cannot inspect every external dependency of custom code.

After evaluation, refit deliberately

Once the recipe is selected, fit a fresh complete pipeline on the intended development data for final testing or deployment. It is normal for this final scaler to learn different statistics from the smaller CV fits.

Rows that served as validation data during model selection may enter that final development fit. Keep the separate final test outside the choice and fitting process. The CV estimate describes the evaluated learning procedure; it is not a measurement of a later model trained on more data.

In a from-scratch implementation, the boundary is the same: pass only the selected training inputs and targets to the pipeline fitting function, and use its returned state for the held-out rows.

References and further reading