Pipelines

The object that makes leakage impossible by construction, because every fold fits its own preprocessing.

Overview

The same demonstration, one level up

The chart is the one from the [leakage](data_leakage.html) module, relabelled: 300 rows of pure noise, five columns chosen by their correlation with the label, and a choice about *when* that choice is made.

By hand, before cross-validation, the reported accuracy sits well above the 50% the data deserves. Inside a pipeline, where every fold selects its own columns from its own training rows, it falls to the truth.

The model is identical in both. What changed is which object owns the fitting, and that is the entire argument for pipelines. It is not about tidiness.

Pipelines

This module needs JavaScript: the numbers are computed in the page rather than recorded.

Worth knowing

A pipeline chains transformers and a final estimator into one object with one fit and one predict.
Cross-validating the pipeline refits every step on each fold's training portion. That is what makes leakage structurally impossible.
It also removes the train/serve gap: the same object that was fitted is the thing you save and deploy.
ColumnTransformer applies different steps to different columns, which is how real tables get handled.

Pipelines

Not a convenience wrapper. The arrangement in which the order of operations cannot be got wrong.

What goes wrong by hand

The by-hand workflow looks reasonable written out:

X = impute(X)
X = scale(X)
X = select_features(X, y)
scores = cross_val_score(model, X, y, cv=5)

Every one of those three steps has been fitted on all the data, and cross_val_score then splits data that has already been contaminated. Each fold was scaled using a mean that included its own validation rows, imputed using a median that included them, and reduced to features chosen partly because they correlated with their labels.

Nothing errors. The score is simply too high, by an amount that depends on the data and cannot be estimated after the fact.

What a pipeline changes

pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale",  StandardScaler()),
    ("select", SelectKBest(k=20)),
    ("model",  KNeighborsClassifier()),
])
scores = cross_val_score(pipe, X, y, cv=5)

cross_val_score now receives one estimator. On each fold it calls fit on the pipeline with that fold's training rows, and the pipeline fits the imputer, then the scaler, then the feature selector, then the model — all on those rows only. Scoring calls transform on the validation rows using the parameters just learned.

The correct order is not something you remembered to do. It is the only thing the object can do.

The three other things it buys

Hyperparameter search over preprocessing. Because the steps are part of one estimator, a grid can range over them:

{"impute__strategy": ["mean", "median"],
 "select__k": [10, 20, 40],
 "model__n_neighbors": [3, 5, 11]}

Whether the median beats the mean becomes a question the search answers, on the same footing as the model's own parameters. Doing this by hand correctly is possible and nobody does it.

No train/serve gap. The fitted pipeline is one object. Save it, load it in the service, call predict on raw input. There is no second implementation of the preprocessing to drift out of step with the first — which is a real and common production failure, and a silent one.

Different columns, different treatment. ColumnTransformer routes numeric columns to an imputer and scaler, categorical ones to a different imputer and a one-hot encoder, and text to a vectoriser, then hands the combined result to the model. That is what an actual table needs, and it stays one object.

Cross-validation still needs care

A pipeline stops leakage between preprocessing and evaluation. It does not fix splits that were wrong to begin with.

If rows are grouped — several scans per patient — use GroupKFold so a patient cannot appear on both sides. If the data is a time series, use TimeSeriesSplit so training always precedes validation. And if you tune hyperparameters on the same folds you report, that number is optimistic too; nested cross-validation is the honest version.

Where it goes wrong

Calling fit on the test data. Use transform. fit_transform on the test set refits everything and undoes the whole point.

Steps that are not fitted. A log transform has no parameters and cannot leak, so it does not have to be in the pipeline — but putting it there keeps the preprocessing in one place, which is worth more than the exemption.

Resampling inside a plain pipeline. SMOTE and friends must run on the training fold only. imblearn's pipeline handles this; scikit-learn's does not.

Assuming a pipeline validates your splits. It fits each fold correctly. It cannot know that your folds mix one patient across both sides.

Check yourself

0 of 3

Answer without scrolling back up.

  1. What does cross-validating a pipeline do that cross-validating a model does not?

  2. Why does a pipeline remove the train/serve gap?

  3. What does a pipeline NOT protect you from?

Cheat sheet

Pipelines

The chart is the one from the [leakage](data_leakage.html) module, relabelled: 300 rows of pure noise, five columns chosen by their correlation with the label, and a choice about *when* that choice is made.

MACHINE LEARNING · vizlearn.in/machine_learning/ml_pipelines.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.