Classification metrics in depth
Confusion matrices, ROC-AUC versus precision-recall curves, choosing an operating threshold, and checking whether probabilities are calibrated.
Everything starts at the confusion matrix
from sklearn.metrics import classification_report, confusion_matrix
cm = confusion_matrix(y_test, y_pred)
tn, fp, fn, tp = cm.ravel()
precision = tp / (tp + fp) # of the alarms raised, how many were real
recall = tp / (tp + fn) # of the real cases, how many were caught
fpr = fp / (fp + tn) # of the clean cases, how many were wrongly flagged
print(cm, precision, recall, fpr)
print(classification_report(y_test, y_pred, digits=3))| Metric | Reads as | A good value means |
|---|---|---|
| Precision | Alarm quality | Few wasted interventions |
| Recall | Coverage | Few missed cases |
| Specificity | 1 - false positive rate | Few false alarms on clean cases |
| F1 | Balance of precision and recall | One number when both matter equally |
| ROC-AUC | Ranking quality across all thresholds | The model separates classes overall |
| Average precision | Area under the PR curve | The same idea, honest under heavy imbalance |
Report the matrix and the two error counts, not just a scalar. Stakeholders can argue about how many false positives they can absorb; almost nobody can argue about an AUC.
ROC-AUC versus precision-recall
from sklearn.metrics import (average_precision_score,
precision_recall_curve, roc_auc_score, roc_curve)
prob = clf.predict_proba(X_test)[:, 1]
print("roc auc", roc_auc_score(y_test, prob))
print("average precision", average_precision_score(y_test, prob))
fpr, tpr, thr = roc_curve(y_test, prob)
prec, rec, thr_pr = precision_recall_curve(y_test, prob)- ROC-AUC uses the true negative rate, so with 1% positives a model can look strong while flagging a large share of the negative class.
- Average precision reacts to the positive class only — it is the metric to quote when positives are rare.
- Both describe ranking, not decisions. You still have to pick a threshold to produce labels.
- Compare a new model against the current one on the same test rows and the same metric, never against a number from a different report.
Thresholds and calibration
import numpy as np
from sklearn.calibration import CalibratedClassifierCV
# pick the threshold from the cost of each error type
cost_fp, cost_fn = 5, 50
expected = cost_fp * (1 - prob) + cost_fn * prob
threshold = np.quantile(prob, 0.10) # flag the riskiest 10%
# are the probabilities themselves trustworthy?
cal = CalibratedClassifierCV(clf, method="isotonic", cv=5).fit(X_train, y_train)
prob_cal = cal.predict_proba(X_test)[:, 1]
print(prob_cal.mean(), y_test.mean()) # predicted rate vs observed rate⚠️
A model can rank perfectly and still be badly calibrated: it may say 0.9 for cases that fail half the time. If a downstream decision uses the probability as a number — pricing, triage, expected value — calibrate it and re-check the reliability curve before shipping.
FAQ
Should I optimise precision or recall?
Neither in isolation. Set a required recall (or precision) from the business constraint, then maximise the other subject to it, and confirm the resulting volume of alerts is one the team can handle.
My AUC is 0.62. Is the model useless?
Not necessarily. Compare it with the strongest simple baseline and to the current process. A 0.62 model can still be valuable if the alternative is random outreach, but it will not support high-stakes automation.
Related
Imbalanced data Supervised algorithms in practice
Last refreshed 2026-09-18.