Menu

Tutorial15 min read

How to Plot a Confusion Matrix in Python (Scikit-learn & Seaborn)

By Francesco VillasmuntaUpdated June 12, 2026
How to Plot a Confusion Matrix in Python (Scikit-learn & Seaborn)

A confusion matrix is the single most useful diagnostic for a classification model. It shows exactly where predictions go wrong — which classes get confused for which — in a way that a single accuracy number never can. This guide shows the two standard ways to plot one in Python (scikit-learn's built-in display and a seaborn heatmap), how to add count and percentage annotations, how to normalize, and how to extend it to multi-class problems.

A confusion matrix is really just a labeled heatmap, so if you want the underlying mechanics, our heatmap in Python guide pairs well with this one.

Rows vs columns

In scikit-learn, rows are the true label and columns are the predicted label. The diagonal is correct predictions.

Normalize

Row-normalized values put per-class recall on the diagonal — essential when classes are imbalanced.

Fix the label order

Always pass labels= so the axis order is deterministic and matches your captions.

1. The built-in way: ConfusionMatrixDisplay

Scikit-learn ships a dedicated display class. Compute the matrix with confusion_matrix(), then render it with ConfusionMatrixDisplay. Passing an explicit ax lets you control the figure size and styling. Set colorbar=False for a cleaner small figure.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

# y_true and y_pred are 1D arrays of labels
labels = ["Healthy", "Disease"]
cm = confusion_matrix(y_true, y_pred, labels=labels)

disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels)
fig, ax = plt.subplots(figsize=(4.8, 4.4))
disp.plot(ax=ax, cmap="Purples", colorbar=False)
ax.set_title("Confusion matrix")
plt.tight_layout()

If you have the fitted estimator and raw features, the one-liner ConfusionMatrixDisplay.from_estimator(clf, X_test, y_test) computes and plots in a single call — convenient for quick checks during model development.

2. The flexible way: seaborn heatmap

For full control over annotations, colormap, and grid lines, plot the matrix as a seaborn heatmap. Use fmt="d" for integer counts, square=True to keep cells square, and thin linewidths to separate cells. This is the version most often used in papers.

import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix

labels = ["Healthy", "Disease"]
cm = confusion_matrix(y_true, y_pred, labels=labels)

fig, ax = plt.subplots(figsize=(4.8, 4.4))
sns.heatmap(cm, annot=True, fmt="d", cmap="Purples", cbar=False,
            xticklabels=labels, yticklabels=labels, square=True,
            linewidths=0.5, linecolor="white", ax=ax)
ax.set_xlabel("Predicted label")
ax.set_ylabel("True label")
plt.tight_layout()

3. Normalize for imbalanced classes

Raw counts mislead when one class dominates: 95% accuracy looks great until you notice the model never predicts the rare class. Pass normalize="true" so each row sums to 1. The diagonal then reads as per-class recall, the most honest single view of a skewed classifier.

from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt

labels = ["Healthy", "Disease"]

# normalize="true" -> each row sums to 1 (per-class recall on the diagonal)
cm_norm = confusion_matrix(y_true, y_pred, labels=labels, normalize="true")

fig, ax = plt.subplots(figsize=(4.8, 4.4))
sns.heatmap(cm_norm, annot=True, fmt=".2f", cmap="Purples", cbar=True,
            xticklabels=labels, yticklabels=labels, square=True, ax=ax)
ax.set_xlabel("Predicted label")
ax.set_ylabel("True label")
ax.set_title("Row-normalized confusion matrix")
plt.tight_layout()

Try it

Try it now: turn this method into your next figure

Apply the same approach to your own dataset and generate clean, publication-ready code and plots in minutes.

Open in Plotivy Analyze

Newsletter

Get a weekly Python plotting tip

One concise tip each week for cleaner, faster scientific figures. Built for researchers who publish.

No spam. Unsubscribe anytime.
normalize=What each cell shows
"true"Fraction of each true class (rows sum to 1) — recall on diagonal
"pred"Fraction of each predicted class (columns sum to 1) — precision on diagonal
"all"Fraction of the whole sample (everything sums to 1)
NoneRaw integer counts (default)

4. Show counts and percentages together

The most informative annotation combines the raw count and the row percentage in each cell. Build a string array and pass it to annot with fmt="" so seaborn prints your labels verbatim.

import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix

labels = ["Healthy", "Disease"]
cm = confusion_matrix(y_true, y_pred, labels=labels)
cm_pct = cm / cm.sum(axis=1, keepdims=True)

# Build "count\npercent" labels for each cell
annot = np.empty_like(cm).astype(str)
for i in range(cm.shape[0]):
    for j in range(cm.shape[1]):
        annot[i, j] = f"{cm[i, j]}\n{cm_pct[i, j]:.0%}"

fig, ax = plt.subplots(figsize=(4.8, 4.4))
sns.heatmap(cm, annot=annot, fmt="", cmap="Purples", cbar=False,
            xticklabels=labels, yticklabels=labels, square=True, ax=ax)
ax.set_xlabel("Predicted label")
ax.set_ylabel("True label")
plt.tight_layout()

5. Multi-class confusion matrices

Everything scales to more than two classes. Pass the full class list to labels= and rotate the x tick labels so they stay readable. For many classes (10+), prefer a row-normalized matrix — absolute counts become hard to compare across classes of different sizes.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

classes = ["setosa", "versicolor", "virginica"]
cm = confusion_matrix(y_true, y_pred, labels=classes)

disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=classes)
fig, ax = plt.subplots(figsize=(5.4, 5))
disp.plot(ax=ax, cmap="Purples", colorbar=False, xticks_rotation=45)
ax.set_title("3-class confusion matrix")
plt.tight_layout()

How to read a confusion matrix

For binary classification with the positive class second, the four cells map to the familiar terms:

CellNameMeaning
Top-leftTrue NegativeNegative cases correctly predicted negative
Top-rightFalse PositiveNegative cases wrongly predicted positive (type I error)
Bottom-leftFalse NegativePositive cases wrongly predicted negative (type II error)
Bottom-rightTrue PositivePositive cases correctly predicted positive

From these you derive precision (TP / (TP + FP)), recall (TP / (TP + FN)), and F1. You rarely need to compute them by hand — classification_report prints all of them, and pairs naturally with the matrix in a results figure.

from sklearn.metrics import classification_report

# Precision, recall, F1 for every class plus macro/weighted averages
print(classification_report(y_true, y_pred, target_names=["Healthy", "Disease"]))

Common mistakes and how to fix them

  • Axes are transposed — remember scikit-learn uses rows = true, columns = predicted. Label both axes explicitly so readers are never guessing.
  • Class order changes between runs — always pass labels= with a fixed list; otherwise the order follows the data and your captions can silently become wrong.
  • Counts hide imbalance — on skewed data, add a row-normalized version so a model that ignores the rare class is obvious.
  • Unreadable annotations — on dark cells, light text disappears. Scikit-learn handles this automatically; in seaborn, pick a colormap with enough contrast or set annot_kws color.
  • Reporting accuracy only — pair the matrix with an ROC curve or precision-recall curve for a threshold-independent view.

Frequently asked questions

ConfusionMatrixDisplay or seaborn heatmap — which should I use?

Use ConfusionMatrixDisplay for fast checks during development; it is one line from a fitted model. Use a seaborn heatmap for the final publication figure when you need custom annotations (counts plus percentages), a specific colormap, or grid styling.

How do I plot a normalized confusion matrix?

Pass normalize="true" to confusion_matrix() and use fmt=".2f" (or ".0%") when annotating. Row normalization puts per-class recall on the diagonal, which is the most useful view for imbalanced datasets.

How do I save the confusion matrix for a paper?

Call fig.savefig("confusion_matrix.png", dpi=300, bbox_inches="tight"), and also export a vector PDF or SVG when the journal accepts it. See our high-resolution export guide.

Upload your predictions and type "normalized confusion matrix with counts and percentages for my classifier," and Plotivy writes the scikit-learn and seaborn code for you — annotations and journal export included.

Generate a confusion matrix in Plotivy
Tags:#confusion matrix#scikit-learn#seaborn#python#machine learning

Related chart guides

Apply this tutorial directly in the chart gallery with ready-to-run prompts and examples.

Found this helpful? Share it with your network.

FV
Francesco Villasmunta

Experimental Physicist & Photonics Researcher

Hands-on experience in silicon photonics, semiconductor fabrication (DRIE/ICP-RIE), optical simulation, and data-driven analysis. Built Plotivy to help researchers focus on discoveries instead of data struggles.

More about the author

Visualize your own data

Apply the techniques from this article to your own datasets. Upload CSV, Excel, or paste data directly.

Start Analyzing - Free