2025, Sep 28 07:16
Как зафиксировать логарифмическую шкалу в тепловой карте: LogNorm с vmin и vmax
Почему цвета в логарифмической heatmap смещаются и как это исправить: задайте LogNorm с vmin=1 и vmax=1e9, чтобы цвет соответствовал порядку величины.
При построении тепловой карты в логарифмическом масштабе часто хочется, чтобы каждый цвет соответствовал одному порядку величины: 10^0, 10^1, 10^2 и так далее. Если же цветовое соответствие выглядит смещённым или «сжатым», даже при корректном числе дискретных цветов в палитре, причина обычно в том, как нормализация выводит диапазон данных.
Постановка задачи
Данные охватывают примерно от 10^0 до значений заметно ниже 10^9, а цель — дискретная логарифмическая шкала, где один цвет — один порядок. Ниже — минимальный пример кода построения, который даёт неожиданное отображение:
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
df_src = pd.read_excel('example_data.xlsx')
palette_rocket = sns.color_palette("rocket", as_cmap=False, n_colors=9)
canvas, axes = plt.subplots(figsize=(20, 10))
axes = sns.heatmap(
    df_src,
    norm=LogNorm(),
    annot=True,
    cmap=palette_rocket,
    linewidths=0.05,
    linecolor='grey'
)
plt.tight_layout()
plt.show()Нижняя граница цветовой шкалы начинается с 10^0, как и задумано, но следующие цвета не совпадают с 10^1, 10^2 и т. д. В подписях значения вроде ~6.3×10^5 и ~9.2×10^5 могут окрашиваться тем же цветом, что и 1×10^6, тогда как 5×10^6 внезапно попадает в другой цвет — хотя все они одного порядка величины.
Почему отображение сбивается
По умолчанию нормализация привязана к данным. Если вы используете логарифмический нормализатор без явных границ, он берёт диапазон из минимума и максимума данных — фактически как будто задано vmin=data.min() и vmax=data.max(). Если набор не дотягивается до 10^9, логарифмическая шкала подстраивается под меньший интервал, и доступные цвета распределяются по этому выведенному диапазону. Поэтому границы цветов не совпадают с аккуратными степенями десятки.
В коде построения нигде не указано, что шкала должна идти строго от 10^0 до 10^9. Задано лишь «использовать 9 цветов в лог-масштабе». Нормализатор не может «догадаться» о целевом диапазоне, если данные его не охватывают.
Решение: зафиксировать домен нормализации
Если нужно, чтобы каждый цвет соответствовал конкретному порядку величины, явно задайте нормализатору «притвориться», что данные занимают требуемый диапазон. Установите границы для логарифмического нормализатора. Поскольку нормализатор отвечает за преобразование данных в интервал [0, 1], передавайте vmin и vmax в LogNorm, а не в сам вызов heatmap.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
df_src = pd.read_excel('example_data.xlsx')
palette_rocket = sns.color_palette("rocket", as_cmap=False, n_colors=9)
canvas, axes = plt.subplots(figsize=(20, 10))
axes = sns.heatmap(
    df_src,
    norm=LogNorm(vmin=1, vmax=1e9),
    annot=True,
    cmap=palette_rocket,
    linewidths=0.05,
    linecolor='grey'
)
plt.tight_layout()
plt.show()Так вы фиксируете дискретное соответствие «по порядкам» от 10^0 до 10^9 — независимо от того, дотягиваются ли данные до верхней границы. Если же выставить vmin=data.min() и vmax=data.max(), получится лишь воспроизвести поведение по умолчанию, зависящее от текущих данных.
Почему это важно
Цвет — это смысл. Если цвет должен означать «значение попадает в заданную декаду логарифмической шкалы», нужен стабильный домен, который не смещается незаметно вслед за текущими минимумом и максимумом. Иначе два графика с одной и той же палитрой могут предполагать разные пороги, и сравнения станут ненадёжными.
Итоги
Если ваша цель — один цвет на каждый порядок, не полагайтесь на неявную нормализацию. Задайте намерение явно: укажите логарифмический домен через LogNorm(vmin=10^a, vmax=10^b). Это исключит неожиданные границы цветов и удержит соответствие по степеням десяти. И помните: границы нужно передавать нормализатору; попытка задать их в вызове heatmap не переопределит пользовательский norm, который уже управляет отображением.