2025, Nov 02 17:00
How to Plot a Black-and-White Confusion Matrix in Matplotlib (with scikit-learn)
Learn to render a black-and-white confusion matrix in matplotlib using a binary colormap and identity mask, with scikit-learn data and readable value labels.
ConfusionMatrixDisplay in scikit-learn is great for a quick visualization, but its default colormap is continuous and colorful. If your requirement is a strictly black-and-white look where the diagonal cells are black and the off-diagonal cells are white, the out-of-the-box plot won’t give you that. Here’s a precise way to build a binary confusion matrix view in matplotlib without losing readability of the numbers.
Baseline code that produces the default colored image
The typical snippet looks like this and renders a colored heatmap:
from sklearn.metrics import ConfusionMatrixDisplay as CMDisplay
import matplotlib.pyplot as plt
mx = cm # your confusion matrix
viewer = CMDisplay(confusion_matrix=mx)
viewer.plot()
plt.show()
What’s going on and why the colors won’t match the requirement
The default ConfusionMatrixDisplay uses a colormap that encodes values continuously. That’s useful for gradients, but it doesn’t map to a strict two-color scheme. To lock the diagonal to black and everything else to white, you need a discrete binary colormap and a matrix that encodes where black and white should be applied. The simplest way to encode that layout is an identity matrix of the same size as the confusion matrix: ones on the diagonal and zeros elsewhere.
Solution: binary colormap + identity mask
Create a custom ListedColormap with two colors and feed imshow a mask matrix aligned with your confusion matrix. Then draw the numeric values on top, switching text color so they stay legible on either background.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import ListedColormap as LMap
# example data
data_mx = np.array([[12, 1],
[ 2,10]])
def render_bw_confusion(mtx, names=None):
palette = LMap(['white', 'black'])
mask_grid = np.eye(mtx.shape[0], dtype=int)
fig_obj, ax_obj = plt.subplots(figsize=(6, 5))
img = ax_obj.imshow(mask_grid, cmap=palette, aspect='equal', vmin=0, vmax=1)
for r in range(mtx.shape[0]):
for c in range(mtx.shape[1]):
tone = 'white' if r == c else 'black'
ax_obj.text(c, r, str(mtx[r, c]), ha='center', va='center',
color=tone, fontsize=16, fontweight='bold')
if names is None:
names = range(mtx.shape[0])
ax_obj.set_xticks(range(mtx.shape[1]))
ax_obj.set_yticks(range(mtx.shape[0]))
ax_obj.set_xticklabels(names)
ax_obj.set_yticklabels(names)
ax_obj.set_xlabel('Predicted label')
ax_obj.set_ylabel('True label')
plt.tight_layout()
plt.show()
render_bw_confusion(data_mx)
In this setup, the identity matrix drives the coloring, and the confusion matrix values are placed as text over the corresponding cells. The diagonal text is white to contrast with the black background; off-diagonal text is black to contrast with white.
Mapping: 0 → white (off-diagonal), 1 → black (diagonal)
For reference on color mapping mechanics, see the official matplotlib docs: matplotlib color mapping.
Why this detail matters
Visual conventions are not cosmetic. In many reports and papers, the diagonal implies correctness, and a stark black diagonal instantly communicates performance without the cognitive load of interpreting a gradient. It also prints cleanly in black-and-white documents and avoids ambiguity when viewed with colorblindness constraints or low-contrast screens.
Takeaways
If you need a binary visual separation in a confusion matrix, don’t fight the default heatmap. Instead, explicitly control the colormap and feed a binary mask to imshow. Keep the numbers readable by inverting the text color per cell, and set axis ticks and labels to match your classes. This small change results in a deterministic, publication-ready figure that preserves meaning across formats.