Supervised Learning Classification
Logistic Regression
A natural first instinct: why not just use Module 3's linear regression directly for a yes/no problem, treating "yes"=1 and "no"=0? This breaks down quickly.
Jr Codex ML Notes
Level: Intermediate Prerequisites: Module 3: Supervised Learning — Regression Time to complete: ~30 minutes
Table of Contents
- Why Linear Regression Doesn't Work for Classification
- The Sigmoid Function
- The Logistic Regression Model
- From Probability to Class Prediction
- The Decision Boundary
- How Logistic Regression Is Trained
- Multi-Class Classification
- Interpreting Coefficients as Odds
- Summary & Next Steps
1. Why Linear Regression Doesn't Work for Classification
A natural first instinct: why not just use Module 3's linear regression directly for a yes/no problem, treating "yes"=1 and "no"=0? This breaks down quickly.
import numpy as np
from sklearn.linear_model import LinearRegression
# Trying to force linear regression onto a binary problem
hours_studied = np.array([[1], [2], [3], [8], [9], [10]])
passed_exam = np.array([0, 0, 0, 1, 1, 1])
model = LinearRegression().fit(hours_studied, passed_exam)
print(model.predict([[15]])) # could easily predict something like 1.4 — NOT a valid probability!
print(model.predict([[-5]])) # could predict something NEGATIVE — meaningless as a probabilityThe Core Problem
─────────────────────────────────────────
Linear regression's output is UNBOUNDED — it can predict any
number from -∞ to +∞. But a class probability MUST be between
0 and 1. Linear regression has NO mechanism to enforce this,
and will happily predict nonsensical values like 1.4 or -0.2
"probability."
─────────────────────────────────────────
This is precisely why classification needs its own dedicated model — not because the underlying idea (weighted sum of features) is wrong, but because the output needs to be squeezed into a valid probability range.
2. The Sigmoid Function
The sigmoid function (also called the logistic function) takes any real number and squeezes it into the range (0, 1) — exactly what's needed to fix Section 1's problem.
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
z_values = np.linspace(-10, 10, 100)
sigmoid_values = sigmoid(z_values)
plt.plot(z_values, sigmoid_values)
plt.axhline(y=0.5, color="gray", linestyle="--")
plt.axvline(x=0, color="gray", linestyle="--")
plt.xlabel("z (linear combination of features)")
plt.ylabel("sigmoid(z) — always between 0 and 1")
plt.title("The Sigmoid Function")
print(sigmoid(0)) # 0.5 — exactly halfway
print(sigmoid(10)) # ~0.99995 — very close to 1
print(sigmoid(-10)) # ~0.00005 — very close to 0Sigmoid's Shape
─────────────────────────────────────────
1.0 ┤ ______------
│ ___/
0.5 ┤ ___/
│ ___/
0.0 ┤___----
└────────────────────────────────
-10 -5 0 5 10
z
As z → +∞, sigmoid(z) → 1
As z → -∞, sigmoid(z) → 0
At z = 0, sigmoid(z) = 0.5 exactly
─────────────────────────────────────────
3. The Logistic Regression Model
Combines Module 3's familiar linear combination of features with the sigmoid function wrapped around it.
The Model
─────────────────────────────────────────
z = β₀ + β₁x₁ + β₂x₂ + ... + βₙxₙ (exactly Module 3's linear combination)
P(y=1) = sigmoid(z) = 1 / (1 + e^(-z)) (squeezed into a valid probability)
─────────────────────────────────────────
from sklearn.linear_model import LogisticRegression
import numpy as np
hours_studied = np.array([[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]])
passed_exam = np.array([0, 0, 0, 0, 1, 0, 1, 1, 1, 1])
model = LogisticRegression()
model.fit(hours_studied, passed_exam)
# predict_proba returns the ACTUAL probability, not just a class
probabilities = model.predict_proba([[3], [6], [9]])
print(probabilities)
# [[0.85, 0.15], ← 85% chance of "not passed", 15% chance of "passed"
# [0.35, 0.65], ← 35% / 65%
# [0.05, 0.95]] ← 5% / 95%4. From Probability to Class Prediction
Once a probability is computed, it must be converted into a concrete class prediction using a threshold — by default, 0.5.
predictions = model.predict([[3], [6], [9]])
print(predictions) # [0, 1, 1] — thresholded at 0.5 by default
# The threshold can be adjusted MANUALLY for different use cases
probabilities_class1 = model.predict_proba(hours_studied)[:, 1]
custom_predictions = (probabilities_class1 >= 0.3).astype(int) # a LOWER threshold — more lenientWhy the Threshold Matters (Preview of Chapter 6)
─────────────────────────────────────────
A LOWER threshold (e.g. 0.3) predicts the positive class MORE
often — catching more true positives, but also more false
positives.
A HIGHER threshold (e.g. 0.7) predicts the positive class LESS
often — fewer false positives, but also missing more true
positives.
This exact trade-off (precision vs recall) is covered fully
in Chapter 6, and the threshold is a genuine, business-relevant
choice, not always the default 0.5.
─────────────────────────────────────────
5. The Decision Boundary
The decision boundary is the line (or surface, with more features) where the model's predicted probability equals exactly 0.5 — everything on one side is classified as one class, everything on the other side as the other.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
np.random.seed(0)
class_0 = np.random.randn(50, 2) + [2, 2]
class_1 = np.random.randn(50, 2) + [5, 5]
X = np.vstack([class_0, class_1])
y = np.array([0]*50 + [1]*50)
model = LogisticRegression().fit(X, y)
xx, yy = np.meshgrid(np.linspace(0, 8, 100), np.linspace(0, 8, 100))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.title("Logistic Regression's Decision Boundary")A Key Limitation Worth Knowing Upfront
─────────────────────────────────────────
Logistic regression's decision boundary is ALWAYS a straight
line (or a flat plane/hyperplane, with more features) — it
CANNOT naturally represent a curved or complex boundary
between classes.
When classes aren't LINEARLY separable, logistic regression
will underfit (Module 1, Ch.4) — this is exactly why Chapters
2-4 introduce KNN, decision trees, and SVMs, each capable of
more flexible decision boundaries.
─────────────────────────────────────────
6. How Logistic Regression Is Trained
Unlike Module 3's linear regression, logistic regression has no closed-form solution — it's trained using gradient descent (Module 3, Chapter 1, Section 4), minimizing a different loss function suited to probabilities.
Log Loss (Binary Cross-Entropy) — the Loss Function Being Minimized
─────────────────────────────────────────
Log Loss = -(1/n) × Σ [ y×log(p) + (1-y)×log(1-p) ]
where p = predicted probability of the positive class
Intuition: HEAVILY penalizes CONFIDENT, WRONG predictions
(e.g. predicting 99% probability of "yes" when the true
answer is "no") — far more harshly than a similarly-wrong
but LESS confident prediction.
─────────────────────────────────────────
def log_loss(y_actual, y_predicted_proba):
epsilon = 1e-15 # avoid log(0), which is undefined
y_predicted_proba = np.clip(y_predicted_proba, epsilon, 1 - epsilon)
return -np.mean(
y_actual * np.log(y_predicted_proba) + (1 - y_actual) * np.log(1 - y_predicted_proba)
)
# A confident WRONG prediction is penalized MUCH more than an unconfident one
print(log_loss(np.array([1]), np.array([0.01]))) # HUGE loss — very confident, very wrong
print(log_loss(np.array([1]), np.array([0.4]))) # smaller loss — wrong, but less confidently soGradient descent (introduced in Module 3, Chapter 1) iteratively adjusts the coefficients to minimize this log loss — the exact same "step downhill" process, just applied to a different loss surface.
7. Multi-Class Classification
Logistic regression naturally extends beyond binary (yes/no) to multiple classes using either the One-vs-Rest strategy or a direct multinomial extension (softmax regression).
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target # 3 classes: setosa, versicolor, virginica
model = LogisticRegression(multi_class="multinomial", max_iter=200)
model.fit(X, y)
print(model.predict_proba(X[:1])) # probability for EACH of the 3 classes, summing to 1
print(model.classes_) # [0, 1, 2]One-vs-Rest vs Multinomial
─────────────────────────────────────────
One-vs-Rest: trains ONE binary classifier PER class
("is it class A, or NOT class A?"),
repeated for every class, then picks
whichever classifier is most confident
Multinomial (softmax): a single, unified model directly
outputting a probability for EACH
class simultaneously, guaranteed to
sum to 1 — generally the more
principled, modern default
─────────────────────────────────────────
8. Interpreting Coefficients as Odds
Logistic regression retains much of Module 3's coefficient interpretability, but through odds rather than a direct additive effect.
import numpy as np
model = LogisticRegression().fit(hours_studied, passed_exam)
coefficient = model.coef_[0][0]
odds_ratio = np.exp(coefficient)
print(f"Coefficient: {coefficient:.3f}")
print(f"Odds ratio: {odds_ratio:.3f}")
# e.g. odds_ratio = 2.1 means: each ADDITIONAL hour studied
# roughly DOUBLES the ODDS of passing (not the raw probability —
# odds and probability are related but distinct concepts)Why "Odds," Not Direct Probability
─────────────────────────────────────────
odds = P(event) / (1 - P(event))
Logistic regression's coefficients are linear in the LOG-ODDS
(this is literally what "logistic" refers to), not in the
probability itself — exponentiating a coefficient gives the
MULTIPLICATIVE effect on ODDS, a genuinely useful, if less
immediately intuitive, interpretation than Module 3's direct
additive coefficients.
─────────────────────────────────────────
9. Summary & Next Steps
Key Takeaways
- Linear regression can't be used directly for classification because its output is unbounded, while probabilities must fall between 0 and 1 — the sigmoid function fixes this.
- Logistic regression combines a linear combination of features (like Module 3) with the sigmoid function, producing valid probabilities, then a threshold (default 0.5) converts probability into a class prediction.
- The decision boundary is always linear — a real limitation when classes aren't linearly separable, motivating Chapters 2-4's more flexible algorithms.
- Logistic regression is trained via gradient descent minimizing log loss, which heavily penalizes confident-but-wrong predictions.
- Coefficients are interpreted through odds ratios (exponentiated coefficients), reflecting the model's "linear in log-odds" structure.
Concept Check
- Why can't linear regression's raw output be used directly as a class probability?
- What's a key limitation of logistic regression's decision boundary, and which later chapters address it?
- Why does log loss penalize a confident wrong prediction more harshly than an unconfident wrong one?
Next Chapter
→ Chapter 2: K-Nearest Neighbors
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index