【matplotlib】可视化解决方案——如何正确使用分割画布函数

概述

在使用 matplotlib 库进行数据可视化的过程中,我们都离不开一个重要载体画布(Figure)。一个 Figure 对象是所有绘图元素的顶层容器,想要实现画布的合理使用,分区使用必不可少。这里引入了一个新的概念:子区。子区顾名思义就是将大画布划分成若干个子画布,这些子画布共同构成绘图区域。因此,子区的本质是在纵横交错的行列网格中添加坐标轴系统。

在 matplotlib 中跟子区相关的函数一共有三个,分别是 plt.subplots()plt.subplot()subplot2grid()。接下来本文将会详细介绍每一个函数。

subplots 函数

概述

该命令可以非常便捷地创建一个行列布局的子区列表,同时创建一个画布对象(Figure),其返回值是 (fig, ax)。方法签名如下:

def subplots(nrows=1, 
             ncols=1, 
             *, 
             sharex=False, 
             sharey=False, 
             squeeze=True,  
             subplot_kw=None, 
             gridspec_kw=None, 
             **fig_kw):

参数说明:

  • nrows:整数型,指定子图网格的行数;
  • ncols:整数型,指定子图网格的列数;
  • sharex:布尔型或字符型,指定是否共享 x 轴;
  • sharey:布尔型或字符型,指定是否共享 y 轴;
  • squeeze:布尔型,指定是否进行压缩
  • subplot_kw:字典型,指定创建每个子图的调用的关键字参数;
  • gridspec_kw:字典型,指定子图放置的网格的关键字参数;
  • fig_kw:字典型,指定传递给 pyplot. figure 方法的关键字参数;
  • 返回值:(Figure 对象、Axes 对象)
  • fig_kw 的参数传递给 pyplot.figure 方法的意思是该参数的所有内容会直接传递给 plt.figure 方法。subplot_kw、subplot_kw 同理;
  • squeeze 参数,如果为 True 且仅构造一个子图,则返回的单个 Axes 对象将作为标量返回。对于 Nx1或1xM 子图,返回的对象是包含 Axes 对象的 numpy 数组。对于 N×M 个子区,该方法会返回一个二维阵列。如果为 False,则完全不进行压缩:返回的 Axes 对象始终是包含 Axes 实例的2D 数组,即使它只有一个子区。

源码分析如下:

  if self.get_constrained_layout():
        gs = GridSpec(nrows, ncols, figure=self, gridspec_kw)   #创建GridSpec类实例
  else:
        # this should turn constrained_layout off if we don't want it
        gs = GridSpec(nrows, ncols, figure=None, gridspec_kw)
  self._gridspecs.append(gs)

  # Create array to hold all axes.
  axarr = np.empty((nrows, ncols), dtype=object)    #创建一个空矩阵,类型是Axes对象
  #循环创建坐标轴系统,下标0开始
  for row in range(nrows):
        for col in range(ncols):
            shared_with = {"none": None, "all": axarr[0, 0],"row": axarr[row, 0], "col": axarr[0, col]} #sharex和sharey的检查字典
            subplot_kw["sharex"] = shared_with[sharex]  #字典取值
            subplot_kw["sharey"] = shared_with[sharey]  #字典取值
            axarr[row, col] = self.add_subplot(gs[row, col], subplot_kw)    #创建坐标轴系统
# turn off redundant tick labeling
    #设置共享X轴
  if sharex in ["col", "all"]:
    # turn off all but the bottom row
    for ax in axarr[:-1, :].flat:
        ax.xaxis.set_tick_params(which='both', labelbottom=False, labeltop=False)
        ax.xaxis.offsetText.set_visible(False)
        
  #设置共享Y轴
  if sharey in ["row", "all"]:
    # turn off all but the first column
    for ax in axarr[:, 1:].flat:
        ax.yaxis.set_tick_params(which='both', labelleft=False, labelright=False)
        ax.yaxis.offsetText.set_visible(False)
        
  if squeeze:
        return axarr.item() if axarr.size == 1 else axarr.squeeze() #调用squeeze方法压删除一维
  else:
        return axarr    #返回一个二维矩阵

总结一下,首先创建 GridSpec 对象,该类用于指定网格的几何形状;然后循环创建坐标轴系统,并添加到一个空矩阵中;接下来两个 if-for 代码块用来设置 X 轴和 Y 轴的共享;最后对 squeeze 参数进行检测,看是否对 axarr对象进行压缩。

示例

首先创建一个1行1列的网格布局,squeeze参数为False,看看其返回值:

fig, ax = plt.subplots(squeeze=False)
ax -> 

接着设置设置多行和多列,看看返回值:

fig, ax = plt.subplots(2, 2, squeeze=True)
ax ->  array([[, ],
        [, ]], dtype=object))
        
fig, ax = plt.subplots(2, 2, squeeze=False)
ax ->  array([[, ],
        [, ]], dtype=object))  

最后是一个综合示例,完整代码如下:

fig, ax = plt.subplots(2, 3)

colors_list = ['#8dd3c7', '#ffffb3', '#bebada']
ax[0, 0].bar([1, 2, 3], [0.6, 0.2, 0.8], color=colors_list, width=0.8, hatch='///', align='center')
ax[0, 0].errorbar([1, 2, 3], [0.6, 0.2, 0.8], yerr=0.1, capsize=0, ecolor='#377eb8', fmt='o:')

ax[0, 1].errorbar([1, 2, 3], [20, 30, 36], xerr=2, ecolor='#4daf4a', elinewidth=2, fmt='s', label='ETN')
ax[0, 1].legend(loc=3, fancybox=True, shadow=True, fontsize=10, borderaxespad=0.4)
ax[0, 1].set_ylim(10, 40)
ax[0, 1].set_xlim(-2, 6)

x3 = np.arange(1, 10, 0.5)
y3 = np.cos(x3) 

ax[0, 2].stem(x3, y3, basefmt='r-', linefmt='b-.', markerfmt='bo', label='life signal')
ax[0, 2].legend(loc=2, fancybox=True, shadow=True, frameon=False, fontsize=8, borderaxespad=0.6, borderpad=0.0)
ax[0, 2].set_ylim(-2, 2)
ax[0, 2].set_xlim(0, 11)

x4 = np.linspace(0, 2*np.pi, 500)
x4_1 = np.linspace(0, 2*np.pi, 1000)
y4 = np.cos(x4)*np.exp(-x4)
y4_1 = np.sin(2*x4_1)
line1, line2 = ax[1, 0].plot(x4, y4, 'k--', x4_1, y4_1, 'r-', lw=2)
ax[1, 0].legend((line1, line2), ('energy', 'patience'), loc=2, fancybox=True, shadow=True, framealpha=0.3,mode='expand', fontsize=8, ncol=2, columnspacing=2, borderpad=0.1, markerscale=0.1)
ax[1, 0].set_ylim(-2, 2)
ax[1, 0].set_xlim(0, 2*np.pi)

x5 = np.random.rand(100)
ax[1, 1].boxplot(x5, vert=False, showmeans=True, meanprops=dict(color='black'))
ax[1, 1].set_yticks([])
ax[1, 1].set_xlim(-1.1, 1.1)
ax[1, 1].set_ylabel('Miscro SD Card')
ax[1, 1].text(-1.0, 1.2, 'net weight', fontsize=15, style='italic', weight='black', family='monospace')

mu = 0.0
sigma = 1.0

x6 = np.random.randn(10000)
n, bins, patches = ax[1, 2].hist(x6, bins=30, histtype='stepfilled', cumulative=True, color='cornflowerblue', 
label='Test', density=True)
y = ((1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-0.5 * (1 / sigma * (bins-mu))  2))
y = y.cumsum()
y /= y[-1]

ax[1, 2].plot(bins, y, 'r--', lw=1.5, label='Theory')
ax[1, 2].set_ylim([0.0, 1.1])
ax[1, 2].grid(ls=':', lw=1, color='grey', alpha=0.5)
ax[1, 2].legend(fontsize=8, shadow=True, fancybox=True, framealpha=0.8)
ax[1, 2].autoscale()

plt.show()

画图结果如下:

subplots函数综合示例

subplot 方法

概述

该方法用于绘制网格区域中的几何形状相同的子区布局。本质上是对 Figure.add_subplot() 方法的包装,深入一点讲,它可以检索 Figure 对象所有的 Axes 对象,也可以创建一个新的 Axes 对象并将其添加到当前的 Figure 对象上。以下是它的源码分析:

fig = gcf() #调用plt.gcf获得当前Figure对象

# First, search for an existing subplot with a matching spec.
key = SubplotSpec._from_subplot_args(fig, args)

#检索撇配现有坐标轴系统
for ax in fig.axes:
    # if we found an axes at the position sort out if we can re-use it
    if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key:
        # if the user passed no kwargs, re-use
        if kwargs == {}:
            break
        # if the axes class and kwargs are identical, reuse
        elif ax._projection_init == fig._process_projection_requirements(
        *args, **kwargs):
                break
        else:
            # we have exhausted the known Axes and none match, make a new one!
            ax = fig.add_subplot(*args, **kwargs)   #如果没有匹配则依据参数创建相应的子图

fig.sca(ax) #将当前轴设置为ax,将当前Figure设置为ax的parent

小结一下,这个方法先调用 gcf() 方法获取当前 Figure 对象;接着对该对象的坐标轴系统进行检索匹配,如果能找到对应索引的 Axes 对象则返回该对象,否则调用 add_subplots() 方法创建创建一个新的 Axes 对象;最后调用 sca() 方法将检索或者新创建的 Axes 对象设置为当前子图,即调用 gca() 的返回值。

subplot 有多种形式,其部分示例如下:

subplot(nrows, ncols, index, kwargs)
subplot(pos, kwargs)
subplot(kwargs)
subplot(ax)

参数说明:

  • nrows:整数型,指定子图网格的行数;
  • ncols:整数型,指定子图网格的列数;
  • index:整数型,指定当前激活的子图索引,索引从左上角的1开始并向右增加。索引也可以是两元素元组指定 (start, end) ,即绘制一个跨区子图;
  • projection:指定坐标轴投影类型;
  • polar:指定是否使用极坐标系,等价于 projection='polar'
  • sharex:指定是否共享 X 轴;
  • sharey:指定是否共享 Y 轴;
  • label:指定返回的 Axes 对象的名称;
  • 返回值:返回 Axes 对象;
  1. polar 这个参数尽量不要用,所有的投影参数最好都使用 projection 参数来传递;
  2. 使用此函数的时候索引下标都是从1开始。而不是0,切记!!!

示例

首先介绍四种地球投影方式,分别是 Aitoff、Hammer、Lambert、Mollweide。
Aitoff 映射程序如下:

plt.figure()
plt.subplot(projection="aitoff")
plt.title("Aitoff")
plt.grid(True)

画图结果如下:

Aitoff

Hammer 映射程序如下:

plt.figure()
plt.subplot(projection="hammer")
plt.title("Hammer")
plt.grid(True)

画图结果如下:

Hammer

Lambert 映射程序如下:

plt.figure()
plt.subplot(projection="lambert")
plt.title("Lambert")
plt.grid(True)

画图结果如下:

lambert

Mollweide 映射程序如下:

plt.figure()
plt.subplot(projection="mollweide")
plt.title("Mollweide")
plt.grid(True)

画图结果如下:

mollweide

最后介绍一种最常用的映射即 polar 映射,也就是极坐标系,polar 映射映射程序如下:

radi = np.linspace(0, 1, 100)
theta = 2*np.pi*radi

ax = plt.subplot(111, polar=True)
ax.plot(theta, radi, color='r', lw=2)
plt.show()

画图结果如下:

polar

综合示例

import matplotlib.pyplot as plt  
import numpy as np  
  
plt.rcParams["font.sans-serif"] = ["SimHei"]  
plt.rcParams["axes.unicode_minus"] = False  
plt.style.use("classic")  
  
x = np.linspace(1, 100, 200)  
y = np.random.normal(75.0, 15.0, 200)  
  
plt.figure().set_facecolor("#FFF")  
  
ax = plt.subplot(2, 2, (1, 2))  
ax.plot(y + np.random.randn(200), ls='-', lw=2, color='c', label=r'$\sin(x)$')  
ax.plot(y + np.random.randn(200), ls='-', lw=2, color='r', label=r'$\cos(x)$')  
ax.legend(title='noise', title_fontsize=15, loc='upper right')  
  
ax = plt.subplot(2, 2, 3)  
ax.plot(y, ls='-', lw=2, color='c', label=r'$\sin(x)$')  
ax.legend(loc='upper right')  
  
ax = plt.subplot(2, 2, 4)  
ax.plot(y, ls='-', lw=2, color='r', label=r'$\cos(x)$')  
ax.legend(loc='upper right')  
  
plt.show()

画图结果如下:

subplot综合示例

subplot2grid 方法

前面介绍的子区函数只能绘制等分画布的图形样式,要想按照绘图区域的不同展示目的,进行非等分画布形式的图形展示,需要向画布多次调用 plt.subplot() 方法完成非等分画布的展示任务,但是频繁的调用显得过于麻烦,而且在划分画布的过程中容易出现疏漏和差错。因此,需要使用一种更高级的定制网格方法,这个方法就是 subplot2grid (),通过指定 rowspancolspan 参数可以让子区跨越固定的网格布局的多个行和列,实现自定义子区分区。该方法的签名如下:

def matplotlib.pyplot.subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs):
    ...

参数说明:

  • shape:指定整个网格的行列数;
  • loc:Axes 对象所处的位置;
  • rowspan:指定该 Axes 对象的行跨度;
  • colspan:指定该 Axes 对象的列跨度;
  • fig:指定要进行切分的 Figure 对象;
  • kwargs:指定向 add_subplot() 方法传递的关键字参数;
  • 返回值:Axes 对象;
  1. 图形位置的索引起点是 0,而不像 subplot() 方法那样从 1 开始。
  2. 网格的实现基于 gridspec 模块,该模块是一个可以指定画布中子区位置或者说是布局的模块,该模块有一个类 GridSpec,它可以指定网格的几何形状,在各个方法的底层可以看到都是调用 fig.add_subplot(gs)

示例

接下来使用一个示例来演示该方法的使用,完整代码如下:

import matplotlib.pyplot as plt  
import numpy as np  
  
plt.style.use("classic")  
plt.rcParams["font.sans-serif"] = ["SimHei"]  
plt.rcParams["axes.unicode_minus"] = False  
  
plt.figure().set_facecolor("#FFF")  
  
plt.subplot2grid((2, 3), (0, 0), colspan=2)  
x = np.linspace(0, 4, 100)  
y = np.random.randn(100)  
plt.scatter(x, y, c='c')  
  
plt.subplot2grid((2, 3), (0, 2))  
plt.title("空白区域")  
  
plt.subplot2grid((2, 3), (1, 0), colspan=3)  
x = np.linspace(0, 4, 100)  
y1 = np.sin(x)  
plt.plot(x, y1, lw=2, ls='-')  
plt.xlim(0, 3)  
plt.grid(True, ls=':', c='r')  
plt.title("折线图")  
  
plt.suptitle("subplot2grid函数实例演示", fontsize=25)  
  
plt.show()

画图结果如下:

subplot2grid函数综合示例

往期回顾

  1. 【matplotlib】可视化解决方案——如何在绘图区域嵌套子绘图区域
  2. 【matplotlib】可视化解决方案——如何正确设置图例
  3. 【matplotlib】可视化解决方案——如何正确设置轴长度和范围
  4. 【matplotlib】可视化解决方案——如何正确理解pyplot和OO-API
  5. 【matplotlib】可视化解决方案——如何正确展示和保存图像
  6. 【matplotlib】可视化解决方案——如何设置字符串的输出字体效果
  7. 【matplotlib】可视化解决方案——如何正确使用颜色映射表
  8. 【matplotlib】可视化解决方案——如何调整轴脊位置
  9. 【matplotlib】可视化解决方案——如何设置坐标系计量方法
  10. 【matplotlib】可视化解决方案——如何正确使用文本注释

文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,相关的知识点也可进行分享,希望大家都能有所收获!!如果觉得我的文章写得还行,不妨支持一下。你的每一个转发、关注、点赞、评论都是对我最大的支持!

你可能感兴趣的:(【matplotlib】可视化解决方案——如何正确使用分割画布函数)