Self-paced re-learning notebook

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.

1 · Recall firstEvery module opens with a diagnostic — answer from memory, then read. Retrieval, not re-reading, is what makes it stick.
2 · Code every moduleSnippets are short on purpose. Type them out in a notebook; change one thing; predict what breaks.
3 · Tick the boxMark a module done only when you can explain it out loud without the page. Progress is stored in your browser.
A
Remember

Modules 00–03 · Diagnostic, core concepts, the math you actually use, and the Python data toolkit. Rebuild the floor before adding weight.

B
Core models

Modules 04–10 · Evaluation, linear models, trees & boosting, distance/probabilistic models, clustering, neural networks. The vocabulary of the field.

C
Get better

Modules 11–13 · Feature work, validation & tuning, error analysis, interpretability and fairness — where most real gains come from.

D
Production

Modules 14–18 · Framing, MLOps pipelines, deployment patterns, a hands-on FastAPI + Docker lab, and monitoring for drift.

E
Apply

Modules 19–20 · A four-rung capstone ladder, portfolio checklist, a 4-week study schedule and a final recall deck.

Track A · Remember

00 · Orientation & diagnostic

⏱ 25 min📎 No code🎯 Retrieve before you read

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

  1. Attempt the diagnostic cold. Write answers first, no peeking. Ten minutes, badly written answers are fine.
  2. Read with a pen. Each module has goals, core ideas, code, pitfall boxes, and recall cards.
  3. Run the code. Type it, don't copy it. Then break it on purpose: change one number, predict, run.
  4. Do the exercise — usually 15–30 minutes. Skipping it is how people finish 20 modules and learn nothing.
  5. Tick "Mark done" only when you can explain it aloud without looking. Progress is saved in your browser.
The forgetting curve

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

  1. In one sentence: what does it mean for a system to learn from data?
  2. Name the three main families of ML and give one real example of each.
  3. A model scores 0.99 during development and 0.60 after launch. Give three possible causes.
  4. What is the validation set for? Why not just train and test?
  5. Write the formula for mean squared error. What kind of mistakes does it punish hardest?
  6. What does the learning rate control? What happens if it is too large? Too small?
  7. For cancer screening: do you optimize precision or recall? Why?
  8. 1% of transactions are fraud. A model that predicts "not fraud" every time scores 99% accuracy. What metric do you use instead?
  9. Regularization, in one line.
  10. What is the difference between what .fit() and .predict() do?
  11. Name two things you must monitor after a model is deployed.
  12. 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.

Scoring yourself

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.

✏️ Exercise · 10 min
  • 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.
Track A · Remember

01 · What machine learning really is

⏱ 35 min📎 No code

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

A system improves at a task by finding structure in experience, judged by a measure of success Take away any one of the three and you no longer have an ML problem.

"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.

ApproachHow it worksWhen it wins
Explicit rulesHumans write every ifRequirements are known and stable, errors must be auditable ("never approve > $10k without ID")
Machine learningHumans define the task + data + metric; the machine finds the rulesThe rules are too many, too contextual, or change over time
Classical statisticsEstimate parameters, test hypotheses, quantify uncertaintyYou care about inference and confidence, not raw prediction
Human + ML hybridModel ranks/triages, human decidesAlmost every real deployment — start here
ML is a means, not a goal

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

FamilyWhat you're givenWhat you produceExamples
SupervisedInputs X with labels yA function X → yHouse price (regression), spam/not-spam (classification)
UnsupervisedInputs X onlyStructure: groups, compressed representations, anomaliesCustomer segments, deduplication, PCA for viz
Self-supervisedRaw data; labels generated from the data itselfGeneral representations / next-token predictorsBERT (masked words), GPT (next token), image inpainting
ReinforcementStates, actions, rewards over timeA policy: state → actionRobotics, 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

Problemdecision + success metric→ Datacollect, label, clean→ Featuresrepresent the signal→ Trainfit candidates→ Evaluatehonest, held-out→ Shipserve predictions→ Monitordrift, quality, cost→ Feedbacklabels → back to Data

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:

  1. 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.
  2. Data exists — and is representative. Enough examples of the thing, from the same distribution you'll meet in production, with labels you can trust.
  3. Actionability. Someone will do something differently when they see the score, and you can measure whether that helped.
Classic traps

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"?
Because training is optimization: something must be differentiated or scored to drive improvement. If success is subjective or unavailable at training time (e.g. "customer happiness in 6 months"), you must find an observable proxy — and then worry about whether the proxy is Goodhart'd (optimizing the proxy while the real goal gets worse).
Supervised vs unsupervised, in one breath each.
Supervised: you have labelled examples and learn the mapping X → y, judged against known answers. Unsupervised: you have only X and look for structure — groups, compressions, outliers — with no answer key to score against.
When should you NOT use ML?
When rules must be explicit and auditable; when you have no data and no cheap way to generate it; when the task is one-off; when a simple heuristic already meets the goal; when nobody will act on the output; or when the cost of being wrong is catastrophic and you cannot bound the model's error.
✏️ Exercise · 20 min

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.

Track A · Remember

02 · The math you actually use

⏱ 60 min📎 Light code🎯 The 20% that carries 80%

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:   w · x = Σᵢ wᵢxᵢ = w₁x₁ + w₂x₂ + … + w_d x_d a weighted sum of features — the single most repeated operation in all of ML
  • 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 @ w computes 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.

f(x) = x²  →  f′(x) = 2x   |   chain rule:   d/dx f(g(x)) = f′(g(x)) · g′(x) chain rule = backpropagation. That's it. That's the whole deep learning training algorithm.

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:

w ← w − η · ∇L(w) η (eta) = learning rate. Bigger step → faster but riskier.

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)²:

  1. Predict: ŷ = 1 × 2 = 2
  2. Loss: (2 − 10)² = 64
  3. Gradient: ∂L/∂w = 2(ŷ − y) · x = 2(−8)(2) = −32
  4. Update: w ← 1 − 0.1(−32) = 4.2
  5. Next round: ŷ = 8.4, loss 2.56 — already downhill.
Remember

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:
P(A|B) = P(B|A) · P(A) / P(B) posterior = likelihood × prior / evidence. P(A) is the base rate — the term everyone forgets.
  • 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

ConceptWhy it matters in ML
Mean / variance / stdScaling features (Module 11); a std of 0 means a dead column
DistributionTrain and production must come from the same one, or you have drift (Module 18)
Sampling biasHistorical data reflects past decisions — if loans were only approved for group A, labels for group B don't exist
Correlation ≠ causationModels learn correlation; decisions made on them create feedback loops (score high → get offer → churn changes → model looks wrong)
Train / test / populationTest is a sample too: a 1,000-row test set gives a noisy estimate, not the truth
Overfitting vs noiseNoise 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
Where beginners actually get stuck

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?
The gradient ∇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?
A vector of shape (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?
Because the positives are a mix of true positives and false positives. With a tiny prior, even a small false-positive rate on the huge healthy population produces more false positives than true ones — so the posterior is low despite "99% accuracy".
Chain rule ↔ neural networks: what is the connection?
Loss depends on layer L's output, which depends on layer L−1's output, and so on. The chain rule lets you multiply the local sensitivities together to get ∂L/∂w for early layers from a single forward pass — computed backwards. That algorithm is backpropagation.
✏️ Exercise · 25 min · no peeking
  1. Loss L = (wx − y)² with x = 3, y = 7, w = 0: find ŷ, L, ∂L/∂w, and the new w after one step with η = 0.01.
  2. X has shape (8, 5). What must w's shape be for X @ w to work? What is the output shape?
  3. 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.)
  4. 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.

Track A · Remember

03 · Python data toolkit refresher

⏱ 60 min📎 Full code · type it

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=0 means.
  • Use scikit-learn's fit / predict / transform convention.
  • 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, once and for all

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
pandas traps

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

estimator.fit(X, y)→ learned paramscoef_, intercept_→ predict.predict(X_new)→ scoreaccuracy, r2, …

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)
Read the report, don't just paste it

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?
After — or, better, inside a scikit-learn 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.
✏️ Exercise · 45 min · do it, don't read it

Take any CSV you like (Kaggle's tips, titanic, or your own export):

  1. Load it. Print shape, dtypes, missing counts, and class balance if it has a target.
  2. Build one new feature with a vectorized NumPy/pandas expression (no loops).
  3. Produce a groupby table with at least two aggregations and sort it.
  4. Split, fit a baseline classifier, print the confusion matrix and classification report.
  5. Write one sentence: which mistake is worse for this problem — false positives or false negatives?
Track B · Core models

04 · The workflow & honest evaluation

⏱ 50 min📎 Light code🎯 The module that keeps you honest

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:

  1. The decision: who will do what differently with this prediction?
  2. The input/output: what is X at the moment of prediction, and what is y?
  3. The primary metric: the one number that decides ship / no-ship.
  4. The baseline: the dumbest thing that could produce that number honestly.
Metric selection cheat-sheet

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

SplitUse whenFailure if you get it wrong
Randomi.i.d. rows, no time or identity structure—
Stratifiedimbalanced classesTest set missing rare class → meaningless metrics
Time-based (train on past, test on future)anything that changes over time: demand, churn, marketsRandom split leaks the future → score collapses in production
Group-based (all rows of one user/session/patient in one split)repeated observations per entityThe model memorizes the entity → inflated score
train (fit)  ·  validation (choose & tune)  ·  test (final verdict, touched once) typical sizes: 60/20/20 · 70/15/15 · or 80/20 with cross-validation doing the validation job

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_cancel in a churn model; total_refunds in 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.
The 30-second leakage test

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

  1. Random / majority class: tells you the floor (and exposes accuracy traps).
  2. Global statistic: always predict the train mean, or the last known value (persistence — shockingly strong for time series).
  3. Human-level: what does a person get in 10 seconds with the same info?
  4. Simple model: logistic regression, shallow tree, one rule.
  5. 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² in plain language

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?
Ask: will I predict on future data? If yes, the split must mimic that — train on older rows, test on newer ones. Random splits let the model see tomorrow's patterns while training on them, which inflates scores whenever the data drifts over time (which is most real data).
You fit the scaler on all the data before splitting. Why is that wrong?
Because test-set statistics (mean/std/min/max) influenced every training row's transformed values — information from the held-out set leaked into training. It's usually a small effect for scaling but a huge one for imputation, feature selection, and target encoding. Fix: fit inside a Pipeline on training data only (Module 11).
What does a baseline actually buy you?
Three things: a floor so you know if learning happened at all, a reality check on whether complexity was necessary, and a communicable reference — "87% vs 79% for always predicting the average" means something to a stakeholder; "F1 0.83" alone does not.
✏️ Exercise · 30 min

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.
Track B · Core models

05 · Regression & gradient descent

⏱ 55 min📎 Full code

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

ŷ = w₁x₁ + w₂x₂ + … + w_d x_d + b   =   w·x + b w = weights (how much each feature moves the prediction), b = bias (the baseline when all x = 0)

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

VariantGradient fromPer stepCharacter
Batchthe entire datasetslow, exactstable, impossible for big data
Stochastic (SGD)one examplefast, noisyescapes flat spots, jitters near the optimum
Mini-batch32–512 examplesfast & smoothwhat everyone actually uses (GPU-friendly)
w ← w − η · ∇L(w) η too large → loss oscillates or explodes. η too small → hours of training for nothing.
# 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
Scale matters more than it seems

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 scorepoorexcellent
Val scorepoor ≈ trainmuch worse than train
Gapsmalllarge
Fixmore features, non-linear model, less regularization, longer trainingmore data, simpler model, regularization, early stopping, better features
generalization error ≈ bias² + variance + irreducible noise noise = the part of y not predictable from X at all. No model beats it. Stop trying.
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 (L2):   L + λ Σ wᵢ²     Lasso (L1):   L + λ Σ |wᵢ|     ElasticNet: both λ (alpha) = how hard you punish big weights. λ = 0 → plain regression.
  • 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?
All else equal, a one-unit increase in that feature changes the prediction by 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?
Bias = error from being too simple to capture the pattern. Variance = error from being so flexible that it chases the noise in whichever sample it trained on. Simple models: high bias, low variance. Complex models: the reverse. You pick the point where their sum is smallest on unseen data — which you can only estimate with held-out data.
When would you pick lasso over ridge?
When you want sparsity — a small, interpretable set of features — and believe most of the 100 columns are irrelevant. Ridge when many features all carry weak signal, or when features are highly correlated (lasso picks one arbitrarily among them).
✏️ Exercise · 35 min
  1. Run the manual gradient descent and deliberately break it: set eta = 5.0 and watch the weights explode. Then eta = 0.000001 and watch it barely move. Report the values where it just works.
  2. 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.
  3. Add 10 pure-noise columns. Which model's val score degrades least? Why?
Track B · Core models

06 · Logistic regression & classification

⏱ 60 min📎 Full code

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:

p(y=1 | x) = σ(w·x + b) = 1 / (1 + e−(w·x+b)) predict class 1 when p ≥ threshold (0.5 by default — but that's a choice, not a law)

Training maximizes likelihood — "make the probability of the observed labels as large as possible". That's equivalent to minimizing the log loss (cross-entropy):

L = − (1/n) Σ [ yᵢ log(pᵢ) + (1 − yᵢ) log(1 − pᵢ) ] confident and wrong → log(0) → infinite penalty. That's the point.
Why not MSE here?

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

precision = TP/(TP+FP)  ·  recall = TP/(TP+FN)  ·  F1 = 2·P·R/(P+R)  ·  specificity = TN/(TN+FP) precision asks "when I flag, am I right?" · recall asks "did I catch them all?"
If the expensive mistake is…You want high…Example
Missing a real case (FN)Recallcancer screening, fraud detection, churn intervention
Wrongly flagging (FP)Precisionspam that eats real mail, blocking a good customer, labelling data for human review
Both, and errors are equal (F1)F1 / PR-AUCimbalanced binary problems with symmetric cost
You rank and only show top-kRecall@k / NDCGsearch results, lead scoring, recommender
Probabilities feed a cost modelLog loss / Brierinsurance 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
Threshold thinking

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?
Spam → precision (a lost important email is worse than seeing spam). Fraud review queue → precision-ish/F1 (analyst time is finite; you want the flagged pile to be mostly real) — though if chargeback loss dominates, flip to recall. Pedestrians → recall, overwhelmingly (missing one is catastrophic; false brakes are annoying). Always name the cost, then the metric.
The model has 0.95 ROC-AUC but only 0.30 precision at threshold 0.5. Explain.
AUC is about ranking: positives mostly rank above negatives — the ordering is good. Precision at 0.5 is about the specific cut: if the model rarely outputs confident probabilities (or the base rate is very low), many good-ranking positives fall below 0.5 while plenty of negatives sit above it. Fixes: lower the threshold, recalibrate (Platt/isotonic), or accept that ranking + top-k is the real use.
What is calibration and when do you care?
Calibration = among all cases where the model says "70% chance", about 70% actually are. You care whenever the number drives an action (expected value, pricing, triage ranking by risk). You don't care much when you only need a class label. Check with a reliability diagram; fix with calibration scaling on a validation set.
✏️ Exercise · 40 min
  1. 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?
  2. 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?
  3. Explain to a rubber duck: why can a model have great AUC and bad precision?
Track B · Core models

07 · Trees, forests & boosting

⏱ 60 min📎 Full code🏆 Winner on most tabular data

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

All rows→ Pick feature + cutmax purity gain→ Left / right branches→ Repeat to depth→ Leaf = mean (regression) or majority vote (classification)
Gini(t) = 1 − Σ pᵢ²   |   Entropy(t) = − Σ pᵢ log₂ pᵢ 0 = pure (all one class). Split = pick the cut that reduces weighted impurity most.
  • 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.

Out-of-bag = free validation

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.

  1. Predict the baseline (mean / prior).
  2. Compute errors (residuals or loss gradients).
  3. Fit a small tree (depth 2–6) to those errors.
  4. Add it: pred += lr × tree(x). Repeat 100–3000 times.
  5. Stop early when validation loss stops improving (early stopping — the key defense against overfitting).
HyperparameterRoleTypical search
n_estimatorshow many trees (with early stopping, just set it high)100 – 3000
learning_ratestep size of each tree's contribution0.01 – 0.3 (smaller + more trees usually wins)
max_depthtree complexity3 – 8 (boosting likes shallow trees)
subsample, colsample_bytreerow/feature randomness → regularization0.6 – 1.0
min_child_weight, reg_alpha/lambdaextra regularizationlog 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))
Tree-specific traps

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.

Which model first?

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.
Bagging: train many models independently on different data samples, in parallel, then average — reduces variance (random forest). Boosting: train sequentially, each model fixing the previous one's mistakes, small steps — reduces bias, careful about variance. Averaging is democracy; boosting is tutoring.
Why do boosting trees stay shallow while forest trees go deep?
They have different jobs. A deep forest tree overfits alone, but averaging many of them cancels the noise — so depth is a feature. Boosting's trees are weak on purpose: each is a small correction, and stacking hundreds of them approximates a complex function gradually. Deep boosting trees would let step 1 overfit the residuals and derail everything.
Why can't a decision tree extrapolate?
Its output is always the average (or majority) of training targets in a leaf. Outside the training range it still routes to the extreme leaf and predicts that leaf's constant — it has no notion of a continuing slope. Linear models extrapolate; trees interpolate. For time series with trends, that matters.
✏️ Exercise · 45 min
  1. 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?
  2. Sweep max_depth for the forest (2 → 40) and plot train vs validation AUC. Find the overfit point.
  3. Set learning_rate high (0.5) vs low (0.03) with early stopping. Compare best iteration counts and validation scores.
  4. Compare impurity importance vs permutation importance. Any feature that swaps places? Why?
Track B · Core models

08 · Distance & probabilistic models

⏱ 35 min📎 Light code

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

predict(x) = majority vote of the k closest training points (by distance) "Learning" is just memorizing the dataset. All the work happens at prediction time.
  • 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.

KnobSmall valueLarge value
C (tolerance for mistakes)soft margin, smoother boundary, may underfithard margin, fits every point, may overfit
gamma (RBF kernel reach)each point influences far away → smoothonly 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

predict = argmaxc P(c) · Πi P(xᵢ | c) assume features are independent given the class — almost never true, usually harmless
  • 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

ModelReads asLovesHates
KNNanalogylow-dim, meaningful distance, small datamany irrelevant features, large n
Logistic / linear SVMweighted evidencelinear signal, text, huge n, interpretabilitycomplex interactions without feature work
RBF SVMflexible boundarymedium n, high dim, clean marginsbig n, needs scaling + tuning
Naive Bayesindependent probabilitiestext, tiny data, quick baselinesstrongly correlated features
Random forestmajority vote of rulesmixed tabular, robustness, little preprocessingextrapolation, very smooth functions
Gradient boostingsequential correctiontabular accuracy, structured dataunstructured data, heavy tuning
Neural netlearned representationimages, text, audio, sequences, huge datasmall 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?
KNN defines the model with distance — unscaled features let the largest-range feature dominate every nearest-neighbour query. Trees make threshold splits per feature ("income > 50k?"), and a monotone rescaling doesn't change where the best cut is. Trees are scale-invariant; distance and gradient-based models are not.
What exactly is the kernel trick?
A function K(x, x') that equals the dot product of x and x' after some implicit high-dimensional transformation — computed without ever doing the transformation. You get non-linear boundaries at the cost of a kernel matrix, not of infinite-dimensional vectors.
Why does Naive Bayes still work when its assumption is false?
Because it's used as a ranker: it only needs P(class|features) to be ordered correctly, and errors from double-counting correlated features tend to cancel across classes. The probabilities come out miscalibrated, but the argmax is usually right.
✏️ Exercise · 25 min
  • Train KNN without scaling and with scaling. Report the accuracy gap.
  • Sweep k from 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.
Track B · Core models

09 · Unsupervised learning

⏱ 45 min📎 Full code

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

  1. Pick k initial centroids (randomly — or use k-means++ for smarter starts).
  2. Assign every point to its nearest centroid.
  3. Recompute each centroid as the mean of its members.
  4. Repeat 2–3 until assignments stop changing.
  5. Report: within-cluster sum of squares (inertia) — how tight the groups are.
k-means assumptions (they bite)

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

MethodIdeaUse when
DBSCANdensity-based: clusters as connected dense regions, marks noisearbitrary shapes, unknown k, need outlier detection; struggles with varying densities
Hierarchicalbuild a tree of merges, cut at any leveltaxonomy (subcategories), small n, want a dendrogram
Gaussian mixturessoft, probabilistic ellipses instead of hard blobsoverlapping clusters, need membership probabilities
Isolation forestanomalies are easy to isolate with random splitsfraud / intrusion / defect outliers, high dim, no labels
How do you evaluate something with no right answers?

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.

Production angle

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?
Both k-means and PCA are driven by distance and variance. An unscaled feature with a large numeric range dominates Euclidean distances (k-means) and becomes the top principal component (PCA) — purely because of its units. Trees split per feature with thresholds, which is invariant to monotone rescaling.
You clustered 6 customer groups. What do you report to the business?
Not the cluster IDs — the profiles: size of each group, distinguishing features in plain language ("high-spend, low-frequency, mobile-only"), their outcome rates (churn, LTV), and the action each group warrants. Include a caveat about stability: re-ran on last month's data, N of 6 groups reproduced.
✏️ Exercise · 35 min
  1. 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?
  2. PCA to 2 components → scatter plot colored by cluster. Do the groups look real or arbitrary?
  3. Describe each cluster in three business sentences. Give them names.
  4. Check: does cluster membership correlate with an outcome you care about? If not — say out loud whether this analysis was worth doing.
Track B · Core models

10 · Neural networks & deep learning

⏱ 75 min📎 Full code · PyTorch🎯 Concepts > framework syntax

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

neuron = activation( w·x + b )    ReLU(z)=max(0,z) · Sigmoid(z)=1/(1+e⁻ᶻ) · tanh · GELU · softmax (outputs that sum to 1)
  • 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

Layermatrix multiply + bias + activation→ Layerlearn features of features→ Layer→ Output1 value · 1 sigmoid · N softmax→ Loss

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

  1. Forward pass: run x through all layers, record intermediate values, get loss.
  2. Backward pass: chain rule from loss backwards → gradient of loss w.r.t. every weight (∂L/∂w for millions of weights in one sweep).
  3. Update: every weight steps a little downhill: w -= lr × grad.
  4. Repeat over mini-batches for many epochs (one epoch = one full pass over the dataset).
OptimizerIdeaNotes
SGDplain gradient stepgreat generalization, needs a tuned lr
SGD + momentumaverages past gradients → rolls downhill with inertiathe classic workhorse
Adam / AdamWper-parameter adaptive step size + momentumfast default, fewer lr headaches; AdamW adds proper weight decay
Learning rate is THE hyperparameter

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

DataArchitectureCore idea
Flat tableMLP (a couple of dense layers)stacked linear+ReLU — often loses to boosting anyway
ImagesCNN (ConvNet)sliding filters detect edges → textures → parts; weight sharing makes it translation-aware
Sequences (older)RNN / LSTM / GRUread step by step with a memory state — slow to train, mostly superseded
Text, and almost everything nowTransformerself-attention: every token looks at every other token and weighs relevance; parallel, scales beautifully
Categories / IDsEmbeddingslearn a dense vector per item so similar items are nearby (users, words, products)
One model, many domainsPretrained (BERT, GPT, ResNet)fine-tune or prompt instead of training from scratch — always start here before training your own
Attention in one sentence

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)
Two things beginners misread

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.

Production angle: choosing the model is a cost decision

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?
Composition of linear functions is linear: 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.
Loss is a function of the output, which is a function of layer 3's weights, which depend on layer 2's outputs, and so on. The chain rule gives each weight's gradient as a product of local derivatives along the path. One backward sweep propagates those products from the loss to every weight; then each weight steps opposite its own gradient.
When should you NOT use deep learning?
Small tabular datasets (boosting wins, by a lot); when you need fast iteration and little compute; when interpretability is required; when training/serving cost is a constraint; when a simple baseline already meets the target. Use it for images, text, audio, sequences, and any problem where hand-designing features is the bottleneck — or when you can leverage a pretrained model.
✏️ Exercise · 60 min
  1. 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.
  2. Plot loss curves for train and val. Circle the epoch where overfitting starts and state the early-stopping rule you'd use.
  3. Swap the data: run the same MLP on a tabular dataset and compare it with HistGradientBoostingClassifier. Note the winner.
  4. One-sentence architecture picks: medical X-rays · customer churn from a CSV · support ticket topic routing · next-word prediction.
Track C · Get better

11 · Features & preprocessing pipelines

⏱ 65 min📎 Full code🏆 Where most real gains live

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 Pipeline so 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

ProblemTechniqueNotes
Missing valuesmean/median (numeric), "missing" category (categorical),
or a model-based imputer (KNNIterative/IterativeImputer)
Missingness can be signal — add a was_missing flag
Categoriesone-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 mismatchStandardScaler (z-score), MinMaxScaler (0–1),
RobustScaler (median/IQR — for heavy outliers)
Required for: NN, SVM, KNN, logistic, PCA, k-means. Not for trees
Skewed distributionslog1p / Box-Cox / Yeo-Johnsonincome, prices, counts — long tails wreck linear models and distance
Outliersclip to quantiles (winsorize), RobustScaler, or let trees handle itDecide based on: real rare event (keep) vs data error (fix)
Datetimehour, day-of-week, month, is_weekend, days_since, cyclical sin/cosNever feed raw timestamps; relative time matters more than absolute
TextTF-IDF (classical), sentence embeddings (transformers)TF-IDF + linear model is a brutally strong text baseline
Geographylat/long, distance to landmarks, region codes, h3 cellsDistance features often beat raw coordinates
IDs / free textdrop themAn ID column lets the model memorize; it helps train, fails test
Feature engineering beats model shopping

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))
± tells you if your gain is real

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

  1. The algorithm: class_weight="balanced" (or scale_pos_weight in XGBoost) — penalizes mistakes on the rare class more. Free, always try first.
  2. 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.
  3. The threshold: train normally, then lower the decision threshold until recall meets your cost target (Module 06). Often the best lever of the three.
Don't reach for SMOTE first

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?
Because those steps compute statistics from data (mean, categories, feature scores). If computed on the whole dataset, held-out rows influence training. Inside a pipeline they're refit within each CV fold automatically — and the same frozen object transforms test data and live traffic.
Name three ways to fight class imbalance and when each applies.
class weights — when you want the model to care more about the rare class without touching the data (first move). resampling — when the dataset is small enough that the rare class barely appears (apply inside CV only). threshold tuning — when probabilities are decent but 0.5 is the wrong cut (often the final step).
Your CV AUC is 0.93, test AUC is 0.71. What went wrong?
Almost certainly leakage or a distribution mismatch between CV and the final split: preprocessing fit globally, target-encoded features computed on all rows, duplicates across folds, time-based data randomly split, or the test set drawn from a later period. Also possible: you tuned so many variants against CV that you overfit CV itself. Rebuild with a pipeline and a group/time-aware split.
✏️ Exercise · 50 min
  1. Take a messy dataset with mixed types and missing values. Build the full ColumnTransformer pipeline above. Print CV mean ± std.
  2. 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.)
  3. Add one engineered feature from your domain intuition. Did the score move more than the model swap you were about to try?
  4. On an imbalanced target, compare: baseline vs class weights vs threshold tuning — report precision, recall, F1 and PR-AUC for each.
Track C · Get better

12 · Tuning, validation & error analysis

⏱ 60 min📎 Full code

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

SchemeHowUse when
K-foldk chunks; train on k−1, test on 1; rotatedefault for i.i.d. data, small datasets
Stratified K-foldk-fold keeping class ratiosimbalanced classification — the default
Group K-foldall rows of one group stay in one foldrepeated users/patients/sessions (avoids entity leakage)
Time-series splitalways train on the past, test on the next blockanything temporal — expanding window
Leave-one-outk = ntiny data only; expensive, high variance
The test set is sacred

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

StrategyCost for p paramsBest for
GridSearchexponential blow-up1–2 params you care about a lot
RandomSearchfixed budget; explores each dim betterdefault choice — covers more space than grid at the same cost
Bayesian (Optuna, hyperopt)samples the promising regionsexpensive models, many params, limited budget
Successive halving / ASHAkills bad configs early with partial trainingbig 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

  1. Get probabilities, not just labels for the whole validation set.
  2. Sort into four buckets: high-confidence correct, high-confidence wrong (the dangerous ones — investigate first), low-confidence correct, low-confidence wrong.
  3. 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.
  4. 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.
  5. 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".
  6. 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))
Experiment hygiene

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?
Collect more data — the curve hasn't plateaued, so data volume is the binding constraint. If both curves are flat and low, more data won't save you: increase model capacity or, better, improve the features. If train is high and validation is low, simplify/regularize.
Why is random search usually better than grid search at the same budget?
Because hyperparameters differ in importance. A grid repeats the same values of the unimportant parameters in every combination and only samples a handful of values of the important one. Random search samples every parameter independently — it covers more distinct values of the parameter that actually matters for the same number of runs.
Your model is 95% accurate but stakeholders are angry. What do you check first?
Slice the errors. Accuracy is an average that hides concentration: one segment, language, device or time window may be failing badly. Also check whether the metric matches the pain (maybe recall on the rare class is terrible while accuracy looks great), and whether labels are right.
✏️ Exercise · 50 min
  1. Run a RandomizedSearchCV with 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.
  2. Plot learning curves for your best model. Write the one-sentence diagnosis and the action.
  3. Produce the error table above. Find your worst slice (n ≥ 30 — small slices lie). Name the feature or data fix you'd try first.
  4. Check 20 misclassified rows against their labels. How many are actually mislabelled?
Track C · Get better

13 · Interpretability & fairness

⏱ 45 min📎 Light code

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

QuestionToolWorks onCaveat
Which features matter overall?Permutation importanceany modelbreaks with correlated features (they mask each other)
Which features matter overall?Impurity/gain importancetreesbiased toward high-cardinality features — prefer permutation or SHAP
How does the output change with feature X?Partial dependence / ICEany modelassumes features are independent (unrealistic); shows averages
Why this individual prediction?SHAP (TreeSHAP for trees)any modelgame-theoretic; needs care with correlated features
Why this prediction? (fast approximation)LIMEany modellocal linear fit; unstable across runs
What rules does the model follow?surrogate tree / rule lists / coefficientsany modelapproximates, may misrepresent
Global vs local

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"
Explanation traps

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

  1. Historical bias: the data reflects past discrimination. A hiring model trained on 10 years of promotions learns who got promoted, not who was good.
  2. Measurement bias: the feature or label is measured differently across groups (better fraud labels for one region; sentiment data noisier for some dialects).
  3. Sampling/aggregation bias: some groups are under-represented → worse performance for them.
  4. 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 definitionInformallyWhen it fits
Demographic parityP(Ŷ=1 | group A) = P(Ŷ=1 | group B)when outcomes themselves must be equally distributed (hiring pipelines)
Equal opportunity (equal TPR)recall equal across groupswhen the positive class is a benefit (loan approval for qualified applicants)
Predictive parityprecision equal across groupswhen the flag is an accusation (recidivism risk)
Calibration per group"70%" means 70% in every groupwhen 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.

The practical fairness workflow

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?
The change in this prediction relative to a baseline (the average output), attributed to feature j considering all possible feature coalitions — i.e. "feature j pushed this prediction this much up or down from the average". Positive → pushed toward the predicted class; negative → away. It's an attribution, not causation.
Give two sources of bias and one concrete mitigation each.
Historical: labels encode past decisions → use a better outcome label where possible, collect fresh data, or reweight underrepresented groups. Measurement: label quality differs by group → audit label accuracy per slice, fix the collection process. Sampling: → targeted data collection / stratified sampling.
Why can't we satisfy demographic parity and equal opportunity simultaneously?
If base rates (the true positive rate in the population) differ between groups, then requiring equal selection rates and equal recall forces precision to differ — the math doesn't close. One of the constraints must give, and choosing which is a societal trade-off, not an engineering default.
✏️ Exercise · 40 min
  1. 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.
  2. 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.
  3. Compute recall per slice for any attribute you have (region, language, device). Report the worst gap in percentage points.
  4. Write a half-page model card: intended use, not-intended use, training data, primary metric, per-slice performance, known failure modes.
Track D · Production

14 · From model to product

⏱ 50 min📎 No code · systems thinking

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

Product decisionwhat changes if we're right→ Data sourcesevents, warehouse, 3rd party→ Feature pipelineclean + join + aggregate→ Training pipelineversioned, reproducible→ Registryapproved artifacts→ ServingAPI / batch / stream→ Experiencewho sees what, and when→ Feedbackoutcomes → new labels

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

value = Σ (decision outcomes) − (build + run + wrong-decision costs) write this in currency before writing a line of code

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.
Model metric ≠ business metric

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

TierExampleProcess
Lowinternal triage, nice-to-have rankingNotebook → ship. Manual monitoring. Rollback is cheap
Mediumcustomer-facing recommendation, marketing scoresTests, staged release, dashboards, owner on call
Highcredit, insurance, health, safety, legal decisionsFormal review, fairness & explainability requirements, shadow mode first, audit trail, human approval, regulatory sign-off

Ship a boring MVP first

  1. Heuristic v0: the rule a smart person would write today. Ship it, measure it. This is your baseline and it takes a day.
  2. Model v1 in shadow: run the model in production, log predictions, nobody sees them. Compare against outcomes and the heuristic for weeks. Free risk.
  3. Model v2 assisted: show scores to humans alongside their own judgement; measure whether they get faster/better.
  4. Model v3 automated for the low-risk slice only, with monitoring and rollback.
  5. Expand coverage as evidence accumulates.
Production questions to ask on day one

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

DocumentContentsAudience
Framing docproblem, decision, metric, baseline, cost model, success criteriastakeholders, yourself
Model cardarchitecture, training data, metrics per slice, intended use, limits, fairnessreviewers, customers, regulators
Runbookhow to deploy, monitor, retrain, and what to do when alerts fireon-call engineers
Decision logwhy threshold 0.35, why drop feature X — one line each, datedfuture you
Why start with shadow mode?
Because it decouples measuring the model from acting on it: you collect real production inputs and real outcomes with zero user-facing risk, discover data/serving bugs that no offline test can reveal, and build the evidence you need to justify the next step.
Give the chain from AUC to dollars for a fraud model.
ΔAUC → changes ranking at the review threshold → changes #transactions flagged (and the precision at that point) → changes analyst hours used and chargebacks caught → (caught fraud $ − review cost − blocked-legitimate-customer cost) = net $/month. If any link is missing, the AUC gain has no value you can claim.
What are the most common production ML failures?
Training/serving skew (feature computed differently at serve time), upstream schema/data-quality breakage, feedback loops, silent fallback defaults, no labels so nobody notices decay, monitoring only latency and not data, and — underlying all — no owner. Model quality itself rarely fails.
✏️ Exercise · 45 min

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.
Track D · Production

15 · Data & training pipelines (MLOps)

⏱ 60 min📎 Light code/config

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

WhatTool examplesWhy it bites without it
Codegit + commit SHA stored with the model"which script produced this model?"
DataDVC, lakeFS, immutable snapshots, row-count + hash manifestretraining on data that silently changed yesterday
ConfigYAML/JSON params, hydra, env varshyperparameters living only in your head
Environmentlock files (pip freeze, poetry, uv), Docker imagesworks in 2026, fails in 2027 after a library upgrade
ExperimentsMLflow, W&B, Neptune — params + metrics + artifactscan'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?

StrategyTriggerGood for
Periodicnightly / weekly / monthlystable domains with fresh data arriving on a rhythm (churn, pricing)
Performance-triggereddrift or metric crosses a thresholdvolatile domains — retrain when it actually needs it (Module 18)
Data-triggerednew labelled batch arriveswhen labels are the bottleneck
On-demandhuman decides, after reviewhigh-risk models where promotion needs sign-off
Online/continualevery batch/stream itemvery 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

StageWhat runsGate
PR / commitunit tests, lint, data schema tests (Great Expectations / Pandera), feature-parity testsmust pass
Nightly / on mergefull training on new data, evaluation vs baseline, drift reportmetric ≥ baseline − tolerance; no data-quality regressions
Deploy to stagingAPI contract tests, latency benchmark, load test, explainability/fairness reportSLOs met
Productionshadow → canary (5%) → ramp, with automatic rollback on alertdashboards healthy for N hours
Tests that matter for ML

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.

Maturity ladder — don't skip rungs

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?
Skew = the feature values (or their distribution) differ between training and inference because the two paths are computed differently or at different times. Prevent with: shared feature code/feature store, point-in-time joins for training, schema and parity tests, and logging live inputs so you can compare distributions offline vs online.
Why keep a data version and a git SHA with every model?
Because a model is a function of (code + data + config + environment). Any one of them changing invalidates the result. With all four recorded you can reproduce a past model exactly, debug a regression by diffing, and answer auditors' "what produced this decision?" question.
Notebook vs pipeline: what belongs where?
Notebooks: exploration, visualizations, error analysis, one-off checks. Pipelines: ingestion, validation, feature building, training, evaluation, registration — anything that must run on a schedule, unattended, with tests. The anti-pattern is a "pipeline" that's a notebook with Restart & Run All every night, held together by a cron job and hope.
✏️ Exercise · 50 min
  1. Convert your notebook from Module 11 into train.py: functions for load → validate → features → train → evaluate → save, driven by a YAML config.
  2. Log three experiments (vary one hyperparameter each) to MLflow or a CSV — anything with params + metrics + timestamp.
  3. Write three tests: schema check, "golden row produces these features", "model beats baseline".
  4. Add a quality gate: exit non-zero if the CV score drops below your threshold. Run it twice and see it pass.
Track D · Production

16 · Deployment patterns & scale

⏱ 55 min📎 Light code

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

PatternHowLatencyExampleWatch out
Batchnightly job scores everyone; results written to a DBhours (fine)churn lists, lead scoring, document ranking, recommendations refreshed dailylate/failed job = stale scores for everyone; no per-request freshness
Online (sync API)request → model → response inside the user's requestms–100s msfraud check at checkout, search ranking, autocompletemodel outage = request outage; need timeouts, fallbacks, p99 budget
Streamingevent bus → trigger → score → sink~secondsreal-time personalization, sensor anomaly, clickstream featuresbackpressure, ordering, exactly-once semantics
Edge / on-devicemodel runs in the app or device<10 ms, works offlinekeyboard suggestions, camera filters, sensor QC, privacy-sensitive scoringmodel size, battery, version drift across devices, no central rollback
Default to the slowest pattern you can afford

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

FormatWhatWhen
joblib/pickleserialized Python objectscikit-learn inside your own service — simplest, but Python-only and unsafe to load from strangers
ONNXportable graph formatcross-language/edge serving, one artifact across frameworks, optimized runtimes (ONNX Runtime, TensorRT)
Framework nativestate_dict (PyTorch), SavedModel/H5 (Keras)default within its own ecosystem
Whole pipelinepreprocessing + model as ONE artifactalways, if you can — kills skew by construction
ContainerDocker image with model + code + depsreproducible 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

shadowscore, don't show→ A/Bsplit traffic, measure outcome→ canary5% → 25% → 100%→ full↺ rollbackautomatic on SLO breach
  • 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.
Failure modes to design for on day one

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.
Marketing segments → batch (a day-old score is fine, millions of rows, cheap). Fraud at checkout → online (the answer must exist before the payment completes). Typo correction → edge (needs <10ms, works offline, and keystrokes shouldn't leave the device).
Why is "whole pipeline as one artifact" important?
Because preprocessing and model are one fitted object: the exact scalers, imputers, categories and feature order used in training travel with the model. It removes an entire class of bugs where the service reimplements transforms and gets subtly different values — training/serving skew.
Canary vs A/B — what does each measure?
Canary measures operational safety: does the new version stay healthy (errors, latency, resource use, drift) on a small slice? A/B measures user/business impact: does it change the outcome metric significantly? Do canary first (don't show users a broken service), then A/B for the decision.
✏️ Exercise · 40 min
  1. For your capstone: write the serving design — pattern, expected request volume, latency budget, breakdown of where the ms go, fallback behavior, and rollback plan.
  2. Estimate monthly cost: requests/day × inference ms × instance price. Then apply caching and cascading — how much does it drop?
  3. Design the rollout: shadow for how long, on what metric do you promote; canary percentages; the exact alert that triggers automatic rollback.
Track D · Production

17 · Serving lab: FastAPI + Docker

⏱ 90 min📎 Full code · hands-on lab🏆 This one you run

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

churn-service/
├── 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
Design notes you'll be asked about

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.

What you'd add for real traffic

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.

✏️ Exercise · 90 min · the milestone
  1. Run train.py on any dataset of yours and get model.joblib.
  2. Start the API with uvicorn app:app --reload; test in /docs and with curl.
  3. Run the test suite: pip install pytest httpx && pytest -q.
  4. Build the Docker image, run it, confirm curl localhost:8000/health.
  5. Break it on purpose: send a wrong-typed field, a negative tenure, and a missing key. Confirm each gives a 422, not a 500.
  6. 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.
Track D · Production

18 · Monitoring, drift & retraining

⏱ 60 min📎 Light code

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

DriftWhat changedSignalExample
Data drift (covariate shift)inputs P(X) changed; P(y|X) sameinput distributions movea new contract type appears; traffic skews mobile after a campaign
Concept driftthe relationship P(y|X) changedperformance drops once labels arrivea recession makes old churn drivers irrelevant; a new fraud technique
Performance driftyour metric felllabels + evaluation — the ground truth signal, but delayedrecall 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

  1. Service health (immediately): request rate, error rate, p50/p95/p99 latency, uptime, saturation. These page you on day one.
  2. Data quality: null %, missing features, unexpected categories, out-of-range values, row counts, schema changes, default-value spikes.
  3. 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.
  4. 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.
  5. 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))
Labels are the bottleneck of monitoring

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

TriggerResponse
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 thresholdPage owner; consider rollback to previous model while investigating
New labelled batchRetrain + evaluate; often the cheapest correct trigger
After an incident / upstream changeManual 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).
Feedback loops — the slow-motion failure

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)

  1. Confirm: is it the model or the service? Check error rate and latency first — most "model" incidents are infrastructure.
  2. Rollback if users are affected: point traffic at the previous model version. Diagnosis happens after the bleeding stops.
  3. Check the data path: upstream job ran? schema changed? null rate spiked? feature values sane? (Fixes 80% of cases.)
  4. Compare distributions: today's inputs vs training reference (the PSI report) and vs yesterday.
  5. Slice: is the failure everywhere or in one segment/region/time window?
  6. 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.
Data drift: input distribution changes while the world's rules stay — e.g. a marketing campaign brings younger users. Detected by input-distribution monitoring (PSI/KS) without labels. Concept drift: the input→outcome relationship changes — e.g. a competitor launches, so "long tenure" stops protecting against churn. Detected by rolling performance once labels arrive (or predicted vs actual divergence).
PSI 0.19 on feature "tenure". What do you do?
Investigate, don't blindly retrain. Check: is it a data bug (bad join, nulls filled with 0, unit change)? An upstream/schema change? A genuine, expected shift (new campaign, new product mix)? If real and material, retrain with recent data and compare against the incumbent; if a bug, fix the pipeline — retraining on garbage teaches the model the garbage.
What do you log at inference time to make future monitoring possible?
Timestamp, request/trace id, model version, input features (or a safe hash + key aggregates, mindful of PII), predicted probability/class, threshold used, latency, and any fallback that fired — plus enough to join later with the ground-truth outcome when it arrives.
✏️ Exercise · 55 min
  1. Write drift_report() above and run it against two different slices of real data (e.g. January vs July). Print the top movers.
  2. 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).
  3. Design the monitoring page for your capstone: five charts (one per layer), each with the threshold and the alert's runbook link.
  4. Write your retraining policy in three sentences: trigger, gate, promotion rule.
  5. Draft the incident runbook for "scores look wrong today" — 6 steps, each with a command or a dashboard to open.
Track E · Apply

19 · Capstone ladder & portfolio

⏱ 45 min plan · weeks of work after🎯 From notebook to deployed

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

RungProjectYou must showTime
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
The bar for "done" at every rung

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)
How to write about it (resume / interview)

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.

✏️ Exercise · 30 min · then go build
  1. Choose your rung-1 dataset and write the framing doc (Module 14 format) — before opening a notebook.
  2. 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.
  3. Calendar-block the work: rung 1 date, rung 2 date. Tell someone the dates.
  4. Write your definition of done for rung 1 as a checklist in the README. Tick it only when it's true.
Track E · Apply

20 · 4-week plan, recall deck & final checklist

⏱ 40 min📎 Schedule · 24 flashcards · checklists

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)

WeekModulesDeliverable
1 · Remember00 diagnostic · 01 · 02 · 03 Diagnostic written; baseline notebook run end-to-end; 5 problems triaged GO/NO-GO
2 · Core models04 · 05 · 06 · 07 · (08–09 skim) · (10 if needed) Rung-1 capstone: complete analysis with CV, error analysis and conclusions
3 · Get better11 · 12 · 13 Pipeline rebuild of rung 1: no leakage, tuned with proper CV, SHAP/slice analysis, model card draft
4 · Production14 · 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

WhenWhat to doTime
Next dayRe-answer yesterday's recall cards from memory (no peeking)10 min
Day 3Re-answer; anything you miss goes back to the top of the pile15 min
Day 7Deck + retake the Module 00 diagnostic25 min
Day 14Deck + rebuild one thing from scratch without notes (a pipeline, an API)45 min
Day 30Full deck + explain 3 random modules aloud as if teaching60 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?
A defined task, a computable success measure, and experience/data that lets the system improve — plus a decision the prediction will actually change.
2 · Train / validation / test — one line each.
Train: fit parameters. Validation: choose models and hyperparameters. Test: one final, untouched estimate of generalization.
3 · What is data leakage, and name two ways it sneaks in.
Using information unavailable at prediction time. Examples: a target-derived feature; preprocessing fit before splitting; time-based data split randomly; duplicate entities across the split.
4 · Gradient descent in one sentence. What does the learning rate do?
Repeatedly nudge each weight opposite the gradient of the loss. The learning rate sets the step size: too big → oscillation/divergence; too small → glacial training.
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?
Spam → precision (lost mail is costly). Fraud detection → recall (catching matters; review capacity handles flags). Screening → recall for the first pass (don't drop good candidates), then precision in later stages.
7 · Why is accuracy misleading at a 1% positive rate?
The trivial all-negative classifier scores 99% while catching nothing. Use confusion matrix, precision/recall/F1, PR-AUC — evaluated at your chosen threshold.
8 · Bias vs variance, and the two diagnostic numbers.
Bias = too simple (train and validation both poor). Variance = fits noise (train excellent, validation much worse). Diagnose from train score, validation score, and the gap between them.
9 · Ridge vs lasso: the difference and the use case.
Ridge (L2) shrinks all coefficients, keeping them. Lasso (L1) pushes some to exactly zero → feature selection. Ridge when many weak signals; lasso when few features matter.
10 · What does the sigmoid output that a linear model doesn't?
A value strictly in (0, 1), interpretable as a probability. Combined with log loss it gives a convex, well-behaved training problem with useful gradients.
11 · Explain bagging vs boosting in one breath.
Bagging: many independent models on different samples, averaged → reduces variance (random forest). Boosting: sequential models each fixing the previous one's errors, small steps → reduces bias.
12 · Why do boosting trees stay shallow?
Each tree is a small correction; stacking hundreds of weak learners gradually builds the fit. Deep trees would overfit the residuals early and poison everything after. Control it with depth, learning rate and early stopping.
13 · Three things that make a distance-based model fail.
Unscaled features; irrelevant/redundant features inflating dimensions (curse of dimensionality); and a distance metric that doesn't match the problem's notion of similarity.
14 · Why scale before PCA/k-means but not before trees?
PCA and k-means depend on variance and Euclidean distance, which are scale-sensitive. Trees split per feature with thresholds, which is invariant to monotone rescaling.
15 · You have 50 labelled images per class. Neural net or something else?
Not a net trained from scratch: use a pretrained model and fine-tune it (transfer learning), or classical models on frozen embeddings. Training deep nets on 50×N examples will memorize.
16 · What breaks if you remove all activations from an MLP?
The whole stack collapses to a single linear function (a composition of linears is linear) — it can only draw a straight boundary, no matter how deep.
17 · Validation loss rises while training loss falls. Three fixes.
Overfitting. Options: early stopping; regularization (dropout, weight decay); a simpler/smaller model; more data or augmentation; better features.
18 · What must a Pipeline guarantee, and why does it matter at serving time?
That every data-dependent transform is fit only on training folds and applied identically to new data. At serving, the same fitted object computes features → no leakage, no train/serve skew, one artifact to version.
19 · Why random search over grid search?
Parameters differ in importance; random search samples each independently and covers more distinct values of the important ones for the same budget, instead of repeating unimportant values across a grid.
20 · Name the five things you must version for reproducibility.
Code (git SHA), data (snapshot/hash), config (hyperparameters), environment (lock file/Docker), and experiments (params + metrics, MLflow/W&B).
21 · Batch vs online serving — the deciding question.
"How stale can the prediction be?" Seconds matter → online (fraud at checkout). Minutes-to-hours fine → batch (nightly churn list). Needs no network / sub-10ms → edge (on-device).
22 · What is training/serving skew and how do you test for it?
Live features differing from the ones the model was trained on (different code, joins, time windows, defaults). Test with feature-parity tests: feed a known row through both paths and assert identical values; compare online vs offline input distributions.
23 · Data drift vs concept drift: detection method for each.
Data drift: input distributions shift → detect with PSI/KS on features, no labels needed. Concept drift: P(y|X) changes → detect via rolling performance once outcomes arrive (or rising prediction error), with output-score shifts as an early hint.
24 · Your model's metric dropped 8 points overnight. First five minutes?
1) Is it the service? (errors, latency, traffic) 2) If users are affected → roll back to the previous version. 3) Check the data path: upstream job, schema, null rates, default spikes. 4) Compare today's input distributions vs training reference (PSI) and vs yesterday. 5) Slice the failures — everywhere or one segment/time window? Then postmortem.

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 depthDesigning Machine Learning Systems (Huyen), Building ML Powered Applications (Auerbach), Made With ML
LLM / modern architecture directionfast.ai course, Hugging Face course, nanoGPT from scratch, then build a RAG system with real evals
Career signalThe four-rung capstone ladder, written up well, beats ten course certificates
To stay sharpThe recall deck on schedule + one small model per month + one public write-up of a project
The three habits that outlast the content

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.

✏️ Final exercise · 40 min
  1. Retake the Module 00 diagnostic cold. Compare: write down which items you still can't answer.
  2. Say the 24 deck answers out loud. Mark the ones you faked — those are your review queue.
  3. Read the pre-production checklist against your current project. Fix the first unchecked item today.
  4. 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.
  5. Go do Rung 1. Everything else in this notebook is scaffolding for that.