十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Matplotlib子图编程:深入理解figure与axes对象

Matplotlib子图编程:深入理解figure与axes对象 1. 项目概述为什么我们需要理解figure和axes如果你刚开始用Python的Matplotlib库画图大概率会从一行简单的plt.plot(x, y)开始。它很友好画个折线图不费吹灰之力。但当你需要把多张图并排展示或者想精细控制图中每一个元素比如坐标轴标签、图例位置、子图间距时仅仅用plt.xxx这一套“全局”命令就会显得力不从心代码会变得混乱且难以维护。这时plt.subplot()和plt.subplots()这两个函数以及它们返回的核心对象——figure和axes就成了你必须跨越的一道坎。很多教程会直接告诉你“用subplots(2, 2)画一个2x2的子图”但很少深入解释为什么返回的是figure和一个axes数组axes对象到底代表了什么subplot和subplots仅仅是一个“s”的区别吗理解不清的后果就是你的代码里可能会混杂着plt.title()和ax.set_title()这两种风格调整子图间距时对着plt.subplots_adjust的参数一头雾水更别提实现复杂的、非均匀网格的子图布局了。今天我们就抛开那些笼统的介绍从绘图引擎的底层逻辑出发彻底搞懂figure和axes让你能真正驾驭Matplotlib而不是被它牵着鼻子走。2. 核心概念拆解Figure与Axes到底是什么在动手写代码之前我们必须建立正确的心理模型。Matplotlib的绘图体系是层次化的理解这个层次是灵活绘图的关键。2.1 Figure画布或舞台你可以把Figure对象想象成一张空白的画布或者一个舞台。它是一个顶级容器所有其他的绘图元素都“生活”在这张画布上。创建一个Figure对象就意味着你准备开始一幅新的、独立的图表。Figure对象负责管理一些全局属性画布尺寸通过figsize参数如figsize(10, 6)设置单位是英寸。整体背景色。子图Axes的布局网格虽然具体的子图位置由Axes自己或布局管理器决定但Figure定义了网格的“潜在”结构。一些全局的艺术家对象比如Figure级别的文字、图例虽然不常见。一个常见的误解认为plt.figure()只是创建一个空窗口。实际上它创建了一个拥有完整坐标系范围是0到1的容器你甚至可以直接在Figure上画图比如添加水印但这通常不是主要用途。2.2 Axes真正的绘图区域这是最核心、也最容易混淆的概念。Axes对象不是指“x轴”或“y轴”那叫axis而是指一个完整的、独立的坐标系区域它包含了轴Axis、刻度Tick、标签Label、数据区域Patch以及所有在这个坐标系里绘制的图形如线Line2D、散点PathCollection等。更直观的理解在一张画布Figure上每一个Axes对象就是一个单独的、有自己坐标系的子图。当我们说“创建一个2x2的子图”时我们实际上是在画布上创建了4个独立的Axes对象。Axes对象是你进行绝大多数绘图操作的地方ax.plot(x, y): 在这个子图里画折线。ax.set_xlabel(‘Time’): 设置这个子图的x轴标签。ax.set_title(‘Subplot A’): 设置这个子图的标题。ax.legend(): 为这个子图添加图例。关键区别plt.xlabel()和ax.set_xlabel()。前者作用于“当前活动的”AxesMatplotlib内部维护的一个状态后者明确指定了要操作哪个Axes对象。在复杂的多子图程序中使用ax.set_xxx风格是更安全、更清晰的做法。2.3 它们与“plt”模块的关系pyplot模块通常导入为plt是一个状态机接口。它维护着“当前的Figure”和“当前的Axes”。当你调用plt.plot()时它会在“当前的Axes”中绘图。如果不存在它会自动创建一个Figure和一个Axes。这种状态机模式对于交互式环境和简单脚本非常方便但对于复杂的、结构化的程序直接操作Figure和Axes对象面向对象接口是更好的选择代码意图更明确耦合度更低。3. 子图创建函数详解subplot() vs subplots()现在我们来看看创建子图的两个核心工具。它们的目标一致但哲学和返回值截然不同。3.1 plt.subplot()逐个创建状态机风格plt.subplot(nrows, ncols, index)是“增量式”或“按需创建”的风格。它修改当前图形Figure的状态将“当前的Axes”切换到指定网格位置的子图上。如果那个位置的子图不存在则创建它。import matplotlib.pyplot as plt import numpy as np # 创建一个新的Figure画布 plt.figure(figsize(10, 4)) # 切换到2行1列的第1个子图顶部 plt.subplot(2, 1, 1) # 现在“当前的Axes”是这个位置 x np.linspace(0, 10, 100) plt.plot(x, np.sin(x)) # 在第一个子图画图 plt.title(‘Sine Wave‘) # 切换到2行1列的第2个子图底部 plt.subplot(2, 1, 2) # “当前的Axes”变成了第二个 plt.plot(x, np.cos(x)) # 在第二个子图画图 plt.title(‘Cosine Wave‘) plt.tight_layout() # 调整子图间距避免重叠 plt.show()返回对象plt.subplot()返回一个Axes对象或它的子类如Subplot它就是被创建或激活的那个子图。但通常我们不需要捕获它因为后续的plt.xxx()命令会自动作用于这个被激活的Axes。优点语法简单直观特别适合在交互式环境如Jupyter Notebook中逐步添加子图。可以创建非均匀网格的子图通过subplot2grid函数灵活性极高。缺点代码耦合度高大量使用plt.xxx难以将子图创建和绘图逻辑分离。容易出错如果忘记切换subplot索引就会在错误的子图上绘图。难以批量操作没有直接的方法获取到所有子图对象的引用进行统一设置如隐藏所有子图的x轴刻度比较麻烦。3.2 plt.subplots()批量创建面向对象风格plt.subplots(nrows1, ncols1, **kwargs)是“声明式”的风格。它一次性创建一个新的Figure对象和一个包含所有Axes对象的数组即使只有1行1列然后返回它们。import matplotlib.pyplot as plt import numpy as np # 一次性创建1个Figure和2x24个Axes fig, axs plt.subplots(nrows2, ncols2, figsize(10, 8)) # fig 是一个Figure对象 # axs 是一个2x2的numpy数组每个元素都是一个Axes对象 print(type(axs), axs.shape) # class ‘numpy.ndarray‘ (2, 2) # 现在可以像操作数组一样操作每个子图 for i in range(2): for j in range(2): x np.linspace(0, 10, 50) y x ** (i j 1) # 每个子图用不同的幂次 axs[i, j].plot(x, y) axs[i, j].set_title(f‘y x^{ij1}‘) axs[i, j].grid(True, linestyle‘--‘, alpha0.6) # 为整个Figure添加一个总标题 fig.suptitle(‘Power Functions‘, fontsize16) plt.tight_layout() plt.show()返回对象fig: 一个Figure实例。axs/ax: 当nrows和ncols都大于1时返回一个numpy.ndarray数组形状为(nrows, ncols)。当只有一个子图时nrowsncols1直接返回一个Axes对象。这是一个非常关键且容易让新手困惑的点。优点代码清晰解耦Figure和所有Axes的引用一目了然便于后续精细控制。便于批量操作通过循环axs.flat可以轻松遍历所有子图进行统一设置。集成高级布局通过gridspec_kw参数可以方便地使用GridSpec进行复杂布局。减少错误明确指定了每个子图对象避免了状态切换导致的错误。缺点对于极其不规则、跨度不同的子图布局原生语法不如subplot2grid直观但结合GridSpec同样强大。实操心得对于绝大多数新的项目或脚本我强烈推荐从plt.subplots()开始。它迫使你以面向对象的方式思考写出的代码更健壮、更易维护。只有在需要动态、交互式地添加子图或者布局复杂到必须用subplot2grid时才考虑plt.subplot()。4. 深入实操从创建到精细控制的完整流程理解了概念和基本函数后我们通过一个综合例子看看如何利用figure和axes对象完成从创建、绘图到美化的全流程。4.1 创建与基础绘图假设我们要分析一个数据集需要绘制1x3的并排子图折线图、散点图和柱状图。import matplotlib.pyplot as plt import numpy as np # 1. 创建画布和子图数组 fig, axs plt.subplots(1, 3, figsize(15, 4)) # 注意axs现在是一个形状为(1, 3)的数组即axs[0, 0], axs[0, 1], axs[0, 2] # 对于单行或单列也可以使用axs[0], axs[1], axs[2]来索引更清晰 line_ax, scatter_ax, bar_ax axs # 解包让变量名更有意义 # 生成示例数据 x np.arange(10) y_line np.random.randn(10).cumsum() # 随机游走 y_scatter np.random.rand(10) categories [‘A‘, ‘B‘, ‘C‘, ‘D‘, ‘E‘] values np.random.randint(10, 50, size5) # 2. 在第一个Axes上绘制折线图 line_ax.plot(x, y_line, marker‘o‘, color‘steelblue‘, linewidth2, label‘Trend‘) line_ax.set_title(‘Line Chart: Random Walk‘) line_ax.set_xlabel(‘Step‘) line_ax.set_ylabel(‘Cumulative Value‘) line_ax.legend() line_ax.grid(True, alpha0.3) # 3. 在第二个Axes上绘制散点图 scatter_ax.scatter(x, y_scatter, s100, cx, cmap‘viridis‘, alpha0.7, edgecolors‘black‘) scatter_ax.set_title(‘Scatter Plot with Colormap‘) scatter_ax.set_xlabel(‘Index‘) scatter_ax.set_ylabel(‘Random Value‘) # 添加颜色条 scatter_plot scatter_ax.collections[0] # 获取散点图的集合对象 plt.colorbar(scatter_plot, axscatter_ax) # 4. 在第三个Axes上绘制柱状图 bars bar_ax.bar(categories, values, color[‘skyblue‘, ‘salmon‘, ‘lightgreen‘, ‘gold‘, ‘violet‘]) bar_ax.set_title(‘Bar Chart by Category‘) bar_ax.set_xlabel(‘Category‘) bar_ax.set_ylabel(‘Value‘) # 在柱子上方添加数值标签 for bar in bars: height bar.get_height() bar_ax.text(bar.get_x() bar.get_width()/2., height 0.5, f‘{int(height)}‘, ha‘center‘, va‘bottom‘) # 5. 调整整体布局 fig.suptitle(‘Multi-plot Dashboard‘, fontsize16, y1.05) # 总标题y参数控制垂直位置 plt.tight_layout() # 自动调整子图参数使子图适合画布区域 plt.show()4.2 高级布局控制GridSpec与子图合并当1xN或Nx1的简单网格无法满足需求时GridSpec是终极武器。它允许你创建行高和列宽不等的复杂网格。import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import numpy as np # 创建一个2x2的网格但定义不同的高度和宽度比例 fig plt.figure(figsize(10, 8)) # 参数nrows, ncols, 以及可选的height_ratios, width_ratios gs gridspec.GridSpec(3, 3, figurefig, height_ratios[2, 1, 1], width_ratios[1, 2, 1]) # 创建跨越多行多列的子图 # 左上角大图占据第0行和第1行第0列和第1列 (rowspan2, colspan2) ax_main fig.add_subplot(gs[0:2, 0:2]) # 切片语法非常直观 # 右上角小图占据第0行第2列 ax_small1 fig.add_subplot(gs[0, 2]) # 中间行右侧小图占据第1行第2列 ax_small2 fig.add_subplot(gs[1, 2]) # 底部长条图占据第2行所有列 (colspan3) ax_bottom fig.add_subplot(gs[2, :]) # 在各个Axes上绘图 x np.linspace(0, 2*np.pi, 100) ax_main.plot(x, np.sin(x), label‘sin‘) ax_main.plot(x, np.cos(x), label‘cos‘) ax_main.legend() ax_main.set_title(‘Main Plot (Trigonometric Functions)‘) ax_small1.plot(x, np.tan(x)) ax_small1.set_title(‘tan(x)‘) ax_small1.set_ylim(-5, 5) # tan函数值域大需要限制 ax_small2.scatter(np.random.rand(20), np.random.rand(20)) ax_small2.set_title(‘Random Scatter‘) categories [‘Q1‘, ‘Q2‘, ‘Q3‘, ‘Q4‘] sales [45, 67, 55, 80] ax_bottom.bar(categories, sales, color‘teal‘) ax_bottom.set_title(‘Quarterly Sales‘) ax_bottom.set_ylabel(‘Units Sold‘) plt.tight_layout() plt.show()注意事项使用GridSpec时我们不再用plt.subplots()而是先创建Figure再创建GridSpec对象最后通过fig.add_subplot(gs[row_slice, col_slice])来添加Axes。这种方式提供了像素级的布局控制能力。4.3 统一风格与批量设置拥有axs数组后批量操作变得异常简单。import matplotlib.pyplot as plt import numpy as np fig, axs plt.subplots(2, 3, figsize(12, 6)) # 批量设置所有子图的标题前缀和网格 for idx, ax in enumerate(axs.flat): # .flat将2D数组迭代为一维 # 生成一些随机数据 data np.random.randn(100).cumsum() ax.plot(data) ax.set_title(f‘Chart {idx1}‘) ax.grid(True, linestyle‘:‘, linewidth0.5) # 批量设置最左侧一列子图的y轴标签 for ax in axs[:, 0]: # 索引第一列 ax.set_ylabel(‘Cumulative Sum‘) # 批量设置最下面一行子图的x轴标签 for ax in axs[-1, :]: # 索引最后一行 ax.set_xlabel(‘Time Step‘) # 共享x轴或y轴让相邻子图的刻度对齐节省空间 # 在创建时指定 sharex 或 sharey fig2, axs2 plt.subplots(2, 2, figsize(10, 6), sharexTrue, shareyTrue) # 现在只有最下面一行子图的x轴标签和最左侧一列子图的y轴标签会显示 plt.tight_layout() plt.show()5. 常见问题与排查技巧实录在实际使用中你一定会遇到各种奇怪的问题。下面是我踩过的一些坑和解决方案。5.1 问题1axs变量是单个对象还是数组如何正确索引这是新手最常遇到的问题源于plt.subplots()返回值的不一致性。症状当你写fig, axs plt.subplots(2, 2)时想用axs.plot(...)绘图却得到‘AxesSubplot‘ object has no attribute ‘plot‘或类似的错误。根源plt.subplots(2, 2)返回的axs是一个2x2的数组。你需要索引到具体的Axes元素如axs[0, 0]才能调用.plot()方法。而plt.subplots(1, 1)或plt.subplots()返回的axs就是一个单独的Axes对象。解决方案通用安全索引法无论axs是数组还是单个对象都先用np.array(axs).flat或np.atleast_1d(axs)将其转换为可迭代的形式。fig, axs plt.subplots(2, 3) axs np.array(axs) # 确保是数组 for ax in axs.flat: ax.plot(...)条件判断法fig, axs plt.subplots(2, 2) if hasattr(axs, ‘ndim‘): # 如果是数组 ax axs[0, 0] else: # 如果是单个Axes对象 ax axs直接解包适用于已知网格很小的情况fig, ((ax1, ax2), (ax3, ax4)) plt.subplots(2, 2) # 现在ax1, ax2, ax3, ax4就是四个独立的Axes变量5.2 问题2子图标题、标签重叠或显示不全症状图挤在一起标题重叠坐标轴标签被切掉。根源画布figsize太小或者子图之间的间距subplots_adjust参数不足。解决方案首选在plt.show()之前调用plt.tight_layout()。这个函数会自动调整子图参数使它们不会重叠。它通常是解决此类问题的第一选择。手动微调如果tight_layout效果不理想可以使用plt.subplots_adjust()或fig.subplots_adjust()进行精细控制。fig.subplots_adjust(left0.1, # 画布左边界 bottom0.1, # 画布底边界 right0.9, # 画布右边界 top0.9, # 画布顶边界 wspace0.4, # 子图之间的水平间距宽度比例 hspace0.3) # 子图之间的垂直间距高度比例wspace和hspace是调整间距的关键参数值越大间距越大。增大画布创建Figure时使用更大的figsize如figsize(12, 8)。5.3 问题3想删除某个子图或清空内容症状在复杂的动态绘图或交互应用中需要移除或重置某个特定Axes。解决方案清空内容使用ax.clear()。这会移除该Axes上所有的艺术家对象线、散点、文本等但保留Axes本身坐标轴、标题框等。彻底删除使用ax.remove()。这将从Figure中移除该Axes对象。之后你可以用fig.add_axes()或fig.add_subplot()在原来的位置或新位置添加一个新的Axes。fig, (ax1, ax2) plt.subplots(1, 2) ax1.plot([1,2,3], [1,2,3]) # 后来决定不要ax2了 ax2.remove() # 在原ax2的位置创建一个新的、不同比例的Axes # 需要计算位置通常用add_axes([left, bottom, width, height]) new_ax fig.add_axes([0.55, 0.1, 0.35, 0.8]) # 参数是相对于画布的比例 new_ax.bar([‘A‘, ‘B‘], [3, 5])5.4 问题4保存的图片分辨率低或尺寸不对症状屏幕上显示很好保存为PNG或PDF后模糊或布局错乱。根源保存图片时没有指定正确的DPI每英寸点数或者画布figsize设置与保存预期不符。解决方案在plt.savefig()时指定dpi参数通常150-300用于打印72用于网页显示和bbox_inches‘tight‘。fig, axs plt.subplots(2, 2, figsize(10, 8)) # ... 绘图操作 ... plt.savefig(‘my_plot.png‘, dpi300, bbox_inches‘tight‘, pad_inches0.1)dpi300: 高分辨率输出。bbox_inches‘tight‘: 自动计算并裁剪图形周围的空白区域。pad_inches0.1: 在裁剪后的图形周围保留0.1英寸的填充边距。5.5 问题5混合使用plt.xxx和ax.xxx导致图形错乱症状图形元素出现在意想不到的子图上或者设置不生效。根源Matplotlib的pyplot状态机维护着“当前Axes”。当你用面向对象方式ax.plot操作一个子图后又调用了全局的plt.xlabel()这个标签可能会被应用到另一个“当前Axes”上而不是你期望的那个。黄金法则在同一个脚本或函数中尽量坚持使用一种风格。如果使用了fig, axs plt.subplots()那么后续所有绘图和设置都通过axs[i, j].xxx()来完成。彻底避免使用plt.title(),plt.xlabel(),plt.legend()等全局命令。这样可以保证代码行为完全可预测。如果你必须混合使用例如在已有面向对象代码中插入一个快速调试的plt.show()请务必在调用plt.xxx函数前使用plt.sca(ax)Set Current Axes来显式设置当前活动的Axes。fig, (ax1, ax2) plt.subplots(1, 2) ax1.plot([1,2,3], [1,4,9]) # 现在想用plt给ax1加个标题不推荐但可行 plt.sca(ax1) # 将“当前Axes”设置为ax1 plt.title(‘This title goes to ax1‘) # 现在这个plt.title会作用在ax1上理解figure和axes对象并熟练掌握subplots()的面向对象编程模式是脱离Matplotlib新手村的关键一步。它带来的不仅是代码的清晰更是对图形元素控制的自由。下次当你需要创建多子图时不要再机械地复制plt.subplot(2,2,1)的代码块了试试先创建fig, axs plt.subplots(2, 2)然后优雅地遍历axs.flat你会发现世界大不相同。
返回列表