news 2026/10/21 0:28:01

Matplotlib从入门到进阶:搞懂绘图逻辑,做出专业级图表

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matplotlib从入门到进阶:搞懂绘图逻辑,做出专业级图表

做数据分析和可视化这几年,我最大的一个感受是:Matplotlib 这玩意,入门容易,画好难。很多人一开始都是plt.plot(x, y)完事,能出图就觉得自己会了,但图一放到报告里、拿到评审会上,就露怯。坐标轴连个标签都没有,刻度长得吓人,图例不是糊在一起就是干脆找不到,配色随便踩中一张默认色板,整张图脏兮兮的。这其实不是你不会用,而是没搞懂它背后的那套绘图逻辑。这篇东西,我就用实际经验把 Matplotlib 从基础到高级捋一遍,重点讲清楚每一层到底在干嘛,以及怎么从“能出图”走到“能见人”。不管是刚接触 Python 数据分析的新手,还是想提升图表质量的开发者,应该都能在里面找到点能直接用的东西。

先说一下这篇文章的定位。我不会按官方文档的顺序给你抄一遍 API,那没意思。我会按照自己实际画图时的思考路径来写:先搞清楚画布和坐标系的关系,再逐个解决基础图表里那些影响观感的细节,然后上双轴、多子图、填充区域这些稍微进阶的操作,最后聊聊性能、中文字体和保存导出的坑。整个过程都配有可跑的代码片段,你照着改数据就能用在自己的项目里。

1. 绘图前的准备与核心概念

1.1 Figure与Axes:先别急着写plot

很多人用了很久 Matplotlib,始终分不清Figure和Axes到底是什么关系。我打个比方:Figure是一块画板,它承载整个图形窗口,可以包含多个绘图区域;Axes是画板里贴着的一张画纸,也就是你真正画折线、柱子的地方。坐标轴、刻度、标题这些,全部属于Axes,而不属于Figure。换句话说,plt.plot()在底层做的事情,是在“当前画纸”上画线,只是绝大多数时候你不知道这张画纸是怎么来的。

搞清楚这个概念之后,你就能理解为什么那么多人推荐用plt.subplots()而不是plt.figure()加plt.add_subplot()。因为subplots()一次调用就把画板和画纸全给你建好了,返回(fig, ax)两个对象,后续所有设置都直接挂在ax上,清晰、可控。我早期的代码全是plt.plot裸跑,后来需要做双轴、多子图的时候,直接改到崩溃,就是因为没从一开始就建立“面向对象”的思维。

import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(8, 4)) ax.plot([0, 1, 2], [1, 4, 9]) ax.set_title("示例:Figure与Axes") fig.savefig("figure_axes_demo.png", dpi=150)

这里figsize=(8, 4)是画板的物理大小,单位是英寸。dpi是输出分辨率,保存时一般设置在 150 到 300 之间,太低了文字发虚,太高了文件体积大、加载慢。截图给别人看、放网页用 150 足够;打印或者放正式报告,用到 200 以上更合适。

1.2 pyplot接口与面向对象接口的选择

Matplotlib 有两种写法并存。一种是pyplot接口,也就是你天天见到的plt.plot()、plt.xlabel()、plt.title()这种。另一种是面向对象接口,也就是先拿到fig, ax,然后调用ax.plot()、ax.set_xlabel()。对新手来说,一开始接触的几乎都是前者,因为写起来顺手,代码短。但我要劝你尽早切换到面向对象写法,原因不只是“显得专业”,而是你再往后做一丁点复杂的活,pyplot接口就会出问题。

举个例子:你画了双 y 轴图,左边是温度,右边是湿度,两个Axes对象共存于一个Figure中。这个时候你想给左轴加标签,用plt.ylabel("温度"),那到底是给哪一个轴加的?图像上是能显示,但代码逻辑已经乱了。你心里清楚要给ax1还是ax2,就得直接用ax1.set_ylabel()。再比如你循环生成一批子图,每个子图都有自己的标题和标签,用plt接口去设置,你根本不知道当前活跃的是哪一个Axes。这种场景下,面向对象是唯一的解。

我现在的习惯是:不管多简单的图,一律写成fig, ax = plt.subplots()开头。短脚本也不吃亏,复杂项目更不会翻车。遇到只需要快速看一眼数据的场景,可以用plt接口省几行代码,但只要图要用于交付、汇报、写进文档,肯定用面向对象的方式写。

2. 基础图表的正确打开方式

2.1 折线图与柱状图:从能出图到能见人

折线图是出现频率最高的图表类型,同时也是最容易画丑的。问题往往不出在plot本身,而在于你给没给读者留下足够的信息。我看到过太多图,就一根光秃秃的蓝线,x 轴不知道是什么,y 轴不知道单位,标题还是默认的。画图这件事,本质上是在替读者省脑子,你要在动手写代码前想清楚:这张图的读者最想看到什么,哪条信息最重要,哪里需要标注。

折线图常见的参数里,linewidth和linestyle是基础,marker经常被人忽略。数据点比较少的时候(少于 20 个),我建议把marker打开,比如marker="o",否则线段会给人造成一种“连续稠密”的错觉。反过来,数据点几千上万个的时候,就别加 marker 了,画出来只是一团墨点。

import matplotlib.pyplot as plt import numpy as np fig, ax = plt.subplots(figsize=(8, 4)) x = np.arange(1, 13) y = np.random.rand(12) * 10 + 20 ax.plot(x, y, marker="o", linestyle="--", linewidth=1.8, color="#2C3E50") ax.set_xticks(x) ax.set_xlim(0.5, 12.5) ax.set_ylim(0, 35) ax.set_xlabel("月份") ax.set_ylabel("销量(万件)") ax.set_title("月度销量走势") ax.grid(axis="y", linestyle=":", alpha=0.5)

柱状图也是同样的逻辑。bar函数里width控制柱宽,默认 0.8,我自己通常调成 0.6 左右,视觉上更清爽。align="center"让柱子和刻度对中,避免强迫症看了难受。横向柱状图用barh,适合类别名称很长的情况,比如部门名、产品名,纵向放不下的时候横过来一了百了。柱状图上的标签如果直接显示数值,可以用ax.bar_label(rects),省得手写循环text。

fig, ax = plt.subplots(figsize=(7, 4)) classes = ["A组", "B组", "C组", "D组"] scores = [78, 92, 65, 84] rects = ax.bar(classes, scores, width=0.55, color=["#3498DB", "#E67E22", "#2ECC71", "#E74C3C"]) ax.bar_label(rects, padding=2) ax.set_ylim(0, 100) ax.set_ylabel("得分") ax.set_title("各组得分对比") ax.spines[["top", "right"]].set_visible(False)

这里有个小知识:ax.spines[["top", "right"]].set_visible(False)可以一次性隐藏上边框和右边框,这张图立刻会干净很多。很多博主管这叫“去除无关干扰”,我觉得更准确的说法是:框架本身就是信息载体,不该出现的地方就不要出现。

2.2 散点图与面积图:数据的另一种表达

散点图的核心价值是展示两个变量之间的关系,以及数据点到趋势线的偏离程度。scatter相比plot,多了s(点面积)、c(颜色)、alpha(透明度)、cmap(颜色映射)这几个关键参数。把握一条原则:点少的时候用实色,点多的时候用透明度和颜色映射,否则重叠区域什么信息都看不到。

fig, ax = plt.subplots(figsize=(6, 5)) rng = np.random.default_rng(0) n = 300 x = rng.normal(50, 15, n) y = rng.normal(60, 12, n) c = x + rng.normal(0, 5, n) sc = ax.scatter(x, y, s=20, c=c, cmap="viridis", alpha=0.7) fig.colorbar(sc, ax=ax, label="综合指标") ax.set_xlabel("特征A") ax.set_ylabel("特征B")

当 n 超过 2000,散点几乎必然会重叠,我常用的办法是引入alpha=0.3~0.5,再用rasterized=True把点栅格化,这样生成的 PDF 文件体积能缩到原来的几十分之一。这一点放到第 5 节再展开。连续型数据建议带上colorbar,否则颜色映射等于白做。如果你想画分布趋势,fill_between是个好东西,尤其适合画置信区间、预测区间。

fig, ax = plt.subplots(figsize=(8, 4)) x = np.linspace(0, 10, 100) y_mean = np.sin(x) * np.exp(-0.1 * x) y_std = 0.15 * np.exp(-0.05 * x) ax.plot(x, y_mean, color="#2C3E50", label="均值") ax.fill_between(x, y_mean - y_std, y_mean + y_std, color="#3498DB", alpha=0.25, label="±1倍标准差") ax.set_xlabel("时间") ax.set_ylabel("响应值") ax.legend()

fill_between的alpha一定不要太大,0.2 到 0.3 比较合适,不然会盖住主曲线,看起来像一团色块上面压了根细线。

3. 让图表真正专业的几个细节

3.1 坐标轴、刻度与辅助元素的精细控制

图表专业与否,很多时候不在图形本身,而在坐标轴和刻度这些“边角料”上。先说刻度。set_xticks可以手动指定刻度位置,不去管它的话 Matplotlib 会自己挑“好看”的位置,但那些位置不一定符合你的业务语义。比如月份 1 到 12,你肯定希望每个刻度就是 1、2、3……12,而不是 2、4、6。再比如百分比数据,y 轴刻度如果显示成 0.2、0.4、0.6,读者还得心算乘 100,这就很不好。更专业的做法是使用PercentFormatter,直接显示成 20%、40%、60%。

import matplotlib.ticker as mticker fig, ax = plt.subplots(figsize=(6, 3)) x = np.arange(0, 11) y = x / 10 ax.plot(x, y, marker="o") ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1)) ax.xaxis.set_major_locator(mticker.MultipleLocator(2)) ax.set_xlim(-0.5, 10.5)

坐标轴范围同样值得花时间。默认的xlim和ylim往往会在数据两端留下不必要的空白,或者切掉不该切掉的区域。我给自己的规则是:时间序列数据,横轴范围永远设置成逻辑上的完整周期,比如某月从 1 号到月末;普通数值轴,则留出 2%~5% 的余量,避免最值点顶在画框上,视觉上太局促。

spines的微调可以单独拿出来练一练。除了隐藏上、右边框之外,还可以把左边框和下边框的线宽调粗一点,比如 1.2,用来强调坐标轴的基准线。tick_params能控制刻度线的方向、长度和宽度。默认刻度线朝里,我个人觉得朝外更好看,尤其在深色背景下区别会更明显。

3.2 图例、注释与信息层级

图例的作用是让读者搞清楚每条线、每个色块分别代表什么,但很多时候图例反而成了噪音来源。你画两条线,两条线自带 label,图例默认摆在一个“自动选出来的位置”,那个位置有时候会挡住你最重要的数据区域。所以我建议:图例位置永远手动指定,loc="best"看起来智能,实际效果并不总是最好,要优先loc="upper left"、loc="lower right"这种明确的位置。

还有一个小技巧:legend可以设置frameon=False去掉边框,或者反过来用framealpha=0.5做半透明背景。如果图里有超过四五个类别的曲线,把图例放在图内很容易遮挡信息,这时候可以放在图外:ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left"),并把fig.subplots_adjust(right=0.75)留出位置。

注释是图表里最容易出彩的地方,也是最容易被忽略的。遇到图上有个峰值、拐点、异常点,别指望读者自己去发现,直接用annotate指出来。画法不复杂:一个箭头 + 一句话。关键参数是xy(箭头指向的数据点)和xytext(文字位置),再用arrowprops控制箭头样式。

fig, ax = plt.subplots(figsize=(7, 4)) x = np.arange(0, 20) y = np.where(x == 12, 8.5, np.random.rand(20) * 3 + 2) ax.plot(x, y, color="#2C3E50", marker="o", markersize=4) ax.annotate("异常峰值", xy=(12, 8.5), xytext=(10, 9.3), arrowprops=dict(arrowstyle="->", color="#E74C3C"), fontsize=11, color="#E74C3C") ax.set_ylim(0, 10.5)

annotate用熟了你会发现,真正难的不是画箭头,而是决定哪些点值得注释。一张图注释超过三条,读者就开始烦了。

3.3 配色与样式:不丑就是成功

我对配色的要求不高:不劝退读者、不遮挡信息就行。默认色板的蓝橙色其实挺好看的,问题出在所有人都用它,你的图放到一堆图里没有辨识度。用 Matplotlib 内置的样式表可以先赢一半:plt.style.use("seaborn-v0_8-whitegrid")或者tableau-colorblind10,一个干净,一个色盲友好。

深色模式和浅色模式下的配色选择不同。在浅色背景上,高饱和颜色容易刺眼,可以选柔和的灰度或降低饱和度的颜色;在深色背景上,则尽量选亮一点的色系,同时把文字颜色也调整成浅色。自己控制颜色时,不建议直接写color="red"这种名字,Matlab 风格颜色在正式场合太扎眼。我一般从十六进制色值里挑:主色#2C3E50类深灰蓝、辅助色#3498DB、强调色#E74C3C、中性色#BDC3C7。

透明度也能帮上大忙。柱状图重叠时,alpha=0.7效果会好很多;多曲线叠加且数值区域交叉时,先把粗线画在底层、细线画在顶层,再用zorder控制顺序。zorder数字越大越靠前,默认情况下这层关系经常让线盖住点、点盖住线,手动设置一次,图面立刻就有层次了。

4. 高级布局与多图层组合

4.1 subplots布局与共享轴

当你要在一张图上展示多个维度时,子图布局是绕不开的。plt.subplots(nrows, ncols)能生成一个网格,但真正讲究的是怎么让子图之间的对齐关系合理。以做运维监控仪表盘为例,上方一个 CPU 使用率折线图,下方一个内存占用堆叠图,两个图共享 x 轴的时间范围,那sharex=True就很重要。共享后你只需要给最下面的子图加 x 轴标签,上面的子图不要重复标注,避免信息冗余。

子图间距不好控制,多试几次fig.tight_layout()你会发现,这一句能解决 80% 的重叠问题。tight_layout会自动调整子图之间的间距、边距,把标题和标签都包进去,少了很多手工subplots_adjust的麻烦。但遇到colorbar或自定义注释时,tight_layout()偶尔会失灵,那就得退回手动调hspace和wspace。

gridspec_kw可以做不等宽布局,比如上面一个大图、下面均分两个小图:

fig, axes = plt.subplots(2, 2, figsize=(9, 6), gridspec_kw={"height_ratios": [2, 1], "width_ratios": [1.2, 1]}) ax_top = axes[0, :] # 合并第一行两个子图

不过我建议新手先别执着于合并单元格,用subplot_mosaic更快。这是较新版本提供的玩法,可以直接用“字母块”定义布局,比如"AB;CC"表示第一行两个子图,第二行一个宽子图。理解成本低,代码可读性也高。

4.2 双轴图与多图层叠加:如何不互相干扰

双轴图(一个 x 轴、两个 y 轴)在同图对比两个量纲差异很大的指标时极其好用,比如一边温度(0~40 度),一边湿度(20%~90%)。实现方法是在已存在的 Axes 上调用ax.twinx(),它会生成一个新的 Axes,和原 Axes 共享 x 轴,但 y 轴在右侧。

这里有个隐藏的坑:twinx()生成的第二个 Axes,坐标轴边框、刻度标签会默认全部绘制,容易跟左边的轴叠加出一堆混乱的边框线。我自己一般会把右边轴的刻度标签颜色调成对应曲线的颜色,再把顶部边框隐藏掉,这样一眼就能看出哪个颜色对哪条轴。

图例合并也是一个常见疑难。直接调两次ax.legend()不会出现两个图例,而是后面覆盖前面,或者两者叠在一起显示不全。正确做法是lines, labels = ax1.get_legend_handles_labels()和lines2, labels2 = ax2.get_legend_handles_labels(),然后合并到一个legend里调用。

多图层叠加更讲究先后顺序。我画某跨平台系统的性能对比图时,遇到过柱状图把折线完全盖住的情况。柱状图数据范围大、折线数据范围小,如果不处理 zorder,折线就算画在后面也白搭。解决方案很简单:bar(..., zorder=1),plot(..., zorder=3),让折线始终浮在柱体之上,必要时再加一条半透明的浅色阴影做辅助线。

5. 性能、保存与常见问题排查

5.1 画图卡顿与性能优化

数据量一大,Matplotlib 就卡。这不是你代码写得不好,而是它本质上是向量式绘制引擎,每个点都要经过完整的图形渲染管线。几十万个数据点,画出来是整个 PDF 都打不开的巨兽。我踩过最大的坑就是拿几十万行的日志去直接plot,生成的图文件让同事的电脑直接未响应。

解决方案有几个梯队。第一梯队是降采样。如果只是看整体趋势,用x[::step]每 N 个点取一个,或者用滑动窗口均值/最大值聚合,几百万个点压到几千个,信息损失极低。第二梯队是改变绘图方式。散点太多时改用等高线或者二维直方图,比如ax.hist2d(x, y, bins=100),一屏就能装下十几万点。

第三梯队是后端切换。matplotlib.use("Agg")在脚本里批量绘图时不弹出 GUI 窗口,还能省一部分开销;同时配合savefig(..., dpi=100)降低导出压力。整体链路优化万,卡顿问题就能缓解大部分。如果真是实时流式数据,建议考虑换可视化库,比如用 Web 端方案,不过这是另外一个话题了。

5.2 中文字体与保存格式的坑

Matplotlib 默认字体是英文的,中文画上去大概率方框一片,这是编码和字体配置的问题。快速解法分两步:第一步准备一个中文字体文件,放到项目目录里;第二步用font_manager注册并设置rcParams。

from matplotlib import font_manager font_manager.fontManager.addfont("cn_font.ttf") prop = font_manager.FontProperties(fname="cn_font.ttf") plt.rcParams["font.family"] = prop.get_name() plt.rcParams["axes.unicode_minus"] = False

axes.unicode_minus=False这行很关键。很多人在解决完中文乱码后,发现负号变成了一个个方框,其实就是 unicode 负号渲染不出来。设置成 False 之后,负号会用普通的 ASCII 连字符替代,显示就正常了。

保存格式方面,需要考虑图的用途。网页上的图一般用 PNG,dpi=150左右即可;论文、打印材料用 PDF 或 EPS,保证内容可无限缩放。保存时记得加bbox_inches="tight",它会把图内多余白边干掉,让图片尺寸正正好好包住内容。如果没有这个参数,你会看到图片四周留着一圈空白,裁剪起来非常痛苦。保存按钮上有个transparent=True,做 PPT 或者深色背景网页时很有用,但注意透明背景会丢掉部分抗锯齿效果,性能上也有略微损耗。

5.3 常见问题速查表

最后,把我这几年操作里遇到最高频的问题整理成一个速查表,按“现象、原因、解决方案”三列组织。这个表里的每个坑我都实际遇到过,不是从文档抄的。

现象常见原因解决方案
中文全部变成方框默认字体不支持中文注册中文字体并设置rcParams["font.family"]
负号显示成方块Unicode 负号缺少字体设置axes.unicode_minus=False
保存图片周围白边巨大savefig未裁剪加bbox_inches="tight"
两张子图挤在一起子图间距太小用fig.tight_layout()或subplots_adjust(hspace=...)
图例总是挡住数据自动位置判断不佳手动指定loc,必要时放图外
图例重叠显示不全调用了多次legend()合并 handles/labels 后一次性调用
数据太多图卡矢量渲染开销大降采样、使用hist2d、切换 Agg 后端
双轴图的右边框杂乱twinx()默认绘制边框隐藏右边轴 top/right 轴脊或统一颜色
保存 PDF 文件巨大数据点过多且未栅格化给plot/scatter加rasterized=True
柱状图掩盖折线图图层顺序错误用zorder控制绘制层级

排查思路说穿了就是:先分清是数据问题、渲染问题,还是配置问题,然后逐项替换验证。不要一个文件里同时堆好几个问题,那样你永远找不到原因。我曾经有一次中文和负号同时乱,单纯把中文字体修好了,负号没了,却查了半小时才想起unicode_minus。这类问题最好一次性把所有rcParams设置写在文件头部,不要分散到各处。

个人体会

做了这么久的图表,我最大的感受是:图表的终点不只是“漂亮”,而是减少沟通成本。你觉得一张图丑,往往不是因为配色不够潮,而是信息模糊、坐标轴混乱、该标注的没标注。把本文提到的基础功夫练扎实之后,再去看那些复杂的高级技巧,你会发现自己已经有足够的能力拆解任何一张图——无非就是画布、图层、坐标轴、标记的组合。最后再分享一个小技巧:每次画图前,先花十秒钟在纸上画一个草图示意,确认这张图要传达的最重要信息是什么。带着这个信息去调整代码,画出来的图基本不会差到哪里去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 3:01:26

Spring Boot面试92题怎么准备:掌握自动配置和请求链路核心

简介:Spring Boot面试题(92题)是一份面向Java开发者与后端求职者的高频考题合集,围绕Spring Boot如何简化传统Spring工程搭建这一主线,梳理了自动配置、Starters启动器、可执行Jar包、内嵌Servlet容器、应用监控等核心…

作者头像 李华
网站建设 2026/10/20 3:25:35

TStarBots1:轻量级星际争霸AI研究框架解析

1. 项目概述:这不是游戏外挂,而是一套可复现的AI竞技研究框架“腾讯星际争霸 TStarBots1”这个标题一出现,很多人第一反应是“哦,腾讯又出游戏AI了?”——但如果你真这么想,就错过了它最硬核的价值。它不是…

作者头像 李华
网站建设 2026/10/18 15:06:53

C语言复试题库精析:指针与运算符优先级考点

简介:这套电子科技大学研究生复试C语言题库(最全无重复修正版)专为报考成电的复试考生整理,聚焦C语言笔试与机试中的高频题型与易错考点。资源包含1个PDF文件,大小5.88MB,所有题目、解析和运行推演集中在一…

作者头像 李华
网站建设 2026/10/19 15:54:18

维海德全场景云视频会议解决方案怎么样?

判断一套会议方案好不好,标准从来不是"功能多不多",而是能不能解决你企业的真实问题。维海德全场景云视频会议方案,简单说就是一套覆盖从微型到大型会议室、打通会前会中会后全链路、兼容多平台多模式的整体会议解决方案。但它到底…

作者头像 李华
网站建设 2026/10/19 14:56:34

虚拟化集群集体失联:一次存储多路径故障的惊魂复盘

某个周五深夜,我手机上的运维告警群开始连续弹出消息。一开始是两条,然后是十几条,最后几乎每过几秒就有新告警刷进来。后台有9台服务器同时失去响应,它们都是我们生产环境的虚拟机,分布在不同的宿主机上,承…

作者头像 李华
网站建设 2026/10/20 10:58:27

狐群算法优化VMD参数:实现数字信号自动去噪的完整方案

1. 从盲目调参到智能寻优:为什么非用狐群算法不可做数字信号去噪的老手应该都有这种体会:碰上变分模态分解(VMD),算法本身不难实现,真正让人抓狂的是它的参数敏感性。模态个数K设大了,模态被过度…

作者头像 李华