BitWithBite
/
Tier 2 › Module 1 › Lesson 1.3
← Prev Next →
Module 1 — The scikit-learn Workflow

Building a Full
Pipeline Object

35 min
📚 Code + Lab
🎯 Lesson 1.3 of 4
You've seen estimators, transformers, and basic pipelines. Now you'll build one that handles a realistic dataset — mixed numeric and categorical columns, missing values, and a final model — all wrapped in a single object you could deploy tomorrow.

The Problem With Real Data

Tutorial datasets are clean: all-numeric, no missing values, ready to feed straight into model.fit(). Real data never looks like this. A typical dataset has:

Numeric columns
age, income, tenure
Need scaling
May have missing values
May have outliers
Categorical columns
city, department, plan_type
Need encoding
May have missing values
May have unseen categories at inference time

Each column type needs different preprocessing — and it all needs to happen correctly inside cross-validation, without leakage, and be saveable as one object. This is exactly what ColumnTransformer + Pipeline solves.

ColumnTransformer — Different Treatment Per Column

ColumnTransformer lets you apply different transformers to different columns, then combines the outputs side by side into one array.

ColumnTransformer data flow
numeric cols
age, salary
StandardScaler
categorical cols
city, dept
OneHotEncoder
↓  both outputs concatenated  ↓
Single combined feature matrix → model
python
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline

numeric_features     = ['age', 'salary', 'tenure_years']
categorical_features = ['city', 'department']

# Numeric pipeline: fill missing values, THEN scale
numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler',  StandardScaler())
])

# Categorical pipeline: fill missing values, THEN encode
categorical_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore'))
])

# Combine both into one ColumnTransformer
preprocessor = ColumnTransformer(transformers=[
    ('num', numeric_pipeline,     numeric_features),
    ('cat', categorical_pipeline, categorical_features),
])
Pipelines inside ColumnTransformer
Notice each branch is itself a small Pipeline (impute → scale, impute → encode). This is normal and powerful — pipelines can be nested. The numeric branch never touches categorical columns and vice versa.

Handling Missing Values: SimpleImputer

Real data has gaps. SimpleImputer fills them using a strategy you choose:

StrategyFills withUse for
meanAverage of the columnNumeric, roughly normal distribution
medianMiddle valueNumeric with outliers (safer default)
most_frequentMode (most common value)Categorical columns
constantA fixed value you specifyWhen missing itself is meaningful (e.g. fill with 'Unknown')
Imputer must come before scaler
StandardScaler can't compute a mean/std on data containing NaN. Always impute first, then scale — that's why the numeric_pipeline above puts imputer as step 1 and scaler as step 2.

Assembling the Final Pipeline

Now attach the preprocessor to a model. The whole thing becomes one fittable, predictable, saveable object:

python — full assembled pipeline
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
import pandas as pd
import numpy as np

# Realistic messy dataset
data = pd.DataFrame({
    'age':           [25, 32, np.nan, 19, 41, 38],
    'salary':        [40000, 75000, 95000, 30000, np.nan, 88000],
    'tenure_years':  [1, 5, 8, 0.5, 12, 6],
    'city':          ['London', 'Paris', 'London', np.nan, 'Berlin', 'Paris'],
    'department':    ['Eng', 'HR', 'Eng', 'Finance', 'Eng', 'HR'],
})
y = [1, 0, 1, 0, 1, 0]

# (preprocessor built exactly as in the previous example)

# Full pipeline: preprocessing + model in ONE object
full_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('model',        GradientBoostingClassifier(random_state=42))
])

X_train, X_test, y_train, y_test = train_test_split(
    data, y, test_size=0.33, random_state=42
)

# One call does: impute → scale/encode → fit model
full_pipeline.fit(X_train, y_train)

# One call does: impute → scale/encode → predict
predictions = full_pipeline.predict(X_test)

# Cross-validate the WHOLE pipeline — no leakage between folds
scores = cross_val_score(full_pipeline, data, y, cv=3)
print(f"CV scores: {scores}")

Saving and Reloading the Pipeline

Because everything is one object, deployment is one line:

python
import joblib

# Save the entire fitted pipeline — preprocessing AND model
joblib.dump(full_pipeline, 'model_pipeline.joblib')

# Later, in production, load and use directly
loaded_pipeline = joblib.load('model_pipeline.joblib')

# New raw data — no manual preprocessing needed
new_data = pd.DataFrame({
    'age': [29], 'salary': [52000], 'tenure_years': [2],
    'city': ['Madrid'],  # unseen city — handled by handle_unknown='ignore'
    'department': ['Eng']
})

prediction = loaded_pipeline.predict(new_data)
print(prediction)
This is the production pattern
Notice: new_data is raw, unprocessed data with a missing-value-free but never-seen city ("Madrid"). The pipeline handles imputation, encoding (including the unknown category), scaling, and prediction — all from one .predict() call. This is exactly how a FastAPI endpoint would use it.
🧪 LAB

Lab: Build Your Own Pipeline

Using a dataset of your choice (or the example above), build a complete pipeline that:

1
Identifies numeric and categorical columns
Write code that programmatically splits columns by dtype rather than hardcoding names — use data.select_dtypes().
2
Builds separate sub-pipelines for each type
Numeric: impute (median) → scale. Categorical: impute (most_frequent) → one-hot encode.
3
Combines them with ColumnTransformer
Make sure handle_unknown='ignore' is set on the encoder.
4
Attaches a model and cross-validates
Use cross_val_score with cv=5 on the full pipeline. Report mean and std.
5
Saves the fitted pipeline with joblib
Then reload it in a fresh Python session and predict on new raw data to confirm it works end-to-end.
✓ Check your understanding
1. Inside a numeric sub-pipeline, why must the imputer come before the scaler?
AIt's just a convention — order doesn't actually matter
BStandardScaler cannot compute mean/std on data containing NaN values
CThe imputer needs scaled data to work correctly
DScikit-learn pipelines always sort steps alphabetically
Correct. StandardScaler's fit() computes mean and standard deviation — both undefined if NaN values are present in the data. The imputer must fill missing values first so the scaler has complete numeric data to compute statistics from.
2. What does handle_unknown='ignore' do in OneHotEncoder?
ASkips rows that contain unknown categories
BCrashes with a clear error message instead of a confusing one
COutputs all-zero columns for a category never seen during fit()
DAutomatically retrains the encoder on new categories
Correct. Without this setting, encountering a brand-new category at prediction time (like a city not in the training set) raises an error. With handle_unknown='ignore', the encoder just outputs all zeros for that category's one-hot columns, letting the pipeline continue instead of crashing in production.
3. What is the main advantage of saving the entire Pipeline with joblib, rather than just the model?
AIt makes the file smaller
BNew raw data can be predicted on directly — no manual preprocessing required at inference time
CIt trains faster the next time you load it
DIt's required by scikit-learn — models can't be saved alone
Correct. If you only saved the model, you'd have to remember and reimplement the exact preprocessing steps (which columns, which imputer strategy, which encoder, fitted statistics) every time you predict. Saving the whole pipeline means raw data goes in, predictions come out — exactly what a production API needs.

Summary

What you learned
ColumnTransformer applies different preprocessing to different column groups, then merges outputs
→ Each branch can be its own mini Pipeline (e.g. impute → scale)
SimpleImputer fills missing values — median/mean for numeric, most_frequent for categorical
→ Imputer always comes before scaler/encoder in a sub-pipeline
handle_unknown='ignore' prevents crashes on unseen categories in production
→ Saving the whole pipeline with joblib means raw data → predictions in one call
← 1.2 Preprocessing: Scaling & Encoding Next: 1.4 Saving and Loading Models →
🗒 Cheat Sheet 📝 Worksheet