Matplotlib笔记 · 绘图区域的结构和子图布局与划分(figure, axes, subplots)

Posted bluishglc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Matplotlib笔记 · 绘图区域的结构和子图布局与划分(figure, axes, subplots)相关的知识,希望对你有一定的参考价值。

文章目录

1. 绘图区域的结构

很多时候,我们需要将多张关系密切的图表放在一起展示,便于分析师比对差异或发现关联关系,这时候,我们就需要将画布切分成多个子区域,然后在选定的子区域上绘制需要的图表了。(本文原文出处:https://blog.csdn.net/bluishglc/article/details/128553539,转载请注明)

Matplotlib对于绘制区域是这样设计的:首先要有一张画布(figure),然后,我们既可以使用画布的全部幅面来绘制一张图表,也可以将画布切分成多个子区域(axes),在每一个子区域上绘制不同的图表,而第一种情况只是将画布切分为一个子区域的特殊情形。我们以下面这张实际的Matplotlib图表为例:

这是一个figure,在这个figure内划分了四个子区域(axes),每个axes可以独立绘制一张图表(plot)。而下图是对figure和axes更加细致的说明:

通过上图我们可以清楚地看到:通过plt.subplot(), plt.subplots(), plt.axes()fig.add_subplot(), fig.subplots(), fig.add_axes()这两组接口都可以创建axes对象,两组API的区别只在于风格上的不同,第一组是脚本风格的调用方式,第二组是面向对象风格的调用方式。

此外,我们可以看到,创建子图对象的方法都会使用subplot的称谓,而创建出的对象都是axes类型,所以在概念上,axes和subplot其实是一回事,完全可以统一称为“子图”。其实,后续将要介绍的axes系方法返回的也是axes类型,这才是一组方法名和返回值呼应的合理命名,个人认为这在某种程度上这反映了Matplotlib在API设计上有些不太规范,实际上,subplot系和axes系方法的差异只是布局模式不同,更好的API设计方案应该是:将布局抽象为独立的类族,添加子图的方法名应该统一,比如叫add_axes,然后将布局以参数形式传给方法,很多GUI类库都是这样设计的。

补充一点:在figure之下还有一个名为canvas的最底层设施,它在导入Matplotlib库时自动创建,实际上,每一个figure是由canvas来绘制的,但由于这个对象处于非常底层,绝大多数用户都不太会直接操纵到这个对象,所以可以不必考虑它的存在。

此外,解释一下axes和axis,尽管axes是axis的复数形式,但实际上两者在Matplotlib中并没有什么关系,应该说是Matplotlib在子图的命名上做得不是很好,如果把axes命名为grids或panels会更贴切一些。如果非要将两者关联在一起解释的话,可以这样认为:axis是单一图表中的坐标轴,与子图没有任何关系,axes作为axis的复数形式,可以理解为“一套坐标轴”,而通常一个图表只有一套坐标轴,所以Matplotlib用axes作为了子图的代名词。

2. subplot系方法 ( subplot布局 )

subplot系方法主要是指plt.subplots()plt.subplot()fig.add_subplot()fig.subplots()四个方法,之所以将它们分在一起是因为:它们使用了同一种布局方法,但是Matplotlib似乎没有给subplot的布局方式起过正式的名字,它非常类似于Java GUI类库中的GridBagLayout,当然并没有后者强大,设计思路是类似。

2.1 使用 add_subplot(nrows, ncols, index) 逐一创建子图

接下来,我们通过代码示例来演示如何使用figure的add_subplot逐一创建子图。下面的代码中:每调用一次add_subplot()就会添加一张子图到当前figure中,添加的位置和画幅大小由它的三个参数控制

import matplotlib.pyplot as plt
# %matplotlib inline
%matplotlib auto

# 创建一个figure
fig = plt.figure()

# 为figure对象上添加子图
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 1, 2)

# 与上述三行代码等价
# ax1 = fig.add_subplot(221)
# ax2 = fig.add_subplot(222)
# ax3 = fig.add_subplot(212)

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax3.annotate('ax3', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

我们先来看一下add_subplot()接受的两种参数风格:

  • 使用三个独立的参数(a, b, c)表示子图在一个切分为a行b列的画布中位于第c个单元格(cell)处创建一个axes,用于后续的图表绘制
  • 使用三位数(abc)表示子图在一个切分为a行b列的画布中位于第c个单元格(cell)处创建一个axes,用于后续的图表绘制

使用第二种“三位数”表示法确实是种很巧妙的设计,唯一的不足之处就是当切分的行,列,位置超过9时,这种方法就不适用了,需要使用第一种方法。

2.2 控制子图大小和位置 ( add_subplot(nrows, ncols, index) 参数详解 )

接下来,可能是让初次接触Matplotlib的人最迷惑的一处都地方就是:在上述代码中fig.add_subplot(212)为什么是占据了画布的下半部分画幅?

我们以add_subplot(a, b, c)为例,解释一下它的三个参数的意义:首先,画幅的大小是由前两个参数a, b决定的,因为a, b指定了画布将被切分成的行数和列数,在画布尺寸固定的前提下,行列数越大,切分出的单个区域(axes)(也可以形象地称为单元格)画幅越小,反之,行列数越小,单个区域(axes)就越大。以(2,2,c)为例:其将画布切分为2行×2列=4个区域(axes),所以单个区域(axes)的画幅只占全画幅的25%。而(2,1,c)则会将画布切分为2行×1列,所以单个axes画幅占全画幅的50%。然后,参数c决定了子图的位置(严格地说,行列数也影响了子图的位置,(2,2,2)和(3,3,2)的位置是有差异的),在前面两个参数a,b切分出的单元格布局下,按从左到右,从上到下的顺序,对单元格从1开始编号,参数c设定的就是目标单元格的编号,指定几就是选中了第几个单元格!

总结一下就是:add_subplot的这套(a, b, c)三参数系统会将画布切分成一个a行b列的表格,单元格的大小即为子图的大小,然后选择第c个单元格,就确定了子图的位置。

回到上面的示例,我们解释一下三次调用add_subplot()发生的故事:第一次调用fig.add_subplot(2,2,1)时,画布的切分方案是2行2列,然后选取第1个单元格作为绘制区域(返回axex对象);第二次调用fig.add_subplot(2,2,2)时,画布的切分方案还是2行2列(注意,这次切分和上一次没有任何关系),然后选取第2个单元格作为绘制区域(返回axex对象);第三次调用fig.add_subplot(2,1,2)时,画布的切分方案是2行1列(再次提醒:这次切分和上两次都没有关系),然后选取第2个单元格作为绘制区域(返回axex对象);我们要注意的是:第三次调用时使用了与前两次不同的切分方案,从2行2列改为了2行1列,这样,子图的画幅必然会拓宽一倍,且指定的是第二个单元格,和前两次绘制的区域也没有重叠,所以出来的效果会如图所示。

add_subplot(a, b, c)的三参数其实是一套用来描述一个子图(单元格)大小和位置的坐标系统,无状态,不是真得对figure进行切割,仅仅是一次测量和定位操作(选择一个期望大小和位置的矩形),可以在画布上执行任意多次,划定各种大小和位置的区域,遇到划定的区域有重叠时,新划定的区域(子图)会覆盖旧的区域(子图)。为了更好的解释和理解,我们再来举一个更复杂的例子:

import matplotlib.pyplot as plt
# %matplotlib inline
%matplotlib auto

fig = plt.figure()

ax1 = fig.add_subplot(1, 3, 1)
ax2 = fig.add_subplot(2, 3, 2)
ax3 = fig.add_subplot(2, 3, 3)
ax4 = fig.add_subplot(2, 6, 9)
ax5 = fig.add_subplot(2, 2, 4)

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax3.annotate('ax3', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax4.annotate('ax4', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax5.annotate('ax5', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

这个布局结果大家可以自行思考比对。复杂的布局其实并不多见,并且合理安排子图的布局在Matplotlib这种子图坐标体系下并不是很好设计,都是要通过尝试不同的行列值和位置参数来不断调整的。此外,作为对前面论述的一个验证,我们再绘制一个有区域叠加的图来让读者更好的体会到这个坐标体系的工作方式:

import matplotlib.pyplot as plt
# %matplotlib inline
# 禁用inline模式,否则图片会自动进行缩放和居中调整
%matplotlib auto

# 绘制边框,凸显子图在整个fig中的位置和大小
fig = plt.figure(linewidth=2, edgecolor='red')

ax1 = fig.add_subplot(2, 2, 2)
ax2 = fig.add_subplot(3, 3, 5)

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

在这次的示例中:ax1是4格漫画中位于右上角的第2图,ax2是九宫格中位于中间的第5图,在相同画布尺寸下,它们解析出的区域就是有重叠的,我们可以从中观察到ax2覆盖掉了ax1的部分区域,因为ax2是后绘制的,新的axes会覆盖旧的axes。

2.3 使用 subplots(nrows, ncols) 批量创建多张子图

如果我们要绘制的所有子图服从统一的布局安排,则没有必要使用add_subplot一张一张创建,可以使用更加便捷的subplots()方法。例如:我们想构建一个2行2列布局的画布,并分别获得4个axes对象去绘制子图,可以这样操作:

import matplotlib.pyplot as plt
# %matplotlib inline
%matplotlib auto

fig = plt.figure()
((ax1, ax2), (ax3, ax4)) = fig.subplots(2, 2)

# 与上面两行等价
# fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax3.annotate('ax3', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax4.annotate('ax4', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

由此可见:在统一布局的前提下,使用subplots要简洁方便很多。值得注意的是:我们还可以直接使用plt.subplots同时将figure和axes元组创建出来,这样更加简洁。

3. axes系方法 ( axes布局 )

从subplot系方法的定位模式可以看处,它适合最为常用的表格式布局,在应对更加复杂的布局需求时就有些力不从心了,这时就可以考虑使用axes系方法了。

axes系方法主要是指plt.axes([l,b,w,h])fig.add_axes([l,b,w,h])两个方法,之所以将它们分在一起是因为:它们使用了同一种布局方法,Matplotlib同样没有给axes的布局方式起过正式的名字,根据它的特性我们可以称之为:(基于百分比的)绝对布局

此外,subplot系和axes系方法并不对立,完全可以联合使用,例如可以通过subplot系方法进行整体布局,在部分比较难控制位置的子图使用axes系方法进行位置的微调。

3.1 使用 add_axes() 逐一创建子图

下面,我们用代码来演示一下add_axes()添加子图的方法。以下方法试图将画布切分成四格漫画式的布局,每个子图占1/4的面积:

import matplotlib.pyplot as plt
# 禁用inline模式,防止图片自动缩放并居中
%matplotlib auto
# 绘制边框,凸显子图在整个fig中的位置和大小
fig = plt.figure(linewidth=2, edgecolor='red')

ax1 = fig.add_axes([0.0, 0.5, 0.5, 0.5])
ax2 = fig.add_axes([0.5, 0.5, 0.5, 0.5])
ax3 = fig.add_axes([0.0, 0.0, 0.5, 0.5])
ax4 = fig.add_axes([0.5, 0.0, 0.5, 0.5])

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax3.annotate('ax3', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax4.annotate('ax4', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

3.2 控制子图大小和位置 ( fig.add_axes([left, bottom, width, height]) 参数详解 )

我们先简单解释一下fig.add_axes([left, bottom, width, height])的四个参数:

  • 四个参数都是介于0-1之间的小数,都是百分比数据
  • left是指子图左下角顶点距离画布左下角顶点(向右)的水平偏移量,数值是偏移量占整个画布宽度的百分比
  • bottom是指子图左下角顶点距离画布左下角顶点(向上)的垂直偏移量,数值是偏移量占整个画布高度度的百分比
  • 前两个参数是用于确定子图位置的,它们合在一起决定了子图左下角顶点在画布上的位置
  • width是指子图的宽度占整个画布宽度的百分比
  • height是指子图的高度占整个画布高度度的百分比
  • 后两个参数是用于确定子大小的,它们合在一起决定了子图的宽和高

至于axes的定位体系为什么是以左下角顶点为原点,我个人的猜测是:大部分图表中的数轴原点都是在左下角的,所以Matplotlib的设计者和用户可能都会倾向于将左下角顶点视为图片的定位点。

上述四个参数还有另一种辅助记忆手段:如果我们把画布也看成一个坐标系的话,其左下角顶点就是坐标系的原点,left和bottom就是某个点的横纵坐标,以这个点为左下角顶点,划定一个宽度是width,高度是height的矩形,这个矩形就是子图的区域了。只不过left, bottom, width, height这四个量都不是绝对值,而是坐标系(这张画布)x,y轴长度百分比值。

现在,我们回看一下上一节绘制的图,和我们的设想有些出入:我们可以看到4张子图的坐标轴都消失了,figure的边框也没有显式出来。当前图像在暗示我们:add_axes的位置和面积都是以子图坐标轴以内的面积占全画幅的百分比计算的(应该说是子图坐标轴外侧的标签或标题等元素本来占用的就是子图外的空白空间),所以在一个面积被完全切割(100%份额都被切分给了子图)的画布上,包括坐标轴,坐标轴标签,子图标题在内的元素都由于超出了画布(figure)边界而无法显示,甚至画布的边框也被子图覆盖了。

为了验证这种猜测,我们把4张子图的面积缩小一些,从占全画幅25%的缩小到16%,即面积参数从(0.5, 0.5)->(0.4,0.4):

import matplotlib.pyplot as plt
# 禁用inline模式,防止图片自动缩放并居中
%matplotlib auto
# 绘制边框,凸显子图在整个fig中的位置和大小
fig = plt.figure(linewidth=2, edgecolor='red')

ax1 = fig.add_axes([0.0, 0.5, 0.4, 0.4])
ax2 = fig.add_axes([0.5, 0.5, 0.4, 0.4])
ax3 = fig.add_axes([0.0, 0.0, 0.4, 0.4])
ax4 = fig.add_axes([0.5, 0.0, 0.4, 0.4])

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax3.annotate('ax3', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax4.annotate('ax4', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

在这一版的改进中,我们可以看到:

  • 由于我们只是缩小了子图的面积,没有更改坐标位置(左下角的定位点),所以每张子图的左下角的顶点位置没有变化
  • 每张子图的画幅从25%降至16%,左下角位置没变,所以图片是向下和向左收缩,所以在它们的上侧和右侧腾出了一小部分的空白空间,部分子图的坐标轴显现了现来,同时部分的边框也显现了出来
  • 由于定位点是左下角的顶点,在不调整顶点位置的情况下,靠近左边框和下边框的子图数轴依然因为超出了画布范围而无法显示

基于上述情况,我们可以进一步体会到axes布局的一些特性。接下来,我们要把这个figure完全调好,思路就是:将4张子图的位置(左下角顶点)整体向右上角的方向移动一点距离,使得靠近左边框和下边框的子图数轴也能得到一定的空白空间而展示出来:

import matplotlib.pyplot as plt
# 禁用inline模式,防止图片自动缩放并居中
%matplotlib auto

# 绘制边框,凸显子图在整个fig中的位置和大小
fig = plt.figure(linewidth=2, edgecolor='red')

ax1 = fig.add_axes([0.08, 0.55, 0.4, 0.4])
ax2 = fig.add_axes([0.55, 0.55, 0.4, 0.4])
ax3 = fig.add_axes([0.08, 0.08, 0.4, 0.4])
ax4 = fig.add_axes([0.55, 0.08, 0.4, 0.4])

ax1.annotate('ax1', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax2.annotate('ax2', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax3.annotate('ax3', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')
ax4.annotate('ax4', (0.5, 0.5), xycoords='axes fraction', va='center', ha='center')

程序输出:

这次改进是将原来四个子图的左下角顶点在原有位置上分别向上多平移了8%(0.0 -> 0.08)向右多平移了0.05(0.5 -> 0.55)。

对比一下subplot系的方法效果,我们能感受到:subplot系的方法能根据子图和画布的相对大小对Padding和 Spacing做出一些自适应的调整(关于手动配置Padding和 Spacing可参考:https://matplotlib.org/stable/tutorials/intermediate/constrainedlayout_guide.html#padding-and-spacing),所以通过它们添加的子图不太会出现数轴或标题被遮盖或超出画布的问题,而axes系方法虽然可以完全掌控子图的位置和大小,但却需要开发者多次微调才能得到想要的效果,这是一把双刃剑。

注:tight_layout和constrained_layoutg两种布局(plt.tight_layout()plt.rcParams['figure.constrained_layout.use'] = True)对axes系方法无效。axes系方法总是使用(基于百分比的)绝对位置和尺寸定位子图位置和大小的!所有subplot系的一些布局和自适应手段对axes系方法全部无效。


参考:

https://towardsdatascience.com/the-many-ways-to-call-axes-in-matplotlib-2667a7b06e06

https://towardsdatascience.com/plt-xxx-or-ax-xxx-that-is-the-question-in-matplotlib-8580acf42f44

https://pub.towardsai.net/day-2-of-matplotlib-how-to-fit-multiple-subplots-in-the-same-window-c964f49ee503

https://pub.towardsai.net/day-3-of-matplotlib-figure-axes-explained-in-detail-d6e98f7cd4e7

https://blog.csdn.net/weixin_46961200/article/details/109131197

以上是关于Matplotlib笔记 · 绘图区域的结构和子图布局与划分(figure, axes, subplots)的主要内容,如果未能解决你的问题,请参考以下文章

Matplotlib 子图的创建

Python Matplotlib绘图笔记草稿 未完成

Matplotlib 误差线的绘制和子图的创建方式

matplotlib:子图绘制

Matplotlib 子图——完全摆脱刻度标签

python matplotlib subplot 上面面积大下面小怎么办