2025, Oct 15 21:21

Две взвешенные 2D‑гистограммы в Matplotlib с одной цветовой шкалой без ловушек pyplot

Показываем, как в Matplotlib корректно построить две взвешенные 2D‑гистограммы с общей цветовой шкалой: работа с Axes вместо pyplot, общая нормализация.

Когда вы размещаете две 2D‑гистограммы рядом и хотите одну общую цветовую шкалу, легко столкнуться с запутанным поведением, если смешивать явный интерфейс Axes в matplotlib с неявной машиной состояний pyplot. Самый частый симптом — один подграфик остаётся пустым, тогда как на другом появляется ваш график, а любая логика цветовой шкалы отказывается работать согласованно.

Постановка задачи

Нужно прочитать CSV, получить день недели и номер недели года, а затем изобразить две взвешенные 2D‑гистограммы в одном ряду, чтобы сравнить, как два показателя меняются в течение года, с одной общей цветовой шкалой.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from datetime import datetime

weekday_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

dataset = pd.read_csv("Mod4Data.csv", nrows=366)

# Выделяем компоненты даты
dataset["Date"] = dataset["Day"].apply(pd.to_datetime)
dataset["Day of Week"] = dataset["Date"].dt.dayofweek
dataset['week_of_year'] = dataset['Date'].dt.strftime('%W')
dataset['Month'] = dataset['Date'].dt.month

# Приводим к числовым типам
dataset['Value1'] = dataset['Value1'].astype(float)
dataset['Value2'] = dataset['Value2'].astype(float)
dataset['week_of_year'] = dataset['week_of_year'].astype(float)

canvas, axes_grid = plt.subplots(1, 2, figsize=(18, 10))

# Попытка построения левого графика
axes_grid[0] = plt.hist2d(
    dataset["Day of Week"],
    dataset["week_of_year"],
    bins=[7, 52],
    weights=dataset["Value1"],
    vmin=1000,
    vmax=20000,
)
plt.title("Value 1 Amounts")
plt.yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63], month_names)
plt.xticks([0, 1, 2, 3, 4, 5, 6], weekday_names, rotation=45)

# Попытка построения правого графика
second_hist = plt.hist2d(
    dataset["Day of Week"],
    dataset["week_of_year"],
    bins=[7, 52],
    weights=dataset["Value2"],
    vmin=1000,
    vmax=20000,
)
plt.title("Value 2 Amounts")
plt.yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63], month_names)
plt.xticks([0, 1, 2, 3, 4, 5, 6], weekday_names, rotation=45)

Что идёт не так и почему

Есть две отдельные проблемы. Во‑первых, код переключается между явным подходом с Axes, который возвращает plt.subplots, и неявным API pyplot, рисующим на текущих осях. Присваивая результат plt.hist2d элементу axes_grid[0], вы перезаписываете объект Axes кортежем, который возвращает hist2d, из‑за чего исходный левый подграфик теряется. После этого вызовы pyplot вроде plt.title и второй plt.hist2d рисуют там, где на тот момент «текущие» оси, а не в запланированных подграфиках.

Во‑вторых, одна общая цветовая шкала требует корректного mappable из одной из гистограмм. Для hist2d это четвёртое возвращаемое значение. Общая цветовая шкала также предполагает, что обе визуализации одинаково переводят числа в цвета, чего и добиваются за счёт общей нормализации.

Исправление компоновки и цветовой шкалы

Надёжный подход — на всём протяжении использовать явный интерфейс Axes, сохранить mappable из hist2d и применить общую нормализацию к обоим графикам, чтобы одна цветовая шкала была корректной.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.colors import Normalize

weekday_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

# Ввод и подготовка признаков
frame = pd.read_csv("Mod4Data.csv", nrows=366)
frame["Date"] = frame["Day"].apply(pd.to_datetime)
frame["Day of Week"] = frame["Date"].dt.dayofweek
frame['week_of_year'] = frame['Date'].dt.strftime('%W')
frame['Month'] = frame['Date'].dt.month

frame['Value1'] = frame['Value1'].astype(float)
frame['Value2'] = frame['Value2'].astype(float)
frame['week_of_year'] = frame['week_of_year'].astype(float)

# Общая нормализация обеспечивает единое отображение цветов на всех подграфиках
shared_norm = Normalize(vmin=1000, vmax=20000)

fig, ax_array = plt.subplots(nrows=1, ncols=2, figsize=(18, 10), layout="constrained")

# Левый тепловой график
_, _, _, img_left = ax_array[0].hist2d(
    frame["Day of Week"],
    frame["week_of_year"],
    bins=[7, 52],
    weights=frame["Value1"],
    norm=shared_norm,
)
ax_array[0].set_title("Value 1 Amounts")
ax_array[0].set_yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63])
ax_array[0].set_yticklabels(month_names)
ax_array[0].set_xticks([0, 1, 2, 3, 4, 5, 6])
ax_array[0].set_xticklabels(weekday_names, rotation=45)

# Правый тепловой график
_, _, _, img_right = ax_array[1].hist2d(
    frame["Day of Week"],
    frame["week_of_year"],
    bins=[7, 52],
    weights=frame["Value2"],
    norm=shared_norm,
)
ax_array[1].set_title("Value 2 Amounts")
ax_array[1].set_yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63])
ax_array[1].set_yticklabels(month_names)
ax_array[1].set_xticks([0, 1, 2, 3, 4, 5, 6])
ax_array[1].set_xticklabels(weekday_names, rotation=45)

# Одна общая цветовая шкала на основе одного mappable, применена к обеим осям
plt.colorbar(img_right, ax=ax_array, location="top")

plt.show()

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

Соблюдение единого стиля работы с API помогает избежать скрытых ошибок состояния, которые трудно заметить и ещё сложнее воспроизвести. Если нужна одна цветовая шкала для нескольких гистограмм, отображение чисел в цвета должно быть идентичным, иначе один и тот же цвет будет означать разные значения на каждом подграфике. Общая нормализация гарантирует эту согласованность, а передача единственного mappable цветовой шкале делает намерение очевидным.

Выводы

Используйте объекты Axes, которые возвращает plt.subplots, и вызывайте методы построения на них, не возвращаясь к неявному интерфейсу pyplot. Не перезаписывайте ссылки на Axes кортежами, которые возвращают функции построения. Для общей цветовой шкалы передайте mappable, который возвращает hist2d, и примените общую нормализацию, чтобы обе панели одинаково переводили данные в цвета. Обращаясь за помощью, приводите минимально воспроизводимый пример с явным показом фигуры — так и проблема, и решение становятся очевидными.

Статья основана на вопросе с StackOverflow от Jen Reif и ответе Liris.