Lesson 12 of 30

Chapter 12 — Preprocessing Pipelines

Chapter purpose
This chapter shows how to combine data preparation and model training into reliable, reusable scikit-learn pipelines. The goal is to prevent leakage, guarantee consistent transformations, simplify evaluation, and make the entire machine-learning workflow reproducible.

Learning objectives

  • Explain why preprocessing pipelines are safer than manually transforming training and test data.
  • Build sequential workflows with scikit-learn Pipeline.
  • Use ColumnTransformer to apply different transformations to numerical and categorical columns.
  • Combine imputation, scaling, encoding, and model fitting in one reusable object.
  • Inspect fitted pipeline components, generated feature names, and learned statistics.
  • Diagnose common pipeline errors and design reusable workflows for classification and regression.

12.1 Why pipelines are essential

A supervised-learning workflow usually contains more than the model itself. Raw data may require missing-value treatment, scaling, encoding, feature selection, and other transformations before a learning algorithm can use it. A pipeline packages these steps into a single estimator-like object.

Preventing leakage

Data leakage occurs when information from validation or test examples influences the transformations learned during training. For example, computing a mean on the entire dataset before splitting allows information from the future test set to influence the training process. Pipelines fit transformers only on the training portion passed to fit().

Leakage rule
Any transformation that learns something from data — means, medians, standard deviations, category vocabularies, selected features, dimensionality reductions, and target-dependent statistics — must be fitted only on training data.

 

Python 12.1 — Manual leakage-safe scaling

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

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

# Correct: fit only on the training data
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

 

 

Applying transformations consistently

A common production failure is applying one set of preprocessing rules during training and a slightly different set during prediction. A pipeline removes this mismatch because the same fitted transformations are automatically applied before every prediction.

Simplifying model evaluation

Because a pipeline behaves like a regular estimator, it can be passed directly to cross-validation functions, search procedures, scoring utilities, and prediction APIs. Cross-validation then refits preprocessing separately inside each training fold, which is critical for honest evaluation.

Reproducing experiments

A pipeline explicitly records the ordered sequence of transformations and the estimator. This makes experiments easier to rerun and review because preprocessing is no longer hidden in scattered notebook cells.

Combining preprocessing with training

The most useful pipeline design places all trainable transformations before the final estimator. A call to fit() then learns preprocessing parameters and model parameters together, while predict() performs all required transformations automatically.

Supporting hyperparameter tuning

Pipeline components are named, which means their parameters can be tuned with GridSearchCV or RandomizedSearchCV using the syntax step_name__parameter_name.

Benefit

Without a pipeline

With a pipeline

Leakage controlEasy to fit preprocessing too earlyTransformers are refitted within each training split
ConsistencyManual train/test transformation codeSame fitted chain used everywhere
EvaluationMore bookkeepingWorks directly with cross-validation
ReproducibilitySteps can be scatteredWorkflow represented as one object
TuningSeparate preprocessing/model logicSearch can tune every named step
DeploymentMultiple objects to coordinateOne fitted object can be saved and loaded

 

12.2 scikit-learn Pipeline

Pipeline represents a sequence of processing steps. Every intermediate step must implement fit() and transform(). The final step normally implements fit() and predict(), such as a classifier or regressor.

Sequential processing steps

  1. Raw input data enters the first transformer.
  2. The first transformer learns its parameters during fit() and transforms the data.
  3. The transformed output becomes the input of the next step.
  4. The final estimator learns from the fully processed feature matrix.
  5. During predict(), the already-fitted transformations are applied in the same order before the estimator predicts.

Python 12.2 — A basic numerical classification pipeline

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

pipe = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler()),
    ('model', LogisticRegression(max_iter=1000))
])

pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)

 

 

Transformer steps

Typical transformer steps include SimpleImputer, StandardScaler, MinMaxScaler, OneHotEncoder, PolynomialFeatures, PCA, and feature-selection transformers. Each transformer is fitted in sequence using the output of the previous step.

Final estimator

The last step is the model that performs the supervised task. Examples include LogisticRegression, Ridge, RandomForestClassifier, RandomForestRegressor, SVC, KNeighborsClassifier, and gradient-boosted estimators.

Naming steps

Step names should be short, descriptive, and unique. Good names such as imputer, scaler, encoder, selector, and model make inspection and parameter tuning easier.

Python 12.3 — Accessing named pipeline steps

print(pipe.named_steps.keys())
print(pipe.named_steps['imputer'])
print(pipe.named_steps['scaler'])
print(pipe.named_steps['model'])

 

 

Accessing fitted components

After fitting, the trained transformers and estimator can be inspected through named_steps. This is useful for auditing learned statistics and debugging unexpected model behavior.

Python 12.4 — Inspecting learned quantities

pipe.fit(X_train, y_train)

median_values = pipe.named_steps['imputer'].statistics_
means = pipe.named_steps['scaler'].mean_
scales = pipe.named_steps['scaler'].scale_
coefficients = pipe.named_steps['model'].coef_

print('Imputation values:', median_values)
print('Scaling means:', means)
print('Scaling std:', scales)
print('Model coefficients:', coefficients)

 

 

Predicting with the complete pipeline

Prediction should be made on raw feature rows that match the expected schema. The pipeline applies each fitted transformation automatically, so the caller should not separately scale or encode the new data.

Python 12.5 — Predicting through the complete pipeline

# Raw rows — no manual scaling required
new_predictions = pipe.predict(X_new)
new_probabilities = pipe.predict_proba(X_new)[:, 1]

print(new_predictions)
print(new_probabilities)

 

 

12.3 ColumnTransformer

Real tabular datasets usually contain mixed feature types. Numerical columns may require median imputation and scaling, while categorical columns may require most-frequent imputation and one-hot encoding. ColumnTransformer applies different preprocessing pipelines to selected groups of columns and concatenates the resulting features.

Applying different transformations to different columns

Python 12.6 — Separate numerical and categorical preprocessing

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''tenure_months']
categorical_features = ['region''contract_type']

numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore'))
])

preprocessor = ColumnTransformer([
    ('num', numeric_pipeline, numeric_features),
    ('cat', categorical_pipeline, categorical_features)
])

 

 

Numerical preprocessing

Numerical pipelines often begin with imputation and continue with a scaler. The exact choice depends on the model and feature distribution. Tree models may not need scaling, but imputation may still be necessary.

Categorical preprocessing

Categorical pipelines typically impute missing labels and then encode them. OneHotEncoder(handle_unknown="ignore") is a robust default for many low- to moderate-cardinality nominal variables because unseen categories do not crash prediction.

Passing through selected columns

Some columns may already be usable as-is. The special transformer value "passthrough" keeps these columns unchanged in the transformed matrix.

Python 12.7 — Passing selected features through unchanged

preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_pipeline, numeric_features),
        ('cat', categorical_pipeline, categorical_features),
        ('keep''passthrough', ['is_premium_member'])
    ],
    remainder='drop'
)

 

 

Dropping unwanted columns

Columns not listed in a ColumnTransformer are dropped by default when remainder="drop". This makes the model input schema explicit and helps prevent accidental use of identifiers or leakage variables.

Column group

Typical operations

Example

NumericalImpute → scaleage, salary, temperature
CategoricalImpute → one-hot encodecity, contract, product
Binary already numericPass throughis_active, has_discount
Identifier / leakageDropcustomer_id, post_event_status

 

12.4 Complete preprocessing pipeline

A production-ready tabular workflow typically nests small pipelines inside a ColumnTransformer and then places that preprocessor inside a top-level Pipeline with the supervised estimator.

Typical structure

  1. Numerical imputation.
  2. Numerical scaling.
  3. Categorical imputation.
  4. Categorical encoding.
  5. Model training.

Python 12.8 — Complete classification pipeline

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

classification_pipeline = Pipeline([
    ('preprocess', preprocessor),
    ('model', LogisticRegression(max_iter=1000))
])

classification_pipeline.fit(X_train, y_train)
print(classification_pipeline.score(X_test, y_test))

 

 

The same preprocessor can often be reused with a regression estimator when the feature schema is the same.

Python 12.9 — Reusing preprocessing for regression

from sklearn.ensemble import RandomForestRegressor

regression_pipeline = Pipeline([
    ('preprocess', preprocessor),
    ('model', RandomForestRegressor(
        n_estimators=300,
        random_state=42
    ))
])

regression_pipeline.fit(X_train_reg, y_train_reg)
pred = regression_pipeline.predict(X_test_reg)

 

 

Pipeline and cross-validation

Passing the entire pipeline to cross-validation is safer than preprocessing once beforehand. Each fold learns its own imputation values, scaling statistics, category vocabulary, and model parameters using only the fold’s training subset.

Python 12.10 — Leakage-safe cross-validation of the full pipeline

from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
    classification_pipeline,
    X,
    y,
    cv=cv,
    scoring='f1'
)

print('Fold F1:', scores)
print('Mean F1:', scores.mean())
print('Std F1:', scores.std())

 

 

Pipeline and hyperparameter tuning

Parameters inside nested components are addressed using double underscores. The search procedure refits the complete preprocessing chain for every candidate configuration and validation fold.

Python 12.11 — Tuning preprocessing and model parameters together

from sklearn.model_selection import GridSearchCV

param_grid = {
    'preprocess__num__imputer__strategy': ['mean''median'],
    'model__C': [0.11.010.0]
}

search = GridSearchCV(
    classification_pipeline,
    param_grid=param_grid,
    cv=5,
    scoring='f1',
    n_jobs=-1
)

search.fit(X_train, y_train)
print(search.best_params_)
print(search.best_score_)

 

 

12.5 Pipeline inspection

Pipelines improve encapsulation, but practitioners still need to inspect what happened inside them. Inspection is especially important when one-hot encoding changes the number of columns or when a model behaves unexpectedly.

Feature names after transformation

Python 12.12 — Recovering transformed feature names

classification_pipeline.fit(X_train, y_train)

prep = classification_pipeline.named_steps['preprocess']
feature_names = prep.get_feature_names_out()

print(feature_names)
print('Number of transformed features:'len(feature_names))

 

 

Number of generated features

One categorical input may become many binary columns. Comparing the raw and transformed dimensions helps detect unexpectedly high cardinality or accidental inclusion of identifier-like columns.

Python 12.13 — Comparing raw and transformed dimensions

X_transformed = prep.transform(X_train)
print('Raw shape:', X_train.shape)
print('Transformed shape:', X_transformed.shape)

 

 

Fitted statistics

Python 12.14 — Auditing fitted preprocessing statistics

num_pipe = prep.named_transformers_['num']
cat_pipe = prep.named_transformers_['cat']

print('Numeric imputation values:')
print(num_pipe.named_steps['imputer'].statistics_)

print('Numeric means learned by scaler:')
print(num_pipe.named_steps['scaler'].mean_)

print('Categories learned by encoder:')
for values in cat_pipe.named_steps['encoder'].categories_:
    print(values)

 

 

Debugging pipeline errors

Symptom

Likely cause

Diagnostic / fix

Unknown category errorEncoder configured to reject unseen valuesUse handle_unknown="ignore" when appropriate
Could not convert string to floatCategorical column reached numeric model unencodedCheck ColumnTransformer column lists
NaN accepted during fit but failure laterMissing-value handling incompleteInspect missingness and imputer placement
Different feature count at predictionManual preprocessing or schema mismatchPredict using raw columns through one fitted pipeline
Unexpected huge sparse matrixHigh-cardinality category one-hot encodedInspect feature names and cardinalities
Cross-validation score much lowerPrevious manual preprocessing leaked informationTrust fold-safe pipeline evaluation

 

Python 12.15 — Pipeline debugging checklist

# A compact inspection checklist
print(classification_pipeline)
print(classification_pipeline.get_params().keys())

prep = classification_pipeline.named_steps['preprocess']
print(prep.transformers_)
print(prep.get_feature_names_out())

 

 

Practical lab — Build reusable classification and regression pipelines

In this lab, students construct a reusable preprocessing system for a mixed-type customer dataset. The same preprocessing logic will be paired with both a classifier and a regressor.

Scenario

A subscription company stores customer age, monthly charges, tenure, region, contract type, and premium membership. Some numerical and categorical values are missing. The company wants two models: one to predict churn and another to predict next-month spending.

Lab objectives

  • Identify numerical and categorical columns.
  • Create separate numerical and categorical preprocessing pipelines.
  • Combine them with ColumnTransformer.
  • Build a classification pipeline and a regression pipeline.
  • Evaluate the classifier with stratified cross-validation.
  • Evaluate the regressor with K-fold cross-validation.
  • Inspect transformed feature names and fitted statistics.
  • Test prediction on new raw records containing an unseen category.

Step 1 — Create the example dataset

Lab code 12.A — Generate a mixed-type dataset

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n = 500

df = pd.DataFrame({
    'age': rng.integers(1875, n).astype(float),
    'monthly_charges': rng.normal(7025, n).round(2),
    'tenure_months': rng.integers(172, n).astype(float),
    'region': rng.choice(['North''South''East''West'], n),
    'contract_type': rng.choice(['Monthly''Annual''Two-year'], n),
    'is_premium_member': rng.integers(02, n)
})

# Add some missing values
for col in ['age''monthly_charges''region']:
    idx = rng.choice(df.index, size=20, replace=False)
    df.loc[idx, col] = np.nan

# Two targets for the practical lab
logit = -1.50.02 * df['monthly_charges'].fillna(70) - 0.03 * df['tenure_months']
p = 1 / (1 + np.exp(-logit))
df['churn'] = rng.binomial(1, p)

df['next_month_spend'] = (
    200.8 * df['monthly_charges'].fillna(70)
    + 0.25 * df['tenure_months']
    + rng.normal(012, n)
)

 

 

Step 2 — Define features and preprocessing

Lab code 12.B — Build the reusable preprocessor

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

features = [
    'age''monthly_charges''tenure_months',
    'region''contract_type''is_premium_member'
]
X = df[features]

numeric_features = ['age''monthly_charges''tenure_months']
categorical_features = ['region''contract_type']

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

categorical_pipe = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore'))
])

preprocessor = ColumnTransformer([
    ('num', numeric_pipe, numeric_features),
    ('cat', categorical_pipe, categorical_features),
    ('binary''passthrough', ['is_premium_member'])
])

 

 

Step 3 — Classification model

Lab code 12.C — Evaluate the classification pipeline

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score

classifier = Pipeline([
    ('preprocess', preprocessor),
    ('model', LogisticRegression(max_iter=1000))
])

y_class = df['churn']
cv_class = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

f1_scores = cross_val_score(
    classifier, X, y_class,
    cv=cv_class,
    scoring='f1'
)

print('Classification mean F1:', f1_scores.mean())

 

 

Step 4 — Regression model

Lab code 12.D — Evaluate the regression pipeline

from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold, cross_val_score

regressor = Pipeline([
    ('preprocess', preprocessor),
    ('model', Ridge(alpha=1.0))
])

y_reg = df['next_month_spend']
cv_reg = KFold(n_splits=5, shuffle=True, random_state=42)

mae_scores = -cross_val_score(
    regressor, X, y_reg,
    cv=cv_reg,
    scoring='neg_mean_absolute_error'
)

print('Regression mean MAE:', mae_scores.mean())

 

 

Step 5 — Fit, inspect, and test unseen categories

Lab code 12.E — Production-style prediction with an unseen category

classifier.fit(X, y_class)

prep = classifier.named_steps['preprocess']
print(prep.get_feature_names_out())

new_customer = pd.DataFrame([{
    'age'34,
    'monthly_charges'92.0,
    'tenure_months'7,
    'region''Central',       # unseen category
    'contract_type''Monthly',
    'is_premium_member'1
}])

print('Predicted class:', classifier.predict(new_customer))
print('Churn probability:', classifier.predict_proba(new_customer)[:, 1])

 

 

Lab questions

  1. Why must the imputer and scaler be placed inside the cross-validated pipeline?
  2. What is the role of handle_unknown="ignore" in this example?
  3. Why can the same ColumnTransformer be reused for both targets?
  4. How many transformed features are generated after one-hot encoding?
  5. What would happen if customer_id were accidentally included and one-hot encoded?
  6. Which preprocessing steps could be removed if the final model were a random forest?

Expected deliverable

  • A notebook containing the dataset preparation and both complete pipelines.
  • Cross-validation results for classification and regression.
  • A table describing the preprocessing applied to each feature group.
  • The transformed feature names and feature count.
  • A short explanation of how the pipeline prevents leakage.
  • A successful prediction for a raw record containing an unseen category.

End-of-chapter review

Key ideas to remember

  • Fit preprocessing only on training data; a pipeline automates this discipline.
  • Pipeline chains sequential transformations and a final estimator.
  • ColumnTransformer routes different column groups through different preprocessing procedures.
  • The complete pipeline should normally receive raw features and produce predictions.
  • Cross-validation and hyperparameter tuning should evaluate the full pipeline rather than preprocessed data created in advance.
  • Inspect feature names, dimensions, learned imputation statistics, scaling parameters, and encoder categories when debugging.
  • A saved fitted pipeline is easier to deploy reliably than multiple disconnected preprocessing objects.

Knowledge check

Question

Answer

1. Why is fitting StandardScaler before cross-validation dangerous?Because the scaler learns statistics from observations that later serve as validation data, causing leakage.
2. What must every intermediate Pipeline step implement?fit() and transform().
3. What does ColumnTransformer solve?It applies different transformations to different column subsets and concatenates the results.
4. How are nested parameters addressed in GridSearchCV?Using step names separated by double underscores, such as model__C.
5. Why is get_feature_names_out() useful?It reveals the actual transformed columns after encoding and other feature-producing transformations.
6. What is a good default strategy for unseen nominal categories in many one-hot encoded workflows?OneHotEncoder(handle_unknown="ignore"), when ignoring unseen categories is appropriate for the application.

 

Mini assignment

Choose a mixed-type tabular dataset. Build a complete pipeline that includes at least one numerical and one categorical transformation, compare two final estimators under the same cross-validation scheme, tune at least one preprocessing parameter and one model parameter, inspect the transformed feature names, and explain how your design prevents leakage.