Pipelines and saving models

Chaining preprocessing with a model so nothing leaks, and persisting the whole pipeline for consistent inference.

Why a Pipeline

If you scale or impute before splitting, statistics from the test set sneak into training and your score is optimistic. A Pipeline applies every step inside each cross-validation fold, which removes that entire class of bug.

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

num = Pipeline([("impute", SimpleImputer(strategy="median")),
                ("scale", StandardScaler())])

cat = Pipeline([("impute", SimpleImputer(strategy="constant", fill_value="missing")),
                ("onehot", OneHotEncoder(handle_unknown="ignore"))])

pre = ColumnTransformer([("num", num, numeric_cols),
                         ("cat", cat, categorical_cols)])

model = Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=1000))])
model.fit(X_train, y_train)
model.predict(X_test)
💡
handle_unknown="ignore" matters in production: a category you never saw in training would otherwise raise an error at inference time.

Tuning without fooling yourself

from sklearn.model_selection import GridSearchCV

grid = {"clf__C": [0.01, 0.1, 1, 10],
        "clf__class_weight": [None, "balanced"]}

search = GridSearchCV(model, grid, cv=5, scoring="roc_auc", n_jobs=-1)
search.fit(X_train, y_train)

print(search.best_params_, search.best_score_)
  • Note the step__parameter naming — the double underscore reaches into a pipeline step.
  • Search on the training folds only; keep the test set untouched until the final report.
  • Randomised search (RandomizedSearchCV) often finds a good enough configuration far faster than a full grid.

Saving the whole pipeline

import joblib

joblib.dump(search.best_estimator_, "churn_v3.joblib")

# later, in the service
model = joblib.load("churn_v3.joblib")
model.predict(new_rows)      # preprocessing travels with the model
⚠️
Persist the pipeline, not just the estimator. Saving a bare model forces the serving code to reimplement preprocessing by hand, and the two will drift until predictions silently change meaning.

FAQ

How do I version a model?
Record the training data snapshot, code commit, hyperparameters and metric together. A model file with no provenance is impossible to debug or justify.
What about monitoring in production?
Log inputs and outputs, track the metric where labels eventually arrive, and alert on input drift. Accuracy decays without any code change when the world moves.

Evaluation and overfitting Series and DataFrame

Last refreshed 2026-09-18.