TAI BUI
Back to curriculum
BuildPython

ML Pipelines

A model is not the product. A pipeline is: everything from raw data to a deployed prediction, every step reproducible.

TIME: ~120 minutesPREREQUISITES: Phase 2, Lesson 12 (Hyperparameter Tuning)

Learning Objectives

  • Build an ML pipeline from scratch that chains imputation, scaling, encoding, and model training into a single reproducible object
  • Identify data leakage scenarios and explain how pipelines prevent them by fitting transformers only on training data
  • Construct a ColumnTransformer that applies different preprocessing to numeric and categorical features
  • Implement pipeline serialization and demonstrate that the same fitted pipeline produces identical results in training and production

The Problem

You have a notebook that loads data, fills missing values with the median, scales features, trains a model, and prints an accuracy number. It works, so you ship it.

A month later someone retrains the model and gets different numbers. The median was computed on the full dataset, test rows included: that's leakage. The scaling parameters were never saved, so inference runs on different statistics than training did. The feature engineering got copy-pasted between the training script and the serving code, and the two copies drifted apart. Then a categorical column shows up in production with a value the encoder has never seen, and the whole thing falls over.

None of this is exotic; it's the ordinary way ML systems break in production. Every one of these failures traces back to the same root cause: the transformations weren't packaged with the model. A pipeline fixes that by fusing every transformation step and the model into one ordered, reproducible object, fit once and applied identically everywhere.

Warm-up0/3
1. Why do we hold out a test set that the model never trains on?Warm-up
Ten row cells, eight marked as the train block and two as the test block, with the model fitting on the train block only.
Illustrative 10-row split, typed.
2. What does standardizing a numeric feature (subtract the mean, divide by the standard deviation) do?Warm-up
A row of eight numbers 2, 4, 4, 4, 5, 5, 7, 9 above a row of eight question marks standing for the transformed values.
Typed example column.
3. A numeric column in a CSV has some empty cells and a model cannot accept missing values. What is a common fix?Warm-up
A row of seven cells under the heading age holding 34, 29, empty, 41, 38, empty and 52.
Typed example column.
Check: The Problem0/2
1. What does fusing the transformations and the model into one object remove that a notebook plus separate serving code leaves in place?Explain
A notebook with five cells from load data to print accuracy, a model weights box, and serving code with receive request and model.predict, joined by arrows.
Typed example.
2. Production metrics stop matching training metrics weeks after launch. The serving code holds a copy of the notebook's feature engineering that was edited in only one place. What is the diagnosis and the fix?Analyze
A line chart over weeks after launch: the training-time metric stays flat while the production metric starts equal and then falls.
Illustrative shape, not measured data.

The Concept

What a Pipeline Is

A pipeline is an ordered sequence of data transformations that ends in a model, each step feeding its output straight into the next. Fit the whole thing once on training data, and at inference time that same fitted pipeline transforms new data and produces predictions — no separate code path, no re-deriving statistics.

Mermaid diagram

The pipeline guarantees:

  • Transformations are fitted only on training data (no leakage)
  • The same transformations are applied at inference time
  • The entire object can be serialized and deployed as one artifact
  • Cross-validation applies the pipeline per fold, preventing subtle leakage

Data Leakage: The Silent Killer

Data leakage is any information from the test set, or from the future, sneaking into training. It inflates whatever accuracy number you report, and pipelines block the most common forms of it by construction.

Leaky (wrong):

python
X = df.drop("target", axis=1)
y = df["target"]
 
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
 
X_train, X_test = X_scaled[:800], X_scaled[800:]
y_train, y_test = y[:800], y[800:]

The scaler saw test data before the model ever did. Its mean and standard deviation already include test samples, so the accuracy you measure afterward is inflated.

Correct:

python
X_train, X_test = X[:800], X[800:]
 
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

With a pipeline, this stops being something you have to remember to get right. It's handled by construction, every time.

sklearn Pipeline

sklearn's Pipeline chains transformers and a final estimator into one object, exposing .fit(), .predict(), and .score() that run every step in order.

python
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)
predictions = pipe.predict(X_test)

When you call pipe.fit(X_train, y_train):

  1. Scaler calls fit_transform on X_train
  2. Model calls fit on the scaled X_train

When you call pipe.predict(X_test):

  1. Scaler calls transform (not fit_transform) on X_test
  2. Model calls predict on the scaled X_test

The scaler never sees test data during fitting; that's the entire point of wrapping it in a Pipeline.

ColumnTransformer: Different Pipelines for Different Columns

ColumnTransformer routes different subsets of columns to different preprocessing pipelines, then combines the results into one feature matrix. Real datasets need this: numeric columns want imputation and scaling, categorical columns want a different imputation strategy and encoding, and hand-rolling that split every time is how column-order bugs get in.

python
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
 
numeric_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale", StandardScaler()),
])
 
categorical_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("encode", OneHotEncoder(handle_unknown="ignore")),
])
 
preprocessor = ColumnTransformer([
    ("num", numeric_pipe, ["age", "income", "score"]),
    ("cat", categorical_pipe, ["city", "gender", "plan"]),
])
 
full_pipeline = Pipeline([
    ("preprocess", preprocessor),
    ("model", GradientBoostingClassifier()),
])

handle_unknown="ignore" on OneHotEncoder is the one setting that actually matters for production. When a new category shows up (a city the model never saw during training), it returns a zero vector instead of raising and taking the request down with it.

Experiment Tracking

Experiment tracking is the record of what happened across every training run: which hyperparameters, which dataset version, what the metrics came out to, which code was actually running. A pipeline makes any single run reproducible, but it doesn't by itself tell you what you tried last week or why run 47 beat run 46.

MLflow is the most common open-source solution:

python
import mlflow
 
with mlflow.start_run():
    mlflow.log_param("max_depth", 5)
    mlflow.log_param("n_estimators", 100)
    mlflow.log_param("learning_rate", 0.1)
 
    pipe.fit(X_train, y_train)
    accuracy = pipe.score(X_test, y_test)
 
    mlflow.log_metric("accuracy", accuracy)
    mlflow.sklearn.log_model(pipe, "model")

Every run gets recorded with its parameters, metrics, artifacts, and the full model, so you can compare runs, reproduce any of them, and deploy whichever version actually won.

Weights & Biases (wandb) provides the same functionality with a hosted dashboard:

python
import wandb
 
wandb.init(project="my-pipeline")
wandb.config.update({"max_depth": 5, "n_estimators": 100})
 
pipe.fit(X_train, y_train)
accuracy = pipe.score(X_test, y_test)
 
wandb.log({"accuracy": accuracy})

Model Versioning

Model versioning is knowing, at any moment, exactly which model is in production, which is staging, and which one you retired last week. Experiment tracking tells you what you tried; versioning tells you what's actually live.

MLflow's Model Registry provides:

  • Version tracking: Every saved model gets a version number
  • Stage transitions: "Staging", "Production", "Archived"
  • Approval workflow: Models must be explicitly promoted to production
  • Rollback: Switch back to a previous version instantly

Data Versioning with DVC

DVC (Data Version Control) versions large data files the way git versions code: a content hash lives in git, the actual bytes live in remote storage. Git alone can't do this, since it chokes on large binary files and was never built to track them.

dvc init
dvc add data/training.csv
git add data/training.csv.dvc data/.gitignore
git commit -m "Track training data"
dvc push

In practice that remote storage is S3, GCS, or Azure, and the .dvc file left behind in git is tiny: just the hash. Switch git commits and run dvc checkout, and it restores the exact data that commit was trained on.

That's the real payoff: every git commit now pins both code and data, so reproducing last month's experiment stops being an archaeology project.

Reproducible Experiments

A reproducible experiment requires four things:

  1. Fixed random seeds: Set seeds for numpy, random, and the framework (torch, sklearn)
  2. Pinned dependencies: requirements.txt or poetry.lock with exact versions
  3. Versioned data: DVC or similar
  4. Config files: All hyperparameters in a config, not hardcoded
python
import numpy as np
import random
 
def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    try:
        import torch
        torch.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)
        torch.backends.cudnn.deterministic = True
    except ImportError:
        pass

From Notebook to Production Pipeline

Mermaid diagram

The typical progression:

  1. Notebook exploration: Quick experiments, visualizations, feature ideas
  2. Extract functions: Move preprocessing, feature engineering, evaluation into modules
  3. Build Pipeline: Chain transformations into a sklearn Pipeline or custom class
  4. Config management: Move all hyperparameters into a YAML/JSON config
  5. Experiment tracking: Add MLflow or wandb logging
  6. Data validation: Check schema, distributions, and missing value patterns before training
  7. Tests: Unit tests for transformers, integration tests for the full pipeline
  8. Deployment: Serialize the pipeline, wrap in an API (FastAPI, Flask), containerize

Common Pipeline Mistakes

MistakeWhy it is badFix
Fitting on full data before splittingData leakageUse Pipeline with cross_val_score
Feature engineering outside pipelineDifferent transforms at train vs servePut all transforms in the Pipeline
Not handling unknown categoriesProduction crash on new valuesOneHotEncoder(handle_unknown="ignore")
Hardcoded column namesBreaks when schema changesUse column name lists from config
No data validationSilently wrong predictions on bad dataAdd schema checks before prediction
Training/serving skewModel sees different features in prodOne Pipeline object for both
Check: The Concept0/25
1. What is data leakage in an ML workflow?Define
Eight lines of Python: drop the target column, fit and apply a StandardScaler to all of X, then slice the first 800 rows as train and the rest as test.
Snippet copied from the lesson.
2. What is a pipeline in the sense this lesson uses?Define
Python code that builds an object from two named steps, a StandardScaler and a LogisticRegression, then calls fit on training data and predict on test data.
Snippet copied from the lesson.
3. What does a ColumnTransformer do?Define
A table with columns age, income, score, city, gender and plan and three example rows, followed by the text ColumnTransformer of dots equals a question mark.
Rows typed for illustration.
4. What is experiment tracking?Define
Three notebook cells that each fit a pipeline and print a score: 0.88, 0.91 and 0.90, followed by the question of which settings produced the 0.91.
Typed example, not captured.
5. What is MLflow?Define
A Python script that imports mlflow, opens a run with start_run, logs three params, fits a pipeline, logs an accuracy metric and logs the model.
Snippet copied from the lesson.
6. What problem does DVC (Data Version Control) solve that git alone cannot?Define
Five shell commands: dvc init, dvc add data/training.csv, git add of two files, git commit, and dvc push.
Commands from the lesson; no output shown.
7. What is a model registry?Define
A listing of a models folder with four pickle files named model, model_new, model_final and model_final_v2, next to questions about which is serving and how to go back to an older one.
Typed example listing.
8. What is training/serving skew?Define
Two rows of boxes: training data to preprocessing code to model, and live request to preprocessing code to model, labelled train time and serve time.
Schematic.
9. What does a reproducible ML experiment require?Define
Two boxes, last month with a trained and scored model and today with a teammate rerunning the experiment, and a question about whether the result will be the same.
Schematic.
10. In the lesson's notebook-to-production progression, which step comes directly after building the Pipeline?Define
Eight numbered slots in two rows; slots 1 to 3 are labelled notebook exploration, extract functions and build pipeline, slot 4 shows a question mark, and slots 5 to 8 are empty.
Slot order from the lesson's list.
11. Why is fitting a scaler on the full dataset before splitting into train and test considered leaky?Explain
Ten values in a row, then two steps: fit and apply the scaler to all ten rows, then split into eight train rows and two test rows.
Typed example column.
12. In sklearn, how does fit_transform differ from transform on a pipeline step?Explain
A fresh StandardScaler with a comment that no mean or std has been learned yet, with the two method names fit_transform and transform listed next to a question about what each does.
Schematic with typed state.
13. Why does cross_val_score on a Pipeline give a more honest score than scaling the full dataset first and then cross-validating?Explain
Two code blocks, A scales X first then calls cross_val_score, B wraps the scaler and model in a Pipeline and passes it to cross_val_score, with five fold boxes underneath.
Typed code; cv=5 chosen for illustration.
14. Why does a real-world dataset usually need a ColumnTransformer?Explain
A table with columns age, income, score, city, gender and plan and three rows, with a type note under each column: number for the first three and text for the last three.
Rows typed for illustration.
15. Why do numeric and categorical columns get separate sub-pipelines inside the lesson's preprocessor?Explain
A table with three example rows where one age cell and one city cell are empty, above two boxes: numeric_pipe for age, income and score, and categorical_pipe for city, gender and plan, each with steps marked as a question mark.
Rows typed for illustration; column groups are the lesson's.
16. How does model versioning differ from experiment tracking?Explain
Four circles numbered 44 to 47 joined by arrows, labelled training runs, and an arrow to a box labelled serving requests.
Run numbers are illustrative.
17. A pipeline's code hardcodes the column names, and the schema later changes. What fix does the lesson give?Explain
Two column lists written into pipeline code, age income score and city gender plan, above a new table header where income has become annual_income.
Typed example of a renamed column.
18. Using the lesson's preprocessor, a row has age missing and city missing. How are the two gaps filled?Apply
A one-row table with columns age, income, score, city, gender and plan where age and city are empty, with a question mark for how each gap is filled.
Typed row.
19. In the lesson's correct snippet, which statistics scale X_test?Apply
A train column of eight numbers, a test column of two numbers, and the line X_test_scaled equals scaler.transform of X_test with the mean and std left as question marks.
Typed example columns.
20. A production model receives a categorical value it never saw during training. Which setting from the lesson keeps the pipeline from crashing?Apply
A list of the city values Paris, Rome and Oslo seen in training and a live request with the city Lima, which was not in that list.
Typed example cities.
21. One team scales the full dataset and then cross-validates, and reports a higher score than a teammate who puts the scaler inside a Pipeline. Which score is the more trustworthy, and why?Analyze
Two flows: team 1 scales all rows then cross-validates and reports a higher score; team 2 cross-validates a pipeline of scaler and model and reports a lower score.
Schematic of the stem's situation.
22. Run 47 scores higher than run 46, but nobody can say which hyperparameters, data version or code each used. Which piece is missing?Analyze
A table with rows for run 46 with a lower score and run 47 with a higher score, where the depth, trees, data and code columns are all question marks.
Schematic of the stem.
23. Your team can adopt only one piece of this lesson's stack before a deadline. Their serving code is a pasted copy of the notebook's preprocessing. Which piece gives the most protection?Design
Two identical three-line code blocks, one from a notebook and one from a serve.py file, followed by a note that the team can adopt only one tool before a deadline.
Typed example of a pasted copy.
24. A new teammate must reproduce last month's model. The code is tagged in git and dependencies are pinned, but the training CSV on a shared drive was edited in place. Which addition fixes this?Design
Three boxes: code tagged in git, dependencies pinned, and training.csv on a shared drive that was edited in place after training.
Schematic of the stem.
25. You add a transformer that creates polynomial features for two numeric columns. Where should it go, and why?Design
Raw data goes to a split and then to a pipeline of impute, scale and model; a serving API calls the model; a dashed box for a new polynomial features transformer has no place yet.
Schematic.

Build It

The code in code/pipeline.py builds a complete ML pipeline from scratch, in four steps:

Step 1: Custom Transformer

python
class CustomTransformer:
    def __init__(self):
        self.means = None
        self.stds = None
 
    def fit(self, X):
        self.means = np.mean(X, axis=0)
        self.stds = np.std(X, axis=0)
        self.stds[self.stds == 0] = 1.0
        return self
 
    def transform(self, X):
        return (X - self.means) / self.stds
 
    def fit_transform(self, X):
        return self.fit(X).transform(X)

Step 2: Pipeline from Scratch

python
class PipelineFromScratch:
    def __init__(self, steps):
        self.steps = steps
 
    def fit(self, X, y=None):
        X_current = X.copy()
        for name, step in self.steps[:-1]:
            X_current = step.fit_transform(X_current)
        name, model = self.steps[-1]
        model.fit(X_current, y)
        return self
 
    def predict(self, X):
        X_current = X.copy()
        for name, step in self.steps[:-1]:
            X_current = step.transform(X_current)
        name, model = self.steps[-1]
        return model.predict(X_current)

Step 3: Cross-Validation with Pipeline

The code demonstrates how cross-validation with a pipeline prevents data leakage: the scaler gets fit separately on each fold's training data, never on the fold it's about to score.

Step 4: Full Production Pipeline with sklearn

A full production pipeline: ColumnTransformer routing to multiple preprocessing paths, a model at the end, trained with proper cross-validation and experiment logging.

Step 5: Serialize and Verify

A fitted pipeline is a Python object like any other, so joblib can freeze it to disk and hand it to a process that never saw the training data:

python
import joblib
 
joblib.dump(pipe, "pipe.joblib")
 
loaded = joblib.load("pipe.joblib")
predictions_original = pipe.predict(X_test)
predictions_loaded = loaded.predict(X_test)
 
assert (predictions_original == predictions_loaded).all()

That assertion is the entire point. The loaded object carries the same fitted imputer, the same fitted encoder, the same trained model as the pipeline you trained. Nothing refits, nothing recomputes a statistic from a different sample, so training and production see exactly the same transformations. This is what "reproducible" cashes out to in practice: not a promise, a passing assert.

Check: Build It0/4
1. After `joblib.dump(pipe, ...)` in training and `joblib.load(...)` in a separate serving process, why do the reloaded pipeline's predictions match the original exactly?Explain
A training process dumps a file named pipe.joblib and a serving process, which never saw the training data, loads it, with a question about whether their predictions match.
Schematic.
2. A CustomTransformer is fit on X = [[5, 1], [5, 3]]. What does transform([[5, 3]]) return?Apply
A 2 by 2 array with rows 5, 1 and 5, 3, a transformer fitted on it, and a call to transform on the single row 5, 3 whose result is a question mark.
Values from the question.
3. A PipelineFromScratch has steps imputer, scaler, model. After fit(X, y), what feature matrix did model.fit receive?Apply
A three-by-two raw matrix with one empty cell, followed by three boxes imputer, scaler and model, and a question mark for what the model's fit received.
Typed example matrix; one cell is empty.
4. A serving team saves pipe.joblib, but their API recomputes the imputer's median from each incoming request batch. Predictions no longer match the notebook. What is the wrong assumption?Analyze
A saved file pipe.joblib and two request batches of ages, 24, 60, empty and 41, 47, empty, each with a question mark for the value used to fill the empty age.
Typed example batches.

Ship It

This lesson produces:

  • outputs/prompt-ml-pipeline.md -- a skill for building and debugging ML pipelines
  • code/pipeline.py -- a complete pipeline from scratch through sklearn
Author's Note

The leak everyone teaches is the scaler fit on the full dataset before the split. I've seen that maybe twice in five years. The leak that actually gets people is quieter: feature engineering that starts life in a notebook cell, gets copy-pasted into the serving code a few weeks later, and drifts the day someone tweaks it in only one of the two places. Nobody notices until the metrics in production stop matching the metrics from training, and by then it's a multi-day investigation instead of a five-minute diff.

Wrapping everything in a Pipeline object doesn't just prevent leakage. It removes the second copy entirely, so there's nothing left to drift. If you're under deadline pressure and can only adopt one piece of this lesson's stack right now, skip the experiment tracker and the model registry and make it this one. Everything else here is about managing complexity you've chosen to take on. The pipeline is about not creating a second, silently diverging copy of your model in the first place.

Exercises

  1. Build a pipeline that handles a dataset with 3 numeric columns and 2 categorical columns. Use ColumnTransformer to apply median imputation + scaling to numerics and most-frequent imputation + one-hot encoding to categoricals. Train with 5-fold cross-validation.

  2. Deliberately introduce data leakage: fit the scaler on the full dataset before splitting. Compare the cross-validation score (leaky) to the pipeline cross-validation score (clean). How large is the difference?

  3. Serialize your pipeline with joblib.dump. Load it in a separate script and run predictions. Verify the predictions are identical.

  4. Add a custom transformer to the pipeline that creates polynomial features (degree 2) for the two most important numeric columns. Where should it go in the pipeline?

  5. Set up MLflow tracking for the pipeline. Run 5 experiments with different hyperparameters. Use the MLflow UI (mlflow ui) to compare runs and pick the best model.

Key Terms

TermWhat people sayWhat it actually means
Pipeline"Chain of transforms + model"An ordered sequence of fitted transformers and a model, applied as one unit to prevent leakage
Data leakage"Test info leaked into training"Using information from outside the training set to build the model, inflating performance estimates
ColumnTransformer"Different preprocessing per column"Applies different pipelines to different subsets of columns, combining results
Experiment tracking"Logging your runs"Recording parameters, metrics, artifacts, and code versions for every training run
MLflow"Track and deploy models"Open-source platform for experiment tracking, model registry, and deployment
DVC"Git for data"Version control system for large data files, storing hashes in git and data in remote storage
Model registry"Model version catalog"A system that tracks model versions with stage labels (staging, production, archived)
Training/serving skew"It worked in the notebook"Differences between how data is processed during training versus inference, causing silent errors
Reproducibility"Same code, same result"The ability to get identical results from the same code, data, and configuration

Further Reading

Code

Ships With