Руководство по подфигурам Matplotlib для создания сложных многоэтажных фигур

Мастеркласс по созданию сложных многоуровневых фигур с помощью Matplotlib

Subfigures — мощный инструмент для красивых многооконных графиков

Мотивация

Сложные (научные) графики часто состоят из нескольких графиков разных размеров или аннотаций. Если вы работаете с экосистемой matplotlib/seaborn, есть много способов создания сложных графиков, например, с использованием gridspec. Однако это может стать вызовом, особенно если вы хотите интегрировать многоосевые графики seaborn, такие как jointplot или pairgrid, в свою фигуру, потому что они не имеют возможности предоставлять оси в качестве входных параметров. Но есть другой способ собрать фигуры в matplotlib, а не просто работать с подграфиками: Subfigures. Мощный фреймворк для создания многооконных фигур, подобных этой:

Цель статьи — показать вам, как сделать эту фигуру.

В этой статье я расскажу о субфигурах и их возможностях. Мы объединим субфигуры с подграфиками и gridspecs, чтобы воссоздать эту фигуру. Чтобы следовать этой статье, вам следует иметь базовое понимание matplotlib subplots и gridspec (если нет, вы можете ознакомиться с приведенными учебниками).

Матплотлиб субфигуры

Сначала мы импортируем matplotlib, seaborn и загружаем некоторые примеры данных, которые мы будем использовать для заполнения графиков:

import matplotlib.pyplot as pltimport seaborn as snsdata = sns.load_dataset('mpg')

Давайте начнем с понятия субфигур в matplotlib. Чтобы создать субфигуры, нам сначала нужно создать фигуру:

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

С этого момента мы можем определить субфигуры аналогично подграфикам. Мы можем создать сетку субфигур, указав количество строк (2) и столбцов (1). Мы также цветим фон фигуры, чтобы выделить их:

(topfig, bottomfig) = fig.subfigures(2, 1)topfig.set_facecolor('#cbe4c6ff')topfig.suptitle('Верх')bottomfig.set_facecolor('#c6c8e4ff')bottomfig.suptitle('Низ')

Простые фигуры без графиков (осей) не будут отображаться, поэтому нам нужно определить подграфики для каждой подфигуры. Здесь мы уже можем увидеть одну отличную особенность субфигур: для каждой подфигуры мы можем определить разные макеты подграфиков:

top_axs = topfig.subplots(2, 4)bottom_axs = bottomfig.subplots(3, 7)plt.show()

Теперь у нас есть две отдельные фигуры, которые мы можем настроить по-разному, но объединить в одной окончательной фигуре. Конечно же, мы также можем играть с соотношениями размеров субфигур:

figure = plt.figure(figsize=(10, 7))figs = figure.subfigures(2, 2, height_ratios=(2,1), width_ratios=(2,1))figs = figs.flatten()for i, fig in enumerate(figs): fig.suptitle(f'Субфигура {i}') axs = fig.subplots(2, 2)plt.show()

Однако у субфигур есть один недостаток. Чтобы избежать перекрывающихся меток или элементов за пределами фигуры, `plt.tight_layout()` — хороший способ аккуратно разместить все внутри фигуры. Однако это не поддерживается для субфигур. Вот что происходит, если вы попытаетесь его использовать:

figure = plt.figure(figsize=(10, 7))
figs = figure.subfigures(2, 2, height_ratios=(2,1), width_ratios=(2,1))
figs = figs.flatten()
for i, fig in enumerate(figs): fig.suptitle(f'Подрисунок {i}')
axs = fig.subplots(2, 2)
plt.tight_layout()
plt.show()

Не то, что нам бы хотелось… Чтобы вставить промежутки между графиками и убрать перекрытия, нам нужно использовать функцию `subplots_adjust`, которая позволяет нам добавить (или удалить) больше пространства между графиками и границами:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)
topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Верх')
bottomfig.set_facecolor('#c6c8e4ff')
bottomfig.suptitle('Низ')
top_axs = topfig.subplots(2, 4)
bottom_axs = bottomfig.subplots(3, 7)
# Добавляем больше пространства между графиками и уменьшаем пространство по бокам
topfig.subplots_adjust(left=.1, right=.9, wspace=.5, hspace=.5)
# Можем также уменьшить промежутки внизу
bottomfig.subplots_adjust(wspace=.5, hspace=.8, top=.7, bottom=.3)
plt.show()

Еще один отличный аспект подрисунков заключается в том, что их можно вкладывать друг в друга, то есть мы можем разделить каждый подрисунок на более мелкие подрисунки:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)
topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Верх')
top_axs = topfig.subplots(2, 4)
(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))
bottomleft.set_facecolor('#c6c8e4ff')
bottomleft.suptitle('Низ слева')
bottom_axs = bottomleft.subplots(2, 2)
bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Низ справа')
bottom_axs = bottomright.subplots(3, 3)
# Промежутки между подграфиками
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomleft.subplots_adjust(left=.2, right=.9, wspace=.5, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
plt.show()

Давайте вставим график `jointplot` в эту фигуру. К сожалению, это не так просто, потому что функция seaborn не имеет возможности использовать объект фигуры в качестве входных данных. Но если мы посмотрим в исходный код функции, мы увидим, что этот график состоит из трех подграфиков с общими осями x и y, которые определяются через gridspec.

Это означает, что мы легко можем построить его внутри подрисунка:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)
topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Верх')
top_axs = topfig.subplots(2, 4)
# Используем нижний левый подрисунок для графика jointplot
(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))
# Этот параметр определяет соотношение размеров основного графика и графиков по краям
ratio=2
# Определение gridspec, где размещены подграфики
gs = plt.GridSpec(ratio + 1, ratio + 1)
# Основной точечный график
ax_joint  = bottomleft.add_subplot(gs[1:, :-1])
# Графики по краям имеют общую ось с основным графиком
ax_marg_x = bottomleft.add_subplot(gs[0, :-1], sharex=ax_joint)
ax_marg_y = bottomleft.add_subplot(gs[1:, -1], sharey=ax_joint)
# Метки осей и деления для графиков по краям делаем невидимыми
# Так как они общие с основным графиком,
# удаление их из графика по краям также удаляет их из основного графика
plt.setp(ax_marg_x.get_xticklabels(), visible=False)
plt.setp(ax_marg_y.get_yticklabels(), visible=False)
plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False)
plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False)
# Заполняем графики данными
sns.scatterplot(data=data, y='horsepower', x='mpg', ax=ax_joint)
sns.histplot(data=data, y='horsepower', ax=ax_marg_y)
sns.histplot(data=data, x='mpg', ax=ax_marg_x)
bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Низ справа')
bottom_axs = bottomright.subplots(3, 3)
# Промежутки между подграфиками
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
plt.show()

Вы можете играть с параметром ratio и увидеть, как меняется график.

Теперь у нас есть все инструменты, необходимые для создания сложных фигур, используя subfigure, subplots и grids. Для таких фигур часто важно аннотировать каждый график буквами, чтобы объяснить их в подписи или ссылаться на них в тексте. Это часто делается с помощью другого программного обеспечения, такого как Adobe Illustrator или Inkscape, после создания фигуры. Но мы также можем сделать это непосредственно в Python, что позволит нам сэкономить дополнительные усилия позже.

Для этого мы определим функцию для создания таких аннотаций:

def letter_annotation(ax, xoffset, yoffset, letter): ax.text(xoffset, yoffset, letter, transform=ax.transAxes,         size=12, weight='bold')

Функция принимает оси в качестве входных данных, а также координаты x и y, которые будут преобразованы в относительные координаты осей. Мы можем использовать это для аннотации некоторых графиков в нашей ранее созданной фигуре:

fig = plt.figure(figsize=(10, 7))(topfig, bottomfig) = fig.subfigures(2, 1)topfig.set_facecolor('#cbe4c6ff')topfig.suptitle('Верхний')top_axs = topfig.subplots(2, 4)letter_annotation(top_axs[0][0], -.2, 1.1, 'A')(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))bottomleft.set_facecolor('#c6c8e4ff')bottomleft.suptitle('Нижний левый')bottoml_axs = bottomleft.subplots(2, 2)letter_annotation(bottoml_axs[0][0], -.2, 1.1, 'B')bottomright.set_facecolor('#aac8e4ff')bottomright.suptitle('Нижний правый')bottomr_axs = bottomright.subplots(3, 3)letter_annotation(bottomr_axs[0][0], -.2, 1.1, 'C')# Расстояние между подграфикамитopfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)bottomleft.subplots_adjust(left=.2, right=.9, wspace=.5, hspace=.4)bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)plt.show()

Теперь мы можем создать диаграмму, показанную в начале статьи. Она состоит из трех подфигур. Одна верхняя подфигура, охватывающая первую строку, и две нижние подфигуры. Левая нижняя подфигура будет использоваться для объединенной диаграммы (как показано ранее), а для правой нижней подфигуры мы определим gridspec, чтобы разместить 4 поддиаграммы разных размеров.

fig = plt.figure(figsize=(10, 7))# Создание подфигуры для первой и второй строки(row1fig, row2fig) = fig.subfigures(2, 1, height_ratios=[1, 1])# Разделение нижней строки подфигуры на две подфигуры(fig_row2left, fig_row2right) = row2fig.subfigures(1, 2, wspace=.08, width_ratios = (1, 2))# ###### Графики первой строки# ###### Создаем 4 подграфикарow1_axs = row1fig.subplots(1, 4)row1fig.subplots_adjust(wspace=0.5, left=0, right=1, bottom=.16)ax = row1_axs[0]sns.histplot(data=data, x='mpg', ax=ax)ax.set_title('MPG')# Аннотируем графики буквамиletter_annotation(ax, -.25, 1, 'A')# Некоторый стиль для графиков, чтобы они выглядели лучше # и имели стандартизированный внешний видsns.despine(offset=5, trim=False, ax=ax)ax = row1_axs[1]sns.histplot(data=data, x='displacement', ax=ax)ax.set_title('Displacement')letter_annotation(ax, -.25, 1, 'B')sns.despine(offset=5, trim=False, ax=ax)ax = row1_axs[2]sns.histplot(data=data, x='weight', ax=ax)ax.set_title('Weight')letter_annotation(ax, -.25, 1, 'C')sns.despine(offset=5, trim=False, ax=ax)ax = row1_axs[3]sns.histplot(data=data, x='horsepower', ax=ax)ax.set_title('Horsepower')letter_annotation(ax, -.25, 1, 'D')sns.despine(offset=5, trim=False, ax=ax)# ###### Графики второй строки# ###### ### Seaborn jointplot# ### Использование кода из класса Seaborn JointGrid# Отношение размера между основными графиками и графиками внешнего поляratio=2# Определение gridspec для внутренней подфигурыgs = plt.GridSpec(ratio + 1, ratio + 1)ax_joint  = fig_row2left.add_subplot(gs[1:, :-1])# Общая ось между графиками внешнего поля и основными графикамиax_marg_x = fig_row2left.add_subplot(gs[0, :-1], sharex=ax_joint)ax_marg_y = fig_row2left.add_subplot(gs[1:, -1], sharey=ax_joint)# Удаление подписей осей и делений осей для графиков внешнего поляplt.setp(ax_marg_x.get_xticklabels(), visible=False)plt.setp(ax_marg_y.get_yticklabels(), visible=False)plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False)plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False)sns.scatterplot(data=data, y='horsepower', x='mpg', ax=ax_joint)sns.histplot(data=data, y='horsepower', ax=ax_marg_y)sns.histplot(data=data, x='mpg', ax=ax_marg_x)sns.despine(offset=5, trim=False, ax=ax_joint)sns.despine(offset=5, trim=False, ax=ax_marg_y)sns.despine(offset=5, trim=False, ax=ax_marg_x)# Оставим немного места справа, чтобы избежать перекрытийfig_row2left.subplots_adjust(left=0, right=.8)letter_annotation(ax_marg_x, -.25, 1, 'E')# ### Графики правой части второй строки# ##gs = plt.GridSpec(2, 3)ax_left   = fig_row2right.add_subplot(gs[:, 0])ax_middle = fig_row2right.add_subplot(gs[:, 1])ax_up     = fig_row2right.add_subplot(gs[0, 2])ax_down   = fig_row2right.add_subplot(gs[1, 2])fig_row2right.subplots_adjust(left=0, right=1, hspace=.5)ax = ax_leftsns.scatterplot(data=data, x='model_year', y='weight', hue='origin', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'F')ax = ax_middlesns.boxplot(data=data, x='origin', y='horsepower', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'G')ax = ax_upsns.kdeplot(data=data, x='mpg', y='acceleration', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'H')ax = ax_downsns.histplot(data=data, x='weight', y='horsepower', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'I')plt.show()

Заключение

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

Для меня субфигуры являются очень удобным инструментом для создания научных рисунков, и я надеюсь, что они могут быть полезными и для вас.

Вы также можете найти весь код из этой статьи на GitHub: https://github.com/tdrose/blogpost-subfigures-code

Если не указано иное, все изображения были созданы автором.