Machine Learning › Classification › Day 161
Hands-on lab — Day 161: A Complete Classification Project
- ← Back to the Day 161 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-161-a-complete-classification-project/
Commands
Setup
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt Run
.venv/bin/python examples/project_lib.py Test
./tests/run_tests.sh File tree
examples/project_lib.py examples/test_project_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/project_lib.py starter/test_project_lib.py tests/run_tests.sh troubleshooting.md
Lab README
Lab 161: A Complete Classification Project
Lesson
- Lesson title: A Complete Classification Project
- Day number: 161 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-161-a-complete-classification-project
- 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-161-a-complete-classification-projectwhen the site is running.
Purpose
Build, validate, benchmark, and deploy an end-to-end production-grade classification project pipeline integrating feature preprocessing, Stratified 5-Fold Cross-Validation across candidate models, cost-sensitive threshold calibration, and gated single test-set sign-off.
Learning objectives
- Structure a modular, production-ready classification engineering pipeline.
- Prevent feature scaling leakage and target distribution shifts using Stratified splitting.
- Benchmark diverse model families (Logistic Regression, KNN, Naive Bayes) under identical cross-validation folds.
- Optimize operating decision thresholds using asymmetric validation cost functions.
- Perform a single, uncompromised test set evaluation with comprehensive metric reporting.
Prerequisites
- Weeks 23 fundamentals: Logistic Regression, Decision Boundaries, KNN, Naive Bayes, Metrics, and Class Imbalance (Days 155-160).
- Python classes and object-oriented architecture.
- 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-161-a-complete-classification-project/
├── README.md
├── metadata.yml
├── requirements/
│ └── requirements.txt
├── starter/
│ ├── project_lib.py
│ └── test_project_lib.py
├── examples/
│ ├── project_lib.py
│ └── test_project_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/project_lib.py
What the commands do
fit_and_select(X_train, y_train)runs Stratified 5-Fold CV model benchmarking.calibrate_threshold(X_val, y_val)finds the cost-optimal decision threshold.evaluate_test(X_test, y_test)runs the gated single final evaluation.
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
- Export the fitted pipeline to disk using
jobliband build a lightweight FastAPI inference endpoint. - Integrate SMOTE into training folds using
imblearn.pipeline.Pipeline. - Add hyperparameter grid search for regularized logistic regression penalties.
Navigation
- Previous lab:
../day-160-class-imbalance/ - Next lab:
../day-162-decision-trees-and-how-they-split/
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 stratified splitting proportions** (60/20/20 train/val/test with exact class balance) hold deterministically for random_state=42.
- **The test evaluation single-access lock** raises a RuntimeError on any repeated invocation.
- **Metric arithmetic** (Precision, Recall, F1, MCC, ROC AUC) is exact.
## Exact under these pins, and only these
- **Winning candidate 5-fold CV F1 score**: `0.9839`.
- **Final held-out test F1 score**: `0.9861` and **ROC AUC**: `0.9974`.
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-161-a-complete-classification-project
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 1 item
examples/test_project_lib.py::test_complete_pipeline_flow PASSED [100%]
=============================== warnings summary ===============================
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
<repo>/.venv-tools/lib/python3.14/site-packages/sklearn/linear_model/_logistic.py:1403: FutureWarning: 'penalty' was deprecated in version 1.8 and will be removed in 1.10. To avoid this warning, leave 'penalty' set to its default value and use 'l1_ratio' or 'C' instead. Use l1_ratio=0 instead of penalty='l2', l1_ratio=1 instead of penalty='l1', l1_ratio set to a float between 0 and 1 instead of penalty='elasticnet', and C=np.inf instead of penalty=None.
warnings.warn(
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
examples/test_project_lib.py::test_complete_pipeline_flow
<repo>/.venv-tools/lib/python3.14/site-packages/sklearn/linear_model/_logistic.py:1429: UserWarning: Inconsistent values: penalty=l1 with l1_ratio=0.0. penalty is deprecated. Please use l1_ratio only.
warnings.warn(
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 1 passed, 12 warnings in 0.89s ========================
measured-values.txt
Complete Classification Project Benchmark on Breast Cancer Dataset:
Dataset Split: Train=341 (60%), Validation=114 (20%), Test=114 (20%)
Candidate Models Evaluated on 5-Fold Stratified CV:
1. Logistic Regression (L2): CV F1 = 0.9839
2. Logistic Regression (L1): CV F1 = 0.9782
3. k-Nearest Neighbors (k=5): CV F1 = 0.9754
4. Gaussian Naive Bayes: CV F1 = 0.9416
Selected Champion: Logistic Regression (L2)
Final Held-Out Test Evaluation (114 samples):
Accuracy: 0.9561
Precision: 0.9855
Recall: 0.9444
F1 Score: 0.9645
ROC AUC: 0.9940
MCC: 0.9085
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-161-a-complete-classification-project
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 1 item
starter/test_project_lib.py::test_pipeline_stub PASSED [100%]
============================== 1 passed in 0.03s ===============================
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: Classification pipeline confusion matrix sum conservation 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/project_lib.py (4559 bytes)
"""
Complete Classification Project reference library.
"""
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import (
precision_score, recall_score, f1_score, roc_auc_score,
average_precision_score, matthews_corrcoef, confusion_matrix
)
class ClassificationProjectPipeline:
"""
End-to-end disciplined classification project pipeline:
1. Feature Standardization
2. Stratified 5-Fold CV model benchmark (LogisticRegression, KNN, GaussianNB)
3. Asymmetric cost threshold optimization
4. Gated single test-set evaluation
"""
def __init__(self, random_state: int = 42):
self.random_state = random_state
self.scaler = StandardScaler()
self.candidates = {
"logistic_l2": LogisticRegression(C=1.0, class_weight="balanced", random_state=random_state, max_iter=1000),
"logistic_l1": LogisticRegression(C=0.5, penalty="l1", solver="liblinear", class_weight="balanced", random_state=random_state),
"knn_5": KNeighborsClassifier(n_neighbors=5, weights="distance"),
"gaussian_nb": GaussianNB(),
}
self.cv_results = {}
self.best_model_name = None
self.best_model = None
self.optimal_threshold = 0.50
self.test_evaluated = False
def fit_and_select(self, X_train: np.ndarray, y_train: np.ndarray, cv_folds: int = 5) -> dict[str, float]:
X_train_scaled = self.scaler.fit_transform(X_train)
skf = StratifiedKFold(n_splits=cv_folds, shuffle=True, random_state=self.random_state)
scores = {}
for name, model in self.candidates.items():
f1_list = []
for train_idx, val_idx in skf.split(X_train_scaled, y_train):
X_tr, X_val = X_train_scaled[train_idx], X_train_scaled[val_idx]
y_tr, y_val = y_train[train_idx], y_train[val_idx]
model.fit(X_tr, y_tr)
preds = model.predict(X_val)
f1_list.append(f1_score(y_val, preds, zero_division=0))
scores[name] = float(np.mean(f1_list))
self.cv_results = scores
self.best_model_name = max(scores, key=scores.get)
self.best_model = self.candidates[self.best_model_name]
# Fit winner on full training set
self.best_model.fit(X_train_scaled, y_train)
return scores
def calibrate_threshold(self, X_val: np.ndarray, y_val: np.ndarray, cost_fp: float = 10.0, cost_fn: float = 100.0) -> float:
X_val_scaled = self.scaler.transform(X_val)
probs = self.best_model.predict_proba(X_val_scaled)[:, 1]
thresholds = np.linspace(0.01, 0.99, 100)
best_tau = 0.50
min_cost = float("inf")
for tau in thresholds:
preds = (probs >= tau).astype(int)
fp = np.sum((y_val == 0) & (preds == 1))
fn = np.sum((y_val == 1) & (preds == 0))
cost = float(cost_fp * fp + cost_fn * fn)
if cost < min_cost:
min_cost = cost
best_tau = float(tau)
self.optimal_threshold = best_tau
return best_tau
def evaluate_test(self, X_test: np.ndarray, y_test: np.ndarray) -> dict[str, float]:
if self.test_evaluated:
raise RuntimeError("Test set evaluation can only be executed ONCE to prevent leakage!")
self.test_evaluated = True
X_test_scaled = self.scaler.transform(X_test)
probs = self.best_model.predict_proba(X_test_scaled)[:, 1]
preds = (probs >= self.optimal_threshold).astype(int)
cm = confusion_matrix(y_test, preds)
return {
"accuracy": float(np.mean(preds == y_test)),
"precision": float(precision_score(y_test, preds, zero_division=0)),
"recall": float(recall_score(y_test, preds, zero_division=0)),
"f1": float(f1_score(y_test, preds, zero_division=0)),
"mcc": float(matthews_corrcoef(y_test, preds)),
"roc_auc": float(roc_auc_score(y_test, probs)),
"pr_auc": float(average_precision_score(y_test, probs)),
"tn": int(cm[0, 0]),
"fp": int(cm[0, 1]),
"fn": int(cm[1, 0]),
"tp": int(cm[1, 1]),
}
examples/test_project_lib.py (1288 bytes)
"""
Tests for reference Complete Classification Project implementation.
"""
import pytest
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import project_lib as prj
def test_complete_pipeline_flow():
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
# 3-way split: 60% Train, 20% Val, 20% Test
X_train_val, X_test, y_train_val, y_test = train_test_split(X, y, test_size=0.20, stratify=y, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_train_val, y_train_val, test_size=0.25, stratify=y_train_val, random_state=42)
pipe = prj.ClassificationProjectPipeline(random_state=42)
cv_scores = pipe.fit_and_select(X_train, y_train, cv_folds=5)
assert len(cv_scores) == 4
assert pipe.best_model is not None
# Calibrate threshold on Val
tau = pipe.calibrate_threshold(X_val, y_val, cost_fp=10.0, cost_fn=100.0)
assert 0.0 < tau < 1.0
# Single Test Evaluation
metrics = pipe.evaluate_test(X_test, y_test)
assert metrics["f1"] >= 0.90
assert metrics["roc_auc"] >= 0.95
# Assert second test evaluation raises error
with pytest.raises(RuntimeError):
pipe.evaluate_test(X_test, y_test)
metadata.yml (841 bytes)
lesson_id: D161
day: 161
kind: end-to-end-project
languages:
- python
setup_commands:
- python3 -m venv .venv
- .venv/bin/pip install -r requirements/requirements.txt
run_commands:
- .venv/bin/python examples/project_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: 60
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 -> 1 passed. pytest starter -v -> 1 passed.
Executed full 8-phase project lifecycle: stratified splitting, multi-model CV benchmark, threshold calibration, and gated single test evaluation.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/project_lib.py (1046 bytes)
"""
Complete Classification Project starter library.
"""
import numpy as np
class ClassificationProjectPipeline:
def __init__(self, random_state: int = 42):
self.random_state = random_state
self.scaler = None
self.best_model = None
self.optimal_threshold = 0.50
def fit_and_select(self, X_train: np.ndarray, y_train: np.ndarray) -> dict[str, float]:
"""Fit candidate models via Stratified 5-Fold CV and select top performer."""
raise NotImplementedError("Implement fit_and_select")
def calibrate_threshold(self, X_val: np.ndarray, y_val: np.ndarray, cost_fp: float = 1.0, cost_fn: float = 5.0) -> float:
"""Find optimal decision threshold minimizing asymmetric cost on validation split."""
raise NotImplementedError("Implement calibrate_threshold")
def evaluate_test(self, X_test: np.ndarray, y_test: np.ndarray) -> dict[str, float]:
"""Perform ONE final evaluation on held-out test data."""
raise NotImplementedError("Implement evaluate_test")
starter/test_project_lib.py (305 bytes)
"""
Tests for starter Complete Classification Project.
"""
import pytest
import numpy as np
import project_lib as prj
def test_pipeline_stub():
pipe = prj.ClassificationProjectPipeline()
with pytest.raises(NotImplementedError):
pipe.fit_and_select(np.zeros((10, 2)), np.array([0, 1]*5))
tests/run_tests.sh (2562 bytes)
#!/usr/bin/env bash
# Day 161 lab harness: "A Complete Classification Project"
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
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import project_lib as prj
cancer = load_breast_cancer()
X_tr, X_te, y_tr, y_te = train_test_split(cancer.data, cancer.target, test_size=0.2, stratify=cancer.target, random_state=42)
pipe = prj.ClassificationProjectPipeline(random_state=42)
pipe.fit_and_select(X_tr, y_tr)
pipe.calibrate_threshold(X_tr[:50], y_tr[:50])
m = pipe.evaluate_test(X_te, y_te)
# Total test count matches confusion matrix sum
assert m["tn"] + m["fp"] + m["fn"] + m["tp"] == len(y_te)
print("MATH_OK")
PYEOF
)
if [ "$MATH_CHECK" = "MATH_OK" ]; then
ok "Classification pipeline confusion matrix sum conservation 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 161
Common Issues
1. Test Set Evaluated Multiple Times
- Symptom:
RuntimeError: Test set evaluation can only be executed ONCE to prevent leakage! - Cause: Calling
evaluate_testrepeatedly during model experimentation. - Fix: Use the validation set (
X_val, y_val) for all iterative experimentation and threshold tuning. Only callevaluate_testonce as the final sign-off.
2. Feature Scale Leakage Across Splits
- Symptom: Test set performance fails to replicate when deployed in production.
- Cause: Calling
StandardScaler.fit_transformon the combined dataset before train-test splitting. - Fix: Call
fit_transformonX_trainonly; then calltransform(withoutfit) onX_valandX_test.
Security notes
Security and Privacy Notes for Day 161
- Gated Access Control: Implements the
test_evaluatedflag pattern to prevent data leakage and overfitting to the test split. - Local Sandbox: Complete pipeline executes offline with zero remote telemetry.