☰
Matplotlib绘图规范化:解决中文乱码、子图排版与高清导出
2026/10/14 1:37:04 网站建设 项目流程

先说明一件事:上一篇我们把plot、scatter这些基础图画出来了,但很多同学私信我说,图是出来了,却怎么看怎么别扭——默认颜色像彩虹糖开会、中文字全部变成方块、图例挤在角落、两张图叠在一个画布里直接“车祸现场”。这些都不是技术难点,只是你还没掌握 Matplotlib 的“规整”手段。这篇《小白级教程(2)》就专门聊怎么解决这些图丑、图乱、图不正经的问题,目标很简单:让你画出来的图能直接拿去汇报、写文档、发朋友圈都拿得出手。

这篇适合两类人:一类是刚把 Matplotlib 装上、只会照着抄代码的纯新手;另一类是已经画过不少图,但一直靠百度搜“怎么改颜色”“怎么加图例”的零散选手。我会把环境配置、坐标轴细节、子图布局、保存导出这些事串成一套完整的思路,而不是继续丢零碎代码。文章里所有的代码我都用自己习惯的写法重写过,保证在常见 Python 环境里能直接跑通。

1. 环境准备搞利索,后面才不会一边画图一边骂人

1.1 中文字体崩坏:90%小白第一次都栽在这

新手用 Matplotlib 遇到的第一个鬼打墙,十有八九是中文乱码。你明明写了plt.title('销售趋势图'),标题区域却冒出一堆方框,像被外星人加密过一样。这不是程序有问题,而是 Matplotlib 默认字体列表里压根没有中文字体,它不知道该用哪套字形去渲染汉字。

解决办法是设置字体参数,常规做法是修改 Matplotlib 的全局配置。在代码文件开头写上下面这段,后面所有图都会自动生效:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC'] plt.rcParams['axes.unicode_minus'] = False

font.sans-serif这一行是告诉 Matplotlib:去这些字体里找能用的中文。axes.unicode_minus必须加,否则坐标轴上的负号会显示成一块小方框,因为默认的 Unicode 负号和字体渲染有兼容问题。不同操作系统的字体名有差异,我列一下实测可用的组合:

操作系统建议字体名
WindowsSimHei(黑体)、Microsoft YaHei(微软雅黑)
macOSPingFang SC、Hiragino Sans GB
LinuxNoto Sans CJK SC、Source Han Sans SC

如果你不确定系统里装了哪个,可以写一段小脚本把字体列表打出来自查:

from matplotlib import font_manager for f in font_manager.fontManager.ttflist: print(f.name)

本质上是做字体注册,只是用rcParams是全局配置,最省事。如果你只想让某一张图用特殊字体,也可以用类似plt.xlabel(..., fontproperties='你的字体名')的写法,但单图指定比较啰嗦,实战中我建议直接全局配置,省下的时间用来调版式。

1.2 figure 和 axes:搞清楚你是在哪张纸上画画

很多小白崩溃的第二件事是“为什么我的图总叠在一起,分开画却总是关不掉旧图”。这里必须先把画图的底层逻辑捋清楚。Matplotlib 里有两个核心对象:figure和axes。你可以把figure想成一张大白纸,axes才是纸上真正画图的那个区域,坐标轴、刻度、线条都挂在axes上。

直接用plt.plot()这类函数时,Matplotlib 会偷偷帮你创建一个figure和一个axes,所以你感觉不到它们的存在。但一旦你想在同一张纸上摆多个图、控制画布大小、统一调整间距,就必须显式地操作这两个对象。

实际工作中,我的习惯是先建一个明确的figure,设置好画布尺寸再动手:

import matplotlib.pyplot as plt fig = plt.figure(figsize=(8, 4), dpi=80) ax = fig.add_subplot(111) ax.plot([1, 2, 3, 4], [2, 4, 6, 8]) plt.show()

figsize=(8, 4)表示宽度 8 英寸、高度 4 英寸,dpi是分辨率。举个例子,figsize=(8, 4), dpi=80就会得到一张 640x320 像素的画布。如果你正用plt.get_current_fig_manager()管理多个窗口,或者在一篇 Jupyter Notebook 里反复画图,显式定义figure的好处是你能随时知道自己在哪张纸上,不会出现画到一半把之前的图弄丢的情况。

还有一个冷知识:plt.show()之后,figure 会保持存在直到你调用plt.close()或新建 figure。如果你写循环画图,注意每一轮都新建figure,别让所有图挤在同一个画布上,不然就是开头说的“车祸现场”。

2. 从“能看”到“好看”:这几个细节决定成图质量

2.1 颜色、线型、标记:参数不是白给的

默认状态下,plt.plot()画出来的线是蓝色实线,散点是蓝色圆点。数据一多就分不清谁是谁,这就是为什么要主动设置颜色、线型和标记。

先看一个综合示例:

import matplotlib.pyplot as plt x = [1, 2, 3, 4, 5] y1 = [1, 4, 9, 16, 25] y2 = [1, 2, 3, 4, 5] plt.plot(x, y1, color='#FF6B6B', linestyle='--', linewidth=2, marker='o', markersize=6, label='平方值') plt.plot(x, y2, color='#4ECDC4', linestyle='-', linewidth=1.5, marker='s', markersize=5, label='原值') plt.legend() plt.show()

color不光能写颜色名,更推荐写十六进制色值,比如#FF6B6B,因为内置颜色名容易撞车,写色值你能精确控制。linestyle可控性很强:'-'实线、'--'虚线、'-.'点划线、':'点线。linewidth控制线粗,建议 1.5 到 2.5 之间,太细打印出来看不清,太粗显得笨拙。

标记类型常用的有'o'圆形、's'方形、'^'三角形、'D'菱形。这里有一个很容易被忽略的点:如果数据点很多,加上标记会让整条线显得很乱。我的个人原则是:数据点少于 30 个才考虑加标记,大于 30 个就只保留线形;反过来,如果数据的重点在于“点的分布”而不是“趋势”,那就应该优先考虑散点图而不是折线图。

2.2 图例、网格与坐标轴标签:让读者不用猜

画图不只是数据可视化,本质是信息传达。你图画完,读者第一眼要知道“横轴是什么、纵轴是什么、每条线代表谁”。这三件事分别由xlabel、ylabel、legend负责。

图例最常见的坑是摆放位置挡住数据。legend的loc参数可以指定位置,常用值如下:

参数值位置
'best'自动选择遮挡最少的地方
'upper left'左上角
`'upper right'右上角
'lower left'左下角
'lower right'右下角
'center'正中间

实测下来'best'并不是每次都智能,数据走势复杂时它也会瞎选。我通常的做法是直接指定'upper left'或'lower right',然后根据肉眼反馈微调。还有一种办法是给legend加ncol=2参数,让图例排成两列,减少占用面积。

网格线默认是关闭的,需要显式设置plt.grid(True)。单独一个True画出来的网格可能太扎眼,建议把透明度调低:

plt.grid(True, linestyle=':', alpha=0.6)

坐标轴标签必须带单位,比如plt.ylabel('价格(元)'),别只写一个“价格”。字太小读者看着费劲,可以通过fontsize参数控制,比如plt.xlabel('时间(天)', fontsize=12)。另外,很多人画完图才发现标题被 label 的字体顶得特别远,这个是正常现象,可以用plt.title('标题', pad=10)微调标题和图表之间的间距。

3. 多图排版:subplots 是主力,subplot 是备用

3.1 为什么推荐 subplots 而不是手写 add_subplot

早期教程经常教你写fig.add_subplot(211)代表两行一列中的第一个子图。这种写法的缺点很明显:每加一个子图都要写一遍 add_subplot,坐标轴对象还散落各处,管理起来乱糟糟。而且 add_subplot 的编码规则对新手不友好,一紧张就写错。

更好的方式是plt.subplots。看这个例子:

import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))

这一行代码直接创建了 2 行 2 列共 4 个子图,axes是一个形状为(2, 2)的二维数组。之后你想在第 1 行第 1 列画图,就写axes[0, 0].plot(...),非常直观。这样做的好处是每个子图的坐标轴对象都被整齐地放在数组里,循环遍历、批量作图都很方便:

for row in range(2): for col in range(2): axes[row, col].plot([0, 1], [0, 1])

还有一个高频参数sharex和sharey。当多张子图的横轴含义相同时,设置sharex=True可以让所有子图共享同一个横轴刻度,这样图与图之间的对比会更直观,也不会有各自顽固的横轴范围。看起来节省空间,实际上是把重复的刻度信息收敛到一条轴上,报告排版上更专业。

3.2 四宫格实战:一图一意,别塞太多东西

子图排版最常见的错误是想在同一个子图里塞下好几组趋势,但数据一多就糊成一团。我建议的拆解思路是“一图一意”:每个子图只表达一个核心信息。

举一个实际场景,某公司要对比四个季度的销售表现,我会这样组织 4 个子图:

  • 左上:折线图展示月度销售额走势
  • 右上:柱状图对比各产品线销售额
  • 左下:散点图展示销量与利润的关系
  • 右下:饼图展示各渠道销售占比

代码结构如下:

import matplotlib.pyplot as plt import numpy as np # 模拟数据 months = ['1月', '2月', '3月', '4月', '5月', '6月'] sales = [120, 135, 118, 160, 172, 189] products = ['A产品', 'B产品', 'C产品'] prod_sales = [420, 310, 260] quantity = np.array([100, 200, 150, 300, 250, 400]) profit = np.array([10, 18, 14, 25, 22, 30]) channels = ['线上', '线下', '合作'] channel_ratio = [45, 35, 20] fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 8)) # 左上:折线图 axes[0, 0].plot(months, sales, marker='o', color='#FF6B6B') axes[0, 0].set_title('月度销售额') axes[0, 0].grid(True, linestyle=':', alpha=0.6) # 右上:柱状图 axes[0, 1].bar(products, prod_sales, color='#4ECDC4') axes[0, 1].set_title('产品线销售额对比') # 左下:散点图 axes[1, 0].scatter(quantity, profit, color='#45B7D1') axes[1, 0].set_xlabel('销量(件)') axes[1, 0].set_ylabel('利润(万元)') axes[1, 0].set_title('销量与利润关系') # 右下:饼图 axes[1, 1].pie(channel_ratio, labels=channels, autopct='%.1f%%') axes[1, 1].set_title('渠道销售占比') plt.tight_layout() plt.show()

plt.tight_layout()这句很重要,它会自动调整子图之间的间距,防止标题跟坐标轴标签叠在一起。如果你的图比较复杂,还是觉得间距挤,可以用plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1, wspace=0.3, hspace=0.4)手动微调,其中wspace是子图之间的水平间距,hspace是垂直间距。

4. 坐标轴进阶:范围、刻度、对数坐标,让数据真正被看见

4.1 对数坐标:跨数量级数据的救星

遇到一组数据,最小是 1,最大是 100000,画出来你会发现前 90% 的图被压成一条平线,只有最大值的地方顶出来一根柱子。这种数据分布你要么只看到极值,要么看不到细节,怎么调ylim都难受。解法是切换到对数坐标。

对数坐标的含义是,坐标轴上相等的间距代表数量级的翻倍而不是固定的数值差。比如10, 100, 1000, 10000这串数,在线性坐标上是越来越稀疏的,在对数坐标上却是等间距排列。

import matplotlib.pyplot as plt import numpy as np x = np.arange(1, 1001) y = np.exp(x / 100) # 模拟快速增长数据 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(x, y) axes[0].set_title('线性坐标') axes[1].plot(x, y) axes[1].set_yscale('log') axes[1].set_title('对数坐标') plt.show()

这个例子里同样是上升曲线,线性坐标下你可能只看到一条平坦到几乎垂直于右边缘的曲线,而对数坐标把前段的变化全展开了。需要注意,set_yscale('log')只对 y 轴生效,如果你想 x 轴也用对数,对应方法叫set_xscale('log')。数据里有 0 或负数时绝对不能使用对数坐标,因为对数函数在 0 和负数上是没有定义的,强行使用只会得到空图或者报错。遇到这种情况,通常的处理方式是给数据加一个正向偏移,或者改用其他变换。

4.2 坐标轴范围与刻度定制:别让 Matplotlib 替你做决定

默认的坐标轴范围是 Matplotlib 根据数据自动算的,大多数情况没问题,但有时它会把不该展示的空白区域也画进去,或者把刻度密度搞得不合我意。手动设定范围是基本功:

plt.xlim(0, 12) plt.ylim(0, 100)

设置范围通常有几个场景:数据本身只有 0 到 10,但你希望跟另一张图对比,所以统一坐标轴让两张图有可比性;或者数据本身包含几个极大异常值,你想把视野聚焦在正常区间。但要有心理预期:一旦手动设了范围,超出范围的数据会被自动裁掉,不显示也不报错,所以设置之前要确认你不想看的数据就是想让它消失。

刻度定制那边,常用的是set_xticks和set_xticklabels。比如你有一个横轴是月份,但数据是按天记录的,刻度间距显示得太密,字跟字重叠,此时可以只挑 1 号、15 号作为刻度,其他日期隐藏:

ax.set_xticks([1, 15, 30]) ax.set_xticklabels(['月初', '月中', '月末'])

还有一个让刻度标签旋转的经验:当分类轴文字太长,比如产品名称有好几个汉字,直接画出来会跟邻居重叠。解决办法是:

plt.xticks(rotation=45)

rotation参数传 45 或 60,字就斜着排,省空间。这类问题在季度报告、年度统计的横轴标签上特别常见,记住这行代码能救很多图。

5. 导出保存:画得再好看,存出来糊了等于白画

5.1 高清图和透明底:savefig 的几个常用参数

在屏幕上看到的图跟保存出来的图有时候完全是两个级别。Matplotlib 默认保存图片的分辨率通常是 100 dpi 左右,拉大到网页或者文档里就会发虚。实际交付时我很看重两组参数:dpi和bbox_inches。

dpi=300是印刷和文档提交的常见要求,300 的像素密度足够保证清晰度。bbox_inches='tight'的作用是自动裁剪掉图片四周的白边,让你的图以最紧凑的方式保存,省去后期用图片编辑器裁边的麻烦。整体用法:

fig.savefig('output.png', dpi=300, bbox_inches='tight')

如果你的图片要放进深色背景的 PPT 或者网页,可以把背景设置成透明,只需加参数transparent=True。不过要提醒一句,透明 PNG 在部分文档编辑器里可能显示正常,但打印出来会变成黑底白字,这个要看你的具体输出场景。

还有一个很多人不知道的点:Matplotlib 支持矢量图导出,savefig('output.pdf')或savefig('output.svg')保存出来的文件不管放多大都清晰锐利,没有像素概念。写论文、做印刷品,矢量格式是优先选择;而朋友圈、网页截图,还是 PNG 更通用。如果发给别人但不确定对方用什么软件打开,我会多导出一份 PDF 和一份 PNG,按需选择。

5.2 一张图串起整篇文章的完整示例

把前面所有知识点串在一起,做一个总复习式的代码。假设要输出一张包含销售额、增长率、目标达成率的汇总图,要求是能放进季度汇报文档:

import matplotlib.pyplot as plt import numpy as np # 全局中文字体设置 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = False # 模拟数据 quarter = ['Q1', 'Q2', 'Q3', 'Q4'] sales = [1024, 1380, 1250, 1650] target = [1100, 1300, 1400, 1600] fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(quarter, sales, marker='o', linewidth=2, label='实际销售额', color='#2E86AB') ax.plot(quarter, target, marker='s', linestyle='--', linewidth=2, label='目标销售额', color='#A23B72') ax.fill_between(quarter, sales, target, color='#2E86AB', alpha=0.1) ax.set_title('季度销售目标完成情况', fontsize=14) ax.set_xlabel('季度') ax.set_ylabel('销售额(万元)') ax.legend(loc='upper left') ax.grid(True, linestyle=':', alpha=0.6) plt.tight_layout() fig.savefig('theme_sales.png', dpi=300, bbox_inches='tight')

fill_between给实际值和目标值之间的区域加了淡淡的阴影,用透明度alpha=0.1填充,可以直观看到目标差距在哪几个季度拉大。alpha参数很万能,所有带颜色的图形都能用它调整透明度,值域在 0(全透明)到 1(不透明)之间。这张图保存出来就是一张带图例、带网格、有清晰坐标说明的完整汇报图。

6. 小白踩坑实录:报错速查与几个独家小技巧

6.1 新手最容易卡住的报错速查表

报错关键词/现象原因解决办法
中文字全部显示为方块没有设置中文字体设置rcParams['font.sans-serif']
负号显示成方块unicode_minus未关闭加上axes.unicode_minus = False
no attribute 'plot'把axes当成普通对象用了确认你取出的是子图axes[0,0]而非 figure
图例空白绘图时没加label参数在plot()或scatter()里补上label='...'
图片保存后白边巨大默认保存带边距bbox_inches='tight'
数字轴显示成科学计数法且很乱数值太大或太小手动set_xticks或调整格式
两张图混在一个画布里没新建figure直接连续画图在下一张图前plt.figure()或使用subplots

还有一个非常隐蔽的坑:保存 PNG 时如果用plt.savefig而当前画布还在交互窗口里显示,某些环境会出现保存的图和看到的图不一样。我的习惯是保存前先fig = plt.gcf()拿到当前 figure 对象,然后fig.savefig(...),明确指定操作对象,而不是靠模糊的全局函数。

6.2 三个能省大量时间的工作习惯

第一个建议是用内置样式表。Matplotlib 自带了plt.style.use('ggplot')、plt.style.use('seaborn-v0_8-whitegrid')、plt.style.use('fivethirtyeight')等多种风格。去掉了手动调颜色的大半工作量。我常备两个风格:白底网格的seaborn-v0_8-whitegrid用来做数据分析探索,ggplot用来做汇报材料。一行代码切换风格,老实用。

第二个建议是“先小图调参,再高清输出”。开发阶段我把figsize=(6, 3)、dpi=72就够了,这样渲染快、改起来不心疼。等布局和参数都确定,再在savefig里用dpi=300出一张正式图。如果一开始就上高清大图,每次修改参数都要等渲染,浪费时间。

第三个建议涉及数据预处理。很多图丑不是因为代码不行,而是数据本身脏——异常值拉高了坐标轴、缺失值把折线截成了断崖。画图之前先用pandas的describe()扫一眼数据分布,做必要的清洗,比在画图参数里反复调试更省力。看着是画图课,到后段拼的反而是数据处理功底。

6.3 最后说点技术之外的体会

可能有人觉得 Matplotlib 命令又多又杂,但用久了你会发现,核心需求来来回回就是那些:颜色、线形、图例、子图、坐标轴、保存。难的不是记住所有 API,而是想清楚这张图要告诉读者什么。

我自己从“调半天参数还是难看”到现在“开箱出图”,中间最大的转折不是背下更多代码,而是养成了一个习惯:画图前先问自己三个问题。这张图是给谁看的?核心信息是什么?用什么图形能最快表达?如果答案是“读者只需要看趋势”,那折线图就够了,不用硬塞柱状图;如果答案是“要对比两个部门差距”,柱状图比折线图更直观。方向定了,代码层面反而很快。

这是《小白级教程(2)》的内容,进阶的方向还有很多——极坐标、3D 图、动态图、复杂布局,但这些都是在今天这套基础框架上盖楼。先把能控制住的每个细节控制住,后面的路会顺畅很多。图是画出来的,也是想出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询