Matplotlib可视化核心指南:从figure到中文字体与高清导出
写Matplotlib最折磨人的地方从来不是API记不住,而是:照着教程画出来了,但换个字体就乱码、调个坐标轴就变形、存个高清图又发虚。很多人问现在有Seaborn、Plotly这么方便的可视化库,为什么还要回头学Matplotlib?我的答案很直接:Matplotlib可视化是Python绘图生态的底座,pandas的plot接口、Seaborn的高层封装、甚至不少高级库的底层渲染都在依赖它。只要你需要精细控制图表的任何一个细节——坐标轴刻度、图例位置、文字标注、颜色映射,早晚要回到Matplotlib。这篇教程不会把官方文档搬一遍,而是按我自己的学习路径和踩坑经验,带你一次把核心概念、高频绘图、中文字体、布局配色、导出这条链路走通。适合刚接触Python数据分析的同学,也适合用Seaborn画过几张图、但一遇到自定义需求就抓耳挠腮的人。
1. 为什么这个老牌库依然是可视化的第一选择
Matplotlib诞生于2003年,最初是模仿MATLAB的绘图风格做的Python库。二十年过去,新的可视化工具层出不穷,但它依然是数据分析和论文绘图里绕不开的基础设施。必须先把它到底解决了什么问题、和当下流行的工具是什么关系讲清楚,你才知道该不该投入时间学。
1.1 它解决的核心问题:可复现的程序化绘图
很多人第一次用Excel画图觉得挺好,为什么还要换成代码画图?因为Excel的图表是"手动操作"出来的:你拖一次数据、改一次样式,下次换一份数据又要重拖一遍。Matplotlib的核心价值是把图表变成一段可执行、可复现、可版本管理的代码。
举个例子,你有一份每日销售额数据,需要每周生成一张折线图发到群里。用Excel做,你每周要打开文件、选中数据、修改图例、导出图片;用Matplotlib写一个函数,数据文件一换,图片自动生成,连水印、标注、文件名都一起处理好。这就是程序化绘图的威力:人只写一次规则,机器重复执行。
另一个价值是精确控制。论文投稿时期刊经常要求"图宽不得小于10厘米、字体统一为小五号、坐标轴刻度朝内",这类要求在海量案例里只有Matplotlib这种具备底层控制力的库能一条条做到。Seaborn当然更漂亮,但漂亮是它给你设定好的审美;Matplotlib是把你直接扔到画室里,每种工具都摆在你面前。
1.2 和Seaborn、Plotly的定位差异
很多新手有个误区:学可视化就只学Seaborn,因为它画出来好看。实际上Seaborn是建立在Matplotlib之上的高级封装,它的每一个图表最终都会转换成Matplotlib的Figure和Axes对象。你用Seaborn画图,等于先绕了一层,但底层的齿轮还是Matplotlib在转。
| 维度 | Matplotlib | Seaborn | Plotly |
|---|---|---|---|
| 控制粒度 | 像素级精细控制 | 中等,封装了常用统计图 | 中等,偏交互配置 |
| 默认美观度 | 一般,需要手动调 | 好看,开箱即用 | 好看,支持交互 |
| 输出形式 | PNG/PDF/SVG静态图 | 静态图 | HTML交互页面 |
| 学习曲线 | 较陡,概念多 | 平缓 | 平缓 |
| 适用场景 | 论文、报表、自定义需求 | 数据探索、统计图形 | 网页展示、仪表盘 |
我个人的建议是:入门先把Matplotlib的核心概念吃透,然后用Seaborn提升出图效率,最后根据场景决定要不要上Plotly。只学Seaborn的问题是,当需要调整Seaborn内部某个细节时,你还是得回去查Matplotlib文档,绕不开。
1.3 学习Matplotlib的正确心态:别背API,理解模型
我见过不少人学Matplotlib时抱着一本API手册死记,今天记住plt.plot的参数,明天忘掉ax.set_xlabel。这样学很痛苦,因为Matplotlib的API多到官方文档都列不完。
正确的路径是理解它的"三层模型":画布层、绘图区层、艺术家对象层。你不需要记住几百个函数,只需要知道"我在哪里画、画在什么区域、这个元素有哪些可调属性"。等到需要某个具体功能,查文档就行。后文我会按照这个思路逐步展开,你会发现Matplotlib一共就那么几件事:建画布、绘图形、调细节、存文件。
2. 先搞懂figure、axes这对核心概念,后面少走一半弯路
Matplotlib入门最大的认知门槛就是figure、axes、axis这三个词。很多人画了半年图,还是搞不清它们之间的关系。我当年就是直接plt.plot()一把梭,等到想画多子图时彻底懵了。这一节把这三个概念一次讲透。
2.1 pyplot脚本层和面向对象接口,两种写法的区别
Matplotlib提供两套使用方式。第一套是直接用plt模块,比如plt.plot(x, y)、plt.title("xxx"),这是脚本层pyplot接口,它内部偷偷维护了一个"当前图表"的全局状态,你调什么函数,它就作用到当前图表上。
import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 100) y = np.sin(x) # pyplot风格:隐式跟踪当前figure和axes plt.figure(figsize=(8, 5)) plt.plot(x, y) plt.title("sin(x)") plt.xlabel("x") plt.ylabel("y") plt.show()第二套是面向对象接口,显式地创建figure和axes对象,然后调用它们的方法:
fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(x, y) ax.set_title("sin(x)") ax.set_xlabel("x") ax.set_ylabel("y") plt.show()两套写法画出的结果几乎一样。区别在于:pyplot风格写起来快,适合交互式探索和一两张图的简单场景;面向对象风格可控性强,特别适合多子图、循环绘图、需要保存figure对象的场景。我推荐从一开始就养成写面向对象风格的习惯,因为你在网上看到的复杂图表案例,绝大多数都是这种写法。
2.2 figure、axes、axis到底各指什么
用我们熟悉的画画流程来类比就很容易记住:
- figure(画布):整张纸。可以包含多个绘图区,它负责设置画布大小、分辨率、背景色。
- axes(绘图区):画布上的一块区域,相当于在纸上框出的一个画框。每个axes有自己的坐标系、坐标轴、刻度、标题。一张figure上可以有一个或多个axes。
- axis(坐标轴):axes里的x轴和y轴,属于axes的一部分,控制刻度位置和刻度标签。
搞混这三个词很正常,因为它们英文长得很像。记住一个关键关系:figure包含axes,axes包含axis。你在plt.subplots(2, 2)时创建的是1个figure和4个axes;你调ax.set_xlabel()时,改的是某个axes的x轴标签。
# 创建一个包含2行2列子图的figure fig, axes = plt.subplots(2, 2, figsize=(10, 8)) # axes是shape为(2,2)的数组,可以用下标访问 axes[0, 0].plot(x, np.sin(x)) axes[0, 0].set_title("sin") axes[0, 1].plot(x, np.cos(x)) axes[0, 1].set_title("cos") axes[1, 0].plot(x, x ** 2) axes[1, 1].plot(x, np.exp(x * 0.1)) plt.tight_layout() plt.show()2.3 一套可以直接复用的标准绘图骨架
关于我每次做Matplotlib图都会用的骨架结构,你可以直接复制使用。它是把绘图过程的"必需动作"都列出来,避免画到一半发现少了某一步:
import matplotlib.pyplot as plt import numpy as np # 1. 数据准备 x = np.linspace(0, 10, 100) y = np.sin(x) + np.random.normal(0, 0.1, size=x.shape) # 2. 创建画布和绘图区,同时指定尺寸 fig, ax = plt.subplots(figsize=(8, 5)) # 3. 画图 ax.plot(x, y, label="signal", color="#2A6F97") ax.axhline(0, color="gray", linewidth=0.8, linestyle="--") # 4. 设置标题和坐标轴标签 ax.set_title("带噪声的正弦信号") ax.set_xlabel("时间") ax.set_ylabel("幅值") # 5. 图例与网格 ax.legend() ax.grid(True, linestyle=":", alpha=0.6) # 6. 自动调整布局,避免元素重叠 fig.tight_layout() # 7. 展示 plt.show()这套骨架每一步都有明确作用:第1步准备数据,第2步创建画布并指定尺寸,第3步画具体图形,第4、5步补全文字信息,第6步防止标题或标签被截断,第7步把图显示出来。后续所有图表都是在这个骨架基础上替换第3步的绘图函数而已。
3. 五种高频图表上手实录:从一行代码到一张完整图
Matplotlib几百种图表类型里,实际工作高频用到的其实就那几种。我按使用频率挑了5类最常遇到的,每类讲清楚"什么时候用、核心参数是什么、常见的坑在哪里"。
3.1 折线图:趋势分析的基础打底
折线图用于展示数据随时间或其他连续变量的变化趋势。最基础的调用是ax.plot(x, y),但要让图有可读性,几个参数值得记住:
fig, ax = plt.subplots(figsize=(8, 5)) x = np.linspace(0, 10, 50) y1 = np.sin(x) y2 = np.cos(x) ax.plot(x, y1, label="sin", color="#E63946", linewidth=2, linestyle="-", marker="o", markersize=4, markevery=5) ax.plot(x, y2, label="cos", color="#457B9D", linewidth=2, linestyle="--", marker="s", markersize=4, markevery=5) ax.set_title("折线图示例") ax.set_xlabel("x") ax.set_ylabel("y") ax.legend() ax.grid(True, alpha=0.3) plt.show()参数的含义:color控制颜色,可以用英文名、十六进制色号;linewidth控制线宽;linestyle控制线型,-实线、--虚线、-.点划线、:点线;marker控制数据点标记形状,o圆点、s方块、^三角;markevery控制每隔几个点画一个标记,数据密集时特别有用。
一个容易踩的坑:数据点很多时不建议直接用默认方式显示所有标记,会导致标记糊成一团。用markevery每隔一段画一个标记,既保留数据分布,又保持图面清爽。
3.2 柱状图:分类对比的常客
柱状图适合对比不同类别之间的数值。比如各产品线销售额、各班级人数、各月份订单量。基础用法是ax.bar(x, height),但实际工作中分组柱状图更常见:
categories = ["产品A", "产品B", "产品C", "产品D"] values_2023 = [34, 28, 45, 39] values_2024 = [40, 31, 43, 52] x = np.arange(len(categories)) width = 0.35 fig, ax = plt.subplots(figsize=(8, 5)) bars1 = ax.bar(x - width / 2, values_2023, width, label="2023", color="#A8D5BA") bars2 = ax.bar(x + width / 2, values_2024, width, label="2024", color="#4A90D9") ax.set_xticks(x) ax.set_xticklabels(categories) ax.set_ylabel("销售额(万元)") ax.set_title("两年产品线销售额对比") ax.legend() plt.show()关键技巧是位置偏移:两个柱子要并排,横坐标得错开。宽度width设为0.35,左边柱子偏移-width/2,右边偏移+width/2。ax.set_xticks和ax.set_xticklabels必须配合使用,前者定位置,后者改名字。
我还常用水平柱状图,当分类名称很长时比垂直柱状图舒服得多,用ax.barh即可。堆叠柱状图则用bottom参数,适合展示"总量构成"。
3.3 散点图:两变量关系的初步判断
散点图用于观察两个连续变量之间的相关性。比如广告投入和销售额、身高和体重。它的核心是可以把第三维信息通过颜色或大小映射到图上:
rng = np.random.default_rng(42) n = 200 x = rng.normal(50, 10, n) y = x * 1.2 + rng.normal(0, 15, n) sizes = rng.uniform(30, 200, n) colors = rng.uniform(0, 1, n) fig, ax = plt.subplots(figsize=(8, 5)) sc = ax.scatter(x, y, s=sizes, c=colors, cmap="viridis", alpha=0.7) cbar = fig.colorbar(sc, ax=ax) cbar.set_label("强度") ax.set_xlabel("广告投入") ax.set_ylabel("销售额") ax.set_title("散点图示例(点大小和颜色映射第三维)") plt.show()ax.scatter的s参数控制点大小,c参数控制颜色值,cmap指定颜色映射表,alpha控制透明度。fig.colorbar(sc, ax=ax)为颜色值添加一个颜色条图例,这在数据点密集、颜色有连续含义时特别重要。
散点图常见的坑是过度绘制:当数据有几万个点且重叠严重时,直接画出来就是一团黑。解决办法一是调低alpha让重叠区域显示出密度差异;二是用plt.hexbin做六边形分箱图,适合展示二维密度。
3.4 直方图:分布形态的快速体检
直方图用来查看数据的分布形态:是否正态、是否有双峰、是否存在极端值。它的难点在于bins(分组数)的选择,分组数量直接影响你看到的信息:
data = np.concatenate([ np.random.normal(0, 1, 2000), np.random.normal(4, 0.8, 500) ]) fig, ax = plt.subplots(figsize=(8, 5)) ax.hist(data, bins=40, color="#2A9D8F", alpha=0.8, edgecolor="white") ax.set_xlabel("数值") ax.set_ylabel("频数") ax.set_title("直方图示例(双峰分布)") plt.show()关于bins,我的经验是:数据量小时用较少分组(比如10到20),数据量大时用较多分组(50到100),但也别盲目多分,否则直方图会变成一堆稀疏的细条。如果不想肉眼猜,可以试试bins="auto",让Matplotlib根据数据量自动计算建议分组数。想比较两组不同样本量的分布时,记得加density=True把频数归一化为密度,否则样本量大的那组会显得“更高”。
3.5 饼图与箱线图:占比和离群值的补充
饼图适合展示整体中各部分占比,但我的实际经验是饼图别超过6个扇区,否则标签会挤成一团,读起来很费劲。优先用柱状图代替饼图是很多可视化的经典建议,但如果一定要用,可以这样:
sizes = [35, 25, 20, 12, 8] labels = ["A类", "B类", "C类", "D类", "E类"] colors = ["#264653", "#2A9D8F", "#E9C46A", "#F4A261", "#E76F51"] fig, ax = plt.subplots(figsize=(6, 6)) ax.pie(sizes, labels=labels, colors=colors, autopct="%1.1f%%", startangle=90, counterclock=False, explode=[0.05, 0, 0, 0, 0]) ax.set_title("占比示例") plt.show()explode参数可以让某个扇区略微突出,适合强调重点;autopct自动显示百分比;startangle控制起始角度。
箱线图展示数据的五数概括:最小值、下四分位数、中位数、上四分位数、最大值,以及异常值点。它可以快速识别数据中的离群点和分布偏态:
rng = np.random.default_rng(7) data_1 = rng.normal(100, 15, 200) data_2 = rng.normal(90, 25, 200) data_3 = rng.normal(105, 10, 200) fig, ax = plt.subplots(figsize=(8, 5)) ax.boxplot([data_1, data_2, data_3], labels=["车间1", "车间2", "车间3"]) ax.set_ylabel("产量") ax.set_title("多组数据箱线图") plt.show()ax.boxplot的第一个参数传入一个列表,每个元素是一组数据。箱体内部的线是中位数,箱子的上下边是四分位数,“须”延伸到非异常值范围内的最远点,而圆点是个别极端值。当你发现某组数据的须特别长或者离群点很多,就该警觉这组数据有异常情况。
4. 中文乱码与全局配置:第一道坎一次解决,别再每次现查
中文乱码几乎是Matplotlib新手第一个会撞上的问题。画图一切正常,标题一用中文就显示成一个个小方框,或者抛出一堆字体警告。这个问题的根源和一次性的解决办法,我在这里讲透。
4.1 乱码的根源:默认字体里没有中文字形
Matplotlib内置的默认字体是DejaVu Sans,它不包含中文字形。所以当你用ax.set_title("销售额")时,渲染器找不到对应字形,只能显示方框或乱码。解决办法就是手动指定一个系统里已有的中文字体。
4.2 先查看系统有哪些可用字体
在配置之前,先确认你的环境里到底装了什么中文字体:
import matplotlib.font_manager as fm # 列出所有已注册的字体名称 fonts = {f.name for f in fm.fontManager.ttflist} # 查看包含常见中文字体的项 zh_fonts = [name for name in fonts if any(k in name for k in ["Hei", "Song", "Kai", "Microsoft", "PingFang", "Noto", "WenQuanYi"])] for name in sorted(zh_fonts): print(name)Windows系统通常有SimHei(黑体)、SimSun(宋体)、Microsoft YaHei(微软雅黑);macOS有PingFang SC、STHeiti;Linux可以安装Noto Sans CJK或文泉驿微米黑。列出来之后,从中选一个你偏好的字体名。
4.3 每次会话里设置rcParams
最直接的方式是在绘图代码前设置全局参数:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows示例,其他系统替换 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示为方块的问题第一行指定无衬线字体优先使用SimHei,第二行关掉Unicode负号处理。为什么第二行必要?因为默认的负号用的是字体里的一种特殊编码,换了中文字体后那个负号往往渲染不出来,变成方块。关掉之后负号会用普通连字符显示。
如果你找不准字体名,或者想一劳永逸,还有一个更稳的方案:注册字体文件路径。尤其在公司服务器上,字体可能没有安装到标准目录,但你有字体文件:
import matplotlib.font_manager as fm font_path = "/path/to/your/font.ttf" # 换成实际路径 fm.fontManager.addfont(font_path) plt.rcParams["font.family"] = fm.FontProperties(fname=font_path).get_name()这样用绝对路径加载字体,不依赖系统是否安装了该字体。
4.4 推荐做法:把配置抽成一个统一的绘图配置模块
与其每次画图前复制那两行配置,不如直接在代码库里建立一个公共配置模块。我在团队里就是给所有画图脚本统一一个公共配置文件,里面一次性定义好中文字体、图幅尺寸、配色、网格样式,大家在各自脚本里直接引用:
# plot_config.py import matplotlib.pyplot as plt def setup_style(): plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False plt.rcParams["figure.dpi"] = 120 plt.rcParams["savefig.dpi"] = 300 plt.rcParams["axes.grid"] = True plt.rcParams["grid.linestyle"] = ":" plt.rcParams["grid.alpha"] = 0.5 plt.rcParams["legend.framealpha"] = 0.8这个文件在其他脚本里用from plot_config import setup_style; setup_style()即可。字体列表写成多个候选值的好处是跨平台兼容,Windows没有Noto时自动沿用列表里的下一个可用字体。
4.5 配置后仍乱码的排查思路
如果你配置了rcParams还是乱码,按这个顺序排查:
- 确认字体名真的存在于
plt.rcParams["font.sans-serif"]的列表里,且系统装有该字体。用第4.2节的方法打印字体列表核对。 - 确认是在绘图之前执行的rcParams设置。如果在
plt.show()之后设置,自然不会生效。 - 在Jupyter Notebook里,如果改过配置,在设置rcParams后即时重启绘图内核,或者重新执行全部单元格。
- 注意字体名带不带空格或大小写。比如"Microsoft YaHei",写成"microsoft yahei"就匹配不到。
5. 子图布局、配色与导出:让图表从"能看"到"好看"
图能画出来只是第一步,实际交付时还会遇到多图排版、配色难受、导出图片发虚的问题。这一节把常用布局和美化手段集中讲一遍。
5.1 subplots多子图布局的完整用法
多子图是Matplotlib的强项之一。用plt.subplots(nrows, ncols)一步创建画布和所有子图:
fig, axes = plt.subplots(2, 2, figsize=(10, 8), constrained_layout=True) x = np.linspace(0, 10, 100) axes[0, 0].plot(x, np.sin(x), color="#E63946") axes[0, 0].set_title("sin") axes[0, 1].plot(x, np.cos(x), color="#457B9D") axes[0, 1].set_title("cos") axes[1, 0].plot(x, x ** 2 / 100, color="#2A9D8F") axes[1, 0].set_title("quadratic") axes[1, 1].plot(x, np.sqrt(x), color="#E9C46A") axes[1, 1].set_title("sqrt") plt.show()constrained_layout=True和tight_layout()都是用来防止子图之间互相重叠、标题被裁剪的布局机制。我推荐直接在subplots里用constrained_layout=True,它比后面再调tight_layout()更省心。如果你希望几个子图共享x轴或y轴,可以加sharex=True或sharey=True。共享坐标轴在对比多组数据时特别有用,因为这样读者才不会被不同的量程误导。
当四个子图的坐标范围差异很大时,需要注意设置统一的坐标轴范围,否则对比效果会被量程差异淹没。
5.2 坐标轴、刻度和网格的精细调整
坐标轴相关的调整是最常用的操作,我把高频的几个功能集中列出来:
fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(x, y) ax.set_xlim(0, 12) # 设置x轴显示范围 ax.set_ylim(-1.5, 1.5) # 设置y轴显示范围 ax.set_xticks([0, 2, 4, 6, 8, 10, 12]) # 手动指定刻度 ax.set_xticklabels(["0", "2", "4", "6", "8", "10", "12"], fontsize=9) ax.tick_params(axis="both", direction="in", length=3) # 刻度朝内 ax.grid(True, which="major", linestyle=":", alpha=0.5)tick_params里的direction="in"是期刊图常用的风格,刻度朝内;which="major"表示只调整主刻度。刻度朝内还是朝外没有绝对标准,关键是全篇统一。在数据范围跨数量级时,可以考虑用对数坐标轴,用ax.set_xscale("log")或ax.set_yscale("log"),在金融数据和天文数据里几乎必用。
5.3 配色与样式:别再用默认的“大蓝大橙”
Matplotlib默认的蓝色和橙色虽然辨识度高,但多了之后显得廉价。想让图好看,两个方向:整体样式和配色方案。
整体样式可以用内置样式快速切换:
plt.style.use("seaborn-v0_8") # 或者看看都有哪些样式 print(plt.style.available)plt.style.available里有很多预设,比如ggplot、bmh、fivethirtyeight。选定一个样式后,后面所有图都会带上这个风格。我个人常用seaborn-v0_8或ggplot作为底子,再盖一层自己的配色。
配色方面,几个值得记住的方案:
- 单个数据系列时,给一个“品牌色”,比如
#2A6F97这类深蓝,不要用默认亮蓝。 - 多类别离散数据时,用
Set2、Set3、tab10等离散色板。 - 连续数值映射时,用
viridis、plasma、cividis,这些颜色映射表对色盲友好,且深浅变化均匀。
手动指定颜色循环的方法是修改rcParams里的axes.prop_cycle,或者更简单地,在每次绘图函数里传color参数。配色没有绝对标准,但有一个底线:不要让色盲读者无法区分你的数据。红绿对比、蓝黄对比在色弱人群眼里很接近,尽量用viridis这类均匀过渡方案。
5.4 导出高清图片:dpi、格式和裁剪
屏幕上看很正常的图,保存到本地之后经常出现两种问题:发虚、四周空白太多。这都和导出参数有关:
fig.savefig("output.png", dpi=300, bbox_inches="tight", facecolor="white") fig.savefig("output.pdf") # 矢量图,论文常用 fig.savefig("output.svg") # 网页矢量图dpi=300是印刷级别的最低标准,屏幕浏览用150足够,论文投稿建议300及以上。bbox_inches="tight"会自动裁掉外围空白,让图片紧贴内容。facecolor="white"把背景设置成白色,防止导出后的图变成透明底。
如果你的图之后要在Word或者论文里使用,PDF或SVG矢量格式是首选。矢量图放大多少倍都不会模糊,能保持文字清晰。PNG类是位图,放大会出现锯齿。
5.5 标注与注释:把关键信息直接画在图上
一张好图不只是数据曲线,还要有必要的文字提醒。我常用四个标注手段:
# 在某个坐标位置添加文字 ax.text(5, 0.5, "峰值区域", fontsize=12, color="#E63946", ha="center") # 添加带箭头的注释,指向某个数据点 ax.annotate("最大值", xy=(np.pi / 2, 1), xytext=(4.5, 1.2), arrowprops=dict(arrowstyle="->", color="#333333")) # 画水平参考线和垂直参考线 ax.axhline(0, color="gray", linestyle="--", linewidth=1) ax.axvline(np.pi, color="gray", linestyle=":", linewidth=1)ax.text的ha参数控制水平对齐方式,常用center或left。ax.annotate的xy是被指向的数据点位置,xytext是注释文字的位置,arrowprops里可以设置箭头样式和颜色。axhline和axvline用于标注阈值线、零线、基准值,在风险分析里几乎必用。
6. 踩坑记录:图例、坐标轴、内存和大数据的几个真实案例
最后这部分是我这几年帮人排查Matplotlib问题时最常遇到的几类坑,每个都给出完整的排查思路,而不是直接甩答案。
6.1 图例不显示或重复
图例不显示的各种原因里,最常见的是:plot的时候忘了加label参数,或者加了label但后面又单独调用了一次plt.legend()。后者的场景通常是画图循环里每画一条线就调一次legend(),最终导致图例堆积了重复的项。
排查顺序:
- 检查
ax.plot()里是否写了label="xxx"。 - 检查是否只有一次
ax.legend()调用。 - 如果用了
plt.legend()而图是用ax.plot()画的,要把两种风格统一。 - 如果图例项太多,用
ax.legend(loc="upper right", ncol=2)分列显示,避免图例遮住数据区域。
6.2 坐标轴自动范围太“聪明”,让图产生误导
Matplotlib默认会根据数据自动调整坐标轴范围,这在大多数时候是好事,但也会带来误导。比如想展示从0开始的变化趋势,结果坐标轴自动从数据最小值开始,把细微波动放大成巨大波动。这在报表里是常见的信息失真源。
解决方式:ax.set_ylim(0, max_value * 1.1)手动固定下界;ax.set_xlim同理。还有一个容易忽略的点:多子图比较时,如果每个子图坐标范围不同但视觉高度相同,读者会误以为量级相当。用sharey=True或手动set_ylim统一几个子图的y轴范围,对比才公平。
6.3 循环绘图内存飙升
如果你在循环里反复创建figure却不关闭,内存会一路涨,最后图形界面卡死或者Kernel重启。这在批量生成几百张图时特别常见。
正确的做法是在循环结束时关闭figure:
for i in range(100): fig, ax = plt.subplots() ax.plot(x, y) ax.set_title(f"图 {i}") fig.savefig(f"output_{i}.png", dpi=150) plt.close(fig) # 关键:关闭figure释放内存plt.close(fig)的作用是显式释放这个figure占用的内存。尤其在脚本批量导出图片时,关与不关,内存表现差一个量级。在Jupyter Notebook里还有一个常见坑:重复执行同一个绘图单元格,旧图不会消失,叠加显示越多越卡。这是因为Notebook会保留上一次执行的输出,最好在执行前清空旧的输出。
6.4 大数据量绘制卡顿
当数据点数量达到几十万甚至上百万时,ax.plot直接画会非常慢,图也糊成一团。几个应对方案按性价比排序:
- 降采样:先抽样再画。比如每10个点取1个,肉眼几乎看不出差别。
- 减少单个点渲染开销:用
linewidth=1而不是linewidth=2,用:rasterized=True让这个图形栅格化。 - 二维大数据用
pcolormesh或imshow:如果你画的是热力图、地形图,优先用网格化的绘图函数,而不是逐个画散点。
我见过最典型的场景是绘制传感器一年内的分钟级数据,大概50万个点。直接plot会卡顿,用plot(x[::10], y[::10])抽稀之后,画面依然完整,但渲染速度提升了几十倍。
6.5 保存后的图片与屏幕显示不一致
屏幕上看是正常的,保存后字体变了、图被裁剪了,这通常是两个原因叠加:
第一,显示时用的是GUI窗口的渲染DPI,保存时用的是rcParams里的savefig.dpi。把保存DPI设成300,文字在图里的物理尺寸就会发生变化。想要保证导出和屏幕一致,就得接受"屏幕预览只是近似"的事实,最终以保存文件的表现为准。
第二个原因是字体嵌入。在PDF里保存时,如果图表使用了系统字体但该字体没有被嵌入,换一台电脑打开PDF就会字体替换、布局错乱。处理方式:在保存前用font_manager显式注册字体,或者导出SVG后用专业软件编辑检查。论文投稿时这个问题尤其烦人,我的习惯是在投稿前把PDF放大200%逐页检查一遍图表字体。
最后再分享一个我自己的效率习惯
我现在做任何可视化项目,都先花5分钟把统一的绘图配置模块、字体方案和主题色固定下来,再针对每类图写一个小的绘图函数。比如折线图的函数接收数据、标题、文件名三个参数,内部自动完成字体设置、配色、图例、导出。这样反复修改数据时,只改数据文件,脚本一行不用动。这个习惯是从第一次给论文紧急重画20多张图被折腾到半夜之后养成的,之后我再也不想手工改图了。Matplotlib的可控性强,但可控的前提是你要有一套自己的固定套路;套路一旦建立,它的强大程度远超那些开箱即用的高级库。