Day 41: Confusion Matrix, Precision, Recall, ROC
✅ Today you will learn
- Confusion matrix: TP, FP, FN, TN
- The accuracy paradox on imbalanced data
- Precision, recall and F1 – when each matters
- ROC-AUC and precision-recall curves
- Pick the threshold that minimises business cost
🏢 In the real world
Only 2 in 100 UPI transactions are fraud. A model that says “never fraud” is 98% accurate – and completely useless. The payments team needs to know: of the frauds, how many do we catch? Of our alerts, how many are real? Today you learn to answer both.
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=20000, n_features=8, n_informative=5, weights=[0.98], class_sep=1.2, random_state=7)
cols = ["amount_z", "hour_z", "device_age", "geo_dist", "merchant_risk", "txn_last_hr", "card_age", "velocity"]
pd.DataFrame(X, columns=cols).assign(is_fraud=y).to_csv("upi_transactions.csv", index=False)
print("Saved upi_transactions.csv | fraud rate:", round(y.mean() * 100, 2), "%")The confusion matrix
| Predicted fraud | Predicted normal | |
|---|---|---|
| Actually fraud | TP – caught ✅ | FN – missed 💸 |
| Actually normal | FP – false alarm 😠 | TN – correctly ignored ✅ |
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.dummy import DummyClassifier
from sklearn.metrics import confusion_matrix, accuracy_score
df = pd.read_csv("upi_transactions.csv")
X, y = df.drop(columns="is_fraud"), df["is_fraud"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
dummy = DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr)
model = LogisticRegression(max_iter=2000).fit(X_tr, y_tr)
for name, m in [("always 'normal'", dummy), ("logistic regression", model)]:
p = m.predict(X_te)
tn, fp, fn, tp = confusion_matrix(y_te, p).ravel()
print(f"{name:20s} accuracy={accuracy_score(y_te, p):.3f} TP={tp} FN={fn} FP={fp} TN={tn}")Both models have ~98% accuracy, yet the dummy catches zero frauds. On imbalanced data, accuracy hides everything that matters.
Precision, recall, F1
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
df = pd.read_csv("upi_transactions.csv")
X, y = df.drop(columns="is_fraud"), df["is_fraud"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
m = LogisticRegression(max_iter=2000).fit(X_tr, y_tr)
print(classification_report(y_te, m.predict(X_te), target_names=["normal", "fraud"], digits=3))
ConfusionMatrixDisplay.from_estimator(m, X_te, y_te, display_labels=["normal", "fraud"], cmap="Blues", colorbar=False)
plt.title("Confusion matrix at threshold 0.5"); plt.show()
| Situation | Priority | Why |
|---|---|---|
| Fraud, cancer screening | Recall | Missing a case is very costly |
| Spam filter, legal notices | Precision | A false alarm hides an important email |
| Loan approval | Precision of “approve” | A bad loan costs lakhs |
| Churn campaign | Balance (F1) + cost | Calls cost money, lost customers cost more |
ROC curve and AUC
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, RocCurveDisplay, PrecisionRecallDisplay, average_precision_score
df = pd.read_csv("upi_transactions.csv")
X, y = df.drop(columns="is_fraud"), df["is_fraud"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
models = {"Logistic": LogisticRegression(max_iter=2000), "Random forest": RandomForestClassifier(n_estimators=200, random_state=0, n_jobs=-1)}
fig, ax = plt.subplots(1, 2, figsize=(11, 4.2))
for name, m in models.items():
m.fit(X_tr, y_tr); p = m.predict_proba(X_te)[:, 1]
RocCurveDisplay.from_predictions(y_te, p, name=f"{name} (AUC={roc_auc_score(y_te, p):.3f})", ax=ax[0])
PrecisionRecallDisplay.from_predictions(y_te, p, name=f"{name} (AP={average_precision_score(y_te, p):.3f})", ax=ax[1])
ax[0].plot([0, 1], [0, 1], "k:", label="random"); ax[0].set_title("ROC curve"); ax[0].legend(fontsize=8)
ax[1].set_title("Precision–recall curve (better for rare events)"); ax[1].legend(fontsize=8)
plt.tight_layout(); plt.show()
0.5 = random guessing, 0.7 = fair, 0.8 = good, 0.9+ = excellent. AUC does not depend on the threshold – it measures how well the model ranks cases.
Choose the threshold by cost
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import precision_score, recall_score
df = pd.read_csv("upi_transactions.csv")
X, y = df.drop(columns="is_fraud"), df["is_fraud"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
p = RandomForestClassifier(n_estimators=200, random_state=0, n_jobs=-1).fit(X_tr, y_tr).predict_proba(X_te)[:, 1]
COST_FN, COST_FP = 8000, 150 # missed fraud ₹8,000; OTP check / annoyed customer ₹150
print("thr precision recall missed alarms total cost")
best = None
for t in [0.02, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 0.7]:
pred = (p >= t).astype(int)
fn = int(((pred == 0) & (y_te == 1)).sum()); fp = int(((pred == 1) & (y_te == 0)).sum())
cost = fn * COST_FN + fp * COST_FP
best = min(best or (cost, t), (cost, t))
print(f"{t:.2f} {precision_score(y_te, pred):.2f} {recall_score(y_te, pred):.2f} {fn:4d} {pred.sum():5d} ₹{cost:>9,}")
print(f"Cheapest threshold: {best[1]} (₹{best[0]:,})")Because a missed fraud costs 50× more than a false alarm, the cheapest threshold is well below 0.5 – we accept more OTP checks to catch more fraud. Always translate errors into rupees.
Handling class imbalance
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import recall_score, precision_score
df = pd.read_csv("upi_transactions.csv")
X, y = df.drop(columns="is_fraud"), df["is_fraud"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
for cw in [None, "balanced"]:
p = LogisticRegression(max_iter=2000, class_weight=cw).fit(X_tr, y_tr).predict(X_te)
print(f"class_weight={str(cw):9s} recall={recall_score(y_te, p):.2f} precision={precision_score(y_te, p):.2f}")- class_weight="balanced" – make mistakes on the rare class cost more during training.
- Threshold tuning – often the simplest and best fix.
- Resampling – undersample the majority or oversample the minority (imbalanced-learn’s SMOTE), only on training data.
- Use PR-AUC / recall / precision, never accuracy alone.
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, roc_auc_score
l = pd.read_csv("loan_applications.csv"); l["lti"] = l["loan_amount"] / (l["monthly_income"] * 12)
X, y = l[["credit_score", "lti", "years_employed"]], l["approved"]
a, b, c, d = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
m = LogisticRegression(max_iter=5000).fit(a, c)
print(classification_report(d, m.predict(b), digits=3)); print("AUC", round(roc_auc_score(d, m.predict_proba(b)[:, 1]), 3))Show solution
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
l = pd.read_csv("loan_applications.csv"); l["lti"] = l["loan_amount"] / (l["monthly_income"] * 12)
X, y = l[["credit_score", "lti", "years_employed"]], l["approved"]
a, b, c, d = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
p = LogisticRegression(max_iter=5000).fit(a, c).predict_proba(b)[:, 1]
costs = {t: int(((p >= t) & (d == 0)).sum() * 200000 + ((p < t) & (d == 1)).sum() * 15000) for t in np.arange(0.3, 1.0, 0.025).round(3)}
print(min(costs, key=costs.get), "→ ₹", min(costs.values()))Show solution
tp, fp, fn = 40, 60, 10 p, r = tp / (tp + fp), tp / (tp + fn) print(round(p, 3), round(r, 3), round(2 * p * r / (p + r), 3))
Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
Precision vs recall – give an example where each matters more.
Recall matters more for fraud or disease screening – missing a case is expensive. Precision matters more for spam filtering or recommending legal action – false alarms are costly. Choose the threshold from the cost of each error.
Your model has 99% accuracy on fraud. Is it good?
Not necessarily. Check the base rate – if fraud is 1%, a dummy gets 99%. Look at recall, precision, PR-AUC and the confusion matrix, and compare with a baseline.
Summary
- Confusion matrix → TP, FP, FN, TN.
- Precision = trust in alerts, recall = coverage of positives, F1 balances.
- ROC-AUC ranks models; PR curves for rare events.
- Pick thresholds by rupee cost; handle imbalance with weights or thresholds.
Day 42: KNN and decision trees – two intuitive classifiers and how they draw boundaries.
