Pipelines
A scikit-learn Pipeline chains preprocessing and a model into one estimator. The wins are huge: the same transformations run at train and inference time, cross-validation never leaks data from validation folds, and gridsearch can tune hyperparameters of the preprocessing AND the model together. Once you write Pipelines, you stop writing bugs around train/test mismatch.
Mixed-type ColumnTransformer + Pipeline + GridSearch
EXAMPLE
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import classification_report
# Toy dataset: predict churn
df = pd.DataFrame({
'tenure_months': np.random.exponential(20, 1000).round(),
'monthly_charge': np.random.gamma(2, 30, 1000),
'contract': np.random.choice(['month', 'year', 'two-year'], 1000),
'city': np.random.choice(['Sydney','Melbourne','Brisbane','Perth'], 1000),
'churned': np.random.binomial(1, 0.25, 1000),
})
X, y = df.drop(columns='churned'), df['churned']
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2,
stratify=y, random_state=42)
num = ['tenure_months', 'monthly_charge']
cat = ['contract', 'city']
preprocess = ColumnTransformer([
('num', Pipeline([
('impute', SimpleImputer(strategy='median')),
('scale', StandardScaler()),
]), num),
('cat', Pipeline([
('impute', SimpleImputer(strategy='most_frequent')),
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
]), cat),
])
pipe = Pipeline([
('prep', preprocess),
('clf', LogisticRegression(max_iter=1000)),
])
# Tune hyperparameters of BOTH preprocessing and model in one search
grid = {
'prep__num__scale__with_mean': [True, False],
'clf__C': [0.1, 1.0, 10.0],
}
search = GridSearchCV(pipe, grid, cv=5, scoring='roc_auc', n_jobs=-1)
search.fit(X_tr, y_tr)
print('best score:', search.best_score_)
print('best params:', search.best_params_)
print(classification_report(y_te, search.predict(X_te)))
# Save the entire pipeline — single artifact for the serving layer
import joblib
joblib.dump(search.best_estimator_, 'churn_pipeline.joblib')
# At inference time:
# model = joblib.load('churn_pipeline.joblib')
# model.predict(pd.DataFrame([{'tenure_months': 6, ...}]))
Why it matters
The reason to bake preprocessing into the Pipeline (rather than a notebook cell) is that a fit_transform on the full dataset leaks the test distribution into the model. A Pipeline inside cross_val_score or GridSearchCV refits the preprocessing per fold automatically, which is what gives you an honest estimate of generalisation.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(max_iter=200)),
]).fit(Xtr, ytr)
print(pipe.score(Xte, yte))
Try it Yourself »
Exercise
Compose preprocessing + estimator.
from sklearn.pipeline import
PascalCase.
Discussion
Loading…