Machine Learning,
from first principles to production.
You learned this once. This notebook rebuilds it properly — starting from the ideas you half-remember, through every core model, and all the way to what happens after the notebook stops: shipping, monitoring, and owning a model in the real world.
Modules 00–03 · Diagnostic, core concepts, the math you actually use, and the Python data toolkit. Rebuild the floor before adding weight.
Modules 04–10 · Evaluation, linear models, trees & boosting, distance/probabilistic models, clustering, neural networks. The vocabulary of the field.
Modules 11–13 · Feature work, validation & tuning, error analysis, interpretability and fairness — where most real gains come from.
Modules 14–18 · Framing, MLOps pipelines, deployment patterns, a hands-on FastAPI + Docker lab, and monitoring for drift.
Modules 19–20 · A four-rung capstone ladder, portfolio checklist, a 4-week study schedule and a final recall deck.
00 · Orientation & diagnostic
This module has one job: find the holes. Answer every question from memory, in writing before you scroll to the answers. The discomfort of half-remembering is the part that works — it is what tells your brain the material is worth keeping.
By the end you can
- State your starting point honestly instead of re-reading things you already know.
- Navigate this notebook and know what "done" means for each module.
- Name the four tracks and where your weak spots are.
How to use this notebook
- Attempt the diagnostic cold. Write answers first, no peeking. Ten minutes, badly written answers are fine.
- Read with a pen. Each module has goals, core ideas, code, pitfall boxes, and recall cards.
- Run the code. Type it, don't copy it. Then break it on purpose: change one number, predict, run.
- Do the exercise — usually 15–30 minutes. Skipping it is how people finish 20 modules and learn nothing.
- Tick "Mark done" only when you can explain it aloud without looking. Progress is saved in your browser.
Re-reading feels productive and is nearly useless. What works: recall (test yourself), spacing (come back in 1 / 3 / 7 days), and generation (write the code or explanation yourself). Every module here is built around those three.
Diagnostic: answer all 12 cold
- In one sentence: what does it mean for a system to learn from data?
- Name the three main families of ML and give one real example of each.
- A model scores 0.99 during development and 0.60 after launch. Give three possible causes.
- What is the validation set for? Why not just train and test?
- Write the formula for mean squared error. What kind of mistakes does it punish hardest?
- What does the learning rate control? What happens if it is too large? Too small?
- For cancer screening: do you optimize precision or recall? Why?
- 1% of transactions are fraud. A model that predicts "not fraud" every time scores 99% accuracy. What metric do you use instead?
- Regularization, in one line.
- What is the difference between what
.fit()and.predict()do? - Name two things you must monitor after a model is deployed.
- Give two situations where you should not use machine learning.
Answers 1–4
1. The system improves at a task by finding structure in examples, instead of having every rule written by a human — performance improves as relevant experience (data) grows.
2. Supervised (labels): predicting churn from past customers. Unsupervised (no labels): grouping customers by behaviour. Reinforcement (rewards over time): a controller learning to balance a robot, or ad bidding policies. (Generative/self-supervised models like LLMs are supervised-style training on raw text — worth knowing.)
3. Top three: data leakage (features that secretly contain the answer or information from the future), overfitting / non-representative dev data, and distribution shift (the world changed, or production traffic differs from your sample). A fourth good answer: metric mismatch — you optimized the wrong thing.
4. The test score is a one-shot estimate; if you tune against it, it stops being honest. Validation is the throwaway arena for choosing models and hyperparameters so the test set remains a final, untouched judgement.
Answers 5–8
5. MSE = (1/n) · Σ (ŷᵢ − yᵢ)². Squaring means large errors are punished
disproportionately (a 10-unit error costs 100× a 1-unit error), so it is sensitive to outliers.
6. The step size of each update toward lower loss. Too large: it overshoots and the loss oscillates or diverges. Too small: training crawls and can get stuck in flat regions.
7. Recall — you cannot afford to miss positive cases (false negatives kill people); you accept more false alarms and let a human triage them.
8. Recall for fraud (of the predicted frauds, how many are real) and precision, plus PR-AUC and the confusion matrix at a chosen threshold. Accuracy measures the wrong thing when classes are imbalanced.
Answers 9–12
9. A penalty added to the loss for having large weights, pushing the model toward simpler solutions that generalize better.
10. fit learns parameters from training data (computes
statistics/gradients and stores them); predict applies what was learned to new
data without changing it.
11. Input/data distributions (drift, missingness, schema changes), model outputs (score distributions, class balance), real-world performance once labels arrive, plus latency and cost.
12. When you need a hard, auditable rule ("never let this exceed X"); when there is no data and no cheap way to get it; when the problem is one-off and a human can just do it; when a simple heuristic already works and the maintenance cost of a model isn't justified.
9–12 right: skim Track A, go deep on Track B/D. 5–8: the normal path — you'll move fast through A and B. 0–4: perfect, that's who this notebook is for. Don't skip Track A; the math module is where most "suddenly I don't understand neural networks" moments are actually fixed.
- Write down the one project or domain you want to be able to build end-to-end by the end of this notebook (e.g. "score support tickets and serve it as an API").
- Write down your weekly hours and the number of weeks you'll give this. Divide the remaining modules by that budget.
- Save your diagnostic answers. You'll retake them in Module 20.
01 · What machine learning really is
Before any model: what ML is, what it isn't, and — most importantly — when it is the wrong tool. Most failed ML projects fail here, not in the math.
By the end you can
- Define ML in terms of a task, a measure of success, and experience.
- Tell supervised, unsupervised and reinforcement learning apart — and name self-supervised/generative as the modern fourth case.
- Decide whether a proposed problem is a good ML problem, in writing, in two minutes.
- Draw the standard ML loop from data to feedback.
The definition you should use
"Improve at a task" means there is a target behaviour and a way to score it. "Experience" usually means historical examples (data), not a person being taught. And the measure of success must be computable — if you cannot write the function that scores a prediction, you cannot train anything.
| Approach | How it works | When it wins |
|---|---|---|
| Explicit rules | Humans write every if | Requirements are known and stable, errors must be auditable ("never approve > $10k without ID") |
| Machine learning | Humans define the task + data + metric; the machine finds the rules | The rules are too many, too contextual, or change over time |
| Classical statistics | Estimate parameters, test hypotheses, quantify uncertainty | You care about inference and confidence, not raw prediction |
| Human + ML hybrid | Model ranks/triages, human decides | Almost every real deployment — start here |
Nobody wants a model. They want fewer refunds, faster triage, lower churn, or an hour of their day back. If you can't name the decision the prediction changes, you don't have a use case — you have a dataset.
The families
| Family | What you're given | What you produce | Examples |
|---|---|---|---|
| Supervised | Inputs X with labels y | A function X → y | House price (regression), spam/not-spam (classification) |
| Unsupervised | Inputs X only | Structure: groups, compressed representations, anomalies | Customer segments, deduplication, PCA for viz |
| Self-supervised | Raw data; labels generated from the data itself | General representations / next-token predictors | BERT (masked words), GPT (next token), image inpainting |
| Reinforcement | States, actions, rewards over time | A policy: state → action | Robotics, game playing, ad bidding, recommender optimization |
Supervised splits into regression (continuous number) and classification (discrete label, one or many). Nearly all beginner and most first production projects are supervised. 80% of applied ML in industry is tabular supervised learning — which is why Trees/Boosting (Module 07) matters more than people expect.
The loop
Notice it is a loop, not a line. The feedback arrow — real outcomes coming back as new labels — is the difference between a system that improves and one that slowly rots. A notebook only shows you the middle four boxes; production is mostly the outer two.
Is this a good ML problem?
Apply all three tests. Fail any one and pick a simpler answer:
- Signal exists. There is a plausible statistical relationship between what you observe before the decision and what you want to predict. Past churn predicts future churn; tomorrow's lottery numbers do not.
- Data exists — and is representative. Enough examples of the thing, from the same distribution you'll meet in production, with labels you can trust.
- Actionability. Someone will do something differently when they see the score, and you can measure whether that helped.
Prediction theater: a model that accurately predicts something nobody acts on. Label fantasy: the label you can get (support-agent dispositions) isn't the thing you care about (customer was actually satisfied). Cheap-heuristic blindness: the model must beat a baseline as dumb as "predict the average" or "always flag the top decile".
Why do we say ML needs a "computable measure of success"?
Supervised vs unsupervised, in one breath each.
When should you NOT use ML?
Pick five problems from your own work or life. For each, write three lines:
- Task + success metric: what exactly is predicted, and how would you score it?
- Data test: does representative, labelled data plausibly exist? Where from?
- Action test: who changes what decision when they see this score?
Mark each GO or NO-GO. Keep the GO ones — Module 19 turns one into a capstone.
02 · The math you actually use
You do not need calculus exams. You need four things: vectors and matrices (data has shape), derivatives (how to step downhill), probability (what a model output means), and statistics (what your data is lying about). That's this module.
By the end you can
- Read any
(n, d)shape in code and say what each axis is. - Compute a dot product and explain what it means geometrically (weighted sum / similarity).
- Take the derivative of a simple loss and perform one gradient-descent step by hand.
- Use Bayes' rule and explain why precision depends on base rates.
- Distinguish train, test and population distributions — and spot the difference between correlation and causation.
1 · Linear algebra: data is shape
A dataset is a matrix X with shape (n, d): n rows =
examples, d columns = features. A label vector y has shape
(n,). A model's weights are vectors or matrices. When you debug shapes in NumPy or
PyTorch, you are doing linear algebra.
- Dot product as similarity: vectors pointing the same way give a large positive value; opposite directions give negative. This powers recommendation and search.
- Matrix × vector:
X @ wcomputes the weighted sum for every row at once — that's what a "layer" does. - Transpose: flips rows/columns; you'll meet it in
X.T @ X(normal equations) and in attention. - Broadcasting: NumPy/PyTorch stretch a shape-
(d,)vector across an(n, d)matrix — this is why a bias can be added to a whole batch without a loop.
import numpy as np
X = np.array([[1700, 3, 12], # house: sqft, bedrooms, age
[1400, 2, 20],
[1900, 4, 5]])
y = np.array([500, 400, 640]) # price in $k
w = np.array([0.2, 40, -5]) # weights per feature
b = 50 # bias / intercept
print(X.shape, y.shape, w.shape) # (3, 3) (3,) (3,)
preds = X @ w + b # one line: every row at once
print(preds) # [470. 350. 570.]
# "which training example is most like this new one?"
new = np.array([1650, 3, 10])
sims = X @ new # dot products = similarity scores
print(sims.argmax()) # 0 → the first house
2 · Calculus: derivatives are slopes, gradients are directions
A derivative answers: if I nudge input a little, how much does the output move? Positive slope → output rises; negative → falls. Training is nothing more than repeatedly nudging each weight downhill on the loss surface.
With many inputs you get a gradient: the vector of all partial derivatives,
∇L = [∂L/∂w₁, ∂L/∂w₂, …]. It points in the direction of steepest ascent, so
training subtracts it:
Worked example: one step of gradient descent
Model ŷ = w·x, single example x = 2, y = 10, start w = 1,
learning rate η = 0.1, loss L = (ŷ − y)²:
- Predict:
ŷ = 1 × 2 = 2 - Loss:
(2 − 10)² = 64 - Gradient:
∂L/∂w = 2(ŷ − y) · x = 2(−8)(2) = −32 - Update:
w ← 1 − 0.1(−32) = 4.2 - Next round:
ŷ = 8.4, loss2.56— already downhill.
Loss went 64 → 2.56 in one step. Gradient descent works fast when the surface is smooth and the learning rate is sane. Do this arithmetic once by hand and every training curve you ever see becomes legible.
3 · Probability: what a score actually means
- Joint / conditional:
P(A,B) = P(A|B)·P(B).P(spam | "free")is what classifiers want;P("free" | spam)is what the training data gives you. - Bayes' rule:
- Base-rate intuition: a test with 99% accuracy on a disease affecting 1 in 10,000 is mostly flagging healthy people — most positives are false. Precision requires the prior.
- Independence:
P(A,B) = P(A)·P(B)when A tells you nothing about B. Naive Bayes (Module 08) pretends features are independent — a lie that often works. - Expected value:
E[X] = Σ x·P(x)— the long-run average; how you compare decisions under uncertainty. - Entropy / uncertainty: a coin is max-uncertain (1 bit); a rigged coin is predictable. Decision trees split where uncertainty drops most.
4 · Statistics: what your data is lying about
| Concept | Why it matters in ML |
|---|---|
| Mean / variance / std | Scaling features (Module 11); a std of 0 means a dead column |
| Distribution | Train and production must come from the same one, or you have drift (Module 18) |
| Sampling bias | Historical data reflects past decisions — if loans were only approved for group A, labels for group B don't exist |
| Correlation ≠ causation | Models learn correlation; decisions made on them create feedback loops (score high → get offer → churn changes → model looks wrong) |
| Train / test / population | Test is a sample too: a 1,000-row test set gives a noisy estimate, not the truth |
| Overfitting vs noise | Noise cannot be learned; fitting it memorizes. More data helps the first, never the second |
| Confidence intervals | "A improved 2% over B" means nothing without knowing the variance of that difference |
Not in integrals. In: shapes (what is (n,d) vs (d,n)),
the chain rule (why layers learn "credit assignment"), and priors
(why accuracy lies on imbalanced data). Master those three and every framework's error messages get
easier.
What does the gradient point at, and which way do we step?
∇L points in the direction of steepest ascent (loss
increasing fastest). We step the opposite way: w ← w − η∇L.
Shape check: X is (5000, 42), w is (42,). What is X @ w?
(5000,) — one prediction per training row, each being the weighted sum
of its 42 features (plus bias, added by broadcasting). Inner dimensions must match (42 = 42);
outer dimensions survive.
Why does P(disease|positive) depend on how rare the disease is?
Chain rule ↔ neural networks: what is the connection?
∂L/∂w for early layers
from a single forward pass — computed backwards. That algorithm is backpropagation.
- Loss
L = (wx − y)²withx = 3, y = 7, w = 0: findŷ,L,∂L/∂w, and the newwafter one step withη = 0.01. Xhas shape(8, 5). What mustw's shape be forX @ wto work? What is the output shape?- A filter is 99% accurate; 0.1% of people have the condition. Of 100,000 tested, roughly how many positive results are false alarms? (Assume a 5% false-positive rate for healthy people.)
- Which of these can gradient descent optimize, and which can't: a linear model, a decision tree, a neural network with ReLU activations? Write one sentence of justification each.
Check yourself: (1) ŷ=0, L=49, ∂L/∂w = 2(0−7)(3) = −42, w = 0.42. (2) w is (5,), output (8,). (3) ~4,995 false alarms vs ~99 true positives → precision ≈ 2%. (4) Linear & neural: yes — differentiable. Tree split selection is combinatorial, so it uses greedy impurity reduction instead.
03 · Python data toolkit refresher
NumPy for math, pandas for tables, scikit-learn for models. This module is muscle memory: if you can't write this from memory, type it three times.
By the end you can
- Load, inspect, clean and aggregate a CSV without Googling.
- Index, slice and reduce NumPy arrays, and say what
axis=0means. - Use scikit-learn's
fit/predict/transformconvention. - Do a complete baseline: load → split → fit → score in ten lines.
Environment
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install numpy pandas scikit-learn matplotlib jupyter jupyter notebook # or: jupyter lab
One rule: never install into your system Python. One virtual environment per project — that habit alone prevents weeks of "it works on my machine" later in Track D.
NumPy — fast homogeneous arrays
import numpy as np
a = np.array([3.0, 1.0, 4.0, 1.0, 5.0])
print(a.shape, a.dtype, a.ndim) # (5,) float64 1
print(a.mean(), a.std(), a.min(), a.argmax())
print(a > 3) # boolean mask → [F F T F T]
print(a[a > 3]) # filter with the mask → [4. 5.]
M = np.arange(12).reshape(3, 4) # build a matrix
print(M.shape) # (3, 4)
print(M[:, 0]) # all rows, first column
print(M.sum(axis=0)) # sum DOWN the rows → shape (4,)
print(M.sum(axis=1)) # sum ACROSS cols → shape (3,)
# axis = the axis you COLLAPSE
# broadcasting: subtract column means without a loop
X = np.random.randn(100, 4)
X_centered = X - X.mean(axis=0) # (100,4) - (4,) → stretched
axis=0 = act along rows (collapse the row dimension → one value per column).
axis=1 = act along columns (one value per row). Say it out loud every time you
type it; it's the #1 NumPy confusion.
pandas — labeled tables
import pandas as pd
df = pd.read_csv("customers.csv")
print(df.shape, df.dtypes)
print(df.head(3))
print(df.describe(include="all")) # summary stats
print(df.isna().sum()) # missing values per column
print(df["segment"].value_counts(normalize=True)) # class balance
# selection: [] for columns, .loc/.iloc for rows+columns
sub = df.loc[df["churn"] == 1, ["tenure", "plan", "charges"]]
top = df.nlargest(10, "charges")
# transforms
df["avg_monthly"] = df["total"] / df["tenure"].clip(lower=1)
df["plan_enc"] = df["plan"].map({"basic": 0, "pro": 1, "max": 2})
# aggregation — the bread and butter
report = (df.groupby("plan")
.agg(customers=("churn", "size"),
churn_rate=("churn", "mean"),
avg_charge=("charges", "mean"))
.sort_values("churn_rate", ascending=False))
print(report)
# joins
df = df.merge(plan_meta, on="plan", how="left")
print(df.duplicated().sum()) # check for dupes
df = df.dropna(subset=["churn"]) # drop rows missing the target
Chained assignment: df[df.a > 0]["b"] = 1 often does nothing —
use df.loc[cond, "b"] = 1. View vs copy: assigning to a slice can
silently modify or not; don't rely on it. Index confusion: after filtering, the
index has gaps — use reset_index(drop=True) before positional work. And
never impute/scale using the whole dataframe before splitting (Module 11).
scikit-learn — one convention, every model
Every model — linear regression, random forest, k-means — uses the same three verbs. Transformers
(scalers, encoders) have fit/transform but no predict. That
uniformity is why you can swap models in ten lines.
Capstone of this module: a full baseline
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
# 1. data
df = pd.read_csv("telco_churn.csv")
df = df.dropna(subset=["Churn"])
print(df["Churn"].value_counts(normalize=True)) # always check balance!
# 2. features / target (keep only what you'd have at prediction time)
X = df.drop(columns=["Churn", "customerID"])
y = (df["Churn"] == "Yes").astype(int)
X = pd.get_dummies(X, drop_first=True) # one-hot categoricals
# 3. split BEFORE touching statistics
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42) # stratify keeps class balance
# 4. fit
scaler = StandardScaler().fit(X_tr) # fit on TRAIN only
model = LogisticRegression(max_iter=1000).fit(scaler.transform(X_tr), y_tr)
# 5. honest evaluation on untouched data
pred = model.predict(scaler.transform(X_te))
print(confusion_matrix(y_te, pred))
print(classification_report(y_te, pred, digits=3))
# 6. look at what it learned
imp = pd.Series(np.abs(model.coef_[0]), index=X.columns).nlargest(10)
print(imp)
Precision = of the ones you flagged, how many were right. Recall = of all the real ones, how many you caught. F1 = harmonic mean of both. Support = how many examples. If support is tiny for a class, your metric there is noise — don't trust it.
What is the difference between .fit() and .transform()?
fit learns statistics from data and stores them (e.g. the scaler's mean
and std, the encoder's categories). transform applies those stored
statistics. Calling fit on test data would leak test statistics into training — always
fit on train, transform both.
Why stratify=train_test_split, and why random_state?
stratify=y keeps the class proportions identical in train and test — essential with
imbalanced data, otherwise your test set might have almost no positives.
random_state makes the split reproducible so results are comparable across runs and
people.
get_dummies before or after the split? Why?
OneHotEncoder in a Pipeline.
Dummy columns created before the split can produce columns in train that don't exist in test (or vice
versa), and any target-informed encoding done globally leaks information from the test set.
Take any CSV you like (Kaggle's tips, titanic, or your own export):
- Load it. Print shape, dtypes, missing counts, and class balance if it has a target.
- Build one new feature with a vectorized NumPy/pandas expression (no loops).
- Produce a
groupbytable with at least two aggregations and sort it. - Split, fit a baseline classifier, print the confusion matrix and classification report.
- Write one sentence: which mistake is worse for this problem — false positives or false negatives?
04 · The workflow & honest evaluation
Before you pick a model: decide what "correct" means, how you'll measure it without cheating, and what dumb baseline you must beat. Get this wrong and a fancier model only makes you more confident in a wrong number.
By the end you can
- Frame a problem as X, y, and a primary metric tied to real cost.
- Choose the right split: random, stratified, time-based, or group-based.
- Recognize data leakage in its four common disguises.
- Build a baseline ladder: dumb → simple → your model.
- Pick the right metric for the mistake that actually hurts.
Step 1 · Frame before you fit
Write four sentences. If you can't, you're not ready to open a notebook:
- The decision: who will do what differently with this prediction?
- The input/output: what is
Xat the moment of prediction, and what isy? - The primary metric: the one number that decides ship / no-ship.
- The baseline: the dumbest thing that could produce that number honestly.
Cost of false negatives is high (missed cancer, missed fraud) → optimize recall (a.k.a. sensitivity, true positive rate) or PR-AUC. Cost of false positives is high (wrongly blocking a good customer) → optimize precision. Both matter, roughly equally → F1. Ranked list (search, recommender) → MAP / NDCG / recall@k. Classes balanced, errors equal → accuracy (rare in the wild). Probabilities will drive decisions → log loss / Brier score + calibration. Regression, errors are money → MAE (interpretable units); big errors are catastrophic → RMSE; relative error across scales → MAPE.
Step 2 · Split the data the way reality splits it
| Split | Use when | Failure if you get it wrong |
|---|---|---|
| Random | i.i.d. rows, no time or identity structure | — |
| Stratified | imbalanced classes | Test set missing rare class → meaningless metrics |
| Time-based (train on past, test on future) | anything that changes over time: demand, churn, markets | Random split leaks the future → score collapses in production |
| Group-based (all rows of one user/session/patient in one split) | repeated observations per entity | The model memorizes the entity → inflated score |
Step 3 · Hunt leakage
Leakage = information about the target that would not be available at prediction time. It is the single most common reason a great dev score means nothing. Four disguises:
- Target leakage: a feature is a consequence of the outcome
(
days_until_cancelin a churn model;total_refundsin a refund model). - Temporal leakage: you used future information (averages computed over the whole timeline, including the test period).
- Preprocessing leakage: scaling/imputation/feature selection fit on all data before splitting.
- Group leakage: duplicate or near-duplicate rows (same user, same session) on both sides of the split.
For every feature, ask: "Would this number exist, unchanged, at the exact moment the prediction is made?" If hesitating, drop it, rebuild, and compare. A model without a suspect feature scoring 0.88 beats one with it scoring 0.99.
Step 4 · Baselines, in order of dumbness
- Random / majority class: tells you the floor (and exposes accuracy traps).
- Global statistic: always predict the train mean, or the last known value (persistence — shockingly strong for time series).
- Human-level: what does a person get in 10 seconds with the same info?
- Simple model: logistic regression, shallow tree, one rule.
- Your model. If it doesn't beat step 4 by a margin wider than your noise, ship step 4 — it's cheaper, faster and explainable.
Evaluation toolkit
from sklearn.metrics import (confusion_matrix, classification_report,
precision_recall_curve, roc_auc_score,
mean_absolute_error, mean_squared_error, r2_score)
# --- classification ---
print(confusion_matrix(y_true, y_pred)) # [[TN FP],[FN TP]]
print(classification_report(y_true, y_pred, digits=3))
print("roc-auc:", roc_auc_score(y_true, proba)) # needs PROBABILITIES
print("pr-auc :", ... ) # precision_recall_curve → auc()
# --- regression ---
print("MAE :", mean_absolute_error(y_true, y_pred)) # average |error|, same units as y
print("RMSE:", mean_squared_error(y_true, y_pred) ** 0.5) # punishes big errors
print("R² :", r2_score(y_true, y_pred)) # variance explained vs predicting the mean
# --- slices: where does it fail? ---
report = X_test.assign(y=y_test, p=y_pred).groupby("segment").apply(
lambda g: (g.y == g.p).mean())
print(report.sort_values()) # overall 0.91, but segment X: 0.62
R² = 1 − (model's error) / (error of just predicting the mean). So R² = 0 means your
model is no better than the mean baseline; 1 = perfect; negative = worse than the mean. Negative is a
real, common outcome — it means something is broken.
Random vs time-based split: how do you decide?
You fit the scaler on all the data before splitting. Why is that wrong?
Pipeline on training data only (Module 11).
What does a baseline actually buy you?
Take your Module 01 GO problem and write the full evaluation contract:
- X (available now), y, and the moment of prediction.
- Primary metric + why that mistake is the costly one. Secondary metric.
- Which split type you'll use and the reason (random / stratified / time / group).
- Your dumb baseline and its expected score.
- Three candidate features you suspect might leak — and the test you'd run to check each.
05 · Regression & gradient descent
Linear regression is the "Hello, world" of ML — and still a top-5 model in production. It's also the clearest place to see loss, gradient descent, overfitting, and regularization all at once.
By the end you can
- Write the linear model and its loss, and interpret coefficients.
- Explain batch vs stochastic vs mini-batch gradient descent.
- Diagnose underfitting vs overfitting from learning curves.
- Choose between no regularization, ridge (L2) and lasso (L1).
- Extend linear regression to non-linear data with polynomial features — and know the risk.
The model
Training = find the w, b that minimize the mean squared error between predictions and
truth. We can't just "see" that answer with many features, but two facts matter: with zero features
the optimum is the mean of y; with many features, the solution is a weighted blend where
correlated features share credit unpredictably (multicollinearity).
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
rng = np.random.default_rng(0)
X = rng.uniform(500, 3500, 300).reshape(-1, 1) # sqft
y = 50 + 0.15 * X.ravel() + rng.normal(0, 25, 300) # price = signal + noise
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
m = LinearRegression().fit(X_tr, y_tr)
pred = m.predict(X_te)
print("weight:", round(m.coef_[0], 4), " bias:", round(m.intercept_, 2))
print("MAE:", round(mean_absolute_error(y_te, pred), 2),
" R²:", round(r2_score(y_te, pred), 3))
# read it: +0.15 price units per sqft, baseline 50 when sqft = 0
Gradient descent, the three flavours
| Variant | Gradient from | Per step | Character |
|---|---|---|---|
| Batch | the entire dataset | slow, exact | stable, impossible for big data |
| Stochastic (SGD) | one example | fast, noisy | escapes flat spots, jitters near the optimum |
| Mini-batch | 32–512 examples | fast & smooth | what everyone actually uses (GPU-friendly) |
# gradient descent from scratch — the whole algorithm in 8 lines
w, b, eta, epochs = 0.0, 0.0, 0.01, 1000
for _ in range(epochs):
pred = X_tr * w + b # forward
err = pred - y_tr
grad_w = np.mean(2 * err * X_tr) # dL/dw
grad_b = np.mean(2 * err) # dL/db
w -= eta * grad_w # step downhill
b -= eta * grad_b
print("from scratch:", round(w, 4), round(b, 2))
print("sklearn :", round(m.coef_[0], 4), round(m.intercept_, 2))
# both should land near 0.15 / 50 — same destination, different engines
If one feature ranges 0–1 and another 0–100000, the loss surface is a steep narrow valley and
gradient descent zig-zags forever. Standardize features (StandardScaler)
before any gradient-based model. Sklearn's linear regression solves it algebraically
(normal equations), so it "works" unscaled — but logistic regression, SVM, and neural nets will
stall. Also: learning rate is the first hyperparameter to try.
Underfitting vs overfitting
| Underfit (high bias) | Overfit (high variance) | |
|---|---|---|
| Train score | poor | excellent |
| Val score | poor ≈ train | much worse than train |
| Gap | small | large |
| Fix | more features, non-linear model, less regularization, longer training | more data, simpler model, regularization, early stopping, better features |
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
# underfit: a line on curved data
under = LinearRegression().fit(X_tr, y_tr)
# just right: mild flexibility
ok = make_pipeline(PolynomialFeatures(2), LinearRegression()).fit(X_tr, y_tr)
# overfit: degree 15 dances through every training point
over = make_pipeline(PolynomialFeatures(15), LinearRegression()).fit(X_tr, y_tr)
for name, model in [("line", under), ("deg2", ok), ("deg15", over)]:
print(name,
"train R²", round(model.score(X_tr, y_tr), 3),
"val R² ", round(model.score(X_te, y_te), 3))
# deg15: train ≈ 0.999, val ≈ 0.5 → the classic overfitting signature
Regularization: shrink to generalize
- Ridge shrinks weights toward zero but keeps them all — default choice when many features contribute a little.
- Lasso drives some weights to exactly zero → automatic feature selection. Use it when you suspect only a few features matter.
- ElasticNet — both, for grouped/correlated features.
- Intuition: regularization is a bet that a simpler explanation that fits the training data slightly worse will fit new data better.
from sklearn.linear_model import Ridge, Lasso
X2 = np.hstack([X_tr, rng.normal(size=(len(X_tr), 20))]) # 20 noise features
for name, model in [("none", LinearRegression()),
("ridge", Ridge(alpha=10.0)),
("lasso", Lasso(alpha=0.1, max_iter=10000))]:
model.fit(X2, y_tr)
kept = np.count_nonzero(model.coef_) if name == "lasso" else len(model.coef_)
print(f"{name:6s} R²={model.score(X2, y_tr):.3f} non-zero coefs={kept}")
# lasso throws the noise features away — that's feature selection for free
What do the coefficients of a linear model mean?
wᵢ units. That "all else equal" is important: with correlated features, coefficients
can be unstable or flip sign. For comparability, standardize features first — then coefficients
reflect one standard deviation of movement.
Bias vs variance: what's the trade-off, practically?
When would you pick lasso over ridge?
- Run the manual gradient descent and deliberately break it: set
eta = 5.0and watch the weights explode. Theneta = 0.000001and watch it barely move. Report the values where it just works. - Take a dataset with ≥8 features. Fit no-regularization, ridge (try α ∈ {0.01, 1, 100, 10000}) and lasso. Plot (or print) train/val R² vs α and find the sweet spot.
- Add 10 pure-noise columns. Which model's val score degrades least? Why?
06 · Logistic regression & classification
The name lies: it's a classifier. It's also the workhorse of production ML — a model that outputs calibrated probabilities, trains fast, and explains itself. This module is where classification metrics finally click.
By the end you can
- Explain sigmoid, log-loss, and why MSE fails for classification.
- Move the decision threshold and predict how precision/recall trade off.
- Read a confusion matrix, ROC curve and PR curve — and say when each is the right one.
- Handle multi-class problems (softmax / one-vs-rest).
- Check whether predicted probabilities are trustworthy (calibration).
From score to probability
Linear regression can output 14.7 or −3 — useless as a probability. Logistic regression takes the linear score and squashes it into (0, 1) with the sigmoid:
Training maximizes likelihood — "make the probability of the observed labels as large as possible". That's equivalent to minimizing the log loss (cross-entropy):
Squaring a sigmoid output makes the loss surface non-convex (many local minima), and when the sigmoid saturates the gradient vanishes — the model confidently wrong learns nothing. Log loss fixes both: convex for logistic regression, and brutally punishing for confident mistakes.
import numpy as np, pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (confusion_matrix, classification_report,
precision_recall_curve, roc_auc_score, roc_curve)
X, y = make_classification(n_samples=4000, n_features=12, n_informative=6,
weights=[0.9, 0.1], random_state=42) # 10% positives
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,
stratify=y, random_state=42)
clf = LogisticRegression(max_iter=1000).fit(X_tr, y_tr)
proba = clf.predict_proba(X_te)[:, 1] # probabilities, not classes
pred = clf.predict(X_te) # threshold 0.5 applied for you
print(confusion_matrix(y_te, pred))
# predicted
# neg pos
# actual neg [TN FP]
# actual pos [FN TP]
print(classification_report(y_te, pred, digits=3))
print("ROC-AUC:", round(roc_auc_score(y_te, proba), 3)) # ranking quality
The confusion matrix is the source of truth
| If the expensive mistake is… | You want high… | Example |
|---|---|---|
| Missing a real case (FN) | Recall | cancer screening, fraud detection, churn intervention |
| Wrongly flagging (FP) | Precision | spam that eats real mail, blocking a good customer, labelling data for human review |
| Both, and errors are equal (F1) | F1 / PR-AUC | imbalanced binary problems with symmetric cost |
| You rank and only show top-k | Recall@k / NDCG | search results, lead scoring, recommender |
| Probabilities feed a cost model | Log loss / Brier | insurance pricing, ad bidding |
ROC vs PR: which curve to trust
- ROC-AUC plots TPR vs FPR at every threshold. It measures ranking: the probability a random positive ranks above a random negative. Optimistic on heavy imbalance because FPR is diluted by the huge number of negatives.
- PR-AUC plots precision vs recall. Focuses on the positive class — the honest choice when positives are rare (fraud, disease, churn). A random model scores ≈ the positive rate here, so always compare against that.
- Both are threshold-free. The threshold is a product decision you make afterwards.
# --- choosing a threshold from real costs, not vibes ---
cost_fp, cost_fn = 10, 100 # false alarm costs $10, a miss costs $100
thresholds = np.arange(0.05, 0.95, 0.01)
best = min(thresholds, key=lambda t:
cost_fp * ((proba >= t) & (y_te == 0)).sum() +
cost_fn * ((proba < t) & (y_te == 1)).sum())
print("cost-optimal threshold:", round(best, 2))
for t in [0.1, 0.3, 0.5, 0.7]:
p = (proba >= t).astype(int)
tn, fp, fn, tp = confusion_matrix(y_te, p).ravel()
print(f"t={t:.1f} precision={tp/(tp+fp):.2f} recall={tp/(tp+fn):.2f}")
# --- is the model overconfident? (needed if you act on the numbers) ---
from sklearn.calibration import calibration_curve
frac, avg = calibration_curve(y_te, proba, n_bins=10)
print(np.round(avg, 2)) # if 0.8-bin samples are right 55% of the time → miscalibrated
Default 0.5 assumes false positives and false negatives cost the same. They almost never do. Sweep thresholds, plot precision/recall against cost, pick the minimum-cost point — and record the chosen threshold with the model so serving doesn't silently use a different one.
Multi-class
- Softmax regression (multinomial): all classes compete, probabilities sum to 1 — the natural model for "which of these 5 topics is this ticket?"
- One-vs-rest: train one binary model per class, take the highest score. This is also how many non-linear models (SVM) handle multi-class.
- Multi-label ≠ multi-class: an article can be both sports and opinion → treat as independent binary problems per label.
Precision or recall for: email spam? Fraud review queue? Self-driving pedestrian detection?
The model has 0.95 ROC-AUC but only 0.30 precision at threshold 0.5. Explain.
What is calibration and when do you care?
- Take an imbalanced dataset. Print metrics at thresholds 0.1 → 0.9 in steps of 0.1 in a table (precision, recall, F1, TP, FP, FN). Find where F1 peaks and where a 10:1 cost ratio peaks — are they different?
- Compare a logistic regression and a random forest on ROC-AUC and PR-AUC. Which one wins each? Which do you trust more for a top-100 review queue?
- Explain to a rubber duck: why can a model have great AUC and bad precision?
07 · Trees, forests & boosting
If your data is a table, gradient boosting is usually the strongest model you can train — and it's what wins most Kaggle competitions and runs inside a lot of bank/retail/insurance systems. Learn trees properly and you understand ensembles, bias-variance, and feature importance in one stroke.
By the end you can
- Explain how a tree splits (impurity) and why depth controls fit.
- Explain bagging and random forests — why averaging helps.
- Explain boosting as sequential error correction, and name its key hyperparameters.
- Read feature importance and know its blind spots.
- Choose between linear models, random forest, and gradient boosting.
How one tree decides
- Greedy: each split optimizes locally; no backtracking. Hence "decision tree", not "optimal tree".
- Depth is the main dial: depth 1 (a stump) underfits; unlimited depth memorizes (every leaf has 1 row → zero training error, garbage generalization).
- Strengths: no scaling needed, handles mixed types and missing values, captures non-linearity and interactions for free, gives interpretable rules.
- Weaknesses: high variance (small data change → very different tree), axis-aligned splits struggle with diagonal boundaries, extrapolation is impossible (a tree can never predict outside the range of its training targets — fatal for price curves).
Bagging → Random Forest
Bagging = aggregate bags of models: train many trees on bootstrap samples (sample with replacement, ~63% unique rows each), then average their predictions. Averaging uncorrelated errors shrinks variance — like asking 100 opinions instead of one. Random forest adds one twist: at each split, only consider a random subset of features. That decorrelates the trees, making the average even better.
The ~37% of rows left out of a bootstrap are that tree's test set. Average their predictions and you get an OOB score with no validation split at all. Handy sanity check.
Boosting → XGBoost / LightGBM / CatBoost
Instead of averaging independent models, boosting builds them sequentially: each new tree fits the residuals (or the gradient of the loss) left by the previous ones — a small, corrective step. Many weak learners × small learning rate = one strong, low-bias model.
- Predict the baseline (mean / prior).
- Compute errors (residuals or loss gradients).
- Fit a small tree (depth 2–6) to those errors.
- Add it:
pred += lr × tree(x). Repeat 100–3000 times. - Stop early when validation loss stops improving (early stopping — the key defense against overfitting).
| Hyperparameter | Role | Typical search |
|---|---|---|
n_estimators | how many trees (with early stopping, just set it high) | 100 – 3000 |
learning_rate | step size of each tree's contribution | 0.01 – 0.3 (smaller + more trees usually wins) |
max_depth | tree complexity | 3 – 8 (boosting likes shallow trees) |
subsample, colsample_bytree | row/feature randomness → regularization | 0.6 – 1.0 |
min_child_weight, reg_alpha/lambda | extra regularization | log scale |
import numpy as np, pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.inspection import permutation_importance
X, y = make_classification(n_samples=6000, n_features=15, n_informative=6,
random_state=42)
X = pd.DataFrame(X, columns=[f"f{i}" for i in range(X.shape[1])])
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,
stratify=y, random_state=42)
# --- random forest ---
rf = RandomForestClassifier(n_estimators=400, min_samples_leaf=3,
n_jobs=-1, random_state=42).fit(X_tr, y_tr)
print("RF ROC-AUC:", round(roc_auc_score(y_te, rf.predict_proba(X_te)[:,1]), 3))
# --- gradient boosting with early stopping ---
from sklearn.ensemble import HistGradientBoostingClassifier
gb = HistGradientBoostingClassifier(max_iter=1000, learning_rate=0.08,
early_stopping=True,
validation_fraction=0.15,
random_state=42).fit(X_tr, y_tr)
print("GBDT ROC-AUC:", round(roc_auc_score(y_te, gb.predict_proba(X_te)[:,1]), 3))
# --- importance, two ways ---
perm = permutation_importance(gb, X_te, y_te, n_repeats=10, random_state=42)
imp = pd.Series(perm.importances_mean, index=X.columns).sort_values(ascending=False)
print(imp.head(8))
Impurity importance favors high-cardinality features (an ID column looks "important" because it splits perfectly — pure leakage). Use permutation importance or SHAP on held-out data instead. Target encoding of categoricals must be computed inside CV folds or it leaks the target. Correlated features split the credit arbitrarily. And a forest trained on a leaking feature will happily use it — trees have no shame.
Need interpretability / small data / linear signal → regularized linear model. Tabular, mixed features, little preprocessing, want robustness → random forest. Tabular, maximum accuracy → gradient boosting (this is the default winner). Unstructured data (text, image, audio, sequences) → deep learning. When in doubt: run linear + boosting, compare, and look at the gap — a tiny gap means the problem may not need complexity.
Bagging vs boosting, in your own words.
Why do boosting trees stay shallow while forest trees go deep?
Why can't a decision tree extrapolate?
- On one dataset, train: logistic regression, random forest, gradient boosting. Make a table of ROC-AUC, PR-AUC, and training time. Which wins? By how much — is the gap worth the cost?
- Sweep
max_depthfor the forest (2 → 40) and plot train vs validation AUC. Find the overfit point. - Set
learning_ratehigh (0.5) vs low (0.03) with early stopping. Compare best iteration counts and validation scores. - Compare impurity importance vs permutation importance. Any feature that swaps places? Why?
08 · Distance & probabilistic models
Three "classical" models worth keeping in your pocket: KNN (learning by analogy), SVM (learning the widest street between classes) and Naive Bayes (learning probabilities, honestly about its own lies). Each has a niche where it still beats everything else.
By the end you can
- Explain KNN's mechanics and why feature scaling is mandatory for it.
- Explain margins and the kernel trick without equations.
- Explain why Naive Bayes works so well on text despite being wrong.
- Choose between these and the models from Modules 05–07.
K-nearest neighbours — no training at all
- k small → wiggly boundary, high variance (memorizes noise). k large → smooth, high bias (averages away real structure). k=1 gives 0 training error and often poor test error.
- Scaling is not optional: with one feature in cm (0–180) and one in income (0–120000), distance is decided entirely by income. Standardize first — always.
- Curse of dimensionality: as features grow, all points become roughly equidistant, "nearest" stops meaning anything. Works great up to ~20 meaningful features; collapses at 1000.
- Cost: zero training time, expensive prediction (must scan the dataset — use KD-trees / approximate nearest neighbours for scale).
- Good niches: recommender baselines, duplicate detection, "similar cases" retrieval, low-data problems with a sensible distance metric.
Support vector machines — the widest street
SVM finds the decision boundary that maximizes the margin — the distance to the closest points on each side (support vectors, the only points that matter). Wider margin → more tolerant of new data. Points inside the margin or misclassified are penalized by C.
| Knob | Small value | Large value |
|---|---|---|
C (tolerance for mistakes) | soft margin, smoother boundary, may underfit | hard margin, fits every point, may overfit |
gamma (RBF kernel reach) | each point influences far away → smooth | only very close points influence → wiggly, overfit-prone |
The kernel trick: instead of transforming data into infinite dimensions by hand, the kernel computes the similarity in that space directly. Intuition: project to a higher space where the problem becomes linearly separable, without ever visiting it. RBF is the "bend the space to fit" default.
SVMs are still excellent for medium-sized, high-dimensional problems (text classification, gene data). On large tabular datasets they're beaten by boosting and scale poorly (training cost grows faster than linearly with n).
Naive Bayes — fast, honest about its assumption
- It multiplies probabilities — so it uses log-space to avoid underflow (never multiply 10,000 small numbers).
- Gaussian NB: continuous features, assumes each is Gaussian per class. Multinomial/Bernoulli NB: word counts / binary presence — the classic spam filter.
- Why it works anyway: for ranking (which class is most likely) the exact probabilities don't need to be right — only the ordering.
- Strengths: trains in seconds, needs very little data, great text baseline, handles thousands of features.
- Add Laplace smoothing (+1 counts) so an unseen word doesn't zero out the whole product.
The classical model map
| Model | Reads as | Loves | Hates |
|---|---|---|---|
| KNN | analogy | low-dim, meaningful distance, small data | many irrelevant features, large n |
| Logistic / linear SVM | weighted evidence | linear signal, text, huge n, interpretability | complex interactions without feature work |
| RBF SVM | flexible boundary | medium n, high dim, clean margins | big n, needs scaling + tuning |
| Naive Bayes | independent probabilities | text, tiny data, quick baselines | strongly correlated features |
| Random forest | majority vote of rules | mixed tabular, robustness, little preprocessing | extrapolation, very smooth functions |
| Gradient boosting | sequential correction | tabular accuracy, structured data | unstructured data, heavy tuning |
| Neural net | learned representation | images, text, audio, sequences, huge data | small tabular data, low compute |
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import cross_val_score
docs = ["free prize click now", "meeting tomorrow agenda",
"win a lottery jackpot", "quarterly report attached",
"claim your free money", "lunch at noon?"]
labels = [1, 0, 1, 0, 1, 0] # 1 = spam
# text → tf-idf vectors → model. The classic two-step.
X = TfidfVectorizer().fit_transform(docs)
print(cross_val_score(MultinomialNB(), X, labels, cv=3).mean())
# KNN on tabular data: scaling is MANDATORY
knn = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
print(cross_val_score(knn, X.toarray(), labels, cv=3).mean())
Why must KNN be scaled but random forest need not?
What exactly is the kernel trick?
Why does Naive Bayes still work when its assumption is false?
- Train KNN without scaling and with scaling. Report the accuracy gap.
- Sweep
kfrom 1 → 60 on one dataset. Plot train vs validation accuracy. - Compare MultinomialNB vs logistic regression on a small text dataset — check whether NB's speed costs accuracy, and by how much.
09 · Unsupervised learning
No labels, no answer key. Unsupervised methods find structure you can't see directly — and their real production value is often indirect: better features, segmentation for the product team, or a sanity check on your data.
By the end you can
- Run k-means, choose k, and describe clusters in business language.
- Use PCA to reduce dimensions for visualization and speed — and know to scale first.
- Explain why evaluating clusters is hard and what proxies exist.
- Recognize when unsupervised work is a stepping stone rather than the deliverable.
k-means — the simplest clustering
- Pick k initial centroids (randomly — or use k-means++ for smarter starts).
- Assign every point to its nearest centroid.
- Recompute each centroid as the mean of its members.
- Repeat 2–3 until assignments stop changing.
- Report: within-cluster sum of squares (inertia) — how tight the groups are.
Spherical, similarly-sized clusters. It cuts non-blob shapes badly (a crescent
moon dataset is destroyed). Sensitive to scale — standardize.
Sensitive to initialization — set n_init=10 and compare.
You must choose k — elbow method (inertia curve bend), silhouette score, or domain
knowledge ("we can act on at most 4 segments").
PCA — shrink dimensions, keep variance
Finds the directions (principal components) along which the data varies most, and projects onto the top few. Component 1 = the axis carrying the most information. It's linear, unsupervised, and deterministic — no iterations.
- Uses: 2D/3D visualization of high-d data, removing correlated features, speeding training, denoising.
- Rule 1: scale first. PCA maximizes variance, so an unscaled feature in dollars will own every component.
- Rule 2: components are mixtures. PC1 = 0.4·age + 0.7·income − 0.5·debt — they're hard to interpret but easy to plot.
- Choose k by cumulative explained variance (e.g. keep 95%).
- Not for non-linear structure (UMAP/t-SNE exist for visualization of that — but t-SNE distances between clusters are not meaningful, don't over-read it).
Others worth knowing
| Method | Idea | Use when |
|---|---|---|
| DBSCAN | density-based: clusters as connected dense regions, marks noise | arbitrary shapes, unknown k, need outlier detection; struggles with varying densities |
| Hierarchical | build a tree of merges, cut at any level | taxonomy (subcategories), small n, want a dendrogram |
| Gaussian mixtures | soft, probabilistic ellipses instead of hard blobs | overlapping clusters, need membership probabilities |
| Isolation forest | anomalies are easy to isolate with random splits | fraud / intrusion / defect outliers, high dim, no labels |
Internal: silhouette (how close to own cluster vs nearest other), inertia, Davies-Bouldin. Stability: re-run on a bootstrap sample — do the same groups come back? External: compare against labels if any exist (ARI, NMI). Best test of all: usefulness — do the segments differ on the outcome you care about (churn, spend)? If a domain expert can't name each cluster in one sentence, it isn't a segment — it's just arithmetic.
Clustering rarely ships as a "predict" endpoint. Its industrial uses: customer segmentation for marketing, near-duplicate/dedup pipelines, feature compression feeding a supervised model, anomaly alerts on unlabeled logs, and cold-start grouping in recommenders. Beware feedback loops: targeting people by cluster changes their behaviour, which changes the clusters.
import numpy as np, pandas as pd
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
X, y_true = make_blobs(n_samples=900, centers=4, cluster_std=1.1,
random_state=42)
Xs = StandardScaler().fit_transform(X) # scale BEFORE clustering
# choosing k: inertia elbow + silhouette
for k in range(2, 8):
km = KMeans(n_clusters=k, n_init=10, random_state=42).fit(Xs)
print(f"k={k} inertia={km.inertia_:8.1f} silhouette="
f"{silhouette_score(Xs, km.labels_):.3f}")
km = KMeans(n_clusters=4, n_init=10, random_state=42).fit(Xs)
# visualize in 2D and profile the segments
coords = PCA(n_components=2).fit_transform(Xs)
prof = pd.DataFrame(Xs, columns=["f0", "f1"]).assign(cluster=km.labels_)
print(prof.groupby("cluster").mean().round(2)) # "cluster 2 = high f0, low f1"
Why scale before k-means and PCA but not before trees?
You clustered 6 customer groups. What do you report to the business?
- Take a real dataset (customers, songs, articles). Standardize, run k-means for k = 2…10, and choose k using both elbow and silhouette. Do they agree?
- PCA to 2 components → scatter plot colored by cluster. Do the groups look real or arbitrary?
- Describe each cluster in three business sentences. Give them names.
- Check: does cluster membership correlate with an outcome you care about? If not — say out loud whether this analysis was worth doing.
10 · Neural networks & deep learning
Deep learning is not magic — it's linear regression stacked with non-linearities, trained with the chain rule at scale. This module builds that picture from a single neuron up to transformers, and is honest about when you need it (rarely, for tabular data).
By the end you can
- Describe a neuron, an activation function, and why non-linearity is essential.
- Trace backpropagation as the chain rule applied layer by layer.
- Choose an optimizer and learning rate, and recognize the symptoms of bad ones.
- List the defenses against overfitting: dropout, weight decay, early stopping, augmentation, more data.
- Match architecture to data type: MLP / CNN / sequence model / transformer.
- State when a neural net is the wrong choice versus gradient boosting.
One neuron
- It's the linear model from Module 05 plus a squashing function.
- Why activations are non-negotiable: stack 50 linear layers without them and the
whole thing collapses into… one linear layer. All that depth would compute
W·x + b. Non-linearity is what lets networks bend to fit reality. - ReLU is the default (cheap, no saturation for positive values). ReLU dying (always 0) is a classic failure — symptom: a layer's outputs are all zero.
From neuron to network
A "hidden layer" learns representations: layer 1 finds edges/word patterns, layer 2 combines them into shapes/phrases, layer 3 into faces/topics. That automatic feature learning is the entire reason deep learning wins on unstructured data — classical models make you hand-design those features.
Training: backprop in one paragraph
- Forward pass: run x through all layers, record intermediate values, get loss.
- Backward pass: chain rule from loss backwards → gradient of loss w.r.t. every
weight (
∂L/∂wfor millions of weights in one sweep). - Update: every weight steps a little downhill:
w -= lr × grad. - Repeat over mini-batches for many epochs (one epoch = one full pass over the dataset).
| Optimizer | Idea | Notes |
|---|---|---|
| SGD | plain gradient step | great generalization, needs a tuned lr |
| SGD + momentum | averages past gradients → rolls downhill with inertia | the classic workhorse |
| Adam / AdamW | per-parameter adaptive step size + momentum | fast default, fewer lr headaches; AdamW adds proper weight decay |
Too high: loss goes NaN or oscillates wildly (drop it 10×). Too low: loss crawls — you'll think the model is broken. Schedule (warmup then cosine/step decay) usually beats a constant value: big steps early, careful steps late. First thing to plot when training misbehaves: loss vs step, on a log scale.
Overfitting: the full defense kit
- More data / data augmentation (rotate, crop, back-translate — the highest-leverage fix for vision/NLP).
- Dropout: randomly zero a fraction of activations during training — forces redundant representations. Off at inference.
- Weight decay (L2): penalize large weights — exactly Module 05's idea.
- Early stopping: monitor validation loss, stop when it stops improving, restore the best weights. Always do this.
- Smaller model: fewer layers/width. Capacity is a dial, not a virtue.
- Batch normalization / layer normalization: stabilizes internal scales → faster, more stable training.
The architecture map
| Data | Architecture | Core idea |
|---|---|---|
| Flat table | MLP (a couple of dense layers) | stacked linear+ReLU — often loses to boosting anyway |
| Images | CNN (ConvNet) | sliding filters detect edges → textures → parts; weight sharing makes it translation-aware |
| Sequences (older) | RNN / LSTM / GRU | read step by step with a memory state — slow to train, mostly superseded |
| Text, and almost everything now | Transformer | self-attention: every token looks at every other token and weighs relevance; parallel, scales beautifully |
| Categories / IDs | Embeddings | learn a dense vector per item so similar items are nearby (users, words, products) |
| One model, many domains | Pretrained (BERT, GPT, ResNet) | fine-tune or prompt instead of training from scratch — always start here before training your own |
For each token, attention computes a weighted average of all other tokens where the weights come from learned relevance scores — "which earlier words matter for understanding this one?" The same trick applies to image patches, audio frames, and time steps, which is why transformers took over every domain.
# pip install torch
import torch, torch.nn as nn
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X, y = make_moons(n_samples=2000, noise=0.25, random_state=42)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler().fit(X_tr) # scale for NNs!
X_tr = torch.tensor(scaler.transform(X_tr), dtype=torch.float32)
X_te = torch.tensor(scaler.transform(X_te), dtype=torch.float32)
y_tr = torch.tensor(y_tr, dtype=torch.float32).unsqueeze(1)
y_te = torch.tensor(y_te, dtype=torch.float32).unsqueeze(1)
model = nn.Sequential(
nn.Linear(2, 64), nn.ReLU(), nn.Dropout(0.2), # hidden layer
nn.Linear(64, 64), nn.ReLU(), nn.Dropout(0.2), # hidden layer
nn.Linear(64, 1), # logit output
)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_fn = nn.BCEWithLogitsLoss() # numerically stable sigmoid+log loss
for epoch in range(300):
model.train()
logit = model(X_tr) # forward
loss = loss_fn(logit, y_tr) # loss
opt.zero_grad(); loss.backward(); opt.step() # backward + update
if epoch % 50 == 0:
model.eval()
with torch.no_grad(): # forward WITHOUT dropout
acc = ((torch.sigmoid(model(X_te)) > 0.5).float() == y_te).float().mean()
print(f"epoch {epoch:3d} loss={loss.item():.4f} val_acc={acc.item():.3f}")
# In Keras this is ~10 lines: model.compile(optimizer="adam",
# loss="binary_crossentropy", metrics=["accuracy"]) + model.fit(X, y, epochs=30)
train loss down, val loss up → overfitting (stop earlier / regularize). Both flat and bad → underfitting (bigger model, better features, higher lr, more epochs). Read the two curves together before touching anything.
Deep learning needs data (thousands to millions of labelled examples), compute (GPU), and serving budget (GPUs are expensive; latency is real). Gradient boosting trains on a laptop in minutes and often wins on tabular data. The senior move: strong baseline → boosting → only then a network. And once you have a trained net: quantize (FP32→INT8), prune, or distill into a smaller model when latency/cost matters — 2–4× speedups are routine.
Why can't a stack of linear layers alone learn e.g. image edges?
W₂(W₁x+b₁)+b₂ = W'x + b'. No matter how many
layers, the model can only draw a straight boundary. Non-linear activations make the function class
grow exponentially with depth — that's where the expressive power lives.
Explain backprop to someone who knows derivatives.
When should you NOT use deep learning?
- Run the moons example. Print train and val accuracy. Then: (a) remove ReLU — what happens to
accuracy? (b) set
lr=1.0— what happens to loss? (c) remove dropout and use 512-unit layers — measure the train/val gap. - Plot loss curves for train and val. Circle the epoch where overfitting starts and state the early-stopping rule you'd use.
- Swap the data: run the same MLP on a tabular dataset and compare it with
HistGradientBoostingClassifier. Note the winner. - One-sentence architecture picks: medical X-rays · customer churn from a CSV · support ticket topic routing · next-word prediction.
11 · Features & preprocessing pipelines
Models don't learn from data — they learn from representations of data. Better features beat better algorithms almost every time, and doing preprocessing safely (no leakage) is the difference between a credible score and a embarrassing one.
By the end you can
- Encode, scale, impute and transform mixed-type columns correctly.
- Wrap everything in a
Pipelineso nothing leaks and serving matches training. - Handle class imbalance with three different levers.
- Do basic feature engineering: datetime, aggregations, text, interactions.
- Know the four feature-selection strategies.
The feature checklist
| Problem | Technique | Notes |
|---|---|---|
| Missing values | mean/median (numeric), "missing" category (categorical), or a model-based imputer (KNNIterative/IterativeImputer) | Missingness can be signal — add a was_missing flag |
| Categories | one-hot (low cardinality), ordinal (inherent order), target encoding (high cardinality — inside CV!), embeddings (huge cardinality) | drop_first=True avoids the dummy-variable trap for linear models |
| Scale mismatch | StandardScaler (z-score), MinMaxScaler (0–1),RobustScaler (median/IQR — for heavy outliers) | Required for: NN, SVM, KNN, logistic, PCA, k-means. Not for trees |
| Skewed distributions | log1p / Box-Cox / Yeo-Johnson | income, prices, counts — long tails wreck linear models and distance |
| Outliers | clip to quantiles (winsorize), RobustScaler, or let trees handle it | Decide based on: real rare event (keep) vs data error (fix) |
| Datetime | hour, day-of-week, month, is_weekend, days_since, cyclical sin/cos | Never feed raw timestamps; relative time matters more than absolute |
| Text | TF-IDF (classical), sentence embeddings (transformers) | TF-IDF + linear model is a brutally strong text baseline |
| Geography | lat/long, distance to landmarks, region codes, h3 cells | Distance features often beat raw coordinates |
| IDs / free text | drop them | An ID column lets the model memorize; it helps train, fails test |
A 1% AUC gain from switching models costs a week. A 5% gain from one good feature costs an afternoon. Spend effort on: domain aggregates (user's last-30-day averages), ratios (spend/visits), counts and recency (days since last purchase), lags (value yesterday), and interaction terms your domain says matter.
Pipelines: the anti-leakage machine
A Pipeline chains transformers and a final estimator so that fit runs
training-only statistics through every step, and predict applies the same
transformations. It also means the exact same code runs at serving time — no "why is the API
preprocessing differently?" bugs.
import numpy as np, pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
df = pd.read_csv("churn.csv")
y = (df.pop("churn") == "yes").astype(int)
num = ["tenure", "monthly", "total", "support_calls"]
cat = ["plan", "region", "device"]
# each column group gets its own treatment; nothing sees the validation data
prep = ColumnTransformer([
("num", Pipeline([("imp", SimpleImputer(strategy="median")),
("sc", StandardScaler())]), num),
("cat", Pipeline([("imp", SimpleImputer(strategy="most_frequent")),
("ohe", OneHotEncoder(handle_unknown="ignore"))]), cat),
])
model = Pipeline([
("prep", prep),
("sel", SelectKBest(f_classif, k=12)),
("clf", HistGradientBoostingClassifier(max_iter=300, random_state=42)),
])
X_tr, X_te, y_tr, y_te = train_test_split(df, y, test_size=0.2,
stratify=y, random_state=42)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X_tr, y_tr, cv=cv, scoring="roc_auc")
print(f"CV ROC-AUC: {scores.mean():.3f} ± {scores.std():.3f}")
model.fit(X_tr, y_tr) # refit on all training data
print("test ROC-AUC:", round(roc_auc_score(y_te, model.predict_proba(X_te)[:,1]), 3))
Always report mean ± std across folds. If model A gets 0.841 ± 0.024 and model B gets
0.849 ± 0.021, the "improvement" is inside the noise. Don't celebrate overlaps.
Imbalanced data — three independent levers
- The algorithm:
class_weight="balanced"(orscale_pos_weightin XGBoost) — penalizes mistakes on the rare class more. Free, always try first. - The data: undersample the majority, oversample the minority, or synthesize (SMOTE: interpolate between minority neighbours). Use inside CV folds only — synthetic points must never be generated before the split.
- The threshold: train normally, then lower the decision threshold until recall meets your cost target (Module 06). Often the best lever of the three.
Class weights + threshold tuning + PR-AUC as the metric solves most imbalance problems without inventing synthetic rows. SMOTE can blur class boundaries and quietly leaks if applied before splitting. And accuracy is not a metric here — it never was.
Feature selection
- Filter (cheap, model-independent): correlation, mutual information, variance threshold, chi-square. Good first pass.
- Wrapper (model-in-the-loop): recursive feature elimination — accurate but slow.
- Embedded (free with the model): lasso's zeroed coefficients, tree/boosting importance, SHAP. Best bang for the buck.
- Stability check: does the selected set survive resampling? If a feature is selected in 3 of 5 folds, its signal is shaky.
Why must imputation/encoding happen inside the pipeline?
Name three ways to fight class imbalance and when each applies.
Your CV AUC is 0.93, test AUC is 0.71. What went wrong?
- Take a messy dataset with mixed types and missing values. Build the full
ColumnTransformerpipeline above. Print CV mean ± std. - Deliberately leak: run the scaler on all data before splitting. Measure the difference. (It's often small — which is exactly why people get away with it until the day it isn't.)
- Add one engineered feature from your domain intuition. Did the score move more than the model swap you were about to try?
- On an imbalanced target, compare: baseline vs class weights vs threshold tuning — report precision, recall, F1 and PR-AUC for each.
12 · Tuning, validation & error analysis
The best ML engineers don't have better models — they have better loops: a trustworthy validation scheme, disciplined search, and a habit of reading errors instead of numbers.
By the end you can
- Pick the right cross-validation scheme for your data structure.
- Design a hyperparameter search that spends compute where it matters.
- Read learning curves to decide: more data, more capacity, or stop.
- Run an error analysis that produces a concrete next action.
- Keep experiments reproducible and comparable.
Cross-validation schemes
| Scheme | How | Use when |
|---|---|---|
| K-fold | k chunks; train on k−1, test on 1; rotate | default for i.i.d. data, small datasets |
| Stratified K-fold | k-fold keeping class ratios | imbalanced classification — the default |
| Group K-fold | all rows of one group stay in one fold | repeated users/patients/sessions (avoids entity leakage) |
| Time-series split | always train on the past, test on the next block | anything temporal — expanding window |
| Leave-one-out | k = n | tiny data only; expensive, high variance |
Every time you look at the test score and change something, it becomes another validation set. Rule: one final look, at the end, with a number you report without editing. If you already burned it, get a fresh holdout or nested CV. Also beware multiple comparisons: trying 50 random hyperparameter configs and keeping the best will beat your validation set through luck alone — that's why you re-check the winner with fresh CV folds.
Searching hyperparameters efficiently
| Strategy | Cost for p params | Best for |
|---|---|---|
| GridSearch | exponential blow-up | 1–2 params you care about a lot |
| RandomSearch | fixed budget; explores each dim better | default choice — covers more space than grid at the same cost |
| Bayesian (Optuna, hyperopt) | samples the promising regions | expensive models, many params, limited budget |
| Successive halving / ASHA | kills bad configs early with partial training | big search spaces (esp. neural nets) |
- Search logarithmically for scales: learning rates, regularization strength
(
log-uniform 1e-5 … 1e-1) — linear grids waste budget. - Tune the influential knobs first: model capacity (depth, layers), learning rate,
regularization. Ignore
n_jobs-style knobs. - Fix a budget of folds × configs, report it, and stop. Unlimited tuning is overfitting with extra steps.
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import (cross_val_score, learning_curve,
RandomizedSearchCV, StratifiedKFold)
from sklearn.ensemble import HistGradientBoostingClassifier
from scipy.stats import loguniform, randint
X, y = make_classification(n_samples=2500, n_features=20, n_informative=10,
weights=[0.85, 0.15], random_state=42)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# --- 1. random search over the knobs that matter ---
space = {"learning_rate": loguniform(1e-3, 0.3),
"max_depth": randint(2, 10),
"min_samples_leaf": randint(5, 100),
"l2_regularization": loguniform(1e-4, 10)}
search = RandomizedSearchCV(HistGradientBoostingClassifier(max_iter=300,
random_state=42),
space, n_iter=40, scoring="roc_auc",
cv=cv, random_state=42, n_jobs=-1).fit(X, y)
print("best params:", search.best_params_)
print("best CV auc:", round(search.best_score_, 3))
# --- 2. learning curves: should you get more data? ---
sizes, tr, val = learning_curve(search.best_estimator_, X, y, cv=cv,
train_sizes=np.linspace(0.2, 1.0, 5),
scoring="roc_auc", n_jobs=-1)
print("train:", np.round(tr.mean(axis=1), 3))
print("valid:", np.round(val.mean(axis=1), 3))
# both rising & close → MORE DATA will help
# both flat & low → model too simple / features bad → more capacity or better features
# train high, val low → overfit → regularize / simplify / more data
Error analysis: the step everyone skips
- Get probabilities, not just labels for the whole validation set.
- Sort into four buckets: high-confidence correct, high-confidence wrong (the dangerous ones — investigate first), low-confidence correct, low-confidence wrong.
- Slice by everything you have: segment, language, device, time of day, price band, source system. Compute per-slice precision/recall and find where it collapses.
- Label a sample of errors by hand. Is the model wrong, or is the label wrong? Noisy labels cap your ceiling more often than model capacity does.
- Turn each finding into one action: a new feature, a data source fix, a per-slice threshold, a rule for an obviously-wrong case, or "collect 500 more examples of slice X".
- Re-measure. One action at a time so you know what worked.
# a compact error-analysis table
import pandas as pd
res = pd.DataFrame({"y": y_val, "p": proba, "segment": df_val.segment,
"device": df_val.device})
res["pred"] = (res.p >= 0.5).astype(int)
res["correct"] = res.pred == res.y
print(res.groupby("segment").agg(n=("correct","size"),
acc=("correct","mean"),
avg_p=("p","mean")).sort_values("acc"))
worst = res[(res.correct == False) & ((res.p > 0.9) | (res.p < 0.1))]
print("confident-but-wrong:", len(worst)) # these are the ones to read
print(worst.sort_values("p", ascending=False).head(20))
One experiment = one hypothesis, written down before you run it. Log: data version, code version (git SHA), split definition, hyperparameters, seed, metrics. Then compare only against your previous best under identical conditions. This is the seed of MLflow/W&B in Track D — starting the habit in a spreadsheet is fine.
Learning curves: validation rising with more data — what's the prescription?
Why is random search usually better than grid search at the same budget?
Your model is 95% accurate but stakeholders are angry. What do you check first?
- Run a
RandomizedSearchCVwith 30 iterations on a real problem. Print the best config and the runner-up. How big is the gap? If it's < the fold std, stop tuning. - Plot learning curves for your best model. Write the one-sentence diagnosis and the action.
- Produce the error table above. Find your worst slice (n ≥ 30 — small slices lie). Name the feature or data fix you'd try first.
- Check 20 misclassified rows against their labels. How many are actually mislabelled?
13 · Interpretability & fairness
You can't explain a score you don't understand, and you can't defend a decision you can't audit. Interpretability is a requirement, not a luxury — and fairness is interpretability applied to people.
By the end you can
- Distinguish global vs local explanations and pick the right tool.
- Run permutation importance, partial dependence and a SHAP analysis.
- Name the four main sources of bias in a dataset.
- Explain two fairness definitions and why they can conflict.
- Write a one-page model card for a project.
The interpretability toolkit
| Question | Tool | Works on | Caveat |
|---|---|---|---|
| Which features matter overall? | Permutation importance | any model | breaks with correlated features (they mask each other) |
| Which features matter overall? | Impurity/gain importance | trees | biased toward high-cardinality features — prefer permutation or SHAP |
| How does the output change with feature X? | Partial dependence / ICE | any model | assumes features are independent (unrealistic); shows averages |
| Why this individual prediction? | SHAP (TreeSHAP for trees) | any model | game-theoretic; needs care with correlated features |
| Why this prediction? (fast approximation) | LIME | any model | local linear fit; unstable across runs |
| What rules does the model follow? | surrogate tree / rule lists / coefficients | any model | approximates, may misrepresent |
Global: how the model behaves in general ("spending drives churn up"). Local: why this customer got 0.82 ("3 support calls, plan downgraded last week"). Stakeholders ask local questions ("why was my loan rejected?"), regulators require local answers, data scientists need global ones. Have both.
# pip install shap from sklearn.inspection import permutation_importance, PartialDependenceDisplay import shap # 1. global: what matters? pi = permutation_importance(model, X_te, y_te, n_repeats=15, random_state=42) top = sorted(zip(X_te.columns, pi.importances_mean), key=lambda t: -t[1])[:8] # 2. how does it behave? PartialDependenceDisplay.from_estimator(model, X_te, ["tenure"]) # upward slope → more tenure, less churn. Flat → irrelevant. Jagged → suspicious. # 3. why this row? explainer = shap.TreeExplainer(fitted_gbdt) sv = explainer(X_te.head(5)) shap.waterfall_plot(sv[0]) # "tenure=2 added +0.14 to the score"
Importance ≠ causation. A feature can be a perfect proxy for the target without causing anything (a "days until cancel" column). Correlated features split credit arbitrarily — the model may be right for the wrong-looking reason. Post-hoc explanations aren't faithful — a surrogate can lie. If a decision really matters, prefer models that are inherently interpretable (linear, rules, small trees) or validate explanations against controlled tests.
Fairness: where bias comes from
- Historical bias: the data reflects past discrimination. A hiring model trained on 10 years of promotions learns who got promoted, not who was good.
- Measurement bias: the feature or label is measured differently across groups (better fraud labels for one region; sentiment data noisier for some dialects).
- Sampling/aggregation bias: some groups are under-represented → worse performance for them.
- Evaluation bias: you only measured an aggregate metric — the failure was hidden in the average. And label choice: using arrests instead of crime, repayment instead of ability to repay.
| Fairness definition | Informally | When it fits |
|---|---|---|
| Demographic parity | P(Ŷ=1 | group A) = P(Ŷ=1 | group B) | when outcomes themselves must be equally distributed (hiring pipelines) |
| Equal opportunity (equal TPR) | recall equal across groups | when the positive class is a benefit (loan approval for qualified applicants) |
| Predictive parity | precision equal across groups | when the flag is an accusation (recidivism risk) |
| Calibration per group | "70%" means 70% in every group | when probabilities drive decisions/risk pricing |
They can't all hold at once when base rates differ between groups (Kleinberg/Ruszak/Sharma-type impossibility results). Choosing which definition to violate is a values decision that belongs to stakeholders and legal — not something you settle by picking a metric in a notebook. Your job: compute them all, show the trade-offs, and never silently pick one.
1) List sensitive attributes (and proxies — zip code ≈ race). 2) Define harm: who loses what when wrong. 3) Evaluate metrics per slice, not just overall. 4) Fix upstream where possible (labels, sampling) before debiasing algorithms. 5) Document: intended use, training data, metrics per group, known limits — a model card. 6) Escalate the definition-of-fair decision. Also: strip PII you don't need, and know the vocabulary — GDPR-style "right to explanation" exists in spirit even where not literal.
SHAP value for feature j on one prediction — what does it mean?
Give two sources of bias and one concrete mitigation each.
Why can't we satisfy demographic parity and equal opportunity simultaneously?
- On your current model: permutation importance, one PDP for the top feature, and one SHAP waterfall for a high-confidence error. Write three sentences describing what the model actually learned.
- Find one proxy for a sensitive attribute in your data (e.g. postcode → ethnicity). Decide: drop it, keep it with monitoring, or handle upstream? Justify in two sentences.
- Compute recall per slice for any attribute you have (region, language, device). Report the worst gap in percentage points.
- Write a half-page model card: intended use, not-intended use, training data, primary metric, per-slice performance, known failure modes.
14 · From model to product
A model is one component of a product. The moment it ships, your job changes: from "is the score high?" to "does the system deliver value, safely, repeatedly, at acceptable cost?"
By the end you can
- Describe the full ML system: data → features → model → serving → UX → feedback.
- Translate a model metric into a business metric with an explicit cost model.
- Choose a risk tier and match it to process (from "ship on Friday" to "formal review").
- Define an MVP that beats a heuristic before any deep learning happens.
- Write the four documents every ML project needs.
The system around the model
Every box can fail independently of model quality. In practice, most production incidents are in the data and serving boxes: an upstream schema changed, a feature job stopped, a default value kicked in, a cache served stale scores. The model itself rarely breaks.
Money math: the cost model
Worked example — churn intervention:
- 100,000 customers, 5% churn. Offer costs $10 each, only given to flagged customers.
- Model at threshold t: precision 0.40, recall 0.60 → flags 750, saves 3,000 × 0.60 = 1,800 customers, each worth $200 → benefit $360,000; cost of offers 750 × $10 = $7,500.
- But: also add offers to people who wouldn't have churned (cannibalization — some accept the discount and were never leaving), model/serving cost, and the cost of annoying customers. Then sweep the threshold and find where net peaks — not where F1 peaks.
AUC improves 2 points → how much more money? Often nothing, because nobody changed a decision, or the improvement only exists in a region of the curve you never use. Always draw the line from metric → threshold → action → outcome → currency.
Risk tiers decide process
| Tier | Example | Process |
|---|---|---|
| Low | internal triage, nice-to-have ranking | Notebook → ship. Manual monitoring. Rollback is cheap |
| Medium | customer-facing recommendation, marketing scores | Tests, staged release, dashboards, owner on call |
| High | credit, insurance, health, safety, legal decisions | Formal review, fairness & explainability requirements, shadow mode first, audit trail, human approval, regulatory sign-off |
Ship a boring MVP first
- Heuristic v0: the rule a smart person would write today. Ship it, measure it. This is your baseline and it takes a day.
- Model v1 in shadow: run the model in production, log predictions, nobody sees them. Compare against outcomes and the heuristic for weeks. Free risk.
- Model v2 assisted: show scores to humans alongside their own judgement; measure whether they get faster/better.
- Model v3 automated for the low-risk slice only, with monitoring and rollback.
- Expand coverage as evidence accumulates.
What's the latency budget (10ms? 2s? overnight?) · Who is the on-call owner? · What happens when the upstream table is empty/late? · What does the API return when the model fails — 0, null, or an error? (Hint: a silent default is a hidden bug.) · How do we roll back in 5 minutes? · Where do ground-truth labels come from, and when? · What is the maximum acceptable error rate before we page someone?
The four documents
| Document | Contents | Audience |
|---|---|---|
| Framing doc | problem, decision, metric, baseline, cost model, success criteria | stakeholders, yourself |
| Model card | architecture, training data, metrics per slice, intended use, limits, fairness | reviewers, customers, regulators |
| Runbook | how to deploy, monitor, retrain, and what to do when alerts fire | on-call engineers |
| Decision log | why threshold 0.35, why drop feature X — one line each, dated | future you |
Why start with shadow mode?
Give the chain from AUC to dollars for a fraud model.
What are the most common production ML failures?
Take your GO problem from Module 01 and write the one-page framing doc:
- Decision, X, y, primary metric, baseline.
- Cost model: cost of FP, cost of FN, cost of running it. Where does net value peak (which threshold region)?
- Risk tier + the process that tier implies.
- Your v0 heuristic (write the actual rule) and your v1 shadow-mode plan.
- The three things most likely to break on the first week of serving.
15 · Data & training pipelines (MLOps)
Notebooks are for thinking; pipelines are for running. MLOps means making every step — data, training, evaluation, packaging — versioned, reproducible, automated and testable. This is the module that turns a demo into something you can re-run in six months.
By the end you can
- List the components of a reproducible training pipeline.
- Choose tools for versioning, experiment tracking and orchestration.
- Explain feature stores and the online/offline consistency problem.
- Describe CI/CD for ML: what to test and where the gates are.
- Design a registry → staging → production promotion flow.
Reproducibility: five things to version
| What | Tool examples | Why it bites without it |
|---|---|---|
| Code | git + commit SHA stored with the model | "which script produced this model?" |
| Data | DVC, lakeFS, immutable snapshots, row-count + hash manifest | retraining on data that silently changed yesterday |
| Config | YAML/JSON params, hydra, env vars | hyperparameters living only in your head |
| Environment | lock files (pip freeze, poetry, uv), Docker images | works in 2026, fails in 2027 after a library upgrade |
| Experiments | MLflow, W&B, Neptune — params + metrics + artifacts | can't answer "was last month's model better?" |
# a training run that is reproducible and self-describing
import json, hashlib, git, mlflow
repo = git.Repo(search_parent_directories=True)
sha = repo.head.object.hexsha[:12]
data_hash = hashlib.sha256(open("train.csv","rb").read()).hexdigest()[:16]
mlflow.set_experiment("churn-v2")
with mlflow.start_run(run_name="ridge-α10"):
mlflow.log_params({"model": "ridge", "alpha": 10.0, "seed": 42})
mlflow.log_metrics({"cv_auc": 0.871, "cv_std": 0.012,
"test_auc": 0.864})
mlflow.set_tags({"git_sha": sha, "data_hash": data_hash,
"sklearn": __import__("sklearn").__version__})
mlflow.sklearn.log_model(model, "model")
mlflow.log_artifact("error_analysis.md")
print("run:", mlflow.last_active_run.info.run_id)
The pipeline as code
# train.py — callable from cron, Airflow, Dagster, GitHub Actions, or bash
"""Usage: python train.py --config configs/churn.yaml"""
import argparse, pathlib, yaml, joblib
from datetime import datetime, timezone
def run(cfg: dict) -> dict:
# 1. ingest — immutable, dated, validated
df = load_snapshot(cfg["data"]["table"], cfg["data"]["as_of"])
validate_schema(df, cfg["data"]["schema"]) # fail loudly, not silently
# 2. features (same code path as serving — see Module 16)
X, y = build_features(df, cfg["features"])
# 3. train + CV + holdout evaluation against a fixed metric
scores = cross_validate(X, y, cfg["model"], cfg["cv"])
gate = scores.mean - scores.std
if gate < cfg["thresholds"]["min_auc"]: # quality gate
raise SystemExit(f"model below threshold: {gate:.3f}")
# 4. package with metadata
artifact = {
"model": joblib.dump(model, "model.joblib")[0],
"created": datetime.now(timezone.utc).isoformat(),
"metrics": scores, "config": cfg,
"git_sha": get_sha(), "data_hash": data_hash,
}
register(artifact) # → registry → staging (never straight to prod)
return artifact
if __name__ == "__main__":
p = argparse.ArgumentParser(); p.add_argument("--config", required=True)
run(yaml.safe_load(open(p.parse_args().config)))
Scheduling: when does training run?
| Strategy | Trigger | Good for |
|---|---|---|
| Periodic | nightly / weekly / monthly | stable domains with fresh data arriving on a rhythm (churn, pricing) |
| Performance-triggered | drift or metric crosses a threshold | volatile domains — retrain when it actually needs it (Module 18) |
| Data-triggered | new labelled batch arrives | when labels are the bottleneck |
| On-demand | human decides, after review | high-risk models where promotion needs sign-off |
| Online/continual | every batch/stream item | very high volume, gradual change — watch for catastrophic drift and feedback loops |
Feature stores: the consistency problem
Training reads features from the warehouse ("last 30-day spend, computed over history"). Serving needs the same feature in 10ms from a low-latency store. If the two implementations differ — timezone handling, default values, a window defined as "last 30 days" differently — you get training/serving skew, the #1 silent killer: the model performs great in validation and mysteriously badly in production.
- Fix: one definition per feature, used by both paths (a shared library, or a feature store like Feast/Tecton/Redis+registry).
- Online store (low latency, latest values) + offline store (full history for training) + a registry with definitions and versions.
- Point-in-time correctness: training must use the feature value as it was at that moment — never join on "current" values or you leak the future.
CI/CD for ML
| Stage | What runs | Gate |
|---|---|---|
| PR / commit | unit tests, lint, data schema tests (Great Expectations / Pandera), feature-parity tests | must pass |
| Nightly / on merge | full training on new data, evaluation vs baseline, drift report | metric ≥ baseline − tolerance; no data-quality regressions |
| Deploy to staging | API contract tests, latency benchmark, load test, explainability/fairness report | SLOs met |
| Production | shadow → canary (5%) → ramp, with automatic rollback on alert | dashboards healthy for N hours |
Data tests: schema, ranges, null %, category sets, row counts. Feature tests: train/serve parity on a known row (the same input must yield the same features). Model tests: metrics above threshold, sanity predictions on golden rows, input-shape validation. Serving tests: response schema, latency p95, failure modes. Not useful: asserting an exact accuracy to 6 decimal places — that just breaks CI.
1) Manual notebook, no tracking → 2) reproducible script + git + metrics file → 3) automated training with experiment tracking and a registry → 4) CI quality gates + staging + canary → 5) monitored, auto-retraining system with drift triggers. Most teams should invest in 2 and 3 before buying anything fancy — and before any feature store.
What is training/serving skew and how do you prevent it?
Why keep a data version and a git SHA with every model?
Notebook vs pipeline: what belongs where?
- Convert your notebook from Module 11 into
train.py: functions for load → validate → features → train → evaluate → save, driven by a YAML config. - Log three experiments (vary one hyperparameter each) to MLflow or a CSV — anything with params + metrics + timestamp.
- Write three tests: schema check, "golden row produces these features", "model beats baseline".
- Add a quality gate: exit non-zero if the CV score drops below your threshold. Run it twice and see it pass.
16 · Deployment patterns & scale
How a prediction gets to the person who needs it. Choose this before choosing infrastructure — latency, cost and failure modes follow from the pattern, not the other way around.
By the end you can
- Pick batch / online / streaming / edge from the latency requirement.
- Package a model so it loads fast and runs identically everywhere.
- Explain shadow, canary, A/B and rollback — in that order of risk.
- Estimate the latency and cost budget of a serving design.
- Know the levers for making inference cheaper.
Four patterns
| Pattern | How | Latency | Example | Watch out |
|---|---|---|---|---|
| Batch | nightly job scores everyone; results written to a DB | hours (fine) | churn lists, lead scoring, document ranking, recommendations refreshed daily | late/failed job = stale scores for everyone; no per-request freshness |
| Online (sync API) | request → model → response inside the user's request | ms–100s ms | fraud check at checkout, search ranking, autocomplete | model outage = request outage; need timeouts, fallbacks, p99 budget |
| Streaming | event bus → trigger → score → sink | ~seconds | real-time personalization, sensor anomaly, clickstream features | backpressure, ordering, exactly-once semantics |
| Edge / on-device | model runs in the app or device | <10 ms, works offline | keyboard suggestions, camera filters, sensor QC, privacy-sensitive scoring | model size, battery, version drift across devices, no central rollback |
Batch is dramatically cheaper, simpler and easier to debug than real-time. "We need real-time" should have to justify itself with a specific cost of a 5-second-old prediction. Most business scoring should be batch.
Packaging a model
| Format | What | When |
|---|---|---|
| joblib/pickle | serialized Python object | scikit-learn inside your own service — simplest, but Python-only and unsafe to load from strangers |
| ONNX | portable graph format | cross-language/edge serving, one artifact across frameworks, optimized runtimes (ONNX Runtime, TensorRT) |
| Framework native | state_dict (PyTorch), SavedModel/H5 (Keras) | default within its own ecosystem |
| Whole pipeline | preprocessing + model as ONE artifact | always, if you can — kills skew by construction |
| Container | Docker image with model + code + deps | reproducible deploy target for any of the above |
- Load once at startup, keep in memory — never load from disk per request.
- Warm up with a sample prediction to avoid a slow first request.
- Big models: serve with a dedicated runtime (Triton, TorchServe, BentoML) or a GPU; A/B the memory/latency trade-off.
Release safely
- Shadow catches data/serving bugs with zero user risk (Module 14).
- Canary: route a small slice to the new version, watch dashboards, expand only if healthy. Requires: automatic rollback, not "someone notices on Monday".
- A/B test: measures business impact on real users. Needs enough sample for significance, a pre-declared metric, and no peeking-until-significant.
- Version your API (
/v1/predict) and keep the previous model loadable — rollback = flip a pointer, not a redeploy.
Latency & cost budget
A request's time = queue + feature lookup + preprocessing + inference + postprocessing + network.
- Measure p50 / p95 / p99, not the average. Users feel the tail.
- Typical ballparks: feature store lookup 1–5ms; small tree/linear model <1ms on CPU; small NN 5–20ms CPU; LLM call 300–2000ms.
- Ways to cut cost: batch multiple requests per forward pass; cache predictions (with a TTL matched to how often inputs actually change); cheaper model for easy cases (cascading: fast filter → heavy model only for the ambiguous 10%); quantize (FP32 → INT8, often 2–4× faster, <1% accuracy loss); distill into a smaller net; prune; downgrade GPU class.
- Don't pay for the GPU twice: batching + autoscaling + spot instances for training; CPU for anything small at inference.
Model service down: what does the caller do? Have a fallback (previous model, heuristic, or a safe default) and a timeout short enough not to break the caller's request. Feature store down: don't fail silently with zeros. Bad input: validate schema at the boundary, reject loudly. Traffic spike: autoscale + queue limits. Bad deploy: automatic rollback on p99/latency/error-rate/drift alerts.
Batch or online? Decide for: nightly marketing segments; fraud check at checkout; on-device typo correction.
Why is "whole pipeline as one artifact" important?
Canary vs A/B — what does each measure?
- For your capstone: write the serving design — pattern, expected request volume, latency budget, breakdown of where the ms go, fallback behavior, and rollback plan.
- Estimate monthly cost: requests/day × inference ms × instance price. Then apply caching and cascading — how much does it drop?
- Design the rollout: shadow for how long, on what metric do you promote; canary percentages; the exact alert that triggers automatic rollback.
17 · Serving lab: FastAPI + Docker
Theory ends here. By the end of this module you will have a trained pipeline served behind an HTTP API,
containerized, tested, and reachable with curl. That's the deliverable — let's build it.
By the end you can
- Serve a scikit-learn pipeline with FastAPI behind typed endpoints.
- Structure a service so the model loads once at startup.
- Write an API contract test and a health check.
- Containerize it with Docker and run it anywhere.
- Explain what you'd add next for real traffic (logging, metrics, autoscaling).
Project layout
├── train.py # from Module 15 → produces model.joblib
├── model.joblib # THE artifact: preprocessing + estimator
├── app.py # FastAPI service
├── schemas.py # request/response contracts
├── tests/test_api.py
├── requirements.txt
├── Dockerfile
└── .dockerignore
1 · Train and save the whole pipeline
# train.py — save preprocessing AND model as one artifact (Module 16)
import joblib, pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import HistGradientBoostingClassifier
FEATURES = ["tenure", "monthly_charges", "total_charges", "support_calls",
"contract", "payment_method"]
NUM = ["tenure", "monthly_charges", "total_charges", "support_calls"]
CAT = ["contract", "payment_method"]
prep = ColumnTransformer([
("num", Pipeline([("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler())]), NUM),
("cat", Pipeline([("impute", SimpleImputer(strategy="most_frequent")),
("ohe", OneHotEncoder(handle_unknown="ignore"))]), CAT),
])
pipe = Pipeline([("prep", prep),
("clf", HistGradientBoostingClassifier(max_iter=300,
random_state=42))])
def train(df: pd.DataFrame, y) -> Pipeline:
pipe.fit(df[FEATURES], y) # fit learns; the fitted object is the artifact
return pipe
if __name__ == "__main__":
df = pd.read_csv("churn.csv")
y = (df.pop("churn") == "yes").astype(int)
train(df, y)
joblib.dump({"pipeline": pipe, "features": FEATURES, "threshold": 0.35},
"model.joblib")
print("saved model.joblib")
2 · The API
# app.py
import time, joblib, logging
import pandas as pd
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
log = logging.getLogger("uvicorn.info")
# ---- load ONCE at startup; never inside a request handler ----
bundle = joblib.load("model.joblib")
MODEL, FEATURES, THRESHOLD = bundle["pipeline"], bundle["features"], bundle["threshold"]
MODEL.predict(pd.DataFrame([{f: 0 for f in FEATURES}])) # warm-up call
class Customer(BaseModel):
tenure: int = Field(..., ge=0, description="months as customer")
monthly_charges: float = Field(..., ge=0)
total_charges: float = Field(..., ge=0)
support_calls: int = Field(..., ge=0, le=50)
contract: str = Field(..., examples=["month-to-month", "one year"])
payment_method: str = Field(..., examples=["electronic check"])
class Prediction(BaseModel):
churn_probability: float
will_churn: bool
threshold: float
model_version: str
app = FastAPI(title="Churn Scorer", version="1.0.0",
description="Scores a customer's churn risk.")
MODEL_VERSION = "churn-2026-10-01"
@app.get("/health")
def health() -> dict:
"""Liveness: is the service up AND is the model loadable?"""
return {"status": "ok", "model": MODEL_VERSION, "features": len(FEATURES)}
@app.post("/predict", response_model=Prediction)
def predict(c: Customer) -> Prediction:
t0 = time.perf_counter()
row = pd.DataFrame([c.model_dump()])[FEATURES] # enforce feature order!
try:
proba = float(MODEL.predict_proba(row)[0, 1])
except Exception as e:
log.error("inference failed: %s", e)
raise HTTPException(status_code=500, detail="scoring failed")
log.info("scored in %.1f ms", (time.perf_counter() - t0) * 1000)
return Prediction(churn_probability=round(proba, 4),
will_churn=proba >= THRESHOLD,
threshold=THRESHOLD,
model_version=MODEL_VERSION)
@app.post("/predict/batch", response_model=list[Prediction])
def predict_batch(rows: list[Customer]) -> list[Prediction]:
"""Batch endpoint: amortizes overhead — use it wherever you can."""
frame = pd.DataFrame([r.model_dump() for r in rows])[FEATURES]
probs = MODEL.predict_proba(frame)[:, 1]
return [Prediction(churn_probability=round(float(p), 4),
will_churn=p >= THRESHOLD, threshold=THRESHOLD,
model_version=MODEL_VERSION) for p in probs]
3 · Contract tests
# tests/test_api.py
import pytest
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
GOOD = {"tenure": 12, "monthly_charges": 70.5, "total_charges": 846.0,
"support_calls": 2, "contract": "month-to-month",
"payment_method": "electronic check"}
def test_health():
assert client.get("/health").json()["status"] == "ok"
def test_predict_shape_and_bounds():
r = client.post("/predict", json=GOOD)
assert r.status_code == 200
body = r.json()
assert 0.0 <= body["churn_probability"] <= 1.0
assert isinstance(body["will_churn"], bool)
assert body["model_version"]
def test_validation_rejects_bad_input():
bad = dict(GOOD, tenure=-3)
assert client.post("/predict", json=bad).status_code == 422 # pydantic catches it
def test_missing_field_is_422():
bad = dict(GOOD); bad.pop("contract")
assert client.post("/predict", json=bad).status_code == 422
def test_batch_matches_single():
a = client.post("/predict", json=GOOD).json()["churn_probability"]
b = client.post("/predict/batch", json=[GOOD]).json()[0]["churn_probability"]
assert a == b
4 · Containerize
# requirements.txt → fastapi uvicorn[standard] scikit-learn pandas numpy joblib
# Dockerfile
FROM python:3.12-slim
WORKDIR /srv
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py schemas.py model.joblib ./
EXPOSE 8000
# single worker is fine to start; scale with replicas, not workers, first
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
# --- build & run ---
docker build -t churn-scorer:1.0 .
docker run --rm -p 8000:8000 churn-scorer:1.0
# --- poke it ---
curl -s localhost:8000/health
curl -s -X POST localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{"tenure":12,"monthly_charges":70.5,"total_charges":846.0,
"support_calls":2,"contract":"month-to-month",
"payment_method":"electronic check"}'
# → {"churn_probability":0.7312,"will_churn":true,"threshold":0.35,...}
# docs are free with FastAPI:
open http://localhost:8000/docs
Why load at startup? Loading per request would take 100s of ms — startup load + warm-up costs once instead. Why pydantic? Validation at the boundary: bad types become a clean 422 instead of a stack trace or garbage prediction. Why /batch? Amortizes DataFrame overhead across rows — throughput for the price of latency. Why log the duration? You can't alert on p95 latency you never measured. Threshold inside the response? So the client knows how the boolean was decided — decisions must be auditable.
Structured logs with request_id + model version + inputs hash (for later analysis,
watch PII) · Prometheus metrics (request rate, p95 latency, error rate, score distribution) ·
autoscaling behind a load balancer · timeouts and a fallback model · graceful shutdown · secrets
management · a staging environment that runs the same container · and the monitoring from Module 18.
- Run
train.pyon any dataset of yours and getmodel.joblib. - Start the API with
uvicorn app:app --reload; test in/docsand withcurl. - Run the test suite:
pip install pytest httpx && pytest -q. - Build the Docker image, run it, confirm
curl localhost:8000/health. - Break it on purpose: send a wrong-typed field, a negative tenure, and a missing key. Confirm each gives a 422, not a 500.
- Extend: add
/model/info(metrics + training date) and log every prediction to a JSONL file. Bonus: deploy the image to a free host (Hugging Face Spaces, Cloud Run free tier, Fly.io, Render) and share the URL — that URL belongs in your portfolio.
18 · Monitoring, drift & retraining
Deploying is the midpoint, not the finish line. Models decay — quietly. Monitoring is how you find out before your customers do, and this module is what separates "we deployed ML" from "we run ML".
By the end you can
- Distinguish data drift, concept drift and performance drift.
- Define a monitoring stack: what to log, compute, chart and alert on.
- Compute PSI or KS drift and choose sane thresholds.
- Design a retraining strategy with clear triggers.
- Walk a drift incident through a runbook.
The three decays
| Drift | What changed | Signal | Example |
|---|---|---|---|
| Data drift (covariate shift) | inputs P(X) changed; P(y|X) same | input distributions move | a new contract type appears; traffic skews mobile after a campaign |
| Concept drift | the relationship P(y|X) changed | performance drops once labels arrive | a recession makes old churn drivers irrelevant; a new fraud technique |
| Performance drift | your metric fell | labels + evaluation — the ground truth signal, but delayed | recall on fraud down from 0.81 to 0.64 |
Data drift often precedes concept drift — that's why you monitor inputs even when you don't have labels yet. And beware sudden vs gradual drift (step change from a release vs slow decay) and recurring drift (weekends, seasons) — alerts must know the difference or you'll drown in false alarms.
What to monitor: five layers
- Service health (immediately): request rate, error rate, p50/p95/p99 latency, uptime, saturation. These page you on day one.
- Data quality: null %, missing features, unexpected categories, out-of-range values, row counts, schema changes, default-value spikes.
- Input distributions: per-feature drift vs a training reference window (PSI / KS-test / KL). Alert on a set of features crossing a threshold, or a sustained shift — not one noisy feature.
- Model outputs: score distribution, predicted class rate, score quantiles, embedding norms. A shifting score distribution is your earliest "something's wrong" signal even without labels.
- Performance + business (delayed): per-slice metrics when labels arrive (hours to months later), and the business KPI the model exists to move.
# a minimal drift report: PSI on every feature vs the training reference
import numpy as np, pandas as pd
def psi(expected: np.ndarray, actual: np.ndarray, bins: int = 10) -> float:
"""Population Stability Index. <0.1 stable · 0.1–0.25 investigate · >0.25 retrain."""
edges = np.quantile(expected, np.linspace(0, 1, bins + 1))
edges[0], edges[-1] = -np.inf, np.inf
e = np.histogram(expected, edges)[0] / len(expected)
a = np.histogram(actual, edges)[0] / len(actual)
e, a = np.clip(e, 1e-6, None), np.clip(a, 1e-6, None) # avoid log(0)
return float(np.sum((a - e) * np.log(a / e)))
def drift_report(ref: pd.DataFrame, live: pd.DataFrame) -> pd.DataFrame:
rows = []
for col in ref.columns:
if pd.api.types.is_numeric_dtype(ref[col]):
s = psi(ref[col].dropna().values, live[col].dropna().values)
else: # categorical: PSI over category frequencies
cats = set(ref[col].dropna()) | set(live[col].dropna())
s = psi(ref[col].value_counts(normalize=True).reindex(cats).values,
live[col].value_counts(normalize=True).reindex(cats).values)
rows.append({"feature": col, "psi": round(s, 4),
"status": "RETRAIN" if s > 0.25
else "investigate" if s > 0.1 else "ok"})
out = pd.DataFrame(rows).sort_values("psi", ascending=False)
return out[out.status != "ok"]
# reference = training features (immutable snapshot)
# live = last 24h of logged inputs (from your API logs)
print(drift_report(reference, live_inputs))
You can compute everything above in real time — except the one metric that matters (accuracy).
Design the label pipeline early: capture outcomes (did they churn? was the fraud confirmed? did the
agent apply the suggestion?) with their delay, join back to predictions by
request_id, and chart rolling performance. Until that exists, you are flying blind
with good dashboards.
Alerting without crying wolf
- Baseline vs absolute: alert on deviation from a seasonal baseline (same hour last week), not on a fixed number that breaks every holiday.
- Two-stage: warn on one window, page only if it persists across two consecutive windows. Single-sample alerts train people to ignore alerts.
- Route by owner. Every alert links to a runbook; every runbook ends with a human decision. An alert with no action is noise — delete it.
- Track alert quality itself: % true positives. If it's low, the thresholds are wrong.
Retraining: triggers and strategy
| Trigger | Response |
|---|---|
| Scheduled (weekly/monthly) | Automatic retrain; promote only if it beats the incumbent on the holdout (champion/challenger) |
| Drift alert (PSI > 0.25 sustained) | Investigate first (schema? upstream bug? real shift?) — not every drift means retrain. If real, retrain with recent data |
| Performance below threshold | Page owner; consider rollback to previous model while investigating |
| New labelled batch | Retrain + evaluate; often the cheapest correct trigger |
| After an incident / upstream change | Manual retrain + review |
- Always beat the incumbent or keep the incumbent. Automated retraining with a quality gate — never "newest model wins".
- Window choice: train on all history (stable) vs recent only (adapts fast, can forget). Recurring seasonality needs at least a full cycle of data.
- Champion/challenger: new model shadows or takes a traffic slice; the incumbent keeps serving until evidence accumulates.
- Auto-retrain ≠ auto-deploy. Automating training is safe; automation of promotion should depend on risk tier (Module 14).
If the model's outputs influence the labels you collect (you only investigate flagged cases → you only get fraud labels where you already looked → the model learns "don't look there"), monitoring will look fine while the system rots. Fixes: log all inputs regardless of decision, sample some "not flagged" cases for review (exploration), and track coverage of your label pipeline.
Incident runbook (the one-page version)
- Confirm: is it the model or the service? Check error rate and latency first — most "model" incidents are infrastructure.
- Rollback if users are affected: point traffic at the previous model version. Diagnosis happens after the bleeding stops.
- Check the data path: upstream job ran? schema changed? null rate spiked? feature values sane? (Fixes 80% of cases.)
- Compare distributions: today's inputs vs training reference (the PSI report) and vs yesterday.
- Slice: is the failure everywhere or in one segment/region/time window?
- Fix, verify, postmortem — one page: timeline, root cause, blast radius, action items, and which monitor should have caught it (add it).
Data drift vs concept drift — give an example of each and how you detect it.
PSI 0.19 on feature "tenure". What do you do?
What do you log at inference time to make future monitoring possible?
- Write
drift_report()above and run it against two different slices of real data (e.g. January vs July). Print the top movers. - Simulate drift: shift one feature of the test set by 30% and confirm PSI flags it. Then shift it by 5% and confirm it doesn't (threshold sanity).
- Design the monitoring page for your capstone: five charts (one per layer), each with the threshold and the alert's runbook link.
- Write your retraining policy in three sentences: trigger, gate, promotion rule.
- Draft the incident runbook for "scores look wrong today" — 6 steps, each with a command or a dashboard to open.
19 · Capstone ladder & portfolio
Reading about driving isn't driving. This ladder takes you from "one good notebook" to "a system I own end-to-end" — each rung adding exactly one new kind of pressure. Do them in order; don't skip rung 2 to jump at an LLM.
By the end you can
- Pick your capstone and know exactly what "done" means at each rung.
- Structure a project so it demonstrates the full loop, not just model scores.
- Write portfolio entries that describe decisions, not tutorials.
The four rungs
| Rung | Project | You must show | Time |
|---|---|---|---|
| 1 · Analyst Modules 01–07 |
Tabular prediction, full analysis. e.g. house prices, loan default, churn — any labelled table. | EDA with insight · leakage-aware split · baseline (mean/majority) · 3+ models compared with CV mean ± std · metrics chosen with justification · error analysis with slices · 500-word conclusions: what worked, what didn't, what you'd do next | 1 weekend |
| 2 · Engineer Modules 11–17 |
Same-or-new problem, shipped as a service. The Module 17 lab, with your own data. | Pipeline artifact (prep + model) · reproducible train.py + config + experiment
log · tests (data, golden row, API contract) · FastAPI endpoints with validation and health check
· Docker image · README with run instructions and a live URL or curl transcript |
1–2 weekends |
| 3 · Owner Modules 14–18 |
The same system, made real. Deployed somewhere durable, with monitoring and a retraining policy. | Deployment (cloud/free tier/HF Spaces) · structured logs + a metrics dashboard · drift report on real traffic · written retraining trigger + promotion rule · runbook for one plausible incident · cost estimate per 1k predictions · model card | 2–4 weekends |
| 4 · Specialist pick one direction |
Depth in one modern direction: LLM app with RAG + evals + guardrails · real-time/streaming scoring · vision or sequence model with a fine-tuned pretrained backbone · A/B experiment with a measured business outcome. | Clear problem framing · honest baseline comparison · latency/cost numbers · evaluation beyond accuracy (human evals, task success) · a write-up of two things that failed | 3–6 weekends |
Not "the notebook runs" — but: someone else can run it from a clean clone, you can defend every decision (split, metric, threshold, features dropped), and you know the weakest part of what you built. Projects that admit their weak spot read as senior work; projects that claim 0.99 read as tutorials.
What a strong project README contains
- One-line problem + why it matters (the decision it serves)
- Data: source, size, time range, known quality issues, license/PII notes
- Baseline and the number it achieved
- Approach: split strategy, metric, models tried, the winner and the margin (with mean ± std)
- Leakage & bias audit: what you checked and what you found
- Errors: worst slice, mislabelled sample, what you'd fix with more time
- Serving: how to run it, endpoint examples, latency/cost measured
- Monitoring: what you'd watch and your retraining rule
- Reproduce: exact commands, environment, seed
- Limitations + intended use (model card, half a page)
Weak: "Built a churn prediction model using XGBoost (AUC 0.94)."
Strong: "Found that a random-split AUC of 0.94 was leakage — rebuilt with a time-based split
(0.79), engineered recency features to recover 0.05, and shipped the pipeline behind a FastAPI +
Docker service with drift monitoring; the review queue cut churn-intervention waste by 18%."
Decisions, traps found, and impact — that's the signal.
- Choose your rung-1 dataset and write the framing doc (Module 14 format) — before opening a notebook.
- Create the repo skeleton now: README, requirements,
src/,data/.gitkeep,.gitignore. A named repo you push to is a project; an untitled notebook is not. - Calendar-block the work: rung 1 date, rung 2 date. Tell someone the dates.
- Write your definition of done for rung 1 as a checklist in the README. Tick it only when it's true.
20 · 4-week plan, recall deck & final checklist
The last module is about keeping it. A schedule you can actually follow, a deck to test yourself against, and the two checklists that tell you whether you're ready for real work.
By the end you can
- Run a 4-week schedule that fits a working adult's calendar.
- Test yourself with the final recall deck — honestly.
- Use the pre-flight checklist before calling a model production-ready.
- Know where to go deeper after this notebook.
The 4-week schedule (8–10 h/week)
| Week | Modules | Deliverable |
|---|---|---|
| 1 · Remember | 00 diagnostic · 01 · 02 · 03 | Diagnostic written; baseline notebook run end-to-end; 5 problems triaged GO/NO-GO |
| 2 · Core models | 04 · 05 · 06 · 07 · (08–09 skim) · (10 if needed) | Rung-1 capstone: complete analysis with CV, error analysis and conclusions |
| 3 · Get better | 11 · 12 · 13 | Pipeline rebuild of rung 1: no leakage, tuned with proper CV, SHAP/slice analysis, model card draft |
| 4 · Production | 14 · 15 · 16 · 17 lab · 18 | Rung-2 capstone: served + containerized + tests; monitoring design written; runbook for one incident |
Falling behind is normal. Compress by: skimming 08 and 09 (read goals + pitfalls + recall cards only), and doing Module 10 only if your direction needs deep learning. Protect the two capstones — they're where the learning actually consolidates.
Spaced repetition: the maintenance plan
| When | What to do | Time |
|---|---|---|
| Next day | Re-answer yesterday's recall cards from memory (no peeking) | 10 min |
| Day 3 | Re-answer; anything you miss goes back to the top of the pile | 15 min |
| Day 7 | Deck + retake the Module 00 diagnostic | 25 min |
| Day 14 | Deck + rebuild one thing from scratch without notes (a pipeline, an API) | 45 min |
| Day 30 | Full deck + explain 3 random modules aloud as if teaching | 60 min |
Put these in a real calendar. The tool doesn't matter (Anki, a paper card box, this notebook's recall boxes) — the schedule matters. Cards you write yourself are 2× better than cards someone hands you.
Final recall deck · 24 questions
Answer out loud first. Click only after you've spoken an answer.
1 · In one sentence: what makes a problem an ML problem?
2 · Train / validation / test — one line each.
3 · What is data leakage, and name two ways it sneaks in.
4 · Gradient descent in one sentence. What does the learning rate do?
5 · Write MSE and say what it punishes disproportionately.
(1/n)Σ(ŷ−y)² — squaring punishes large errors quadratically, making it sensitive to
outliers. (MAE doesn't have that sensitivity.)
6 · Precision or recall: spam filter? Fraud detection? Job-candidate screening?
7 · Why is accuracy misleading at a 1% positive rate?
8 · Bias vs variance, and the two diagnostic numbers.
9 · Ridge vs lasso: the difference and the use case.
10 · What does the sigmoid output that a linear model doesn't?
11 · Explain bagging vs boosting in one breath.
12 · Why do boosting trees stay shallow?
13 · Three things that make a distance-based model fail.
14 · Why scale before PCA/k-means but not before trees?
15 · You have 50 labelled images per class. Neural net or something else?
16 · What breaks if you remove all activations from an MLP?
17 · Validation loss rises while training loss falls. Three fixes.
18 · What must a Pipeline guarantee, and why does it matter at serving time?
19 · Why random search over grid search?
20 · Name the five things you must version for reproducibility.
21 · Batch vs online serving — the deciding question.
22 · What is training/serving skew and how do you test for it?
23 · Data drift vs concept drift: detection method for each.
24 · Your model's metric dropped 8 points overnight. First five minutes?
Pre-production checklist · run before you call it live
- Framing: decision, metric, baseline, and cost model written down
- Split: matches reality (time/group aware where needed); test set untouched until the end
- Leakage audit: every feature passes the "exists at prediction time?" test
- Pipeline: one artifact for preprocessing + model; feature-parity test passes
- Metrics: mean ± std across folds; beats baseline by more than the noise
- Slices: per-segment performance known; worst slice is acceptable or documented
- Threshold: chosen from cost, recorded with the model
- Validation: API input validation, clear errors, health check, graceful failure mode
- Tests: data schema, golden row, API contract — running in CI
- Release: shadow → canary → ramp, with automatic rollback
- Monitoring: five layers charted; alerts have owners and runbooks
- Labels: pipeline exists to capture outcomes and join them to predictions
- Retraining: trigger, quality gate and promotion rule written down
- Docs: model card, runbook, decision log, README with reproduce commands
- Ownership: a named human on call, and a max tolerable error rate
After the notebook: where to go deeper
| If you want… | Go to |
|---|---|
| Deeper theory (learning theory, causal inference, information theory) | ESL (Hastie), PRML (Bishop), Causal Inference (Peters) |
| Practical applied depth | Designing Machine Learning Systems (Huyen), Building ML Powered Applications (Auerbach), Made With ML |
| LLM / modern architecture direction | fast.ai course, Hugging Face course, nanoGPT from scratch, then build a RAG system with real evals |
| Career signal | The four-rung capstone ladder, written up well, beats ten course certificates |
| To stay sharp | The recall deck on schedule + one small model per month + one public write-up of a project |
1 · Baseline first — never report a number without the dumb reference next to it. 2 · Slice the errors — averages hide everything that matters. 3 · Think in systems — data, serving, monitoring, feedback, owner. The model is the smallest part.
- Retake the Module 00 diagnostic cold. Compare: write down which items you still can't answer.
- Say the 24 deck answers out loud. Mark the ones you faked — those are your review queue.
- Read the pre-production checklist against your current project. Fix the first unchecked item today.
- Open your calendar and schedule the day-1, day-3, day-7, day-14, day-30 review sessions. This is the highest-ROI action on this page.
- Go do Rung 1. Everything else in this notebook is scaffolding for that.