MODELCATALOG

Chain steps into a single estimator.

sklearn.pipeline

pipeline

Chain steps into a single estimator.

1 Get data Something to practise on
2 Split & tune An honest test set
3 Prepare Into numbers a model accepts
4 Compose One object, no leakage
5 Model The algorithm itself
6 Unsupervised No labels available
7 Measure Is it any good, and why
8 Utilities Glue and extension

What it is for

A Pipeline behaves exactly like a model — it has fit and predict — but internally runs a sequence of transformers before the final estimator.

The reason to use one is not tidiness. It is that inside a Pipeline, fit only ever sees training data. Vectorising or scaling before you split lets test-set statistics into your preprocessing and inflates your metrics. The Pipeline makes that mistake structurally impossible.

Contents

Pipeline common Named steps, so GridSearchCV can address them
make_pipeline Same thing, auto-names the steps
FeatureUnion Run transformers in parallel and concatenate outputs

Worked examples 3

Runnable, with the arguments that change the outcome called out. Output shapes are shown as comments where they help.

sklearn.pipeline

from sklearn.pipeline import Pipeline
from sklearn.model_selection import RandomizedSearchCV

pipe = Pipeline([
    ("tfidf", TfidfVectorizer(min_df=3)),
    ("clf", LogisticRegression(max_iter=1000, class_weight="balanced")),
])

# Step names become hyperparameter prefixes, separated by double underscore.
# This tunes the vectoriser and the classifier TOGETHER, which is correct -
# the best ngram_range depends on the model that consumes it.
search = RandomizedSearchCV(pipe, {
    "tfidf__ngram_range": [(1, 1), (1, 2), (1, 3)],
    "tfidf__min_df": [2, 3, 5],
    "clf__C": [0.1, 1.0, 10.0],
}, n_iter=20, cv=5, scoring="f1_macro", n_jobs=-1, random_state=42)

search.fit(X_train, y_train)
print(search.best_params_)

Why a Pipeline instead of doing the steps yourself

Not tidiness. It makes a specific and very common leakage bug impossible.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score

# WRONG - the vectoriser has seen the whole corpus, including the rows
# that will land in the validation fold. Vocabulary and IDF weights are
# computed from data the model is about to be tested on.
vec = TfidfVectorizer()
X_all = vec.fit_transform(texts)
print(cross_val_score(LogisticRegression(max_iter=1000), X_all, y, cv=5).mean())
# 0.9143   <- optimistic

# RIGHT - the vectoriser is refit on each training fold only
pipe = Pipeline([("tfidf", TfidfVectorizer()),
                 ("clf", LogisticRegression(max_iter=1000))])
print(cross_val_score(pipe, texts, y, cv=5).mean())
# 0.8967   <- honest

# The gap is small here and much larger with target encoding, feature
# selection or imputation in the chain. It is always in the same
# direction: the wrong way flatters you.

Reach inside a fitted Pipeline

named_steps gives you the fitted objects, which is how you get coefficients, vocabularies and feature names back out.

pipe.fit(X_train, y_train)

vec = pipe.named_steps["tfidf"]     # or pipe["tfidf"]
clf = pipe.named_steps["clf"]

print(len(vec.vocabulary_), "features")
print(clf.coef_.shape)

# Everything up to but not including the final step
X_transformed = pipe[:-1].transform(X_test)
print(X_transformed.shape)

# Feature names survive the chain in recent versions
print(pipe[:-1].get_feature_names_out()[:5])