CO6 Topic 21 of 25
ML engineering foundations: from notebook to production
A notebook is great for exploring, and poor as a production system. Split it into tested modules, move settings into configuration, and log what happens.
You will be able to
- List the problems with notebook-only systems
- Split a project into modular pipeline steps
- Use YAML and argparse for configuration
- Add structured logging
- Write unit tests for ML components
Read
- Reference book 2, Ch. 6, pp. 173–210; Ch. 7, pp. 211–246
- Reference book 4, Ch. 5, pp. 103–138
Intuition
Your notebook works, on your laptop, if the cells run in the right order. Could a teammate rerun it six months from now and get the same model? Engineering habits make that answer yes.
How it works
| Notebook problem | Engineering answer |
|---|---|
| Hidden state and cell order | Scripts that run top to bottom |
| Settings buried in code | A YAML config, plus argparse overrides |
print everywhere |
Structured logging with levels |
| "It worked last time" | Unit tests, pinned versions, recorded seeds |
| One long file | Modules with one job each |
A pipeline run:
python -m src.trainer --config configs/train.yaml --C 0.5
What to test in ML:
| Test | Example |
|---|---|
| Data | Required columns exist; CGPA lies between 0 and 10 |
| Features | No NaN after the pipeline; correct output width |
| Model | Output is a probability; beats a baseline |
| Invariance | An unknown Stream doesn't crash |
| Reproducibility | The same seed gives the same score |
Logging levels: DEBUG, INFO, WARNING, ERROR. Log the run ID, row counts and metrics, never personal data.
Run it: a tiny modular project
Each function stands for a file in the project. Python's yaml and logging run here just as they do on your computer.
import logging
import sys
import numpy as np
import yaml
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import make_pipeline
from placement_predict import FEATURES, load_data, preprocessor, train_val_test
logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(name)s: %(message)s", stream=sys.stdout, force=True)
CONFIG = yaml.safe_load("""
data: {rows: 3000, seed: 42}
model: {C: 1.0, max_iter: 1000}
quality: {min_auc: 0.75}
""")
def data_loader(cfg): # src/data_loader.py
log = logging.getLogger("data_loader")
df = load_data(n=cfg["rows"], seed=cfg["seed"])
missing = set(FEATURES) - set(df.columns)
if missing:
raise ValueError(f"Missing columns: {missing}")
log.info("loaded %d rows", len(df))
return df
def trainer(X, y, cfg): # src/trainer.py
model = make_pipeline(preprocessor(), LogisticRegression(C=cfg["C"], max_iter=cfg["max_iter"]))
model.fit(X, y)
logging.getLogger("trainer").info("fitted with C=%s", cfg["C"])
return model
def evaluate(model, X, y, cfg): # src/evaluate.py
log = logging.getLogger("evaluate")
auc = roc_auc_score(y, model.predict_proba(X)[:, 1])
log.info("validation AUC %.4f", auc)
if auc < cfg["min_auc"]:
log.error("below the quality gate of %s", cfg["min_auc"])
raise SystemExit(1)
return auc
df = data_loader(CONFIG["data"])
X_train, X_val, _, y_train, y_val, _ = train_val_test(df)
model = trainer(X_train, y_train, CONFIG["model"])
evaluate(model, X_val, y_val, CONFIG["quality"])
# tests/test_pipeline.py: run these with pytest in a real project
def test_no_nan_after_features():
assert not np.isnan(model[0].transform(X_val)).any()
def test_output_is_probability():
p = model.predict_proba(X_val)[:, 1]
assert ((p >= 0) & (p <= 1)).all()
def test_unknown_stream_does_not_crash():
row = X_val.iloc[[0]].copy()
row["Stream"] = "AI-DS"
model.predict_proba(row)
def test_reproducible():
a = trainer(X_train, y_train, CONFIG["model"]).predict_proba(X_val)
assert np.allclose(a, model.predict_proba(X_val))
def test_beats_baseline():
assert model.score(X_val, y_val) > max(y_val.mean(), 1 - y_val.mean())
for test in [test_no_nan_after_features, test_output_is_probability, test_unknown_stream_does_not_crash, test_reproducible, test_beats_baseline]:
test()
print("PASSED", test.__name__)
Try this: set min_auc: 0.99 in the config, and watch the quality gate stop the run.
Common mistakes
- Testing only that code runs. Check the values too.
- Keeping secrets in config files. Use environment variables.
- Logging personal data. Records in logs are hard to remove, and can leak.
Problems
- Give three reasons a notebook is a weak production system.
- Add a
testsentry for CGPA range checking. - Add
argparseso that--Coverrides the config (on your own computer). - Lab 13 (part): create
data_loader.py,feature_pipeline.pyandtrainer.py. - Why is a quality gate useful in an automated retraining job?
Flashcards
Say the meaning to yourself, then select the card to check.
- Why notebooks struggle in production
- Hidden state, and dependence on cell order.
- Quality gate
- Stops a run when metrics are below a limit.
- Structured logging
- Levelled records of run IDs and metrics, with no personal data.
- Unit test for ML
- For example, predicted probabilities lie between 0 and 1.