Reading a confusion matrix as a list of investigations
What it is
The four cells of a binary confusion matrix are four different populations: rows the model caught, rows it missed, rows it falsely flagged, and rows it correctly left alone. Each cell is a cohort you can pull, describe and compare against the others.
Why it matters
Accuracy, precision and recall are summaries *of* this table, and a summary cannot tell you that 80% of the false negatives share one product category. The table's value is as an index into the data, not as a source of numbers.
In the real world: Each group of mistakes, like good customers blocked, becomes a list of cases to review with the fraud team.
Visual example
Fraud model, 10,000 val rowsNot four numbers — four cohorts to pull and describe. The interesting ones are off the diagonal.
The same table, four ways
accuracy: (9380 + 320) / 10000 = 0.970 — dominated by the majority class
precision: 320 / (320 + 120) = 0.727 — of those we block, 73% were fraud
recall: 320 / (320 + 180) = 0.640 — we catch 64% of fraud
Baseline 'always legit': accuracy 0.950, recall 0.000. Accuracy alone cannot see the difference
Why this matters
The two highlighted cells have different business costs and different root causes. Collapsing them into one accuracy figure discards both the cost structure and the debugging lead.
Common misconception
Wrong idea: a high-accuracy model has few errors worth investigating.
Correct idea: with a 5% positive rate, accuracy is mostly a report on the majority class. The 180 missed frauds are the entire product problem, and they are 1.8% of the table.
from sklearn.metrics import confusion_matrix, classification_report
import numpy as np
proba = pipe.predict_proba(X_val)[:, 1]
pred = (proba >= 0.5).astype(int)
print(confusion_matrix(y_val, pred))
print(classification_report(y_val, pred, digits=3))
# The point: cells as cohorts, not counts.
fn = X_val[(y_val == 1) & (pred == 0)]
fp = X_val[(y_val == 0) & (pred == 1)]
fn['product'].value_counts(normalize=True).head(3)
fn['amount'].describe()[['mean', '50%']], X_val['amount'].describe()[['mean', '50%']]confusion_matrix returns rows as truth and columns as prediction, so the off-diagonal cells are [0, 1] = false positive and [1, 0] = false negative. Reading it transposed swaps precision and recall — and a report claiming 94% recall when it is actually 94% precision can survive a long way through a project.
Practise it: Macro and Micro F1, From the Confusion Matrix Alone, Precision, Recall and F1 by Hand