2D decision surface on the left, sigmoid vs linear score on the right.
CSCI 1109 — Practical Data Science
By the end of this module, you should be able to:
scikit-learn and interpret its coefficients.Tempting idea:
Problems:
We want a model that:
Goal: Model the probability that \(y=1\) given features \(x\).
Two key steps:
Compute a linear score \(z\): \[ z = w^\top x + b. \]
Squash \(z\) into a probability with the sigmoid (logistic) function: \[ \sigma(z) = \frac{1}{1 + e^{-z}}. \]
Then:
Geometric intuition:
2D decision surface on the left, sigmoid vs linear score on the right.
Scenario:
Why this is an anti-example of correct interpretation:
Takeaway
The sigmoid function:
\[ \sigma(z) = \frac{1}{1 + e^{-z}}. \]
If we define \[ \hat{p} = \sigma(w^\top x + b), \] then we can write the odds and log-odds:
So logistic regression makes the log-odds a linear function of features.
Interpretation
Training logistic regression:
For each example with label \(y \in \{0, 1\}\) and predicted probability \(\hat{p} = \hat{p}(y=1 \mid x)\):
\[ \ell_{ \text{log}}(\hat{p}, y) = -\left( y \log \hat{p} + (1-y) \log (1-\hat{p}) \right). \]
Properties (intuition, no derivation):
In practice: - 🔗 sklearn.linear_model.LogisticRegression handles the optimization for us. - We focus on interpretation and evaluation, not gradient formulas.
Very high-level sketch:
Tip
You don’t need to memorize the derivation – just know that the model is consistent with a familiar probability story.
We’ll use a classic dataset from scikit-learn: digitized images of breast tissue, reduced to numeric features like:
Task:
We’ll:
load_breast_cancer.import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
accuracy_score, confusion_matrix, ConfusionMatrixDisplay
)
# Load dataset as a pandas DataFrame
cancer = load_breast_cancer(as_frame=True)
X = cancer.data # features
y = cancer.target # 0 = malignant, 1 = benign
target_names = cancer.target_names # ["malignant", "benign"]
X.head()| mean radius | mean texture | mean perimeter | mean area | mean smoothness | mean compactness | mean concavity | mean concave points | mean symmetry | mean fractal dimension | ... | worst radius | worst texture | worst perimeter | worst area | worst smoothness | worst compactness | worst concavity | worst concave points | worst symmetry | worst fractal dimension | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 17.99 | 10.38 | 122.80 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | 0.2419 | 0.07871 | ... | 25.38 | 17.33 | 184.60 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 |
| 1 | 20.57 | 17.77 | 132.90 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | 0.1812 | 0.05667 | ... | 24.99 | 23.41 | 158.80 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 |
| 2 | 19.69 | 21.25 | 130.00 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | 0.2069 | 0.05999 | ... | 23.57 | 25.53 | 152.50 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 |
| 3 | 11.42 | 20.38 | 77.58 | 386.1 | 0.14250 | 0.28390 | 0.2414 | 0.10520 | 0.2597 | 0.09744 | ... | 14.91 | 26.50 | 98.87 | 567.7 | 0.2098 | 0.8663 | 0.6869 | 0.2575 | 0.6638 | 0.17300 |
| 4 | 20.29 | 14.34 | 135.10 | 1297.0 | 0.10030 | 0.13280 | 0.1980 | 0.10430 | 0.1809 | 0.05883 | ... | 22.54 | 16.67 | 152.20 | 1575.0 | 0.1374 | 0.2050 | 0.4000 | 0.1625 | 0.2364 | 0.07678 |
5 rows × 30 columns
# Train/validation split
X_train, X_val, y_train, y_val = train_test_split(
X, y,
test_size=0.3,
random_state=47,
stratify=y,
)
# Pipeline: scale features, then logistic regression
log_reg = Pipeline(steps=[
("scale", StandardScaler()),
("model", LogisticRegression(max_iter=2000))
])
log_reg.fit(X_train, y_train)
# Validation predictions
y_val_pred = log_reg.predict(X_val)
acc = accuracy_score(y_val, y_val_pred)
acc0.9707602339181286
For binary classification with positive class “malignant”:
| Predicted: malignant | Predicted: benign | |
|---|---|---|
| Actual: malignant | True Positive (TP) | False Negative (FN) |
| Actual: benign | False Positive (FP) | True Negative (TN) |
From these counts:
🧠 Intuition:
From 🔗 here
Reading the matrix (with labels [malignant, benign]):
✏️ reflection questions:
Scenario:
Goal:
is_fraud (1 = fraud, 0 = not fraud).We’ll simulate a small dataset with:
rng = np.random.default_rng(1109)
n = 2000
amount = rng.lognormal(mean=3.0, sigma=1.0, size=n)
is_international = rng.binomial(1, 0.15, size=n)
is_high_risk_country = is_international * rng.binomial(1, 0.3, size=n)
hour = rng.integers(0, 24, size=n)
device_trust = rng.uniform(0, 1, size=n)
# True (hidden) fraud probability model
logit_true = (
-3.0
+ 0.003 * amount
+ 1.5 * is_international
+ 2.0 * is_high_risk_country
+ 0.02 * (hour - 12)
- 3.0 * device_trust
)
p_fraud = 1 / (1 + np.exp(-logit_true))
y = rng.binomial(1, p_fraud)
data = pd.DataFrame({
"amount": amount,
"is_international": is_international,
"is_high_risk_country": is_high_risk_country,
"hour": hour,
"device_trust": device_trust,
"is_fraud": y,
})
data["is_fraud"].mean()0.0415
from sklearn.metrics import precision_score, recall_score, f1_score
X = data[["amount", "is_international", "is_high_risk_country", "hour", "device_trust"]]
y = data["is_fraud"]
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.3, random_state=47, stratify=y
)
log_reg_fraud = LogisticRegression(max_iter=1000)
log_reg_fraud.fit(X_train, y_train)
y_val_pred = log_reg_fraud.predict(X_val)
y_val_proba = log_reg_fraud.predict_proba(X_val)[:, 1]
acc = accuracy_score(y_val, y_val_pred)
prec = precision_score(y_val, y_val_pred, zero_division=0)
rec = recall_score(y_val, y_val_pred, zero_division=0)
f1 = f1_score(y_val, y_val_pred, zero_division=0)
acc, prec, rec, f1(0.9583333333333334, 0.0, 0.0, 0.0)
Remember:
import matplotlib.pyplot as plt
thresholds = np.linspace(0.05, 0.95, 19)
precisions = []
recalls = []
for t in thresholds:
y_pred_t = (y_val_proba >= t).astype(int)
precisions.append(precision_score(y_val, y_pred_t, zero_division=0))
recalls.append(recall_score(y_val, y_pred_t, zero_division=0))
fig, ax = plt.subplots()
ax.plot(thresholds, precisions, marker="o", label="precision")
ax.plot(thresholds, recalls, marker="s", label="recall")
ax.set_xlabel("Decision threshold")
ax.set_ylabel("Metric value")
ax.legend()
ax.set_title("Precision/recall trade-off vs threshold");
As we increase the threshold:
As we decrease the threshold:
Choosing a threshold is a business / policy decision:
In logistic regression:
Important
For a binary classification project in this course, a sensible checklist:
Using logistic regression + metrics in the real world can go wrong when:
Simple mitigations at our level:
Where logistic regression shows up:
Even in large ML systems:
Conceptual (MCQ)
You fit a logistic regression model and get \(\hat{p}(y=1 \mid x) = 0.8\) for a patient. Which interpretation is best?
A. The model is 80% sure this specific patient will definitely get the disease.
B. If we saw many similar patients, about 80% of them would get the disease.
C. There is an 80% chance the model is correct.
D. The patient has exactly 80% of the symptoms.
Metrics (short answer)
A classifier for a rare disease achieves 99% accuracy but 20% recall on the positive class.
Confusion matrix (numeric)
Suppose on a small test set you have:
Compute accuracy, precision, and recall for the positive class.
Threshold reasoning (conceptual)
In a fraud detection system, when might you deliberately choose a lower decision threshold than 0.5?
