DEV Community

Cover image for Using Scikit-learn Pipelines: A Practical Technical Guide
Jonathan kip
Jonathan kip

Posted on

Using Scikit-learn Pipelines: A Practical Technical Guide

Think of a scikit-learn Pipeline as a “recipe” for your machine learning workflow: it lists the steps (cleaning, transforming, modeling) in order and runs them automatically every time you train or predict. This keeps your code tidy, prevents mistakes, and makes your models easier to reuse.

What Problem Do Pipelines Solve?

Without pipelines, you might:

  • Impute missing values, then scale, then train a model—using separate commands.
  • Accidentally fit preprocessing on the whole dataset (including test data), which leaks information.
  • Repeat the same steps for training, validation, and deployment.

Pipelines fix this by:

  • Bundling all steps into one object.
  • Ensuring preprocessing is fit only on training data during cross-validation.
  • Letting you call fit and predict once on the whole workflow.

The Basic Idea: Steps in Order

A pipeline is just an ordered list of named steps:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression())
])

pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
Enter fullscreen mode Exit fullscreen mode

What happens inside:

  • On fit: StandardScaler learns mean and std from X_train, transforms X_train, then LogisticRegression fits on the scaled data.
  • On predict: StandardScaler transforms X_test using the training statistics, then LogisticRegression predicts.

You never manually scale test data; the pipeline does it consistently.

A Simpler Constructor: make_pipeline

If you don’t care about step names, use make_pipeline:

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipe = make_pipeline(StandardScaler(), LogisticRegression())
Enter fullscreen mode Exit fullscreen mode

scikit-learn names the steps automatically (standardscaler, logisticregression).

Handling Different Column Types: ColumnTransformer

Real data has numeric and categorical columns that need different treatment. ColumnTransformer lets you apply different preprocessing to different columns, then combines them.

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

numeric_features = ["age", "income"]
categorical_features = ["city", "subscription"]

numeric_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

categorical_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore"))
])

preprocess = ColumnTransformer([
    ("num", numeric_pipe, numeric_features),
    ("cat", categorical_pipe, categorical_features)
])

model = Pipeline([
    ("preprocess", preprocess),
    ("clf", LogisticRegression())
])

model.fit(X_train, y_train)
y_pred = model.predict(X_test)
Enter fullscreen mode Exit fullscreen mode

Key idea: define “what to do” for each group of columns once, then reuse.

Why This Prevents Data Leakage

Data leakage happens when information from the test set influences training. Common mistake:

# Wrong: scaling before split or on full X
scaler = StandardScaler().fit(X)
X_scaled = scaler.transform(X)
Enter fullscreen mode Exit fullscreen mode

Correct pattern with pipelines:

model = Pipeline([
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression())
])

model.fit(X_train, y_train)  # scaler learns only from X_train
y_pred = model.predict(X_test)  # scaler uses training stats on X_test
Enter fullscreen mode Exit fullscreen mode

During cross-validation, scikit-learn refits the entire pipeline on each training fold, so preprocessing never sees the validation fold.

Tuning Hyperparameters Across Steps

Pipelines let you tune parameters from any step using stepname__param syntax.

from sklearn.model_selection import GridSearchCV

param_grid = {
    "scaler__with_mean": [True, False],
    "clf__C": [0.1, 1, 10]
}

grid = GridSearchCV(model, param_grid, cv=5)
grid.fit(X_train, y_train)
Enter fullscreen mode Exit fullscreen mode

This searches over both scaling options and model regularization jointly.

Custom Steps When You Need Them

Sometimes you need your own transformation. You can:

  • Wrap a simple function with FunctionTransformer.
  • Or write a small class that follows scikit-learn’s transformer interface.

Simple function example:

from sklearn.preprocessing import FunctionTransformer
import numpy as np

pipe = Pipeline([
    ("log", FunctionTransformer(np.log1p, validate=False)),
    ("clf", LogisticRegression())
])
Enter fullscreen mode Exit fullscreen mode

For more complex logic, define a class with fit and transform methods and use it like any other step.

Saving and Reusing Your Model

Once your pipeline is trained, save the whole thing:

import joblib

joblib.dump(model, "model_pipeline.joblib")

# Later, in another script or service:
loaded = joblib.load("model_pipeline.joblib")
y_pred = loaded.predict(X_new)
Enter fullscreen mode Exit fullscreen mode

You don’t need to re-implement preprocessing; it’s all inside the saved pipeline.

Minimal Complete Example

from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
import pandas as pd

# Load a sample dataset
data = fetch_openml("adult", version=2, as_frame=True)
X = data.data
y = (data.target == ">50K").astype(int)

num_cols = X.select_dtypes(include=["int64", "float64"]).columns
cat_cols = X.select_dtypes(include=["object", "category"]).columns

num_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

cat_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore"))
])

preprocess = ColumnTransformer([
    ("num", num_pipe, num_cols),
    ("cat", cat_pipe, cat_cols)
])

model = Pipeline([
    ("preprocess", preprocess),
    ("clf", RandomForestClassifier(n_estimators=200, random_state=42))
])

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

model.fit(X_train, y_train)
print("Test accuracy:", model.score(X_test, y_test))
Enter fullscreen mode Exit fullscreen mode

This single model object handles imputation, encoding, scaling, and prediction.

Quick Checklist for Using Pipelines

  • Put all preprocessing inside the pipeline, not outside.
  • Fit only on X_train, y_train; use the pipeline to predict on new data.
  • Use ColumnTransformer when you have mixed column types.
  • Use make_pipeline for quick experiments; use Pipeline when you want clear step names.
  • Save the entire pipeline for deployment.

Top comments (0)