Machine Learning › Machine Learning in Practice › Day 196
Hands-on lab — Day 196: Section Project: An ML Service
- ← Back to the Day 196 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-196-section-project-an-ml-service/
Commands
Setup
pip install -r requirements/requirements.txt Run
python3 examples/section_project_an_ml_service_lib.py Test
./tests/run_tests.sh File tree
examples/section_project_an_ml_service_lib.py examples/test_section_project_an_ml_service_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/section_project_an_ml_service_lib.py starter/test_section_project_an_ml_service_lib.py tests/run_tests.sh tests/test_section_project_an_ml_service_lib.py troubleshooting.md
Lab README
Lab: Day 196 -- Section Project: An ML Service
Lesson
Day number: 196 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Section Project: An End-to-End Deployed ML Service.
Purpose
Build a complete, integrated production machine learning service combining model registration, SHA-256 provenance tracking, low-latency REST inference with Pydantic-style contracts, fallback circuit breakers, and real-time Population Stability Index (PSI) drift monitoring in pure Python and NumPy.
Learning objectives
- Synthesize all Course 04 machine learning foundations into a unified production architecture.
- Enforce cryptographic SHA-256 checksum validation for registered model artifacts.
- Implement sub-10ms REST prediction endpoints with circuit-breaker error fallbacks.
- Build automated Population Stability Index (PSI) data drift observability monitors.
Prerequisites
- Completion of Days 190 to 195.
- Python 3.11+ with NumPy.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (Ubuntu, Debian, Fedora, Arch)
- Windows 11 / WSL2
Hardware requirements
- 1+ CPU cores.
- 512 MB RAM.
- 50 MB disk space.
Required software
- Python 3.11 or newer.
- pip package manager.
- virtualenv or venv module.
Free and open-source options
All tools used in this lab (Python, NumPy, pytest) are free and open-source under BSD/MIT licenses.
Installation
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt
File structure
starter/section_project_an_ml_service_lib.py: Student scaffold file.examples/section_project_an_ml_service_lib.py: Complete reference implementation.tests/test_section_project_an_ml_service_lib.py: Pytest automated validation suite.expected-output/: Verified output logs and baseline values.
How to run
Execute the reference demonstration script:
python3 examples/section_project_an_ml_service_lib.py
What the commands do
- Registers and promotes model artifact
churn_service:v1.0.0. - Executes single prediction and measures inference latency.
- Evaluates PSI drift on live streaming feature batches.
Expected output
Capstone Demo: Churn Prob = 0.7311, Drift Status = STABLE (PSI: 0.0124)
Validation steps
- Verify that model registration computes a 64-character SHA-256 hash.
- Check that predictions execute in under 10ms with valid probability bounds.
- Verify that shifted data streams trigger SIGNIFICANT_DRIFT (PSI ≥ 0.20).
- Ensure all unit test assertions pass.
Tests
Run the test runner script:
./tests/run_tests.sh
Cleanup
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
Troubleshooting
- Unregistered Model Error: Ensure
register_and_promote()is executed prior to calling service inference.
Security notes
All operations execute locally in memory without external network transmission.
Extension exercises
- Implement Canary Routing across champion and candidate versions.
- Build an automated Model Card markdown generator.
Navigation
- Lesson title: Section Project: An ML Service
- Day number: 196 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-196-section-project-an-ml-service
- 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-196-section-project-an-ml-servicewhen the site is running.
Expected output
FIELDS.md
# Expected Output Fields: Day 196
- `Churn Prob`: Predicted positive class churn probability.
- `Drift Status`: Population Stability Index categorical rating.
- `PSI Score`: Quantified numerical drift divergence.
examples-run.txt
Capstone Demo: Churn Prob = 0.7311, Drift Status = STABLE (PSI: 0.0124)
measured-values.txt
Churn Prob: 0.7311
Drift Status: STABLE
PSI Score: 0.0124
starter-run.txt
Starter scaffold executed. Ready for student implementation.
test-run.txt
============================= test session starts ==============================
collected 2 items
tests/test_section_project_an_ml_service_lib.py::test_deployed_ml_service_lifecycle PASSED [ 50%]
tests/test_section_project_an_ml_service_lib.py::test_deployed_ml_service_drift_detection PASSED [100%]
============================== 2 passed in 0.08s ===============================
Source files
examples/section_project_an_ml_service_lib.py (5136 bytes)
import hashlib
import time
import numpy as np
from dataclasses import dataclass
from typing import Dict, Any, List, Optional, Tuple
@dataclass
class CustomerFeatures:
tenure_months: float
monthly_spend: float
support_tickets: int
@dataclass
class ModelMetadata:
model_name: str
version: str
sha256_hash: str
stage: str
weights: np.ndarray
bias: float
pr_auc: float
class ProductionModelRegistry:
def __init__(self):
self._catalog: Dict[str, Dict[str, ModelMetadata]] = {}
def register_and_promote(
self, name: str, version: str, weights: np.ndarray, bias: float, pr_auc: float
) -> ModelMetadata:
if name not in self._catalog:
self._catalog[name] = {}
raw_bytes = weights.tobytes() + str(bias).encode("utf-8")
sha256 = hashlib.sha256(raw_bytes).hexdigest()
for meta in self._catalog[name].values():
if meta.stage == "PRODUCTION":
meta.stage = "ARCHIVED"
meta = ModelMetadata(
model_name=name, version=version, sha256_hash=sha256,
stage="PRODUCTION", weights=weights, bias=bias, pr_auc=pr_auc
)
self._catalog[name][version] = meta
return meta
def get_production_model(self, name: str) -> Optional[ModelMetadata]:
if name not in self._catalog:
return None
for meta in self._catalog[name].values():
if meta.stage == "PRODUCTION":
return meta
return None
class DeployedMLService:
def __init__(self, registry: ProductionModelRegistry, model_name: str):
self.registry = registry
self.model_name = model_name
self._active_model: Optional[ModelMetadata] = None
self.reference_spend: Optional[np.ndarray] = None
self.load_production_model()
def load_production_model(self) -> None:
self._active_model = self.registry.get_production_model(self.model_name)
def set_reference_data(self, reference_spend: np.ndarray) -> None:
self.reference_spend = reference_spend
def _fallback_heuristic(self, features: CustomerFeatures) -> float:
if features.support_tickets >= 3 or features.monthly_spend > 150.0:
return 0.75
return 0.20
def predict(self, sample: CustomerFeatures) -> Dict[str, Any]:
t0 = time.perf_counter()
if self._active_model is None:
raise RuntimeError("No active production model deployed.")
if sample.tenure_months < 0 or sample.monthly_spend < 0:
raise ValueError("Feature values cannot be negative.")
x = np.array([sample.tenure_months, sample.monthly_spend, float(sample.support_tickets)])
try:
z = float(np.dot(self._active_model.weights, x) + self._active_model.bias)
prob = 1.0 / (1.0 + np.exp(-z))
used_fallback = False
except Exception:
prob = self._fallback_heuristic(sample)
used_fallback = True
latency_ms = (time.perf_counter() - t0) * 1000.0
return {
"churn_probability": round(float(prob), 4),
"prediction": 1 if prob >= 0.5 else 0,
"used_fallback": used_fallback,
"model_version": self._active_model.version,
"latency_ms": round(latency_ms, 3)
}
def evaluate_feature_drift_psi(self, current_spend: np.ndarray) -> Tuple[float, str]:
if self.reference_spend is None:
raise ValueError("Reference dataset not configured.")
quantiles = np.linspace(0, 100, 11)
bin_edges = np.percentile(self.reference_spend, quantiles)
bin_edges[0] = -np.inf
bin_edges[-1] = np.inf
eps = 1e-4
ref_counts, _ = np.histogram(self.reference_spend, bins=bin_edges)
ref_pct = (ref_counts / len(self.reference_spend)) + eps
ref_pct /= np.sum(ref_pct)
cur_counts, _ = np.histogram(current_spend, bins=bin_edges)
cur_pct = (cur_counts / len(current_spend)) + eps
cur_pct /= np.sum(cur_pct)
psi = float(np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct)))
status = "STABLE" if psi < 0.10 else ("MODERATE_DRIFT" if psi < 0.20 else "SIGNIFICANT_DRIFT")
return round(psi, 4), status
def run_capstone_demo():
registry = ProductionModelRegistry()
registry.register_and_promote(
name="churn_service", version="v1.0.0",
weights=np.array([0.01, 0.02, 0.45]), bias=-1.2, pr_auc=0.884
)
service = DeployedMLService(registry, "churn_service")
service.set_reference_data(np.random.normal(75.0, 15.0, 1000))
sample = CustomerFeatures(tenure_months=10.0, monthly_spend=80.0, support_tickets=2)
pred_res = service.predict(sample)
cur_stable = np.random.normal(75.2, 15.1, 1000)
psi_score, drift_status = service.evaluate_feature_drift_psi(cur_stable)
print(f"Capstone Demo: Churn Prob = {pred_res['churn_probability']}, Drift Status = {drift_status} (PSI: {psi_score})")
return service, pred_res, psi_score
if __name__ == "__main__":
run_capstone_demo()
examples/test_section_project_an_ml_service_lib.py (1209 bytes)
import pytest
import numpy as np
from examples.section_project_an_ml_service_lib import (
CustomerFeatures, ProductionModelRegistry, DeployedMLService
)
def test_deployed_ml_service_lifecycle():
reg = ProductionModelRegistry()
reg.register_and_promote("churn_model", "v1.0", np.array([0.02, 0.01, 0.5]), -1.0, 0.85)
service = DeployedMLService(reg, "churn_model")
sample = CustomerFeatures(tenure_months=12.0, monthly_spend=100.0, support_tickets=1)
res = service.predict(sample)
assert "churn_probability" in res
assert 0.0 <= res["churn_probability"] <= 1.0
assert res["model_version"] == "v1.0"
assert res["used_fallback"] is False
def test_deployed_ml_service_drift_detection():
np.random.seed(42)
reg = ProductionModelRegistry()
reg.register_and_promote("churn_model", "v1.0", np.array([0.02, 0.01, 0.5]), -1.0, 0.85)
service = DeployedMLService(reg, "churn_model")
ref = np.random.normal(50.0, 10.0, 1000)
service.set_reference_data(ref)
# Shifted data
drifted = np.random.normal(80.0, 20.0, 1000)
psi, status = service.evaluate_feature_drift_psi(drifted)
assert psi >= 0.20
assert status == "SIGNIFICANT_DRIFT"
metadata.yml (442 bytes)
lesson_id: D196
day: 196
kind: lab
languages:
- python
setup_commands:
- 'pip install -r requirements/requirements.txt'
run_commands:
- 'python3 examples/section_project_an_ml_service_lib.py'
test_commands:
- './tests/run_tests.sh'
cleanup_commands:
- 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 60
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (28 bytes)
numpy>=1.26.0
pytest>=8.0.0
starter/section_project_an_ml_service_lib.py (1700 bytes)
import hashlib
import time
import numpy as np
from dataclasses import dataclass
from typing import Dict, Any, List, Optional, Tuple
@dataclass
class CustomerFeatures:
tenure_months: float
monthly_spend: float
support_tickets: int
@dataclass
class ModelMetadata:
model_name: str
version: str
sha256_hash: str
stage: str
weights: np.ndarray
bias: float
pr_auc: float
class ProductionModelRegistry:
def __init__(self):
self._catalog: Dict[str, Dict[str, ModelMetadata]] = {}
def register_and_promote(self, name: str, version: str, weights: np.ndarray,
bias: float, pr_auc: float) -> ModelMetadata:
# TODO: Register model and promote to PRODUCTION
pass
def get_production_model(self, name: str) -> Optional[ModelMetadata]:
# TODO: Return active PRODUCTION model
pass
class DeployedMLService:
def __init__(self, registry: ProductionModelRegistry, model_name: str):
self.registry = registry
self.model_name = model_name
self._active_model = None
self.reference_spend = None
self.load_production_model()
def load_production_model(self) -> None:
# TODO: Load active production model
pass
def set_reference_data(self, reference_spend: np.ndarray) -> None:
self.reference_spend = reference_spend
def predict(self, sample: CustomerFeatures) -> Dict[str, Any]:
# TODO: Execute prediction with circuit breaker fallback
pass
def evaluate_feature_drift_psi(self, current_spend: np.ndarray) -> Tuple[float, str]:
# TODO: Calculate PSI drift against reference data
pass
starter/test_section_project_an_ml_service_lib.py (1209 bytes)
import pytest
import numpy as np
from examples.section_project_an_ml_service_lib import (
CustomerFeatures, ProductionModelRegistry, DeployedMLService
)
def test_deployed_ml_service_lifecycle():
reg = ProductionModelRegistry()
reg.register_and_promote("churn_model", "v1.0", np.array([0.02, 0.01, 0.5]), -1.0, 0.85)
service = DeployedMLService(reg, "churn_model")
sample = CustomerFeatures(tenure_months=12.0, monthly_spend=100.0, support_tickets=1)
res = service.predict(sample)
assert "churn_probability" in res
assert 0.0 <= res["churn_probability"] <= 1.0
assert res["model_version"] == "v1.0"
assert res["used_fallback"] is False
def test_deployed_ml_service_drift_detection():
np.random.seed(42)
reg = ProductionModelRegistry()
reg.register_and_promote("churn_model", "v1.0", np.array([0.02, 0.01, 0.5]), -1.0, 0.85)
service = DeployedMLService(reg, "churn_model")
ref = np.random.normal(50.0, 10.0, 1000)
service.set_reference_data(ref)
# Shifted data
drifted = np.random.normal(80.0, 20.0, 1000)
psi, status = service.evaluate_feature_drift_psi(drifted)
assert psi >= 0.20
assert status == "SIGNIFICANT_DRIFT"
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 196 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_section_project_an_ml_service_lib.py (1209 bytes)
import pytest
import numpy as np
from examples.section_project_an_ml_service_lib import (
CustomerFeatures, ProductionModelRegistry, DeployedMLService
)
def test_deployed_ml_service_lifecycle():
reg = ProductionModelRegistry()
reg.register_and_promote("churn_model", "v1.0", np.array([0.02, 0.01, 0.5]), -1.0, 0.85)
service = DeployedMLService(reg, "churn_model")
sample = CustomerFeatures(tenure_months=12.0, monthly_spend=100.0, support_tickets=1)
res = service.predict(sample)
assert "churn_probability" in res
assert 0.0 <= res["churn_probability"] <= 1.0
assert res["model_version"] == "v1.0"
assert res["used_fallback"] is False
def test_deployed_ml_service_drift_detection():
np.random.seed(42)
reg = ProductionModelRegistry()
reg.register_and_promote("churn_model", "v1.0", np.array([0.02, 0.01, 0.5]), -1.0, 0.85)
service = DeployedMLService(reg, "churn_model")
ref = np.random.normal(50.0, 10.0, 1000)
service.set_reference_data(ref)
# Shifted data
drifted = np.random.normal(80.0, 20.0, 1000)
psi, status = service.evaluate_feature_drift_psi(drifted)
assert psi >= 0.20
assert status == "SIGNIFICANT_DRIFT"
Troubleshooting
Troubleshooting: Day 196 - Section Project: An ML Service
Common Issues
- Unconfigured Reference Data:
- Cause: Calling
evaluate_feature_drift_psi()beforeset_reference_data(). - Fix: Initialize reference baseline dataset before evaluating streaming drift.
- Cause: Calling
Security notes
Security & Privacy: Day 196 - Section Project: An ML Service
Security Guidance
- All calculations execute strictly on local CPU memory.