做数据分析这几年,Matplotlib里的pyplot几乎是我每天都会打开的模块。别人问我会不会画图,我说会,其实就是会用pyplot把数据变成可视化图形。但真正把绘图这件事做得又快又顺手,不是记住几个API就够的,你得理解它的绘图逻辑,知道怎么把多张图拼在一起,还得学会处理那些奇奇怪怪的报错,比如最近经常有人问的“请安装matplotlib以显示图标”到底是怎么回事。
这篇博文就围绕Matplotlib.pyplot绘图展开。我会从pyplot的底层思路讲起,重点拆解“六张图合在一个图上”这类多子图拼接需求,再分享一套完整可复用的实操代码,最后把安装配置、中文乱码、图表不显示这些高频问题一次性讲透。不管你是刚装好Matplotlib的新手,还是已经画过一阵子图但总感觉不系统的同学,这篇应该都能帮上忙。
1. 先把思路理顺:pyplot到底解决什么问题
1.1 为什么选 pyplot,而不是直接上手面向对象
很多人第一次接触Matplotlib,上来就学习figure、axes这些概念,结果一头雾水。我个人的建议是:先从pyplot入手,因为它是一层更贴近直觉的封装。
pyplot设计得很像MATLAB的绘图方式,核心思路是一个“状态机”。你调用plt.plot(),它就把数据画到“当前坐标系”上;再调用plt.xlabel(),它就给刚才这张图加标签;再调用plt.title(),就给这张图加标题。你不需要时刻盯着figure和axes对象,只管像喊口令一样一步一步来就行。
这种模式特别适合交互式探索场景。比如你在Jupyter Notebook里分析数据,临时想看看某两列的关系,两条命令就出图了。但是,如果要做正式的、复杂的图表,我建议还是要用fig, ax = plt.subplots()这种面向对象的方式。原因很简单:pyplot的隐式状态在复杂场景下容易“串图”,尤其当你需要同时管理多个子图时,显式的axes对象会让代码更清晰、更好控制。
1.2 安装配置:pip install matplotlib 前后的那些坑
先解决环境问题。安装Matplotlib本身很简单,一行命令:
pip install matplotlib但实际项目中,这条命令可能踩到几个典型坑。最常见的是“装到了错误的Python环境”。比如你电脑里既有Anaconda,又有系统Python,还在某个虚拟环境里开发,如果你没激活虚拟环境就执行pip install matplotlib,装完发现导入仍报错,那多半就是装错地方了。
我的习惯是,在项目根目录建一个虚拟环境,然后老老实实激活后再装:
python -m venv venvWindows下激活:
venv\Scripts\activatemacOS/Linux下激活:
source venv/bin/activate然后再装matplotlib。装完验证一下:
python -c "import matplotlib; print(matplotlib.__version__)"能输出版本号,说明环境没问题。如果你的网络下载比较慢,可以用国内镜像源,比如清华的PyPI镜像:
pip install matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple另外,很多人遇到的“请安装matplotlib以显示图标”并不是matplotlib本体报错,而是IDE或者Jupyter环境检测不到已安装的matplotlib时给出的提示。解决思路很简单:在对应环境里装好matplotlib,然后重启Notebook内核或重启IDE。如果装好了还是提示,就检查一下是不是内核选的不是当前虚拟环境,这属于老生常谈但确实容易搞混的问题。
2. 核心细节解析:从一张图到多子图拼接
2.1 先吃透 pyplot 的基本绘图链路
我把pyplot绘图的基本流程概括成六步,大部分图表都逃不出这个套路:
- 准备数据,一般就是列表、NumPy数组或者Pandas列。
- 创建画布,用
plt.figure(figsize=(宽, 高)),这一步可选,但建议显式设置画布尺寸,否则好看不到哪去。 - 添加坐标系,或者直接用
plt.plot()等函数隐式创建。 - 调用具体绘图函数,比如
plot、scatter、bar、hist。 - 设置细节,包括标题、轴标签、图例、网格、刻度范围。
- 显示或保存,交互环境用
plt.show(),脚本或批量生成用plt.savefig()。
如果你关注热搜词里的“matplotlib六张图合在一个图上”,那核心痛点就出在第3步到第6步之间。很多人知道了单张图画法,但一到拼图就懵了。其实原理不复杂,关键在于理解“坐标系”这个概念。
简单打个比方:一张画布是figure,画布上面可以划分出很多个格子,每个格子里放一张图,每个格子就是一个axes对象。你要画六张图,本质就是创建一张画布,在上面划分出2行3列的网格,然后往每个格子里填内容。
2.2 三种实现“六张图合一个图”的方案对比
具体实现多子图,目前主流有三种方式,我列个表格对比一下:
| 方式 | 核心函数 | 优点 | 适用场景 |
|---|---|---|---|
| 逐个定位 | plt.subplot(2, 3, index) | 写法直观,适合手动控制某一张图 | 子图数量少、布局固定 |
| 批量创建 | plt.subplots(2, 3) | 返回画布和坐标轴数组,代码整洁,推荐 | 规则网格布局,比如2行3列 |
| 复杂布局 | GridSpec | 支持跨行跨列、不等宽布局 | 仪表盘、报告封面、复杂排版 |
如果你就是想把6张图均匀地拼成一个2行3列的大图,最省事的方案就是plt.subplots(2, 3)。它会一次性返回一个figure对象和一个形状为(2, 3)的axes数组,然后你用axes[0][0]、axes[1][2]这些下标去定位每一张子图就行。
这里我要特别说一下plt.subplot(2, 3, 1)和plt.subplots(2, 3)的区别。前者是每次调用都指定“第几个位置”,你要画6张图就得调用6次;后者是一次性建好6个位置,后续只需要在对应位置画图。如果你用面向对象方式写,后续调整样式、共享坐标轴都会方便很多。
2.3 构图参数:figsize、dpi、共享坐标轴和布局调整
多子图拼在一起之后,最常见的两个问题:一是图太小看不清,二是子图之间挤在一起。前者靠figsize解决,后者靠tight_layout()或constrained_layout=True解决。
figsize的单位是英寸,dpi是每英寸像素点数。比如plt.subplots(2, 3, figsize=(12, 8), dpi=120),意思是画布宽12英寸、高8英寸,分辨率120 DPI,最终生成的图片宽就是1440像素。这个参数不是随便拍脑袋定的,要看你最终用途:公众号配图一般100-150 DPI就够,打印或投稿则需要300 DPI。
再说共享坐标轴。六个子图画在同一张画布上,有时候它们代表的是同一套时间轴或同一套数值范围,那可以用sharex=True或sharey=True,这样缩放一个子图时其他子图会联动,而且刻度标签不会重复占空间,整体观感会很紧凑专业。
最后提醒一点:拼图完成后,一定要调用plt.tight_layout()或者创建figure时设置constrained_layout=True,否则子图标题和轴标签很容易被切掉或者重叠。这个小细节很多人忽略,结果图一出来边距奇怪,还以为是代码问题。
3. 实操过程:用一份代码完成六图拼接
3.1 场景设定:一份数据分析报告需要六张图
为了把这个过程讲得具体,我模拟一个真实场景。假设你在做一份商品销售数据分析,报告里需要展示以下六张图:
- 销售额随时间的变化趋势(折线图)
- 各渠道销售额占比(饼图)
- 订单金额的分布情况(直方图)
- 商品价格与销量的关系(散点图)
- 各区域销售额对比(柱状图)
- 不同门店销售额的分布差异(箱线图)
这就是典型的“六张图合在一个图上”的需求,用plt.subplots(2, 3)来做非常合适。我先构造一份模拟数据,然后一步步来画。
3.2 核心代码实现与逐步讲解
先导入库和准备数据:
import matplotlib.pyplot as plt import numpy as np # 解决中文乱码(重点是SimHei字体和负号设置) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 模拟数据 np.random.seed(42) months = np.arange(1, 13) sales = np.array([120, 135, 128, 150, 165, 180, 175, 190, 205, 210, 225, 240]) channels = ['App', 'Web', '小程序', '线下'] channel_sales = [360, 220, 280, 140] order_amounts = np.random.normal(150, 40, 500) price = np.random.uniform(10, 200, 200) qty = (500 / price + np.random.normal(0, 8, 200)).clip(1, 60) regions = ['华东', '华南', '华北', '西南'] region_sales = [820, 735, 620, 410] store_sales = [np.random.normal(80, 15, 30) for _ in range(4)]然后创建画布和子图:
fig, axes = plt.subplots(2, 3, figsize=(14, 9), dpi=120)这一行的意思就是,创建一个14英寸宽、9英寸高的画布,划分成2行3列共6个子图,axes的形状是(2, 3)。
接下来逐个填充子图:
# 1. 销售额趋势 axes[0, 0].plot(months, sales, marker='o', color='#1f77b4', linewidth=2) axes[0, 0].set_title('月度销售额趋势') axes[0, 0].set_xlabel('月份') axes[0, 0].set_ylabel('销售额(万元)') axes[0, 0].grid(True, linestyle='--', alpha=0.6) # 2. 渠道占比 axes[0, 1].pie(channel_sales, labels=channels, autopct='%.1f%%', startangle=90) axes[0, 1].set_title('各渠道销售额占比') # 3. 订单金额分布 axes[0, 2].hist(order_amounts, bins=30, color='#2ca02c', edgecolor='white', alpha=0.8) axes[0, 2].set_title('订单金额分布') axes[0, 2].set_xlabel('订单金额(元)') axes[0, 2].set_ylabel('频数') # 4. 价格与销量关系 axes[1, 0].scatter(price, qty, alpha=0.5, s=30, color='#ff7f0e') axes[1, 0].set_title('商品价格与销量关系') axes[1, 0].set_xlabel('价格(元)') axes[1, 0].set_ylabel('销量(件)') # 5. 各区域销售额 axes[1, 1].bar(regions, region_sales, color='#d62728', alpha=0.85) axes[1, 1].set_title('各区域销售额对比') axes[1, 1].set_xlabel('区域') axes[1, 1].set_ylabel('销售额(万元)') for i, v in enumerate(region_sales): axes[1, 1].text(i, v + 10, str(v), ha='center', fontsize=10) # 6. 门店销售额箱线图 axes[1, 2].boxplot(store_sales, tick_labels=regions, patch_artist=True) axes[1, 2].set_title('门店销售额分布') axes[1, 2].set_xlabel('区域') axes[1, 2].set_ylabel('销售额(万元)') plt.tight_layout() plt.savefig('sales_report.png', bbox_inches='tight', dpi=150) plt.show()这段代码跑完之后,你应该能看到一张2行3列的大图,六个子图分别是折线图、饼图、直方图、散点图、柱状图、箱线图。整张图会被自动保存为sales_report.png,同时弹窗展示。
几个我特别想强调的细节:
第一,plt.rcParams的中文字体设置必须放在绘图之前,否则画出来的中文全是方块。而且axes.unicode_minus=False这个也别忘了,不然坐标轴的负号会显示成乱码。
第二,boxplot函数里我用了patch_artist=True,这样箱线图才能填充颜色,否则默认是透明的,打印出来观感差很多。新版matplotlib里tick_labels代替了旧的labels参数,写代码时注意兼容。
第三,饼图里的autopct='%.1f%%'是显示百分比格式,保留一位小数。这个格式很常用,如果你要保留两位,就写成'%.2f%%'。
3.3 让图更专业:样式、配色与出版级导出
六张图都能画出来,只是“能用”;真正让图显专业的,往往是细节。尽可能统一配色就是最简单有效的方法。Matplotlib默认的颜色循环虽然不难看,但用久了会有“默认脸”的感觉。我个人比较喜欢用color参数手动指定色系,或者直接换主题风格:
plt.style.use('seaborn-v0_8-whitegrid')这句可以让背景变白、网格变淡,整体瞬间精神很多。你可以自己试试ggplot、fivethirtyeight等风格,找到适合你项目的。
导出图片时,savefig的参数比你想的重要。bbox_inches='tight'会自动裁剪多余白边,dpi控制清晰度。如果需要插入到Word或PPT里,我一般用150 DPI;如果是要打印或者投稿,我会用到300 DPI。另外,savefig支持透明背景,参数是transparent=True,在某些版式下特别有用。
如果你的图片要放到深色背景的PPT里,还可以把文字颜色调成浅色,这个属于进阶玩法,但掌握之后非常出彩。
4. 常见问题与排查技巧实录
4.1 安装和导入阶段的报错速查表
我整理了一份我在工作中真实遇到的报错对照表,算是排坑经验集合:
| 报错信息 | 可能原因 | 解决方案 |
|---|---|---|
ModuleNotFoundError: No module named 'matplotlib' | 当前环境未安装matplotlib | 激活正确环境后执行pip install matplotlib |
| “请安装matplotlib以显示图标” | IDE或Notebook内核检测不到matplotlib | 安装matplotlib并重启内核或IDE |
ImportError: DLL load failed | 依赖库版本冲突,常见于Windows | 重装matplotlib或升级numpy |
| 安装时网络超时 | 下载源不稳定 | 使用国内镜像源pip安装 |
| 安装后import很慢 | 首次缓存构建 | 多运行几次即可,或考虑换更轻量的后端 |
“请安装matplotlib以显示图标”这个提示,我特别想多说两句。我第一次看到这个提示是在一个Jupyter Notebook里,当时明明在终端里可以正常导入matplotlib,但Notebook就是不认。后来才意识到,Notebook的内核指向的是base环境,而我安装matplotlib是在另一个虚拟环境里。解决方法是确认Notebook内核对准当前环境,或者直接在当前内核环境里重新安装。
如果你用的是VS Code,这个错误还会出现在“交互式窗口”里,解决办法是在VS Code右下角切换Python解释器,让它指向你安装了matplotlib的那个环境。
4.2 图表显示和保存阶段的经典问题
画图没有弹窗,这个问题在脚本运行场景下出现频率极高。本质原因是,Matplotlib的默认后端是TkAgg或QtAgg,但在某些服务器或无界面环境下,没有图形显示器可以弹窗。解决方案有几种:
- 脚本里检查有没有写
plt.show()。 - 如果是服务器上运行,直接用
plt.savefig()保存图片,不调用plt.show()。 - 如果需要交互,提前设置后端:
import matplotlib; matplotlib.use('TkAgg')。
另一个经典问题是“保存图片是空白的”。这个现象通常是把plt.savefig()放在plt.show()之后。plt.show()会清空当前图形缓存,所以先show()再savefig(),保存到的对象就是空的。正确顺序是先savefig(),后show()。
中文显示为方块也几乎人人都遇到过。原因很简单:Matplotlib默认字体不包含中文字符。用plt.rcParams['font.sans-serif'] = ['SimHei']是Windows下最快的解决办法。macOS可以用['Arial Unicode MS']或['PingFang SC']。Linux服务器上如果字体不够,你可以用fc-list :lang=zh查看系统已装的中文字体,没有就装一个fonts-wqy-zenhei之类的字体包。
4.3 绘图性能和布局排错的进阶心得
当你用Matplotlib画几十万个点的散点图时,渲染性能会明显下降,图表拖起来卡顿。这种场景下我建议,调低点的大小和透明度,或者用rasterized=True让图层栅格化,图片体积和渲染速度会改善不少。
坐标轴刻度重叠也是个高频问题。比如柱状图的横轴标签太长,或者日期格式的刻度太多。最简单的方案是用plt.xticks(rotation=45)旋转标签,或者用plt.tight_layout()调整边距。如果刻度太多,还可以用MaxNLocator限制刻度数量:
from matplotlib.ticker import MaxNLocator ax.xaxis.set_major_locator(MaxNLocator(integer=True, nbins=6))这样横轴最多显示6个刻度,干净利落。
布局排错方面,我刚接触子图时,经常因为axes的下标写错而报IndexError。比如创建的是(2, 3)的网格,你却写了axes[0, 3],那肯定越界。这种问题没有捷径,只能多看多写。写多了之后,你会形成肌肉记忆:第一维是行,第二维是列,axes[1, 0]就是左下角那张图。
我个人在实际操作中的体会是,Matplotlib这条路,真正难的不是API记不住,而是心态。你越是急着画完,越容易卡在细节上。倒不如先搭建一个固定的绘图模板,把常见配置写成函数,需要的时候改数据、改标题就行。如果你要把这套技能用到真正的项目里,建议从复制上面那段六图拼接代码开始,把每个参数都改一遍,观察效果变化,这个过程比背文档高效得多。最后再分享一个小技巧:设置plt.rcParams时,可以把常用配置做成一个mpl_config.py文件,每次项目里直接import,省得每个脚本重复写那段中文乱码设置。