Skillversitas

Scikit-learn Pipelines

A pipeline binds preprocessing steps and the model into a single estimator: one fit, one predict, and - crucially - each transformer is fitted only on the training rows of each cross-validation fold, which makes data leakage structurally impossible. If you have ever fitted a scaler on the full dataset before splitting, a pipeline is the fix you keep.

The Basic Pipeline

from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(max_iter=2000)),
])

pipe.fit(Xtr, ytr)                 # imputer and scaler see only the train rows
pipe.predict(Xte)                  # all steps run in order, automatically
pipe.predict_proba(Xte)[:, 1]      # methods are forwarded to the final estimator

Every step must implement fit and transform; the final step implements fit and predict. make_pipeline builds the same object with auto-generated step names from the class names - faster to type, but the explicit names are what you address in a search grid, so prefer them for anything real.

Addressing Parameters Inside a Pipeline

The step__parameter syntax reaches into any step, which is what makes pipelines tunable as a unit:

from sklearn.model_selection import GridSearchCV

grid = {
    "model__C": [0.01, 0.1, 1.0, 10.0],
    "model__penalty": ["l1", "l2"],
    "imputer__strategy": ["mean", "median"],
}
search = GridSearchCV(pipe, grid, cv=5, n_jobs=-1)
search.fit(Xtr, ytr)
print(search.best_params_)
# every configuration is evaluated with the fit-on-train discipline
# per fold - the search cannot leak by construction

Change a step's parameters after construction the same way, with pipe.set_params(model__C=0.5). The fitted state of each step lives under pipe.named_steps["scaler"], which is how you inspect a trained transformer or reuse its scaling at inference time.

Separate Paths with ColumnTransformer

Real tables mix numeric and categorical columns that need different treatment; ColumnTransformer applies a sub-pipeline per column group and joins the results side by side:

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

num = Pipeline([("scaler", StandardScaler())])
cat = Pipeline([("ohe", OneHotEncoder(handle_unknown="ignore",
                                       sparse_output=False))])

pre = ColumnTransformer([
    ("num", num, ["age", "income", "tenure"]),
    ("cat", cat, ["region", "plan"]),
])
pipe = Pipeline([("pre", pre), ("model", LogisticRegression(max_iter=2000))])

handle_unknown="ignore" is the production detail: a category first seen in production becomes an all-zeros row instead of an exception. This two-part preprocessor plus a model is the standard shape for tabular data, and it is what the end-to-end project assembles.

Why It Matters

  • Leakage-proof: transforms are fit inside each CV fold on the fold's training rows only.
  • One object to ship: joblib.dump(pipe, "model.joblib") serializes preprocessing and model together; deployment can no longer forget the scaler.
  • One interface: pipelines expose the estimator API, so cross-validation, search, and scoring treat the whole chain like a single model.

Summary

Build every real model as a Pipeline of named steps ending in the estimator: it enforces fit-on-train-only per fold, makes hyperparameter search addressable with step__param, and serializes the entire transform chain with the model. Add a ColumnTransformer for mixed-type tables, and the class of bug where the scaler is fitted on the wrong rows simply stops existing.