2025, Dec 16 09:01

Как сохранять и загружать violinplot в Matplotlib без повторной обработки данных

Как сохранить контейнеры violinplot в Matplotlib и потом быстро оформлять фигуры без пересчёта: единый массив Artists, numpy.save и allow_pickle для многопанельных графиков.

Когда вы строите многопанельные виолин-плоты по большим наборам данных моделей в netCDF4, естественно хотеть быстрый цикл обратной связи по компоновке и оформлению — без повторной обработки гигабайт данных. Простая идея — сохранить объекты виолин на диск и позже подгрузить их для стилизации — действительно работает, но только если сохранять и загружать их так, чтобы сохранялась связь, с которой Matplotlib прикрепляет эти объекты к фигурам и осям.

Обзор проблемы

Конвейер разбирает многогигабайтные поля «время–широта–долгота», вычисляет региональные выборки и рисует много виолин в каждой панели. Один прогон может занимать часы. Цель — сериализовать созданные объекты виолин-плотов, а затем позже вносить правки в фигуру (отступы, шрифты, цвета, подписи) без пересчета базовой статистики и без повторной загрузки больших данных. Наивная попытка вызвать numpy.save для каждого контейнера виолин из Matplotlib дала объект, который выглядит как словарь коллекций Matplotlib, но не готов к использованию между запусками, и правки макета перестают работать как ожидается.

Минимальный код, воспроизводящий проблему

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

import matplotlib.pyplot as plt
import numpy as np

# Создаем многопанельную фигуру
canvas, ax_stack = plt.subplots(2, 1, figsize=(4.8, 6.4))

for axis in ax_stack:
    block = np.random.normal(size=(10, 3))
    vpkg = axis.violinplot(block)
    # Сохраняем виолины одной панели изолированно
    np.save('panel_0.npy', vpkg)

# На этом этапе последующее оформление в другом процессе не будет знать
# как обрабатывать их как части одной и той же фигуры.

Что происходит на самом деле

Объекты, которые возвращает Matplotlib с помощью violinplot, — это Artists, принадлежащие конкретной фигуре и осям. Если сохранять их по панелям, при загрузке вы получите коллекции, оторванные от общего контекста фигуры. Поэтому правки всего макета после перезагрузки применяются несогласованно. Отсюда два практических вывода. Во‑первых, сериализуйте все контейнеры виолин вместе, чтобы после загрузки можно было единообразно обходить и оформлять их. Во‑вторых, загруженные Artists принадлежат новой инстанции фигуры, а не той исходной, которую вы рисовали перед сохранением; это ожидаемо. Если не хотите видеть обе версии при вызове plt.show, явно закройте исходную фигуру.

Рабочий подход: сохраняйте все объекты виолин вместе, затем загружайте и оформляйте

Ниже показан рабочий шаблон для Matplotlib v3.10.3 и numpy v2.3.1. Он строит фигуру, собирает контейнеры виолин в один массив NumPy с dtype=object, сохраняет его, затем загружает с allow_pickle=True и применяет цвета и заголовки к загруженным объектам. Техника масштабируется на большее число панелей и более крупные фигуры.

import matplotlib.pyplot as plt
import numpy as np

# Строим фигуру с двумя осями, расположенными друг над другом
fig_box, grid_axes = plt.subplots(nrows=2, figsize=(4.8, 6.4))

# Массив объектов для хранения всех контейнеров виолин с каждой оси
artist_bundle = np.empty((2,), dtype=object)

# Рисуем и собираем
for idx, axh in enumerate(grid_axes):
    matrix = np.random.normal(size=(10, 3))
    vpack = axh.violinplot(matrix)
    artist_bundle[idx] = vpack

# Сохраняем все виолины вместе
np.save('all_violins.npy', artist_bundle)

# При желании спрячьте исходную фигуру в этой же сессии
# plt.close(fig_box)

# --- Позже, в отдельном запуске для оформления ---

loaded = np.load('all_violins.npy', allow_pickle=True)

# Постфактум-оформление: перекрасить и задать заголовки для загруженных объектов
for bloc, tint, label in zip(loaded, ['tab:purple', 'tab:pink'], ['foo', 'bar']):
    for key, coll in bloc.items():
        if key == 'bodies':
            for poly in coll:
                poly.set_facecolor(tint)
        else:
            coll.set_color(tint)

    # Получаем оси через атрибут .axes любого объекта
    axh = bloc['bodies'][0].axes
    axh.set_title(label.title())

# Получаем новую фигуру из одной из осей и сохраняем
final_fig = axh.figure
final_fig.savefig('violin_layout.png')

plt.show()

Это дает нужное разделение: тяжелые вычисления выполняются один раз, вы сериализуете контейнеры объектов, а последующие сеансы оформления лишь загружают и настраивают эти объекты. Все действия после сохранения можно выполнять независимо. Если не хотите видеть исходную и загруженную фигуры одновременно, перед вторым этапом закройте исходную с помощью plt.close(fig_box).

Зачем это важно

Когда важна каждая точка данных и нельзя ни обрезать, ни переcэмплировать, этап рендеринга неизбежно дорог. Разделение собственно рисования и оформления дает быстрый цикл принятия решений по компоновке, сохраняя точность по отношению ко всему набору данных. Совместное сохранение всех контейнеров виолин удерживает контекст между панелями, чтобы легенды, цвета и заголовки согласованно применялись после перезагрузки.

Заключение

Для больших многопанельных статистических графиков используйте объекты Matplotlib (Artists) как границу сериализации. Сохраняйте их вместе в одном массиве объектов, загружайте с allow_pickle, обращайтесь к компонентам через словареподобный интерфейс, который возвращает violinplot, и получайте оси через атрибут axes любого «тела». Если держать объекты сгруппированными, можно быстро перебирать варианты отступов, шрифтов и цветов, не оплачивая каждый раз пересчет распределений.