2025, Oct 03 19:17

Бинарные контуры в Matplotlib: как настроить BoundaryNorm и colorbar

Разбираем, почему в Matplotlib при бинарных контурах и пороге сбивается colorbar из-за BoundaryNorm, и показываем корректную настройку с двумя корзинами.

Когда нужна бинарная визуализация контуров в Matplotlib — синим для значений ниже порога и красным для значений выше — логично взять дискретную цветовую карту с двумя «корзинами» и добавить цветовую шкалу для наглядности. Однако если на шкале неожиданно появляются белые участки или даже лишняя метка, которой вы не просили, чаще всего виновата настройка BoundaryNorm.

Как воспроизвести проблему

Пример ниже строит интерполированную поверхность по рассеянным точкам, запрашивает двухцветные контуры с порогом и добавляет цветовую шкалу. График выглядит нормально, но colorbar в итоге показывает белый и синий ниже порога, затем белый выше него и даже добавляет загадочную четвёртую метку.

from matplotlib import colors as mpl_colors, cm as mpl_cm
import numpy as nps
import matplotlib.pyplot as pl
import matplotlib.tri as mtri
nps.random.seed(19680801)
pt_count = 200
grid_x = 100
grid_y = 200
x_vals = nps.random.uniform(-2, 2, pt_count)
y_vals = nps.random.uniform(-2, 2, pt_count)
z_vals = x_vals * nps.exp(-x_vals**2 - y_vals**2)
x_lin = nps.linspace(-2.1, 2.1, grid_x)
y_lin = nps.linspace(-2.1, 2.1, grid_y)
tri_obj = mtri.Triangulation(x_vals, y_vals)
lin_interp = mtri.LinearTriInterpolator(tri_obj, z_vals)
Xg, Yg = nps.meshgrid(x_lin, y_lin)
z_grid = lin_interp(Xg, Yg)
fig_obj = pl.figure()
axis = fig_obj.subplots()
palette = ['blue', 'red']
cutoff = nps.mean(z_vals)
normer = mpl_colors.BoundaryNorm([cutoff - 1e-3, cutoff], ncolors=2, clip=True)
cmap_obj = mpl_colors.LinearSegmentedColormap.from_list('name', palette, N=2)
axis.contour(x_lin, y_lin, z_grid, norm=normer, cmap=cmap_obj)
scalar = mpl_cm.ScalarMappable(norm=normer, cmap=cmap_obj)
bar = fig_obj.colorbar(scalar, ax=axis)
bar.set_ticks([nps.min(z_grid), cutoff, nps.max(z_grid)])
bar.set_ticklabels([nps.min(z_grid), cutoff, nps.max(z_grid)])

Что на самом деле не так

BoundaryNorm делит диапазон данных на корзины, заданные явными границами. Если вам нужны ровно два цвета, нужны две корзины — а это три граничных значения. Подача только значений вокруг порога приводит к неверному разбиению. В результате цветовая шкала не отражает чистое разделение по порогу, могут появиться неожиданные «пробелы» и лишняя метка.

Решение

Задайте границы корзин так, чтобы они покрывали весь диапазон данных, а порог находился посередине. Тогда у вас получится две корзины: [минимальное значение, порог] и [порог, максимальное значение].

from matplotlib import colors as mpl_colors, cm as mpl_cm
import numpy as nps
import matplotlib.pyplot as pl
import matplotlib.tri as mtri
nps.random.seed(19680801)
pt_count = 200
grid_x = 100
grid_y = 200
x_vals = nps.random.uniform(-2, 2, pt_count)
y_vals = nps.random.uniform(-2, 2, pt_count)
z_vals = x_vals * nps.exp(-x_vals**2 - y_vals**2)
x_lin = nps.linspace(-2.1, 2.1, grid_x)
y_lin = nps.linspace(-2.1, 2.1, grid_y)
tri_obj = mtri.Triangulation(x_vals, y_vals)
lin_interp = mtri.LinearTriInterpolator(tri_obj, z_vals)
Xg, Yg = nps.meshgrid(x_lin, y_lin)
z_grid = lin_interp(Xg, Yg)
fig_obj = pl.figure()
axis = fig_obj.subplots()
palette = ['blue', 'red']
cutoff = nps.mean(z_vals)
normer = mpl_colors.BoundaryNorm([nps.min(z_vals), cutoff, nps.max(z_vals)], ncolors=2, clip=True)
cmap_obj = mpl_colors.LinearSegmentedColormap.from_list('name', palette, N=2)
axis.contour(x_lin, y_lin, z_grid, norm=normer, cmap=cmap_obj)
scalar = mpl_cm.ScalarMappable(norm=normer, cmap=cmap_obj)
bar = fig_obj.colorbar(scalar, ax=axis)
bar.set_ticks([nps.min(z_grid), cutoff, nps.max(z_grid)])
bar.set_ticklabels([nps.min(z_grid), cutoff, nps.max(z_grid)])

Так мы задаём две границы корзин: [минимальное значение, среднее значение] и [среднее значение, максимальное значение], и цветовая карта с цветовой шкалой выстраиваются в соответствии с задуманной бинарной логикой порога.

Почему это важно

Бинарные пороги — распространённый способ подсветить аномалии, флаги или границы решений. Если цветовая шкала не согласована с семантикой графика, есть риск неверно прочитать порог или донести противоречивое состояние до пользователей дальше по цепочке. Корректные границы корзин сохраняют верность визуализации исходному правилу.

Итоги

Когда используете двухцветные контуры, разделённые порогом, всегда передавайте в BoundaryNorm три границы, покрывающие весь диапазон данных, с порогом в середине. Это обеспечивает согласованность графика и цветовой шкалы и делает замысел визуализации очевидным.

Статья основана на вопросе с StackOverflow от Auguste Eclancher и ответе Matt Pitkin.