Classification Metrics — Deep Dive
.score() (plain accuracy) to check classifiers. This lesson goes much deeper: the confusion matrix and its four building blocks (TP/FP/TN/FN), precision, recall, and F1-score with their exact formulas, sklearn.metrics.confusion_matrix and classification_report, the ROC curve and AUC, and — maybe most important of all — how to decide whether precision or recall should be your priority for a given real problem.
Why Accuracy Alone Can Mislead You
Accuracy — (correct predictions) / (total predictions) — is easy to compute and easy to understand, which is why it's been this course's default metric so far. But accuracy has a blind spot: it treats every mistake the same, and it says nothing about WHICH kind of mistake a model tends to make.
Consider a classifier that predicts whether an email is spam. Two very different models could both score "90% accuracy" — one that correctly catches most spam while rarely bothering real email, and another that is great at recognizing real email but quietly lets more spam through. Accuracy alone can't tell those two models apart. The metrics in this lesson can.
The Confusion Matrix — TP, FP, TN, FN
Every other metric in this lesson is built from four counts. For a binary classifier (predicting a "positive" class, like "spam" or "has disease", against a "negative" class):
scikit-learn computes this directly from true and predicted labels with confusion_matrix:
from sklearn.metrics import confusion_matrix # y_test and y_pred are 0/1 arrays: 1 = positive class (e.g. "spam") cm = confusion_matrix(y_test, y_pred) print(cm) # [[85 5] # [10 50]] # Illustrative output for a toy dataset. Rows = actual class (0, then 1), # columns = predicted class (0, then 1), sorted ascending by default. # -> TN=85, FP=5, FN=10, TP=50
confusion_matrix sorts labels ascending, so for binary 0/1 labels: row 0 = actual negatives, row 1 = actual positives; column 0 = predicted negatives, column 1 = predicted positives. cm[0][0]=TN, cm[0][1]=FP, cm[1][0]=FN, cm[1][1]=TP. Pass labels=[...] explicitly if you ever want a different, guaranteed order.Precision, Recall, and F1-Score
These three metrics each combine TP/FP/TN/FN in a different way, answering three different plain-language questions.
Precision — "Of everything I flagged as positive, how much was right?"
Recall — "Of everything that actually was positive, how much did I catch?"
F1-Score — a single number balancing both
Using the illustrative confusion matrix from Section 2 (TN=85, FP=5, FN=10, TP=50):
from sklearn.metrics import precision_score, recall_score, f1_score, accuracy_score print("Accuracy:", accuracy_score(y_test, y_pred)) print("Precision:", precision_score(y_test, y_pred)) print("Recall:", recall_score(y_test, y_pred)) print("F1-score:", f1_score(y_test, y_pred)) # Accuracy: 0.900 -> (85+50) / (85+5+10+50) # Precision: 0.909 -> 50 / (50+5) # Recall: 0.833 -> 50 / (50+10) # F1-score: 0.870 -> 2 * (0.909*0.833) / (0.909+0.833)
classification_report — Every Metric at Once
Instead of calling four separate functions, classification_report prints precision, recall, F1, and support (the number of true instances) for every class in one readable block — plus accuracy and two kinds of averages.
from sklearn.metrics import classification_report print(classification_report(y_test, y_pred, target_names=["Not Spam", "Spam"]))
| precision | recall | f1-score | support | |
|---|---|---|---|---|
| Not Spam | 0.89 | 0.94 | 0.92 | 90 |
| Spam | 0.91 | 0.83 | 0.87 | 60 |
| accuracy | 0.90 | 150 | ||
| macro avg | 0.90 | 0.89 | 0.89 | 150 |
| weighted avg | 0.90 | 0.90 | 0.90 | 150 |
y_test — useful context for judging how much a class's score should be trusted.ROC Curve and AUC
Precision, recall, and F1 all depend on one fixed decision threshold — usually "predict positive if predicted probability > 0.5". The ROC curve (Receiver Operating Characteristic) instead shows how a classifier performs across EVERY possible threshold at once.
TP / (TP + FN). Plotted on the y-axis.FP / (FP + TN) — the fraction of actual negatives incorrectly flagged as positive. Plotted on the x-axis.The ROC curve traces (FPR, TPR) pairs as the decision threshold sweeps from 0 to 1. roc_curve computes the raw points; roc_auc_score computes the Area Under that Curve as one summary number.
from sklearn.metrics import roc_curve, roc_auc_score # predict_proba returns [P(class 0), P(class 1)] per row — take column 1 y_probs = model.predict_proba(X_test)[:, 1] fpr, tpr, thresholds = roc_curve(y_test, y_probs) auc = roc_auc_score(y_test, y_probs) print(f"AUC: {auc:.3f}") # AUC: 0.923 (illustrative — one toy-dataset run, not a benchmark claim)
| AUC range | General interpretation |
|---|---|
| 0.50 | No better than random guessing |
| 0.50 – 0.70 | Poor |
| 0.70 – 0.80 | Fair |
| 0.80 – 0.90 | Good |
| 0.90 – 1.00 | Excellent |
roc_auc_score summarizes a model's ability to RANK positives above negatives overall, without committing to any single cutoff. That makes it useful for comparing two models' general discriminative power, even before deciding where to set the operating threshold in Section 6.predict_proba(...)[:, 1] (or decision_function(...) for models without predict_proba) — NOT model.predict(X_test). Hard 0/1 predictions collapse every threshold into one, which defeats the entire point of the ROC curve.Precision vs. Recall — Which Should You Prioritize?
There's no universally "correct" metric to optimize — the right choice depends entirely on which TYPE of mistake, false positives or false negatives, is more costly for the specific problem. Two classic contrasting examples:
Reasoning through this tradeoff — rather than defaulting to accuracy — is the single biggest mindset shift this lesson aims to build. The right metric always follows from the real-world cost of each type of mistake, never from habit.
Lesson Summary
Use any binary classifier from Section 2 (e.g. LogisticRegression, SVC(probability=True), or GradientBoostingClassifier) on a dataset of your choice for these tasks.
Train a classifier, generate predictions on a test set, and print
confusion_matrix, classification_report, and the individual precision_score/recall_score/f1_score. Confirm the individual scores match what classification_report shows for the positive class.
Using
roc_curve's returned fpr and tpr arrays, plot them with matplotlib (plt.plot(fpr, tpr)), add a diagonal reference line from (0,0) to (1,1) representing random guessing, and print the AUC in the plot title.
Instead of the default 0.5 cutoff, manually threshold
predict_proba(X_test)[:, 1] at 0.3 and again at 0.7 (e.g. (probs > 0.3).astype(int)). Recompute precision and recall at each threshold. Confirm that the lower threshold raises recall (and lowers precision) compared to the higher one.
💡 Show hints if you're stuck
- Task 1: Remember
classification_report's numbers for the positive class should exactly matchprecision_score/recall_score/f1_scorecalled directly — they're computed the same way. - Task 2:
plt.plot([0,1],[0,1],'r--')draws the "random guessing" diagonal — a real model's ROC curve should bow up and to the left of it. - Task 3: A LOWER threshold means more predictions get labeled positive overall, which typically increases recall (catches more true positives) but also increases false positives, lowering precision — exactly the tradeoff from Section 6.