Plotly
Confusion Matrix
Heatmap
Data Visualization
Python

Plotly How to make an annotated confusion matrix using a heatmap?

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

An annotated confusion matrix is just a heatmap with the cell values written on top of the colors. Plotly is a good fit for this because it gives you hover labels, custom color scales, and easy axis labeling without much boilerplate.

The main tasks are straightforward: compute the confusion matrix, pass it to a heatmap trace, and provide text annotations so each cell shows the exact count or normalized value.

Build the Matrix from Predictions

Start by computing the confusion matrix from your true and predicted labels.

python
1from sklearn.metrics import confusion_matrix
2
3true = ["cat", "dog", "dog", "cat", "cat", "dog"]
4pred = ["cat", "dog", "cat", "cat", "dog", "dog"]
5labels = ["cat", "dog"]
6
7cm = confusion_matrix(true, pred, labels=labels)
8print(cm)

That produces a two-dimensional array that Plotly can render directly.

If you want percentages instead of raw counts, normalize first:

python
1import numpy as np
2
3cm_normalized = cm.astype(float) / cm.sum(axis=1, keepdims=True)
4cm_normalized = np.nan_to_num(cm_normalized)

Counts and normalized values are both useful. Counts show absolute errors, while normalized values make class imbalance easier to see.

Create an Annotated Heatmap with Plotly

The simplest modern pattern is to use plotly.graph_objects.Heatmap and provide a text matrix plus texttemplate.

python
1import plotly.graph_objects as go
2
3fig = go.Figure(
4    data=go.Heatmap(
5        z=cm,
6        x=labels,
7        y=labels,
8        colorscale="Blues",
9        text=cm,
10        texttemplate="%{text}",
11        textfont={"color": "black", "size": 16},
12        hovertemplate="Actual: %{y}<br>Predicted: %{x}<br>Count: %{z}<extra></extra>",
13    )
14)
15
16fig.update_layout(
17    title="Confusion Matrix",
18    xaxis_title="Predicted label",
19    yaxis_title="Actual label",
20)
21
22fig.update_yaxes(autorange="reversed")
23fig.show()

Two details matter here:

  • the y-axis is reversed so the first actual label appears at the top in the conventional matrix layout
  • 'texttemplate writes the annotation directly inside each cell'

This is often cleaner than manually placing one annotation object per cell.

Show Counts and Percentages Together

A common improvement is to show both values in the same cell.

python
1text = [
2    [f"{count}<br>{pct:.1%}" for count, pct in zip(row_count, row_pct)]
3    for row_count, row_pct in zip(cm, cm_normalized)
4]
5
6fig = go.Figure(
7    data=go.Heatmap(
8        z=cm_normalized,
9        x=labels,
10        y=labels,
11        colorscale="Blues",
12        text=text,
13        texttemplate="%{text}",
14        hovertemplate="Actual: %{y}<br>Predicted: %{x}<br>Rate: %{z:.2%}<extra></extra>",
15    )
16)
17
18fig.update_layout(
19    title="Normalized Confusion Matrix",
20    xaxis_title="Predicted label",
21    yaxis_title="Actual label",
22)
23fig.update_yaxes(autorange="reversed")
24fig.show()

This is especially useful in multiclass problems where raw counts alone can hide the relative quality of each row.

Improve Readability for Larger Label Sets

As the number of classes grows, readability becomes the main challenge.

Helpful layout tweaks:

python
1fig.update_layout(
2    width=800,
3    height=700,
4    margin={"l": 120, "r": 40, "t": 80, "b": 120},
5)
6fig.update_xaxes(tickangle=45)

For long class names, rotate the x-axis labels and widen the figure rather than shrinking the font until it becomes unreadable.

If the matrix is very large, consider interactive hover-only values with lighter in-cell text, or show only normalized values in the text layer.

Common Pitfalls

The most common mistake is mixing up the axes. Be explicit about whether rows are actual labels and columns are predicted labels, then label the axes accordingly.

Another common issue is forgetting to reverse the y-axis. Plotly heatmaps start from the bottom by default, which can make the matrix look upside down compared with the usual machine learning presentation.

People also normalize incorrectly. If you want per-class recall-style normalization, divide each row by its row sum. If you want global proportions, divide by the full matrix sum. Those are different visual stories.

Finally, avoid adding dozens of manual annotation objects unless you need very custom placement. text plus texttemplate is simpler and easier to maintain.

Summary

  • Compute the confusion matrix first, then pass it directly to a Plotly heatmap.
  • Use text and texttemplate to annotate each cell.
  • Reverse the y-axis for the conventional confusion matrix layout.
  • Decide whether you want raw counts, normalized values, or both.
  • Adjust figure size and tick labels for multiclass readability.
  • Be explicit about which axis is actual and which is predicted.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.