learn-classification-regression-with-phoebe / Session 3 of 10
Learn Classification & Regression with Phoebe · Session 3 of 10

Logistic regression, from the inside

In intro-ml, LogisticRegression was a black box you fit, scored, and trusted. Today we take the lid off. You will see how a plain linear score becomes a probability (the sigmoid), climb the ladder from probability to odds to log-odds - the scale where logistic really is linear - and then do the most satisfying thing in this course so far: fit the model on Lumen, exponentiate its coefficients, and watch it recover, almost digit for digit, the machine that generated the data. By the end, "email has an odds ratio of about 4" is a sentence you can say, defend, and translate for a stakeholder.

🟡 Core zoo Builders Python · scikit-learn 45 min
0-3 · Setup 3-18 · Sigmoid, odds, coefficients 18-40 · Build: read the machine 40-45 · Wrap
Part 0

The box you have been trusting

You have fit this model at least four times across the courses - and never once asked what the numbers inside it mean. Fair: the workflow had to come first. But logistic regression is the single most-deployed classifier in industry precisely because its insides are readable - every coefficient is a claim about the world that you can state, check, and challenge. A model you can read is a model your stakeholders can trust, and today it becomes yours.

Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today The ability to read any logistic coefficient as an odds ratio and explain it to a stakeholder without hand-waving: email multiplies a session's conversion odds by ~4.1, a returning customer's by ~2.8 - and the discipline to say "odds", not "probability", when it matters. Plus one genuinely magical moment: your fitted model recovering the exact coefficients the Lumen generator used to create the data.
Part 1 · the squash

From line to probability 7 min live

Under the hood, logistic regression is session 2's model wearing a disguise. It computes the same weighted sum - z = w·x + b - a plain score that can be any number from minus infinity to plus infinity. Then one function, the sigmoid, squashes that score into (0, 1) so it can be read as a probability. Big positive z, probability near 1. Big negative z, near 0. z exactly 0, probability exactly 0.5 - and that is where the model changes its mind.

The sigmoid - any score in, a probability out z = w·x + b p = sigma(z) -6 0 +6 0 0.5 1 z = 0, p = 0.5 the decision boundary z below 0: predict no z above 0: predict convert sigma(z) = 1 / (1 + e^-z) The boundary is where z = 0: in feature space that equation draws a straight line - one calm seam.
🔍 Click to zoom - the sigmoid and the z = 0 decision boundary
LiveThe sigmoid - what it does and why it is the right squash3 min▶

sigma(z) = 1 / (1 + e^-z). Three properties earn it the job. It is monotonic: a higher score always means a higher probability, so the linear ranking survives the squash. It is saturating: past z of about plus or minus 4 it flattens near 1 or 0, so absurd scores cannot produce probabilities above 1 or below 0. And it is symmetric around z = 0: sigma(0) = 0.5 exactly, which is what makes "score positive or negative" and "probability above or below a half" the same question.

So the model is two moving parts, not one mysterious blob: a linear scorer (all of session 2's machinery - weights, features, an intercept) and a fixed, parameter-free squash. Everything the model learns lives in the line. The sigmoid never changes.

LiveThe decision boundary is a line - see it on real Lumen data4 min▶

Where does the model flip from "no" to "convert"? Where p = 0.5, which is where z = 0, which is the equation w·x + b = 0 - and in feature space that is a straight line (a flat plane in more dimensions). This is session 1's boundary explorer coming home: of the four boundary signatures you met, logistic regression owns the calm straight seam.

Same explorer, now locked to logistic on the real sample - 2 features (spend x pages, standardized), balanced converters vs non-converters. Notice how the seam stays a straight, unbothered line at ~59% test accuracy: two features of real checkout data carry a weak, honest signal. Real data whispers.

Do not panic at 59%: that is 2 features on a deliberately balanced sample - a geometry lesson, not the production model. The full 7-feature model later this session reaches ROC-AUC ~0.69 on the real 3%-conversion data.
Part 2 · the ladder

Odds, log-odds, and the coefficient ladder 9 min live

To read the coefficients you need one ladder with three rungs. Probability: the chance of converting, lives in (0, 1). Odds: p / (1 - p), the ratio of converting to not converting, lives in (0, infinity). Log-odds: ln(odds), lives on the whole number line - and this is the rung where logistic regression is literally linear: z IS the log-odds. Every coefficient is a statement on this third rung, and one exp() climbs it back down to something a human can say.

One Lumen session, three rungs of the same ladder Probability p = 0.03 lives in (0, 1) 3 in 100 sessions buy p / (1 - p) Odds 0.031 lives in (0, infinity) 0.031 buys per pass ln(odds) Log-odds -3.5 lives in (-inf, +inf) where logistic IS linear to climb back down: sigmoid(log-odds) returns the probability Coefficients live on the log-odds rung: +1 unit of a feature adds its coefficient to the log-odds.
🔍 Click to zoom - probability to odds to log-odds, and back down via the sigmoid
LiveCoefficients are log-odds bumps; exp(coef) is the odds ratio4 min▶

Because z is the log-odds, each coefficient says: "one unit of this feature adds this much to the log-odds". Adding on a log scale is multiplying on the raw scale - so exp(coef) tells you what one unit of the feature multiplies the odds by. That multiplier is the odds ratio (OR), and it is the currency logistic models are read in. Here is Lumen's near-unregularized fit, dummies vs the affiliate reference channel:

Featureexp(coef) = odds ratioRead it as
channel = email~4.1email sessions carry ~4.1x the conversion odds of affiliate
channel = direct~3.2~3.2x the odds of affiliate
returning customer~2.8~2.8x the odds of a new visitor, all else equal
channel = sms~2.7~2.7x the odds of affiliate
pages_viewed~1.13each extra page multiplies the odds by ~1.13
prior_30d_spend~1.8 per $100coef +0.006 per $1; exp(0.6) is ~1.8 per $100
session_hour~1.00multiplies the odds by ~1 - the planted noise feature, correctly ignored
interceptlog-odds ~ -5.9the reference session's starting point, deep in "no" territory

Notice the model's honesty: it found the strong channels, priced spend and pages sensibly, and gave the decoy feature a multiplier of one. You will verify all of this against the generator's true coefficients in the build.

LiveThe hard rule: an odds ratio is NOT a probability ratio2 min▶

"Returning customers have OR ~2.8" does not mean "returning customers are 2.8x as likely to convert". Odds and probability are different rungs of the ladder, and the ratio of one is not the ratio of the other - the gap widens as probabilities grow. At Lumen's ~3% base rate the two happen to be numerically close, which is exactly why sloppy phrasing survives unchallenged - until someone applies it at a 40% base rate and the claim becomes mathematically impossible.

The discipline: say "2.8x the odds", never "2.8x as likely". If a stakeholder needs probabilities, compute the actual predicted probabilities for two concrete scenarios and show those - it takes one extra line of code and zero asterisks.
Self-studysklearn regularizes by default - the C trap3 min▶

Surprise for statisticians: LogisticRegression() is regularized out of the box - L2 penalty, C=1, where C is the inverse regularization strength (small C = strong penalty; the reciprocal of the alpha convention from session 2's ridge card). Great default for prediction; quietly biased for interpretation, because the penalty shrinks every coefficient toward zero and your odds ratios come out closer to 1 than the data says.

The convention this course uses: for clean, readable odds ratios, fit near-unregularized with C=1e6 - the statisticians' habit. For pure prediction pipelines, keep the default and tune C by cross-validation - the ML habit. Both are correct; they answer different questions. This is sklearn section 1.1.11's territory - read the whole section once, it is short and saves careers.

Self-studySolvers in one card - lbfgs, saga, liblinear2 min▶

lbfgs is the default and the right answer most days: fast, handles L2, no drama. saga is the one to reach for when you need L1 or elastic-net penalties or have very large data - but it converges properly only on scaled features. liblinear is the old workhorse with a documented quirk worth knowing: it penalizes the intercept along with the weights, which subtly shifts fitted values - avoid it when you plan to read coefficients. If you ever see a convergence warning, raise max_iter first, check scaling second, switch solvers third.

Part 3 · the loss

The loss: why not MSE? 6 min live

Session 2's model minimized squared error, so the obvious move is to reuse it: squash predictions through the sigmoid, measure squared distance to the 0/1 labels, minimize. Obvious - and wrong. Push MSE through a sigmoid and the loss surface stops being a clean bowl: it turns non-convex, a bumpy landscape of flats and dents where an optimizer can settle into a local dip and stall, far from the best weights. Logistic regression instead uses log-loss, which comes straight from maximum likelihood: choose the weights under which the observed labels were most probable. That loss is convex - one bowl, one bottom, and any decent optimizer finds it from anywhere.

LiveConfidently wrong is expensive3 min▶

Log-loss has a personality: it charges for wrongness in proportion to confidence. Predict p = 0.6 for a session that does not convert and you pay a modest fee. Predict p = 0.99 for that same session and the penalty is brutal - the loss grows without bound as your confidence approaches certainty. A model trained on log-loss learns not just to be right, but to be calibrated: to reserve its 0.99s for cases that deserve them.

That is exactly the incentive you want in a conversion model - a scorer that shouts only when it should. Compare naive Bayes in session 6, whose probabilities you will learn not to trust.

Real worldA churn team once ranked customers by a model that was accurate but badly calibrated - its 0.95s were really 0.6s. Retention offers went to customers who were never leaving, at 0.95-level spend. Log-loss trained models, calibration checked, the same budget saved twice the churners the next quarter. Confidence is a product feature; log-loss is how it gets built.
Cliffhanger for session 4: unlike least squares, log-loss has no closed-form solution - no formula spits out the best weights. Something has to walk downhill along the loss surface, step by step. That something is gradient descent, and next session you build it from scratch in 15 lines and match sklearn to two decimals.
Build-along

Read the machine that made the data ★ 18 min · everyone builds

This build has a punchline no real dataset can offer: because Lumen is generated, we KNOW the true machine. The generator computes z = -5.13 + 0.95*returning + 0.006*spend + 0.11*pages + channel lifts, then rolls the dice. Your job: fit a logistic model that has never seen those numbers, and watch it find them.

Fit in statistician mode. get_dummies(drop_first=True), unscaled features, LogisticRegression(C=1e6, max_iter=8000) on the train split - near-unregularized, so coefficients stay honest.

Print the odds ratios, biggest first. np.exp(coef), sort descending. This is Part 2's table coming off the press: email ~4.1, direct ~3.2, returning ~2.8, sms ~2.7, pages ~1.13, spend ~1.01 per $1, hour ~1.00.

Compare against the truth. The generator used +0.95 on returning: exp(0.95) = 2.6, and you fitted ~2.8 - recovered through 30,000 noisy dice rolls. Spend's 0.006 comes back exactly. The decoy hour lands at ~1.00. The model found the machine that made the data.

The two headline models. Refit b3's numeric-only replica (scaled: spend, pages, hour) - ROC-AUC ~0.61. The full-feature model - ~0.69. Same algorithm; the features carry the signal.

read_the_machine.py - the coefficients meet their makerimport numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import roc_auc_score df = pd.read_parquet("lumen_sessions.parquet") cat = ["channel", "device", "new_vs_returning", "product_category"] num = ["pages_viewed", "session_hour", "prior_30d_spend"] X = pd.get_dummies(df[cat + num], drop_first=True) y = df["converted"] X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y) # --- Steps 1-2: statistician mode - near-unregularized, unscaled --- machine = LogisticRegression(C=1e6, max_iter=8000).fit(X_tr, y_tr) ors = pd.Series(np.exp(machine.coef_[0]), index=X.columns) print(ors.sort_values(ascending=False).round(2).head(7)) print("intercept (log-odds):", machine.intercept_[0].round(1)) # channel_email 4.1 # channel_direct 3.2 # new_vs_returning_returning 2.8 <- generator used exp(0.95) = 2.6 # channel_sms 2.7 # pages_viewed 1.13 <- generator used exp(0.11) # prior_30d_spend 1.01 <- 0.006 per $1, ~1.8 per $100 # session_hour 1.00 <- the planted noise, correctly ignored # intercept (log-odds): -5.9 # --- Step 4: the two headline models --- b3 = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)) b3.fit(X_tr[num], y_tr) print("numeric-only AUC:", roc_auc_score(y_te, b3.predict_proba(X_te[num])[:, 1]).round(2)) full = LogisticRegression(max_iter=8000).fit(X_tr, y_tr) print("full-feature AUC:", roc_auc_score(y_te, full.predict_proba(X_te)[:, 1]).round(2)) # numeric-only AUC: 0.61 # full-feature AUC: 0.69 <- same algorithm; the features carry the signal
Why unscaled here, scaled in b3? Scaling changes coefficient units (per standard deviation instead of per dollar or per page), which is right for optimization and wrong for reading. Statistician mode reads raw units; prediction pipelines scale inside the pipeline. Know which mode you are in before you print a coefficient.
Homework

Before session 4 ◐ 25 min

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · The fitted odds ratio for returning customers is ~2.8. What exactly does that mean?

Odds ratios multiply odds, not probabilities. At Lumen's ~3% base rate the two are numerically close - which is exactly why the sloppy version survives - but at higher base rates they diverge badly. Say "odds" and stay right at every base rate.

2 · Why does logistic regression train on log-loss instead of MSE?

Two reasons, one answer: convexity guarantees the optimizer finds the global best, and the unbounded penalty on confident misses trains calibrated probabilities - the model learns to reserve its 0.99s for cases that earn them.

3 · Where exactly is logistic regression's decision boundary?

z = 0 and p = 0.5 are the same place, because sigma(0) = 0.5. The set of points where w·x + b = 0 is linear - which is why logistic regression's boundary signature in the explorer is the one calm straight seam.

Source material

What this session covers

Session 3 covers the logistic-regression core of the anchor sources - sigmoid, odds ladder, coefficients, maximum likelihood - with pseudo-R² named and the solver table condensed to one card. Videos, labs, and certificates stay with the official providers.

✓StatQuest Logistic Regression - the overview episodeParts 1-3
✓StatQuest Details Part 1 - coefficientsPart 2 + demo
✓StatQuest Details Part 2 - maximum likelihoodPart 3
◐StatQuest Details Part 3 - pseudo-R²named, not derived - watch the episode if you need a fit statistic for a stats audience
✓StatQuest Odds and Log(Odds) + Odds Ratiostaught inline as the ladder, Part 2
✓Ng C1 W3 - sigmoid, decision boundary, logistic cost / why-not-MSEParts 1 + 3
✓sklearn 1.1.11 - C as inverse regularization + regularized-by-default warningPart 2 self-study card
◐sklearn 1.1.11 - solver tableone card: lbfgs default, saga for L1/large data, liblinear intercept quirk

Session 3 cheat sheet · pin this

The sigmoidsigma(z) = 1/(1 + e^-z) squashes the linear score z = w·x + b into (0, 1). Monotonic, saturating, sigma(0) = 0.5. The learning lives in the line; the squash never changes.
The odds ladderprobability (0,1) -> odds p/(1-p) (0,inf) -> log-odds ln(odds) (all reals). Logistic is LINEAR on the log-odds rung: z is the log-odds. exp() climbs down one rung.
The headline ORsexp(coef): email ~4.1, direct ~3.2, returning ~2.8, sms ~2.7 (vs affiliate); pages ~1.13 per page; spend ~1.8 per $100; hour ~1.00 (the decoy, correctly ignored); intercept ~ -5.9.
The hard ruleOdds ratio is not a probability ratio. Say "2.8x the odds", never "2.8x as likely". Close at a 3% base rate; badly wrong at high base rates. Show real predicted probabilities when stakeholders need them.
The C trapsklearn regularizes by default (C=1; C is INVERSE strength). For readable odds ratios fit C=1e6, unscaled. For prediction, keep the default and tune C by CV. Different questions, different fits.
The lossLog-loss = maximum likelihood. Convex (one bowl, one bottom) where MSE-on-sigmoid is bumpy - and it punishes confident wrongness without bound, which trains calibration. No closed form: session 4 walks downhill.