Skip to content
ExponaBLOG
PRACTICAL AI & MACHINE LEARNING · DAY 023 / 100

scikit-learn pipelines: AI & ML best practices explained

Bundle preprocessing and model into one object.

Expona Admin7 min read

Introduction

In production machine learning systems, the gap between training and inference is where bugs hide. You preprocess your training data one way, deploy a model, then realize your inference pipeline applies transformations differently—or skips them altogether. scikit-learn pipelines: AI & ML best practices explained are the antidote. A pipeline bundles your preprocessing steps and model into a single estimator object, ensuring identical transformations every time and eliminating data leakage.

This article walks through why pipelines matter in real systems, how to handle mixed data types with ColumnTransformer, tune the entire pipeline at once, and deploy a single serialized artifact instead of juggling multiple files.

Why Pipelines—Same Transformations in Training and Inference, No Leakage

Consider a common scenario: your team builds a churn prediction model. You train on historical customer data, standardize numeric features, one-hot encode categorical columns, and achieve 82% accuracy on your validation set. Weeks later, you integrate the model into production. The inference service loads the trained model, but the preprocessing logic lives in a separate, hastily written Python script that someone maintains in a different repo.

One day, you notice predictions drift. Investigation reveals that the inference script applies standardization using different mean and variance values than those learned during training. The numeric features are on a completely different scale. Your model accuracy collapses to 71%.

This is training-inference skew, and it's common enough that it deserves prevention. Pipelines prevent it by design.

A scikit-learn pipeline chains together transformers (preprocessing steps) and a final estimator (your model). When you fit the pipeline on training data, all transformers learn their parameters—StandardScaler records the mean and standard deviation, OneHotEncoder records which categories were seen. When you call predict(), the pipeline applies the exact same transformations, using the exact same learned parameters, in the exact same order.

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

# Define pipeline: preprocessing then model
pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression(max_iter=1000))
])

# Fit on training data
pipeline.fit(X_train, y_train)

# Predict on new data—preprocessing happens automatically
y_pred = pipeline.predict(X_test)

No separate preprocessing script. No version mismatch. The pipeline is your source of truth.

ColumnTransformer—Different Preprocessing per Column Type

Real datasets mix numeric and categorical columns. You want to standardize numbers, one-hot encode categories, and leave other columns untouched. A simple Pipeline applies the same transformers to all columns, which doesn't work.

Enter ColumnTransformer. It lets you specify different preprocessing for different column groups, then combines the results. Under the hood, it's a transformer itself, so it fits seamlessly into a pipeline.

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

# Define which columns are numeric and which are categorical
num_cols = ["age", "income", "credit_score"]
cat_cols = ["region", "product_type", "subscription_level"]

# ColumnTransformer: apply different preprocessing to each group
preprocessor = ColumnTransformer([
    ("num", StandardScaler(), num_cols),
    ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols)
])

# Full pipeline: preprocessing then model
model = Pipeline([
    ("preprocessor", preprocessor),
    ("clf", LogisticRegression(max_iter=1000))
])

# Fit and predict
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

The ColumnTransformer receives a list of tuples: (name, transformer, columns). Each transformer is fitted only on its assigned columns. When you transform data, it applies each transformer to its columns, then concatenates the results horizontally.

Notice the handle_unknown="ignore" parameter on OneHotEncoder. During inference, you may encounter categories that didn't appear in training. Ignoring them is safer than throwing an error—the category gets mapped to all zeros, and the model learns to handle that representation.

For a production system, you often have additional column groups: text features (TfidfVectorizer), dates (extracting day-of-week, month), or passthrough columns that need no transformation.

python
from sklearn.feature_extraction.text import TfidfVectorizer

preprocessor = ColumnTransformer([
    ("num", StandardScaler(), num_cols),
    ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
    ("text", TfidfVectorizer(max_features=100), "review_text"),
    ("passthrough", "passthrough", ["id", "timestamp"])
])

The "passthrough" transformer leaves columns unchanged, useful when you need to preserve identifiers or metadata alongside predictions.

Tune the Whole Pipeline—GridSearchCV Works on Pipeline Parameters

Hyperparameter tuning becomes more complex with pipelines, because you now have parameters from preprocessing and from the model. Do you standardize before or after removing outliers? What's the right threshold for the OneHotEncoder? How many features should the TfidfVectorizer keep?

GridSearchCV handles this elegantly. You define a parameter grid using double-underscore notation: "step_name__parameter_name". GridSearchCV searches over all combinations, cross-validates the entire pipeline each time, and returns the best parameters.

python
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC

# Pipeline with SVM
model = Pipeline([
    ("preprocessor", preprocessor),
    ("clf", SVC())
])

# Parameter grid: preprocessing and model params
param_grid = {
    "preprocessor__num__with_mean": [True, False],
    "preprocessor__cat__max_features": [50, 100],
    "clf__C": [0.1, 1, 10],
    "clf__kernel": ["linear", "rbf"]
}

# Grid search
grid_search = GridSearchCV(model, param_grid, cv=5, n_jobs=-1, scoring="f1")
grid_search.fit(X_train, y_train)

print(f"Best params: {grid_search.best_params_}")
print(f"Best score: {grid_search.best_score_}")

# Use best model
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test)

The best_estimator_ is the fitted pipeline with optimal hyperparameters. All transformers and the model are already fitted on the full training set (after the best parameters were determined), so you can predict immediately.

This approach scales to complex pipelines with many preprocessing steps and tuning parameters. It's slower than tuning a model in isolation, but you're optimizing what actually matters: the entire system, not just the model.

Save One Artifact—joblib.dump the Pipeline, Not Separate Pieces

When deployment day arrives, you need to serialize your pipeline. Use joblib, which handles scikit-learn objects efficiently and preserves NumPy arrays and Pandas DataFrames better than pickle.

python
import joblib

# Fit pipeline
model.fit(X_train, y_train)

# Save as single file
joblib.dump(model, "churn_pipeline.pkl")

# Load later
loaded_model = joblib.load("churn_pipeline.pkl")
y_pred = loaded_model.predict(X_new)

One file, one checksum, one version control entry. No risk of loading a model without its scaler, or loading an outdated encoder. Your inference code is trivial: load, predict, done.

In a real system, you version the pipeline alongside your model card or metadata file. Your serving infrastructure knows: "Model version 3.2 is in S3 at s3://models/churn/v3.2/pipeline.pkl, was trained on data from 2024-01-15, and expects CSV input with columns X, Y, Z."

If you need to deploy to a language other than Python (Java, Go, Node.js), you have options: export the pipeline to ONNX format, or build a simple REST API wrapper in Python that serves predictions. The pipeline stays in Python, complexity stays minimal.

Common Mistakes

  • Fitting the preprocessor separately. Avoid fitting ColumnTransformer before the pipeline, then fitting the pipeline. The preprocessor will be fitted twice—once alone, once inside the pipeline. Fit only the pipeline; it manages everything.
  • Forgetting column names in ColumnTransformer. If your input is a Pandas DataFrame, ensure column_types matches actual column names. If your input is a NumPy array, you must pass column indices (integers) instead of names.
  • Not handling unknown categories in production. If your OneHotEncoder encounters a category during inference that wasn't in training, it will fail unless you set handle_unknown="ignore". Real data drifts; plan for it.
  • Leaking information via global statistics. If you standardize the entire dataset before splitting, or use test set statistics in training, the pipeline won't save you. Always fit transformers on training data only, then transform test data. The pipeline enforces this discipline.
  • Mixing pandas and NumPy inconsistently. If your DataFrame column names are strings, pass strings to ColumnTransformer. If you convert to NumPy arrays, use integer indices. Inconsistency causes silent bugs.

Key Takeaways

  • Pipelines bundle preprocessing and models into one estimator, ensuring identical transformations in training and inference, eliminating training-inference skew.
  • Use ColumnTransformer to apply different preprocessing to numeric, categorical, and text columns, then chain it into a pipeline.
  • Tune the entire pipeline with GridSearchCV, optimizing both preprocessing and model hyperparameters simultaneously.
  • Serialize the fitted pipeline with joblib.dump()—one file, one source of truth, no version mismatches.
  • Handle unknown categories and data drift by setting handle_unknown="ignore" on encoders and testing on held-out data.
  • Always fit transformers on training data only; the pipeline enforces this by design when you chain it correctly.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles