Matplotlib: Как использовать одну цветовую шкалу для нескольких подграфиков?

При визуализации данных с помощью Matplotlib часто возникает необходимость отобразить несколько подграфиков (subplots), каждый из которых использует цветовую карту (colormap) для представления значений. Стандартный подход добавления colorbar к каждому подграфику приводит к дублированию шкал, что загромождает визуализацию и затрудняет сравнение данных между графиками, поскольку диапазоны цветов могут не совпадать.

Зачем нужна общая цветовая шкала?

Использование единой цветовой шкалы для группы подграфиков решает несколько задач:

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

Обзор основных подходов к решению проблемы

Существует несколько способов реализовать общую цветовую шкалу в Matplotlib:

  1. Использование matplotlib.cm.ScalarMappable: Создание отдельного объекта, отвечающего за отображение данных в цвета, и привязка colorbar к нему.
  2. Явное задание пределов vmin и vmax: Определение общих минимального и максимального значений для всех подграфиков и передача их в функции отрисовки.
  3. Применение matplotlib.gridspec: Использование более гибкой системы компоновки для явного выделения места под общую цветовую шкалу.

Рассмотрим каждый из этих подходов подробнее.

Способ 1: Использование ScalarMappable и colorbar

Этот метод является наиболее гибким и рекомендуемым. Он заключается в создании объекта ScalarMappable, который инкапсулирует нормализацию данных (mapping data values to the [0, 1] interval) и цветовую карту. Затем colorbar создается на основе этого объекта, а не конкретного подграфика.

Создание экземпляра ScalarMappable

Сначала необходимо определить нормализатор (matplotlib.colors.Normalize) и цветовую карту (cmap). Нормализатор обычно создается на основе глобальных минимального и максимального значений данных (vmin, vmax).

import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.cm as cm
import numpy as np
from typing import List, Tuple

# Пример: Глобальные мин/макс значения для всех данных
global_vmin: float = 0.0
global_vmax: float = 100.0

# Выбор цветовой карты
cmap: mcolors.Colormap = cm.viridis

# Создание нормализатора
norm: mcolors.Normalize = mcolors.Normalize(vmin=global_vmin, vmax=global_vmax)

# Создание ScalarMappable
sm: cm.ScalarMappable = cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([]) # Важно: передаем пустой массив или None

Отображение данных на подграфиках с использованием ScalarMappable

При построении графиков (например, imshow или pcolormesh) необходимо использовать ту же цветовую карту (cmap) и нормализатор (norm), которые были переданы в ScalarMappable.

# Пример данных (имитация тепловых карт эффективности рекламных кампаний)
data1: np.ndarray = np.random.rand(10, 10) * 70
data2: np.ndarray = np.random.rand(10, 10) * 100
data3: np.ndarray = np.random.rand(10, 10) * 50
data4: np.ndarray = np.random.rand(10, 10) * 90

all_data: List[np.ndarray] = [data1, data2, data3, data4]

fig, axes = plt.subplots(2, 2, figsize=(8, 8))

for ax, data in zip(axes.flat, all_data):
    im = ax.imshow(data, cmap=cmap, norm=norm)
    ax.set_title(f'Max Value: {data.max():.2f}')
    ax.set_xticks([])
    ax.set_yticks([])

Добавление colorbar для общей цветовой шкалы

Цветовая шкала добавляется с помощью fig.colorbar(), передавая ей объект ScalarMappable (sm) и указывая оси (ax), рядом с которыми она должна быть размещена.

# Добавление colorbar
# fig.colorbar(sm, ax=axes.ravel().tolist(), shrink=0.6)
# Более гибкий вариант - размещение относительно всей фигуры
cbar_ax = fig.add_axes([0.92, 0.15, 0.03, 0.7]) # [left, bottom, width, height]
fig.colorbar(sm, cax=cbar_ax)

plt.suptitle('Эффективность кампаний (Общая шкала)')
plt.show()

Пример кода с пояснениями

import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.cm as cm
import numpy as np
from typing import List, Tuple, Optional

def plot_multiple_heatmaps_shared_cbar(
    data_list: List[np.ndarray],
    grid_shape: Tuple[int, int],
    cmap_name: str = 'viridis',
    global_min: Optional[float] = None,
    global_max: Optional[float] = None,
    figsize: Tuple[int, int] = (10, 8),
    cbar_rect: List[float] = [0.92, 0.15, 0.03, 0.7]
) -> None:
    """
    Отображает несколько тепловых карт с общей цветовой шкалой.

    Args:
        data_list: Список 2D numpy массивов для отображения.
        grid_shape: Кортеж (rows, cols) для сетки подграфиков.
        cmap_name: Название цветовой карты Matplotlib.
        global_min: Глобальное минимальное значение для шкалы. Если None, вычисляется по данным.
        global_max: Глобальное максимальное значение для шкалы. Если None, вычисляется по данным.
        figsize: Размер фигуры (width, height).
        cbar_rect: Позиция и размер цветовой шкалы [left, bottom, width, height].
    """
    rows, cols = grid_shape
    if len(data_list) > rows * cols:
        raise ValueError("Количество данных превышает размер сетки")

    # Определение глобальных min/max, если они не заданы
    if global_min is None:
        global_min = min(data.min() for data in data_list)
    if global_max is None:
        global_max = max(data.max() for data in data_list)

    # Настройка ScalarMappable
    cmap: mcolors.Colormap = cm.get_cmap(cmap_name)
    norm: mcolors.Normalize = mcolors.Normalize(vmin=global_min, vmax=global_max)
    sm: cm.ScalarMappable = cm.ScalarMappable(cmap=cmap, norm=norm)
    sm.set_array([]) # Обязательно для независимого colorbar

    fig, axes = plt.subplots(rows, cols, figsize=figsize, squeeze=False)

    # Отображение данных
    for i, ax in enumerate(axes.flat):
        if i < len(data_list):
            im = ax.imshow(data_list[i], cmap=cmap, norm=norm)
            ax.set_title(f'Dataset {i+1} (Max: {data_list[i].max():.2f})')
            ax.set_xticks([])
            ax.set_yticks([])
        else:
            ax.axis('off') # Скрыть неиспользуемые оси

    # Добавление общей цветовой шкалы
    cbar_ax = fig.add_axes(cbar_rect)
    fig.colorbar(sm, cax=cbar_ax)

    plt.suptitle(f'Сравнение данных с общей шкалой [{global_min:.1f}, {global_max:.1f}]')
    plt.subplots_adjust(right=0.88, wspace=0.1, hspace=0.2) # Корректировка отступов
    plt.show()

# Пример использования
data1 = np.random.rand(10, 10) * 80 # CTR % * 100
data2 = np.random.rand(10, 10) * 100
data3 = np.random.rand(10, 10) * 65
data4 = np.random.rand(10, 10) * 95

plot_multiple_heatmaps_shared_cbar([data1, data2, data3, data4], grid_shape=(2, 2))

Способ 2: Явное задание пределов цветовой шкалы (vmin, vmax)

Этот подход проще в реализации для стандартных случаев. Он основан на том, что colorbar автоматически определяет свой диапазон на основе параметров vmin и vmax, переданных в функцию отрисовки (например, imshow).

Реклама

Определение минимального и максимального значений для всех подграфиков

Перед построением графиков необходимо вычислить единые минимальное (vmin) и максимальное (vmax) значения по всем наборам данных, которые будут отображаться.

import numpy as np
from typing import List

# Пример данных (конверсии по регионам)
conversions_region_a: np.ndarray = np.random.rand(5, 5) * 5 + 1 # Конверсии от 1% до 6%
conversions_region_b: np.ndarray = np.random.rand(5, 5) * 3 + 0.5 # Конверсии от 0.5% до 3.5%
all_conversions: List[np.ndarray] = [conversions_region_a, conversions_region_b]

# Вычисление глобальных min/max
global_vmin: float = min(data.min() for data in all_conversions)
global_vmax: float = max(data.max() for data in all_conversions)

print(f"Global Min: {global_vmin:.2f}, Global Max: {global_vmax:.2f}")

Передача vmin и vmax в функции построения графиков

При вызове imshow, pcolormesh, scatter (с параметром c) и других подобных функций необходимо явно указать вычисленные vmin и vmax.

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
cmap_name = 'plasma'

im1 = axes[0].imshow(conversions_region_a, cmap=cmap_name, vmin=global_vmin, vmax=global_vmax)
axes[0].set_title('Регион А')

im2 = axes[1].imshow(conversions_region_b, cmap=cmap_name, vmin=global_vmin, vmax=global_vmax)
axes[1].set_title('Регион Б')

for ax in axes:
    ax.set_xticks([])
    ax.set_yticks([])

Добавление colorbar

Теперь colorbar можно добавить, привязав его к любому из объектов AxesImage (результат вызова imshow), поскольку все они используют одинаковые vmin, vmax и cmap. Matplotlib автоматически создаст правильную шкалу.

# Добавляем colorbar, ссылаясь на последний AxesImage (im2), но можно и на im1
# Используем fig.colorbar для размещения относительно фигуры
cbar_ax = fig.add_axes([0.93, 0.15, 0.02, 0.7])
fig.colorbar(im2, cax=cbar_ax, label='Уровень конверсии (%)')

plt.suptitle('Сравнение конверсий по регионам')
plt.subplots_adjust(right=0.9) # Оставляем место для colorbar
plt.show()

Преимущества и недостатки данного подхода

  • Преимущества:
    • Простота реализации: не требует создания ScalarMappable.
    • Интуитивно понятен.
  • Недостатки:
    • Требует предварительного вычисления vmin и vmax по всем данным.
    • Менее гибок, если требуется сложная настройка нормализации или цветовой карты.

Способ 3: Использование matplotlib.gridspec для более сложной компоновки

GridSpec предоставляет более мощный контроль над расположением подграфиков и других элементов фигуры, чем стандартный plt.subplots. Это позволяет явно выделить область для colorbar.

Настройка компоновки с помощью GridSpec

GridSpec делит фигуру на сетку, и вы можете указать, какие ячейки этой сетки будет занимать каждый подграфик и colorbar.

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib.cm as cm
import matplotlib.colors as mcolors

# Данные и параметры шкалы (как в Способе 1)
data1 = np.random.rand(10, 10) * 70
data2 = np.random.rand(10, 10) * 100
data3 = np.random.rand(10, 10) * 50
data4 = np.random.rand(10, 10) * 90
all_data = [data1, data2, data3, data4]
global_vmin, global_vmax = 0.0, 100.0
cmap = cm.viridis
norm = mcolors.Normalize(vmin=global_vmin, vmax=global_vmax)
sm = cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])

fig = plt.figure(figsize=(10, 8))

# Создаем GridSpec: 2 строки, 2 колонки для графиков + 1 колонка для colorbar
# width_ratios управляет относительной шириной колонок
gs = gridspec.GridSpec(2, 3, width_ratios=[1, 1, 0.1], wspace=0.1, hspace=0.2)

# Создаем оси для подграфиков
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])
ax3 = fig.add_subplot(gs[1, 0])
ax4 = fig.add_subplot(gs[1, 1])
axes_list = [ax1, ax2, ax3, ax4]

# Создаем ось для colorbar, занимающую обе строки в последней колонке
cbar_ax = fig.add_subplot(gs[:, 2])

Размещение подграфиков и цветовой шкалы

Отрисовка данных происходит как обычно, а colorbar размещается в специально выделенной оси (cbar_ax).

# Отображение данных
for ax, data in zip(axes_list, all_data):
    im = ax.imshow(data, cmap=cmap, norm=norm)
    ax.set_title(f'Max: {data.max():.1f}')
    ax.set_xticks([])
    ax.set_yticks([])

# Добавление colorbar в выделенную ось
fig.colorbar(sm, cax=cbar_ax)

plt.suptitle('Использование GridSpec для общей шкалы')
plt.show()

Преимущества использования GridSpec

  • Точный контроль: Позволяет точно позиционировать colorbar относительно подграфиков.
  • Сложные макеты: Идеально подходит для неравномерных сеток или когда нужно встроить colorbar внутрь макета сложным образом.
  • Явное разделение: Четко отделяет пространство для графиков от пространства для шкалы.

Рекомендации и заключение

Выбор подходящего метода в зависимости от задачи

  • ScalarMappable (Способ 1): Наиболее универсальный и рекомендуемый подход. Используйте его, если нужна гибкость, сложная нормализация или вы хотите явно управлять объектом, представляющим шкалу.
  • Явные vmin/vmax (Способ 2): Хороший выбор для простых случаев, когда достаточно задать общие пределы и использовать стандартные функции отрисовки. Требует предварительного расчета пределов.
  • GridSpec (Способ 3): Используйте, когда требуется точный контроль над компоновкой фигуры, особенно для сложных или нерегулярных макетов подграфиков.

Часто ScalarMappable используется совместно с GridSpec или ручным размещением оси для colorbar (fig.add_axes), чтобы добиться наилучшего результата.

Дополнительные возможности настройки цветовой шкалы

Объект colorbar имеет множество параметров для настройки внешнего вида:

  • label: Добавление подписи к шкале.

  • orientation: Изменение ориентации (‘vertical’ или ‘horizontal’).

  • shrink, aspect: Управление размером шкалы.

  • ticks: Явное задание позиций делений.

  • extend: Позволяет указать стрелками на шкале, что данные выходят за пределы vmin/vmax (значения ‘min’, ‘max’, ‘both’, ‘neither’). Это особенно полезно, когда нужно показать наличие выбросов, не искажая основную шкалу.

    # Пример использования extend
    norm_extended = mcolors.Normalize(vmin=10, vmax=90)
    sm_extended = cm.ScalarMappable(cmap=cmap, norm=norm_extended)
    sm_extended.set_array([])
    # ... (plotting code using norm_extended)
    # fig.colorbar(sm_extended, cax=cbar_ax, label='Значение (с обрезкой)', extend='both')
    

Полезные ресурсы

Для более глубокого изучения рекомендуется обратиться к официальной документации Matplotlib, в частности к разделам, посвященным colorbar, ScalarMappable, Normalize и GridSpec. Примеры в галерее Matplotlib также могут быть очень полезны для понимания различных техник визуализации.

Создание единой цветовой шкалы — важный навык для представления сложных данных в понятной и профессиональной манере. Выбор правильного метода зависит от конкретной задачи и желаемого уровня контроля над визуализацией.


Добавить комментарий