The table every classification metric is computed from - and the one place you can see which mistakes the model is actually making.
Overview
What the table is
The confusion matrix cross-tabulates what was true against what was predicted. Rows are the actual classes, columns are the predicted ones, and each cell counts how many samples fell into that combination.
The diagonal is where the two agree — correct predictions. Everything off the diagonal is a mistake, and *which* off-diagonal cell it lands in tells you what kind.
For two classes the convention is worth memorising because ravel() returns it in this order: true negative, false positive, false negative, true positive. Reading it as tn, fp, fn, tp = cm.ravel() is the standard line.
The naming is easier than it looks once you read it as two words rather than one phrase. The second word is what the model *said*. The first word is whether it was *right*. So a false negative is a case the model called negative and was wrong about — a miss.
Worth knowing
Rows are the truth, columns are the prediction. The diagonal is correct; everything off it is a mistake.
For two classes, cm.ravel() unpacks to tn, fp, fn, tp in that order.
Precision, recall, accuracy and the rest are all arithmetic on these four counts - the table is the thing they summarise.
normalize="true" divides by row, showing what happened to each actual class regardless of its size.
Pass labels= to fix the order and keep a never-predicted class in the table.
Confusions are usually asymmetric: A mistaken for B is a different count from B mistaken for A.
The Confusion Matrix: A Practical Guide
Every classification metric is a fraction computed from four numbers. Looking at the four directly tells you things no fraction can.
The four counts
Rows are what was true, columns are what was predicted. Everything else on this page is arithmetic on these four numbers.
example_01.pyscikit-learn
Output
Every metric, derived from the table
Precision, recall and accuracy are three different fractions of the same four counts.
example_02.pyscikit-learn
Output
With several classes it stops being 2x2
And the interesting part is which classes get mistaken for which - which no single metric can tell you.
example_03.pyscikit-learn
Output
Normalise by row to compare unequal classes
Raw counts are dominated by whichever class is largest. Row proportions are not.
example_04.pyscikit-learn
Output
labels= fixes the order and the missing classes
Without it, a class the model never predicted can vanish from the table entirely.
example_05.pyscikit-learn
Output
Read the rows the model got wrong
The matrix says how many. The rows themselves say why.
example_06.pyscikit-learn
Output
Why it beats any single number
A metric compresses the table into one figure, and compression loses exactly the information you need to improve the model.
Accuracy of 0.94 could be 88 true negatives and 6 true positives with 4 misses and 2 false alarms — or it could be 94 true negatives, no true positives, and 6 misses, with the model never predicting the positive class at all. Those are completely different situations and accuracy reports them identically.
The matrix distinguishes them immediately, and it does so before you have decided which metric matters. That is the practical argument for printing it first: it tells you what the model is doing, and only then do you choose the number that summarises it.
It also tells you which direction to push. Too many false positives and too few false negatives means the threshold is too low. The reverse means it is too high. Both visible at a glance, neither visible in an accuracy score.
The multi-class version, and what it reveals
With k classes the matrix is k by k, and the off-diagonal structure carries information that no averaged metric preserves.
The editor above fits iris on sepal measurements only — deliberately insufficient — and the result is instructive. Setosa is classified perfectly, never confused with anything. Versicolor and virginica are confused with each other repeatedly, and not symmetrically: versicolor is called virginica 8 times, while virginica is called versicolor 5 times.
That asymmetry is a real finding and it is invisible in an F1 score. It tells you the boundary between those two classes is drawn slightly off centre, and it tells you setosa is a solved problem while the other two need better features. A macro F1 of 0.78 tells you none of that.
The general habit: on a multi-class problem, find the largest off-diagonal cell. It is almost always one specific pair being confused, and it is almost always the most productive thing to work on.
Normalising, and which way
Raw counts are dominated by class size. On a 99-to-1 problem the true-negative cell is enormous and the rest are visually invisible, which makes the table hard to read even though the numbers are correct.
normalize="true" divides each row by its total, giving the proportion of each actual class that went to each prediction. The diagonal then reads as per-class recall, and a row that is mostly off the diagonal shows a class the model handles badly regardless of how few samples it has.
normalize="pred" divides by column instead, giving the proportion of each prediction that was correct — the diagonal reads as per-class precision.
normalize="all" divides by the grand total, which is occasionally useful and usually the least informative of the three.
Row normalisation is the default worth reaching for. It answers "what does the model do with each kind of case", which is the question that survives changes in class balance.
The labels argument
confusion_matrix infers the classes from the data it is given, sorted. Two consequences bite.
A class present in y_true but never predicted still appears — but a class absent from both, perhaps because the test split happened to contain none of it, disappears entirely, and the matrix silently becomes smaller than expected. Code that indexes into it by class position then reads the wrong cell.
And the order is sorted, which for string labels is alphabetical: ["cat", "dog", "fish"]. Assuming a different order and labelling the axes accordingly produces a table that is confidently wrong.
Passing labels= fixes both. It guarantees the size, guarantees the order, and makes the code independent of which classes happened to turn up in a particular split. It is worth passing habitually rather than when a problem appears.
From the table to the rows
The matrix says how many mistakes of each kind. The next question is always why, and answering it means looking at the samples themselves.
Selecting the misclassified rows takes one line — np.flatnonzero(pred != y_true) — and reading a handful of them is consistently the most informative ten minutes available. Usually they have something in common: values near a boundary, a category the training data barely covered, missing fields that were imputed, or labels that are simply wrong in the source data.
Each of those has a different response, and none of them is "try a different model". Mislabelled ground truth in particular is far more common than people expect, and no amount of tuning fixes it — the model is being penalised for being right.
The editor above does exactly this for the iris confusion, and the misclassified rows are all sepal measurements in the overlapping middle of the two species. That is not a model problem; it is the honest answer that these two features do not separate those two classes, and the fix is petal measurements rather than a fancier classifier.
The other convention, and why it causes trouble
scikit-learn puts truth on the rows and predictions on the columns. A good deal of the statistics literature does the opposite, and some textbooks put the positive class first rather than second.
That means a matrix copied from a paper, a blog post or a lecture slide may be transposed relative to what confusion_matrix produces, and a transposed matrix swaps precision and recall without changing anything visible. The numbers all look plausible; they are simply answering the other question.
Two habits prevent it. Print the matrix with labelled axes rather than as a bare array, which takes three lines and removes the ambiguity permanently. And sanity-check against a metric you trust: compute recall with recall_score and confirm it matches the diagonal cell divided by its row total. If it matches the column total instead, the orientation is not what you assumed.
The same caution applies to the binary case, where the positive class is whichever label sorts second — 1 before 0 is not the order, 0 then 1 is. With string labels, "no" sorts before "yes", which usually happens to be right, and with "negative" and "positive" it also happens to be right. With "benign" and "malignant" it is right for the wrong reason, and with labels where it is not, pos_label= is the argument that says so explicitly.
What it cannot tell you
Two limits worth stating, because the matrix is otherwise so useful that it gets asked questions it cannot answer.
It says nothing about confidence. Every cell counts hard predictions, so a case predicted positive at 0.51 and one at 0.99 are the same entry. A model can have an excellent matrix and terrible probabilities, or the reverse, and only the probability-based metrics distinguish them.
And it describes one threshold. Change the threshold and every cell moves — that is the whole mechanism of the precision-recall trade. So a confusion matrix is a snapshot of one operating point, not a description of the model, and comparing two models by their matrices means comparing them at whatever threshold each happened to use.
Is there a plotting version?ConfusionMatrixDisplay.from_estimator(model, X, y) draws it with matplotlib, which is worth using once the matrix is bigger than about four by four and the numbers stop being readable as text.
Which class is "positive" in a binary problem? Whichever label sorts second, so 0 then 1 puts 1 positive. Pass pos_label= when the sorted order is not what you mean.
Can I get one matrix per class for a multi-label problem? Yes - multilabel_confusion_matrix returns a stack of 2x2 tables, one per label, which is the right shape when a sample can carry several labels at once.
Putting a cost on each cell
The matrix becomes a decision tool rather than a diagnostic the moment you attach a number to each kind of mistake.
The four cells rarely cost the same. A false negative on a fraud check is the value of the fraud; a false positive is a few minutes of an analyst's time. A false negative on a medical screen is a missed diagnosis; a false positive is a second test. A false negative on a spam filter is one unwanted email; a false positive is a lost message the recipient never knew about.
Once those numbers exist, the expected cost of a model is the sum of each cell multiplied by its price, and comparing two models becomes arithmetic rather than argument. It also settles the threshold question directly: sweep the threshold, compute the total cost at each one, and pick the minimum. That is a better procedure than choosing a threshold to hit a round-numbered precision, and it takes about the same effort.
The exercise is worth doing even when the costs are rough. Estimating that a miss is roughly ten times worse than a false alarm is enough to rule out most of the range, and it forces the conversation with whoever owns the problem — who usually has a much clearer view of the relative costs than the person building the model, and who is rarely asked.
Where it goes wrong is treating the estimated costs as precise. They are not, and a threshold tuned to the third decimal place of an invented cost ratio is false precision. The useful output is a region rather than a point: anywhere in this range is sensible, and outside it is not.
Things to try
Unpack the four counts. In the first editor, compute precision and recall yourself from tn, fp, fn, tp before looking at what the library returns.
Find the worst pair. In the third editor, locate the largest off-diagonal cell and note that it is not mirrored.
Switch the normalisation. In the fourth editor, compare normalize="true" with normalize="pred" and work out which diagonal is recall and which is precision.
Read the mistakes. In the last editor, raise the slice to see all thirteen and look for what they have in common.
Where this leaves you
Four counts for two classes, k by k for more, rows as truth and columns as prediction. Print it before choosing a metric, normalise by row when the classes are uneven, pass labels= so the shape is guaranteed, and read the rows behind the largest off-diagonal cell before changing anything.
Check yourself
0 of 4
Answer without scrolling back up.
In a scikit-learn confusion matrix, what do the rows represent?
Rows are the truth and columns the prediction. The diagonal is correct; everything off it is a mistake of a specific kind.
What does cm.ravel() return for a binary problem?
True negative, false positive, false negative, true positive - reading order across the 2x2 table.
Why pass labels= to confusion_matrix?
Without it the classes are inferred and sorted, so a class missing from a split silently shrinks the matrix and code indexing by position reads the wrong cell.
What does normalize="true" give you on the diagonal?
Dividing by row totals gives the proportion of each actual class predicted correctly, which is recall. normalize="pred" divides by column and gives precision.
Cheat sheet
The Confusion Matrix
The confusion matrix cross-tabulates what was true against what was predicted. Rows are the actual classes, columns are the predicted ones, and each cell counts how many samples fell into that combination.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.