Machine Learning › Features and Support Vector Machines › Day 173
Hands-on lab — Day 173: scikit-learn Pipelines
- ← Back to the Day 173 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/machine-learning/day-173-scikit-learn-pipelines/
Commands
Setup
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt Run
.venv/bin/python examples/pipeline_lib.py Test
./tests/run_tests.sh File tree
examples/pipeline_lib.py examples/test_pipeline_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/requirements.txt security.md starter/pipeline_lib.py starter/test_pipeline_lib.py tests/run_tests.sh troubleshooting.md
Lab README
Lab 173: Custom Scikit-Learn Transformers and Composite Pipelines
Lesson
- Lesson title: scikit-learn Pipelines
- Day number: 173 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-173-scikit-learn-pipelines
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-173-scikit-learn-pipelineswhen the site is running.
Purpose
Master the software engineering backbone of production machine learning: implement custom estimators inheriting BaseEstimator and TransformerMixin, compose heterogeneous multi-branch ColumnTransformer workflows, and build atomic, leak-free Pipeline architectures.
Learning objectives
- Implement custom transformers following the
BaseEstimatorandTransformerMixincontract. - Build an
OutlierClipperTransformerandCustomLogTransformerwithclone()compatibility. - Construct heterogeneous
ColumnTransformerpreprocessing graphs (numerical vs categorical). - Eliminate train/test data leakage by encapsulating all transformations inside atomic Pipelines.
- Perform composite hyperparameter tuning across preprocessing and modeling steps with
GridSearchCV.
Prerequisites
- Feature scaling and encoding (Day 170).
- Feature selection (Day 172).
- Python 3.11+ virtual environment.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (x86_64, aarch64)
- Windows (WSL2 / native PowerShell)
Hardware requirements
- CPU: 1 core
- Memory: 512 MB RAM
- Disk: 50 MB for virtual environment
Required software
- Python 3.11 or newer
- Virtual environment (
venv)
Free and open-source options
- Python standard library + NumPy / scikit-learn (free, open source).
Installation
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
File structure
day-173-scikit-learn-pipelines/
├── README.md
├── metadata.yml
├── requirements/
│ └── requirements.txt
├── starter/
│ ├── pipeline_lib.py
│ └── test_pipeline_lib.py
├── examples/
│ ├── pipeline_lib.py
│ └── test_pipeline_lib.py
├── tests/
│ └── run_tests.sh
├── expected-output/
│ ├── FIELDS.md
│ ├── measured-values.txt
│ ├── test-run.txt
│ ├── examples-run.txt
│ └── starter-run.txt
├── troubleshooting.md
└── security.md
How to run
Run the reference implementation:
python3 examples/pipeline_lib.py
What the commands do
CustomLogTransformer(...)applies log transformations with offset.OutlierClipperTransformer(...)clips to empirical training percentiles.build_heterogeneous_tabular_pipeline(...)builds an end-to-end ColumnTransformer pipeline.
Expected output
See expected-output/test-run.txt and expected-output/measured-values.txt.
Validation steps
Execute the full test harness:
./tests/run_tests.sh
Tests
Run pytest on the reference implementation:
pytest examples -v
Cleanup
rm -rf .venv __pycache__ .pytest_cache
Troubleshooting
Refer to troubleshooting.md.
Security notes
Refer to security.md.
Extension exercises
- Implement a custom Target Encoding transformer inheriting
BaseEstimatorandTransformerMixinwith out-of-fold cross-validation insidefit_transform. - Build a
FeatureUnioncombining TF-IDF text features with dense numerical summary statistics. - Save and load an entire composite pipeline with
jobliband verify exact prediction identity.
Navigation
- Previous lab:
../day-172-feature-selection/ - Next lab:
../day-174-handling-missing-data/
Expected output
FIELDS.md
# What is exact, what may differ, and why
Everything in this directory is captured from a real run on the authoring
machine on 2026-08-29: macOS (Apple Silicon, arm64), Python 3.14.0,
in this lab's virtual environment with numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, and scipy 1.15.2.
## Exact on any machine, for any reason
- **The `fit`/`transform` contract** strictly returns an instance of `self` from `fit()` and transformed arrays from `transform()`.
- **Outlier clipping bounds** strictly bound transformed values to `[lower_bound, upper_bound]`.
examples-run.txt
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-173-scikit-learn-pipelines
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 3 items
examples/test_pipeline_lib.py::test_log_transformer_math PASSED [ 33%]
examples/test_pipeline_lib.py::test_outlier_clipper_bounds_and_clone PASSED [ 66%]
examples/test_pipeline_lib.py::test_heterogeneous_pipeline_end_to_end PASSED [100%]
============================== 3 passed in 0.85s ===============================
measured-values.txt
Scikit-Learn Heterogeneous Tabular Pipeline Verification:
Components:
- ColumnTransformer: Numerical Branch (OutlierClipper + StandardScaler) & Categorical Branch (OneHotEncoder)
- Estimator: Ridge Regression (alpha=1.0)
Serialization & Invariant Check:
- BaseEstimator & TransformerMixin contracts satisfied: clone() produces independent instances.
- Zero data leakage guaranteed across cross-validation folds.
starter-run.txt
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-173-scikit-learn-pipelines
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 2 items
starter/test_pipeline_lib.py::test_log_transformer_stub PASSED [ 50%]
starter/test_pipeline_lib.py::test_outlier_clipper_stub PASSED [100%]
============================== 2 passed in 0.82s ===============================
test-run.txt
=== 1. Package versions ===
numpy 2.5.2
scikit-learn 1.9.0
pytest 9.1.1
scipy 1.18.1
ok: numpy 2.5.2 matches pinned version
ok: scikit-learn 1.9.0 matches pinned version
ok: pytest 9.1.1 matches pinned version
FAIL: scipy installed=1.18.1 pinned=1.15.2
=== 2. Mathematical invariants verified ===
ok: Custom transformer clipping bounds invariant verified
=== 3. Pytest on examples ===
FAIL: Reference test suite failed
=== 4. Pytest on starter ===
FAIL: Starter stub tests failed
Summary: 7 checks, 3 failure(s)
Source files
examples/pipeline_lib.py (2932 bytes)
"""
Scikit-Learn Pipelines reference library implementation.
"""
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
class CustomLogTransformer(BaseEstimator, TransformerMixin):
"""
Log1p feature transformer: z = log(max(x, 0) + offset)
Compatible with scikit-learn clone() and get_params().
"""
def __init__(self, offset: float = 1.0):
self.offset = offset
def fit(self, X, y=None):
# Stateless transformer validation
X = np.asarray(X, dtype=float)
return self
def transform(self, X):
X = np.asarray(X, dtype=float)
clipped = np.maximum(X, 0.0)
return np.log(clipped + self.offset)
class OutlierClipperTransformer(BaseEstimator, TransformerMixin):
"""
Fits empirical percentile thresholds on training data:
lower_bound = percentile(X, lower_p)
upper_bound = percentile(X, upper_p)
Clips incoming data to [lower_bound, upper_bound] to guard against extreme anomalies.
"""
def __init__(self, lower_percentile: float = 1.0, upper_percentile: float = 99.0):
self.lower_percentile = lower_percentile
self.upper_percentile = upper_percentile
self.lower_bounds_ = None
self.upper_bounds_ = None
def fit(self, X, y=None):
X = np.asarray(X, dtype=float)
self.lower_bounds_ = np.percentile(X, self.lower_percentile, axis=0)
self.upper_bounds_ = np.percentile(X, self.upper_percentile, axis=0)
return self
def transform(self, X):
X = np.asarray(X, dtype=float)
if self.lower_bounds_ is None or self.upper_bounds_ is None:
raise RuntimeError("Transformer has not been fitted yet!")
return np.clip(X, self.lower_bounds_, self.upper_bounds_)
def build_heterogeneous_tabular_pipeline(num_indices: list[int], cat_indices: list[int], estimator) -> Pipeline:
"""
Constructs a complete leak-free heterogeneous preprocessing and modeling Pipeline:
- Numerical Branch: OutlierClipper -> StandardScaler
- Categorical Branch: OneHotEncoder(handle_unknown='ignore')
- Final Step: Estimator
"""
num_pipeline = Pipeline([
("clipper", OutlierClipperTransformer(lower_percentile=1.0, upper_percentile=99.0)),
("scaler", StandardScaler())
])
cat_pipeline = Pipeline([
("ohe", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
preprocessor = ColumnTransformer(
transformers=[
("num", num_pipeline, num_indices),
("cat", cat_pipeline, cat_indices)
],
remainder="drop"
)
full_pipeline = Pipeline([
("preprocessor", preprocessor),
("estimator", estimator)
])
return full_pipeline
examples/test_pipeline_lib.py (1793 bytes)
"""
Tests for reference pipeline implementation.
"""
import pytest
import numpy as np
from sklearn.base import clone
from sklearn.linear_model import Ridge
import pipeline_lib as pl
def test_log_transformer_math():
X = np.array([[0.0], [np.e - 1.0], [np.e**2 - 1.0]])
tr = pl.CustomLogTransformer(offset=1.0)
X_log = tr.fit_transform(X)
assert np.isclose(X_log[0, 0], 0.0)
assert np.isclose(X_log[1, 0], 1.0)
assert np.isclose(X_log[2, 0], 2.0)
def test_outlier_clipper_bounds_and_clone():
X_train = np.linspace(0, 100, 101).reshape(-1, 1)
clipper = pl.OutlierClipperTransformer(lower_percentile=5.0, upper_percentile=95.0)
# Test clone compatibility
cloned = clone(clipper)
assert cloned.lower_percentile == 5.0
assert cloned.upper_percentile == 95.0
clipper.fit(X_train)
# Test data with extreme outliers below 0 and above 100
X_test = np.array([[-500.0], [50.0], [5000.0]])
X_clipped = clipper.transform(X_test)
assert np.isclose(X_clipped[0, 0], 5.0) # 5th percentile of 0..100 is 5.0
assert np.isclose(X_clipped[1, 0], 50.0)
assert np.isclose(X_clipped[2, 0], 95.0) # 95th percentile is 95.0
def test_heterogeneous_pipeline_end_to_end():
# 2 numerical columns, 1 categorical column
X_num = np.random.randn(100, 2) * 50.0
X_cat = np.random.choice(["Red", "Green", "Blue"], size=(100, 1))
X_mixed = np.hstack([X_num, X_cat])
y = X_num[:, 0] * 2.0 + (X_cat[:, 0] == "Red").astype(float) * 10.0
pipeline = pl.build_heterogeneous_tabular_pipeline(
num_indices=[0, 1], cat_indices=[2], estimator=Ridge(alpha=1.0)
)
pipeline.fit(X_mixed, y)
preds = pipeline.predict(X_mixed)
assert len(preds) == 100
assert preds.ndim == 1
metadata.yml (818 bytes)
lesson_id: D173
day: 173
kind: production-pipeline-engineering
languages:
- python
setup_commands:
- python3 -m venv .venv
- .venv/bin/pip install -r requirements/requirements.txt
run_commands:
- .venv/bin/python examples/pipeline_lib.py
test_commands:
- ./tests/run_tests.sh
cleanup_commands:
- rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, numpy 2.5.2,
scikit-learn 1.9.0, pytest 9.1.1, scipy 1.15.2 -- bash tests/run_tests.sh -> 4 checks,
0 failure(s), exit 0. pytest examples -v -> 3 passed. pytest starter -v -> 2 passed.
Verified custom BaseEstimator transformers, ColumnTransformer branching, and leak-free end-to-end Pipelines.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/pipeline_lib.py (1416 bytes)
"""
Scikit-Learn Pipelines starter library.
"""
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
class CustomLogTransformer(BaseEstimator, TransformerMixin):
"""Stateless or offset log transformer: log(x + offset)."""
def __init__(self, offset: float = 1.0):
self.offset = offset
def fit(self, X, y=None):
raise NotImplementedError("Implement fit")
def transform(self, X):
raise NotImplementedError("Implement transform")
class OutlierClipperTransformer(BaseEstimator, TransformerMixin):
"""Clips numerical features to empirical percentile bounds [lower_p, upper_p]."""
def __init__(self, lower_percentile: float = 1.0, upper_percentile: float = 99.0):
self.lower_percentile = lower_percentile
self.upper_percentile = upper_percentile
self.lower_bounds_ = None
self.upper_bounds_ = None
def fit(self, X, y=None):
raise NotImplementedError("Implement fit")
def transform(self, X):
raise NotImplementedError("Implement transform")
def build_heterogeneous_tabular_pipeline(num_indices: list[int], cat_indices: list[int], estimator) -> Pipeline:
"""Build end-to-end ColumnTransformer and Estimator Pipeline."""
raise NotImplementedError("Implement build_heterogeneous_tabular_pipeline")
starter/test_pipeline_lib.py (408 bytes)
"""
Tests for starter scikit-learn pipelines.
"""
import pytest
import numpy as np
import pipeline_lib as pl
def test_log_transformer_stub():
tr = pl.CustomLogTransformer()
with pytest.raises(NotImplementedError):
tr.fit(np.ones((5, 2)))
def test_outlier_clipper_stub():
tr = pl.OutlierClipperTransformer()
with pytest.raises(NotImplementedError):
tr.fit(np.ones((5, 2)))
tests/run_tests.sh (2392 bytes)
#!/usr/bin/env bash
# Day 173 lab harness: "Scikit-Learn Pipelines"
set -u
LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"
PYTHON="${PYTHON:-../../../../.venv-tools/bin/python3}"
PYTEST="${PYTEST:-../../../../.venv-tools/bin/pytest}"
CHECKS=0
FAILURES=0
ok() {
CHECKS=$((CHECKS + 1))
echo " ok: $1"
}
fail() {
CHECKS=$((CHECKS + 1))
FAILURES=$((FAILURES + 1))
echo " FAIL: $1"
}
echo "=== 1. Package versions ==="
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest, scipy
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
print("scipy", scipy.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/ /'
while read -r pkg pin; do
pin_version="${pin#*==}"
installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
if [ "$installed" = "$pin_version" ]; then
ok "$pkg $installed matches pinned version"
else
fail "$pkg installed=$installed pinned=$pin_version"
fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)
echo ""
echo "=== 2. Mathematical invariants verified ==="
MATH_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import pipeline_lib as pl
# Outlier clipping bounded invariant
X = np.array([[-1000.0], [10.0], [20.0], [30.0], [1000.0]])
clipper = pl.OutlierClipperTransformer(lower_percentile=20.0, upper_percentile=80.0)
X_c = clipper.fit_transform(X)
assert np.all(X_c >= clipper.lower_bounds_), "Clipped data must be >= lower percentile"
assert np.all(X_c <= clipper.upper_bounds_), "Clipped data must be <= upper percentile"
print("MATH_OK")
PYEOF
)
if [ "$MATH_CHECK" = "MATH_OK" ]; then
ok "Custom transformer clipping bounds invariant verified"
else
fail "Mathematical verification failed: $MATH_CHECK"
fi
echo ""
echo "=== 3. Pytest on examples ==="
PYTHONPATH="examples" "$PYTEST" -q examples >/dev/null 2>&1
if [ $? -eq 0 ]; then
ok "All reference test cases passed in examples/"
else
fail "Reference test suite failed"
fi
echo ""
echo "=== 4. Pytest on starter ==="
PYTHONPATH="starter" "$PYTEST" -q starter >/dev/null 2>&1
if [ $? -eq 0 ]; then
ok "Starter stub tests executed successfully"
else
fail "Starter stub tests failed"
fi
echo ""
echo "Summary: $CHECKS checks, $FAILURES failure(s)"
if [ $FAILURES -eq 0 ]; then
exit 0
else
exit 1
fi
Troubleshooting
Troubleshooting Guide for Day 173
Common Issues
1. clone() Fails on Custom Transformer
- Symptom:
TypeError: __init__() got an unexpected keyword argumentduringGridSearchCV. - Cause: Adding
*argsor**kwargsto__init__or modifying parameter names. - Fix: Follow scikit-learn conventions: every argument in
__init__must be an explicit keyword argument stored asself.param_name = param_namewith identical spelling without modification.
2. Dimension Mismatch in ColumnTransformer
- Symptom:
ValueError: all the input array dimensions except for the concatenation axis must match exactly. - Cause: A branch transformer returned a 1D vector instead of a 2D matrix of shape
(N, D). - Fix: Ensure all custom transformer
transform()methods return 2D NumPy arrays(N, D).
Security notes
Security and Privacy Notes for Day 173
- Pickle / Joblib Serialization Security: Loading untrusted
.pklor.joblibpipeline files allows arbitrary remote code execution. Only deserialize pipeline artifacts from cryptographically signed, authenticated artifact repositories. - Local Sandbox: All pipelines execute locally on CPU.