“使用Matplotlib子图创建复杂多面板图形的指南”

“Matplotlib子图创建复杂多面板图形的完全指南”

子图 — 一个用于创建美丽的多面板图的强大工具

动机

复杂(科学)图常常由多个具有不同大小或批注的绘图组成。如果您使用matplotlib/seaborn生态系统,有很多方法可以创建复杂图,例如使用gridspec。然而,这很快会变得具有挑战性,特别是如果您想要将seaborn中的多轴绘图,如jointplot或pairgrid整合到您的图中,因为它们没有提供轴作为输入参数的选项。然而,在matplotlib中有另一种组装图的方法,而不仅仅是使用子图,那就是子图。它是一个用于创建像这样的多面板图的强大框架:

本文的目标是向您展示如何创建此图。

在本文中,我将介绍子图及其功能。我们将结合子图、子图和gridspec来重新创建此图。要阅读本文,您需要对matplotlib子图和gridspec有基本的了解(如果不了解,您可以查看链接的教程)。

Matplotlib子图

首先,我们导入matplotlib、seaborn并加载一些示例数据,我们将使用这些数据来填充绘图内容:

import matplotlib.pyplot as plt
import seaborn as sns
data = 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()

让我们在此图中插入一项联合绘图。不幸的是,这并不直接,因为 seaborn 函数没有提供 figure 对象作为输入的可能性。但是,如果我们查看函数的源代码,我们可以看到这个绘图由三个公用 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)# 我们使用左下子图进行联合绘图(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))# 此参数定义主绘图和边缘绘图的大小比例ratio=2# 定义一个网格规范,将子绘图放置在回收站中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()

您可以尝试使用比例参数并查看图形如何变化。

现在,我们拥有了创建复杂图形所需的所有工具,通过使用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')#子图之间的间距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()

现在我们可以创建本文开头显示的图。它由三个子图组成。有一个顶部子图,跨越第一行,还有两个底部子图。左下子图将用于联合绘图(如前所示),而右下子图将定义一个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))# ###### 第1行的绘图# ###### 为第一行子图创建四个子图row1_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('每加仑行驶英里数')#使用字母对绘图进行注释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('排量')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('重量')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('马力')letter_annotation(ax, -.25, 1, 'D')sns.despine(offset=5, trim=False, ax=ax)# ###### 第2行的绘图# ###### ### Seaborn联合绘图# ### 使用Seaborn JointGrid类的代码#主要绘图和边距绘图之间的尺寸比例ratio=2#为子图定义一个gridspecgs = 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')# ### Row 2 right plots# ##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()

结论

Subfigures是matplotlib中的一个相对较新的概念。它们使得组合包含多个图的大图变得简单。在本文中展示的所有内容也可以完全使用gridspec来实现。然而,这需要一个包含许多子图大小考虑的大网格。Subfigures更加即插即用,可以用更少的工作实现相同的结果。

对我来说,subfigures是一个非常方便的工具,用于创建科学图形,我希望它们也能对您有所帮助。

您还可以在GitHub上找到本文中的所有代码:https://github.com/tdrose/blogpost-subfigures-code

除非另有说明,所有的图像均由作者创建。