Python Matplotlib 布局
Python Matplotlib 布局
flyfish
gridspec.GridSpec
是 Matplotlib 中用于灵活布局子图的工具,特别适合创建非均匀分布的子图(如不同大小、不同比例的子图组合)。相比普通的 plt.subplots()
(仅支持规则网格),GridSpec
能实现更复杂的布局,例如“一个大子图+多个小子图”“不同行高/列宽的子图”等。
一、概念
GridSpec
的本质是在画布(Figure)上划分一个“虚拟网格”,通过指定网格的行数、列数以及行高/列宽比例,来精确控制子图的位置和大小。
- 网格划分:将画布想象成一个网格,
GridSpec(nrows, ncols)
定义网格的行数和列数。 - 子图定位:通过网格的索引(如
gs[0]
、gs[1:3]
)指定子图占据的网格区域。 - 比例控制:通过
height_ratios
(行高比例)和width_ratios
(列宽比例)设置不同行/列的相对大小。
二、基础用法:创建非均匀子图
1. 安装与导入
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import gridspec # 导入GridSpec# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
2. 最简单的GridSpec示例(2行1列,不同高度)
# 创建画布
fig = plt.figure(figsize=(8, 6)) # 宽8英寸,高6英寸# 定义GridSpec:2行1列,行高比例为3:1(第一行高度是第二行的3倍)
gs = gridspec.GridSpec(nrows=2, ncols=1, height_ratios=[3, 1])# 添加子图:通过索引指定子图位置
ax1 = fig.add_subplot(gs[0]) # 第一行(索引0)
ax2 = fig.add_subplot(gs[1]) # 第二行(索引1)# 填充数据
x = np.linspace(0, 10, 100)
ax1.plot(x, np.sin(x), color='blue')
ax1.set_title('主图(占3/4高度)')ax2.plot(x, np.cos(x), color='red')
ax2.set_title('副图(占1/4高度)')# 调整布局避免重叠
plt.tight_layout()
plt.show()
效果:画布被分为上下两部分,上方子图高度是下方的3倍,分别显示正弦和余弦曲线。
三、关键参数:控制行高与列宽
GridSpec
的核心参数是 height_ratios
(行高比例)和 width_ratios
(列宽比例),用于设置非均匀大小。
示例:3列不同宽度的子图
fig = plt.figure(figsize=(10, 4))# 定义GridSpec:1行3列,列宽比例为1:2:1(中间列宽度是两侧的2倍)
gs = gridspec.GridSpec(nrows=1, ncols=3, width_ratios=[1, 2, 1])# 添加3个子图
ax1 = fig.add_subplot(gs[0]) # 第一列
ax2 = fig.add_subplot(gs[1]) # 第二列(最宽)
ax3 = fig.add_subplot(gs[2]) # 第三列# 填充数据
ax1.bar(['A', 'B'], [3, 5], color='green')
ax1.set_title('列1(宽1份)')ax2.scatter(np.random.rand(50), np.random.rand(50), color='orange')
ax2.set_title('列2(宽2份)')ax3.pie([1, 2, 3], labels=['X', 'Y', 'Z'], autopct='%1.1f%%')
ax3.set_title('列3(宽1份)')plt.tight_layout()
plt.show()
效果:一行三列的子图,中间子图宽度是左右两侧的2倍,适合突出中间内容。
四、进阶:子图跨多行/多列
通过索引切片(如 gs[0:2, 0]
)可以让子图占据多个行或列,实现“合并单元格”的效果。
示例:复杂布局(大子图+多个小子图)
fig = plt.figure(figsize=(10, 8))# 定义GridSpec:3行3列
# 行高比例:2:1:1(第一行最高)
# 列宽比例:1:2:1(中间列最宽)
gs = gridspec.GridSpec(nrows=3, ncols=3,height_ratios=[2, 1, 1],width_ratios=[1, 2, 1]
)# 子图1:占据第1行的所有3列(0:3表示第0到2列)
ax1 = fig.add_subplot(gs[0, 0:3])
ax1.plot(np.linspace(0, 10, 100), np.sin(np.linspace(0, 10, 100)))
ax1.set_title('第1行(跨3列)')# 子图2:占据第2-3行的第1列(0列)
ax2 = fig.add_subplot(gs[1:3, 0]) # 1:3表示第1-2行(共2行)
ax2.barh(['A', 'B', 'C'], [10, 20, 15], color='purple')
ax2.set_title('第2-3行(第1列)')# 子图3:占据第2行的第2-3列
ax3 = fig.add_subplot(gs[1, 1:3])
ax3.scatter(np.random.rand(30), np.random.rand(30), color='red')
ax3.set_title('第2行(第2-3列)')# 子图4:占据第3行的第2列
ax4 = fig.add_subplot(gs[2, 1])
ax4.pie([40, 60], labels=['成功', '失败'], autopct='%1.1f%%')
ax4.set_title('第3行(第2列)')# 子图5:占据第3行的第3列
ax5 = fig.add_subplot(gs[2, 2])
ax5.hist(np.random.randn(100), bins=10, color='green', alpha=0.7)
ax5.set_title('第3行(第3列)')plt.tight_layout()
plt.show()
效果:整个画布被划分为3x3的虚拟网格,5个子图分别占据不同的网格区域,形成“顶部大标题图+左侧竖图+右侧多张小图”的复杂布局。
五、GridSpec vs plt.subplots:适用场景对比
工具 | 优势 | 适用场景 |
---|---|---|
plt.subplots(nrows, ncols) | 语法简单,适合规则网格(如2x2、3x1) | 子图大小相同、排列整齐的情况 |
gridspec.GridSpec | 支持非均匀比例、跨行列布局 | 子图大小不同、需要突出重点内容的复杂布局 |
步骤:
- 用
gridspec.GridSpec(nrows, ncols)
定义虚拟网格的行数和列数; - 通过
height_ratios
和width_ratios
控制行高和列宽比例; - 用索引或切片(如
gs[0]
、gs[1:3, 0]
)指定子图占据的区域; - 用
fig.add_subplot(gs[...])
添加子图并绘图。
# 导入必要库
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import gridspec# 全局配置:解决中文显示与负号问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42) # 固定随机种子,确保结果可复现# -------------------------- 1. 基础:非均匀行高(2行1列,3:1比例)--------------------------
def plot_grid_height_ratio():# 1. 创建画布fig = plt.figure(figsize=(8, 6)) # 宽8,高6英寸# 2. 定义GridSpec:2行1列,行高比例3:1gs = gridspec.GridSpec(nrows=2, # 行数ncols=1, # 列数height_ratios=[3, 1], # 第一行高度是第二行的3倍hspace=0.3 # 行间距(可选,避免子图标题重叠))# 3. 添加子图(通过GridSpec索引定位)ax1 = fig.add_subplot(gs[0]) # 占据第1行(索引0)ax2 = fig.add_subplot(gs[1]) # 占据第2行(索引1)# 4. 填充数据与样式设置x = np.linspace(0, 10, 100)# 子图1:正弦曲线ax1.plot(x, np.sin(x), color='#1f77b4', linewidth=2)ax1.set_title('主图:正弦曲线(占3/4高度)', fontsize=13)ax1.set_xlabel('x轴', fontsize=11)ax1.set_ylabel('sin(x)', fontsize=11)ax1.grid(alpha=0.3)# 子图2:余弦曲线ax2.plot(x, np.cos(x), color='#ff7f0e', linewidth=2)ax2.set_title('副图:余弦曲线(占1/4高度)', fontsize=13)ax2.set_xlabel('x轴', fontsize=11)ax2.set_ylabel('cos(x)', fontsize=11)ax2.grid(alpha=0.3)# 5. 调整布局并保存为JPGplt.tight_layout()plt.savefig('grid_height_ratio.jpg', dpi=300, bbox_inches='tight', format='jpg')plt.close() # 关闭画布释放内存print("1. 非均匀行高布局图已保存:grid_height_ratio.jpg")# -------------------------- 2. 基础:非均匀列宽(1行3列,1:2:1比例)--------------------------
def plot_grid_width_ratio():# 1. 创建画布fig = plt.figure(figsize=(12, 4)) # 宽12,高4英寸# 2. 定义GridSpec:1行3列,列宽比例1:2:1gs = gridspec.GridSpec(nrows=1,ncols=3,width_ratios=[1, 2, 1], # 中间列宽度是两侧的2倍wspace=0.3 # 列间距(可选))# 3. 添加子图ax1 = fig.add_subplot(gs[0]) # 第1列ax2 = fig.add_subplot(gs[1]) # 第2列(最宽)ax3 = fig.add_subplot(gs[2]) # 第3列# 4. 填充数据与样式# 子图1:柱状图categories = ['A', 'B', 'C']ax1.bar(categories, [5, 8, 3], color='#2ca02c', alpha=0.8)ax1.set_title('列1:类别对比(1份宽)', fontsize=12)ax1.set_ylabel('数值', fontsize=10)ax1.grid(axis='y', alpha=0.3)# 子图2:散点图(突出显示)x_scatter = np.random.randn(100)y_scatter = 2 * x_scatter + np.random.randn(100) * 0.5ax2.scatter(x_scatter, y_scatter, color='#d62728', alpha=0.6)ax2.set_title('列2:相关性散点图(2份宽)', fontsize=12)ax2.set_xlabel('x', fontsize=10)ax2.set_ylabel('y', fontsize=10)ax2.grid(alpha=0.3)# 子图3:饼图pie_data = [30, 50, 20]ax3.pie(pie_data, labels=['甲', '乙', '丙'], autopct='%1.1f%%', colors=['#9467bd', '#8c564b', '#e377c2'])ax3.set_title('列3:占比分布(1份宽)', fontsize=12)ax3.axis('equal') # 确保饼图为圆形# 5. 保存图片plt.tight_layout()plt.savefig('grid_width_ratio.jpg', dpi=300, bbox_inches='tight', format='jpg')plt.close()print("2. 非均匀列宽布局图已保存:grid_width_ratio.jpg")# -------------------------- 3. 进阶:子图跨多行多列(3x3网格复杂布局)--------------------------
def plot_grid_span_rows_cols():# 1. 创建画布fig = plt.figure(figsize=(12, 10)) # 宽12,高10英寸(适合复杂布局)# 2. 定义GridSpec:3行3列,行高比例2:1:1,列宽比例1:2:1gs = gridspec.GridSpec(nrows=3,ncols=3,height_ratios=[2, 1, 1], # 第1行最高,占2份width_ratios=[1, 2, 1], # 第2列最宽,占2份hspace=0.3, # 行间距wspace=0.3 # 列间距)# 3. 添加子图(通过切片实现跨行列)# 子图1:跨第1行所有3列(行索引0,列索引0-2)ax1 = fig.add_subplot(gs[0, :]) # ":" 表示所有列# 子图2:跨第2-3行的第1列(行索引1-2,列索引0)ax2 = fig.add_subplot(gs[1:, 0]) # "1:" 表示第1行及以下# 子图3:跨第2行的第2-3列(行索引1,列索引1-2)ax3 = fig.add_subplot(gs[1, 1:])# 子图4:第3行第2列(行索引2,列索引1)ax4 = fig.add_subplot(gs[2, 1])# 子图5:第3行第3列(行索引2,列索引2)ax5 = fig.add_subplot(gs[2, 2])# 4. 填充数据与样式x = np.linspace(0, 10, 100)# 子图1:顶部大标题图(折线图)ax1.plot(x, np.sin(x), label='sin(x)', color='#1f77b4', linewidth=2)ax1.plot(x, np.cos(x), label='cos(x)', color='#ff7f0e', linewidth=2, linestyle='--')ax1.set_title('顶部主图:正弦与余弦曲线(跨3列)', fontsize=14)ax1.legend(loc='upper right')ax1.grid(alpha=0.3)# 子图2:左侧竖图(水平柱状图)ax2.barh(['A', 'B', 'C', 'D'], [15, 25, 10, 30], color='#2ca02c', alpha=0.8)ax2.set_title('左侧副图:类别数值(跨2行)', fontsize=12)ax2.set_xlabel('数值', fontsize=10)ax2.grid(axis='x', alpha=0.3)# 子图3:中间右图(散点图)ax3.scatter(np.random.rand(50), np.random.rand(50), s=50, color='#d62728', alpha=0.6)ax3.set_title('中间右图:随机分布(跨2列)', fontsize=12)ax3.grid(alpha=0.3)# 子图4:右下角图1(饼图)ax4.pie([40, 60], labels=['成功', '失败'], autopct='%1.1f%%', colors=['#9467bd', '#8c564b'])ax4.set_title('饼图:成功率', fontsize=11)ax4.axis('equal')# 子图5:右下角图2(直方图)ax5.hist(np.random.randn(200), bins=15, color='#e377c2', alpha=0.7)ax5.set_title('直方图:正态分布', fontsize=11)ax5.set_xlabel('值', fontsize=9)ax5.grid(axis='y', alpha=0.3)# 5. 保存图片plt.tight_layout()plt.savefig('grid_span_rows_cols.jpg', dpi=300, bbox_inches='tight', format='jpg')plt.close()print("3. 跨行列复杂布局图已保存:grid_span_rows_cols.jpg")# -------------------------- 4. 实战:论文常见布局(大子图+2个小子图)--------------------------
def plot_grid_paper_layout():# 1. 创建画布fig = plt.figure(figsize=(10, 8)) # 符合论文图片比例# 2. 定义GridSpec:2行2列,行高比例3:2,列宽比例2:1gs = gridspec.GridSpec(nrows=2,ncols=2,height_ratios=[3, 2],width_ratios=[2, 1],hspace=0.3,wspace=0.3)# 3. 添加子图ax1 = fig.add_subplot(gs[0, 0]) # 第1行第1列(大子图)ax2 = fig.add_subplot(gs[1, 0]) # 第2行第1列(下方小子图)ax3 = fig.add_subplot(gs[:, 1]) # 所有行第2列(右侧竖图)# 4. 填充数据(模拟论文数据)# 子图1:主结果图(折线图+误差线)x_data = [1, 2, 3, 4, 5]y_data = [10, 15, 12, 18, 20]y_err = [1, 0.8, 1.2, 0.9, 1.1]ax1.errorbar(x_data, y_data, yerr=y_err, fmt='o-', color='#1f77b4', capsize=5, linewidth=2)ax1.set_title('主结果:随参数变化趋势', fontsize=13)ax1.set_xlabel('参数值', fontsize=11)ax1.set_ylabel('性能指标', fontsize=11)ax1.grid(alpha=0.3)# 子图2:对比柱状图models = ['模型A', '模型B', '本文模型']scores = [14, 16, 20]ax2.bar(models, scores, color=['#ff7f0e', '#2ca02c', '#d62728'], alpha=0.8)ax2.set_title('方法对比', fontsize=13)ax2.set_ylabel('得分', fontsize=11)ax2.grid(axis='y', alpha=0.3)# 子图3:右侧数据分布(箱线图)box_data = [np.random.randn(50) * 0.5 + i for i in range(4)] # 4组数据ax3.boxplot(box_data, labels=['组1', '组2', '组3', '组4'])ax3.set_title('数据分布箱线图', fontsize=13)ax3.set_ylabel('数值', fontsize=11)ax3.grid(axis='y', alpha=0.3)# 5. 保存图片plt.tight_layout()plt.savefig('grid_paper_layout.jpg', dpi=300, bbox_inches='tight', format='jpg')plt.close()print("4. 论文常见布局图已保存:grid_paper_layout.jpg")# -------------------------- 运行所有布局案例 --------------------------
if __name__ == '__main__':plot_grid_height_ratio()plot_grid_width_ratio()plot_grid_span_rows_cols()plot_grid_paper_layout()print("\n 所有GridSpec布局图已保存完成!")print("文件列表:")print("1. grid_height_ratio.jpg (非均匀行高:2行1列)")print("2. grid_width_ratio.jpg (非均匀列宽:1行3列)")print("3. grid_span_rows_cols.jpg (跨行列复杂布局:3x3网格)")print("4. grid_paper_layout.jpg (论文实战布局:大子图+2小子图)")