CO5 Topic 18 of 25
Performance metrics: classification and regression
Accuracy can lie. Use the confusion matrix, precision and recall, ROC and PR curves for classifiers, and RMSE, MAE and R² for regressors.
You will be able to
- Derive metrics from TP, FP, TN and FN by hand
- Explain when accuracy and ROC-AUC mislead
- Use PR-AUC under class imbalance
- Tune the decision threshold
- Compare RMSE and MAE on outliers
Read
- Textbook 1, Ch. 3, pp. 83–106
- Textbook 3, Ch. 4, pp. 174–196
Intuition
If 2% of students were placed, a model that says "not placed" every time is 98% accurate, and useless. Which mistake costs more? Telling a strong student they won't be placed, or giving a weak one false hope? The right metric depends on the answer.
How it works
| Predicted positive | Predicted negative | |
|---|---|---|
| Actually positive | TP | FN |
| Actually negative | FP | TN |
Accuracy = (TP + TN) / all
Precision = TP / (TP + FP)
Recall = TP / (TP + FN)
F1 = 2·P·R / (P + R)
FPR = FP / (FP + TN)
Worked. TP = 80, FP = 20, FN = 40, TN = 860: accuracy 0.94, precision 0.80, recall 0.667, F1 = 0.727, FPR = 0.023.
ROC curve: recall against FPR, across all thresholds. Its area, ROC-AUC, is the chance that a random positive scores above a random negative. 0.5 is random, and 1 is perfect.
PR curve: precision against recall. A random model scores about the positive rate as its PR-AUC, so its baseline changes with imbalance.
When positives are rare, FPR stays small even with many false alarms, because TN is huge. So ROC-AUC can look great while precision is poor. Use PR-AUC when the positive class is rare.
Regression metrics:
| Metric | Formula | Note |
|---|---|---|
| RMSE | √(mean (y − ŷ)²) | Squares errors, so it is sensitive to outliers |
| MAE | mean |y − ŷ| | More robust |
| MAPE | mean |y − ŷ| / |y| | A percentage, undefined at y = 0 |
| R² | 1 − SS_res / SS_tot | Share of variance explained |
Worked. Errors 1, 1, 1, 9: MAE = 3, and RMSE = √(84/4) = 4.58. One big error raises RMSE much more.
Run it: the metrics, by hand and by library
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.metrics import (average_precision_score, confusion_matrix, f1_score, mean_absolute_error,
mean_squared_error, precision_recall_curve, r2_score, roc_auc_score, roc_curve)
from sklearn.pipeline import make_pipeline
from placement_predict import FEATURES, load_data, preprocessor, train_val_test
df = load_data()
X_train, X_val, X_test, y_train, y_val, y_test = train_val_test(df)
model = make_pipeline(preprocessor(), LogisticRegression(max_iter=1000)).fit(X_train, y_train)
p = model.predict_proba(X_val)[:, 1]
pred = (p >= 0.5).astype(int)
tn, fp, fn, tp = confusion_matrix(y_val, pred).ravel()
print(f"TP {tp} FP {fp} FN {fn} TN {tn}")
precision, recall = tp / (tp + fp), tp / (tp + fn)
print(f"By hand: accuracy {(tp + tn) / len(pred):.3f}, precision {precision:.3f}, recall {recall:.3f}, F1 {2 * precision * recall / (precision + recall):.3f}")
print(f"Library: F1 {f1_score(y_val, pred):.3f}, ROC-AUC {roc_auc_score(y_val, p):.3f}, PR-AUC {average_precision_score(y_val, p):.3f}")
def threshold_analysis(p, y, recall_target=0.95):
rows = []
for t in np.arange(0.05, 0.96, 0.05):
h = p >= t
tp = (h & (y == 1)).sum()
rows.append({"threshold": round(t, 2), "precision": tp / max(h.sum(), 1), "recall": tp / (y == 1).sum()})
table = pd.DataFrame(rows)
ok = table[table["recall"] >= recall_target]
best = ok.iloc[-1] if len(ok) else None
print(f"Highest threshold with recall >= {recall_target}: {None if best is None else best['threshold']}")
return table
table = threshold_analysis(p, y_val.to_numpy())
print(table.iloc[::3].round(3).to_string(index=False))
print("\nThe same ranking, with placed students made rare (only 25 kept)")
rng = np.random.default_rng(0)
pos, neg = np.where(y_val == 1)[0], np.where(y_val == 0)[0]
keep = np.r_[rng.choice(pos, 25, replace=False), neg]
print(f"ROC-AUC {roc_auc_score(y_val.iloc[keep], p[keep]):.3f} barely moves. PR-AUC falls to {average_precision_score(y_val.iloc[keep], p[keep]):.3f}")
placed = df[df["PlacementStatus"] == 1].dropna(subset=["CodingTestScore"])
A, y = placed[["CodingTestScore", "CGPA", "Internships"]], placed["Salary Package"]
fit = LinearRegression().fit(A, y).predict(A)
print(f"\nSalary regression: RMSE {np.sqrt(mean_squared_error(y, fit)):.3f}, MAE {mean_absolute_error(y, fit):.3f}, R2 {r2_score(y, fit):.3f}")
broken = fit.copy(); broken[0] += 100
print(f"One prediction off by 100: RMSE {np.sqrt(mean_squared_error(y, broken)):.3f}, MAE {mean_absolute_error(y, broken):.3f}")
fig, ax = plt.subplots(1, 2, figsize=(9, 3.3))
fpr, tpr, _ = roc_curve(y_val, p); ax[0].plot(fpr, tpr, color="#0f766e"); ax[0].plot([0, 1], [0, 1], "k--", lw=0.7)
ax[0].set(title="ROC", xlabel="false positive rate", ylabel="recall")
pr, rc, _ = precision_recall_curve(y_val, p); ax[1].plot(rc, pr, color="#c2410c"); ax[1].axhline(y_val.mean(), color="k", ls="--", lw=0.7)
ax[1].set(title="Precision-recall", xlabel="recall", ylabel="precision")
plt.tight_layout()
plt.show()
Try this: set recall_target=0.8. How much precision do you win back?
Common mistakes
- Quoting accuracy on imbalanced data. Always check the baseline.
- Comparing ROC-AUC alone when positives are rare. Add PR-AUC.
- Treating 0.5 as fixed. The right threshold depends on the cost of each mistake.
Problems
- TP = 30, FP = 10, FN = 20, TN = 40. Calculate all five metrics by hand.
- Errors are 2, 2, 2, 10. Calculate MAE and RMSE.
- A system should alert the placement cell about at-risk students. Which metric matters most?
- Skill 20: write
threshold_analysisas above, sweeping 0.05 to 0.95. - Explain ROC-AUC = 0.9 to a non-technical colleague.
Flashcards
Say the meaning to yourself, then select the card to check.
- Precision
- TP / (TP + FP).
- Recall
- TP / (TP + FN).
- ROC-AUC
- The chance that a random positive scores above a random negative.
- PR-AUC
- Preferred when positives are rare. A random model scores about the positive rate.