Pipelines

scikit-learn · Practical · 10 min

Pipelines prevent data leakage, keep training and serving consistent, and make tuning whole workflows possible.

A Pipeline chains transformers and a final estimator: Pipeline([("scale", StandardScaler()), ("model", LogisticRegression())]). The helper make_pipeline(...) does the same and names steps automatically after their lowercased class names.

Calling pipe.fit(X, y) runs fit_transform on each transformer in turn, feeding its output to the next step, then fits the final model. pipe.predict(X) runs only transform on each step, using what was learned during fit, then predicts. The pipeline itself is an estimator, so it has score, works with cross_val_score and can be saved as one object.

Why it matters:

  • No leakage: in cross-validation the scaler is refitted on each training fold, never on validation data.
  • One artefact: the exact preprocessing used in training is applied at prediction time.
  • Tunable: any step's parameter is reachable as stepname__param, for example model__C, in set_params or grid search.

You can inspect steps with pipe.named_steps["model"] or pipe[-1], and slice it: pipe[:-1].transform(X) shows what the model actually receives. A step can be replaced with "passthrough" to skip it, which is handy when searching over whether a step helps at all.

Example

from sklearn.datasets import load_wine
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_wine(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, random_state=0)

pipe = Pipeline([("scale", StandardScaler()),
                 ("model", LogisticRegression(C=0.1))])
pipe.fit(X_tr, y_tr)                    # scaler fitted on X_tr only
print(round(pipe.score(X_te, y_te), 3))  # 1.0

pipe.set_params(model__C=0.001).fit(X_tr, y_tr)
print(round(pipe.score(X_te, y_te), 3))  # 0.778 (far too much regularisation)
print(pipe.named_steps["scale"].mean_[:2].round(2))  # [13.02  2.33]
print(pipe[:-1].transform(X_te[:1]).shape)           # (1, 13)

Remember

  • A pipeline fits transformers on training data and only transforms at predict time
  • Reach nested parameters with step__param
  • Treat the whole pipeline as the model you evaluate and ship

Watch out for

  • Scaling the full dataset before cross-validation instead of putting the scaler in the pipeline.

Check yourself

  1. In `Pipeline([("scale", StandardScaler()), ("svc", SVC())])`, how do you set the SVC's C to 10?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: scikit-learn.org/stable/modules/compose.html#pipeline