☰
Matplotlib入门:彻底搞懂plt绘图接口与三层结构
2026/10/16 8:39:27 网站建设 项目流程

第一次接触Matplotlib的时候,其实有点懵。明明照着网上代码敲了plt.plot(x, y),窗口却怎么都不弹出来;好不容易弹出来了,中文全变方块,负号也缺了一角。后来用顺手了才明白,Matplotlib 的plt不是玄学,它只是一套带状态的绘图接口,你理解了它的三层结构、两条使用路线、几个高频参数,后面所有漂亮图表都是从这开始的。这篇文章就是系列的第一章,专门讲plt本身:它是什么、为什么大家都写plt、第一张图怎么画、底层到底发生了什么,以及新手最容易踩的坑。适合刚接触 Python 数据分析和可视化的人,哪怕你完全没画过图,跟着走一遍也能把基础打牢。

1. 别急着画图:先弄明白 plt 到底是什么

1.1 pyplot 只是 Matplotlib 的其中一张脸

Matplotlib 是一个完整的绘图库,它内部有一套非常严谨的面向对象体系:Figure(画布)、Axes(坐标系)、Line2D(线条)、Text(文本)等等,全部是对象。对象之间有明确的父子关系,你可以精确控制每一个元素。

而plt是matplotlib.pyplot的简写,它是 Matplotlib 提供的一个"快速操作面板"。这个模块的设计借鉴了 MATLAB 的绘图交互方式,内部维护了一个"当前 Figure"和"当前 Axes"的状态。你调用plt.plot()、plt.xlabel()、plt.title(),它其实都是先找到"当前正在操作的坐标系",然后往上面添加内容。

用一个生活化的类比:Matplotlib 本体是一整间工具房,里面扳手、螺丝刀、电钻分类齐全;pyplot则是工具房门口那张工作台,你把工具拿到工作台上就能随手干活,不需要每次都在脑子里想"这个扳手该放回哪个抽屉"。绝大多数日常分析场景,用工作台就够;一旦要精细控制多面板布局或嵌入复杂 GUI,才需要直接去拿工具。

所以plt不是 Matplotlib 的替代品,而是它的"快捷入口"。理解这一点,你就不会在以后遇到"为什么有人用fig, ax = plt.subplots(),有人直接用plt.plot()"时犯迷糊——两种写法操作的是同一套东西,只是控制粒度不同。

1.2 为什么全世界的 Python 教程都写 plt

原因很直接:短、顺手、生态绑定。

import matplotlib.pyplot as plt是社区默认约定,几乎所有数据分析教程、学术论文代码、开源项目都这么写。当你从网上复制一份代码,看到plt就知道它来自 pyplot,而不用去猜那个mp、mpl到底是什么缩写。这种约定俗成带来的好处是巨大的:检索方便、交流成本低。

更关键的是,Pandas 的.plot()方法底层调用的也是 Matplotlib。当你对 DataFrame 直接执行df.plot(),它内部就是在帮你 new 一个Axes,然后调用 Matplotlib 的绘图函数。也就是说,plt不仅仅是一个绘图库的接口,它已经成了 Python 数据分析生态里的"公共画板":Pandas、Seaborn、Plotnine 画完图,最终呈现的很多操作仍然要回到 Matplotlib 来控制细节。

在这个阶段你只需要记住一件事:以后画图,第一行写import matplotlib.pyplot as plt就没有错。

2. 15 分钟跑通第一张图:环境与最小绘图流程

2.1 安装与导入:先确认你的环境里有没有

绝大多数 Python 发行版会自带 Matplotlib;如果没带,用 pip 安装也很简单:

pip install matplotlib

国内网络环境慢的话,可以指定镜像源安装,比如用清华源:

pip install matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后,在脚本或 Jupyter Notebook 里导入:

import matplotlib.pyplot as plt import numpy as np

这里我建议引入 NumPy,因为 Matplotlib 的很多操作都和数组打交道,后面生成示例数据也离不开它。只导入 plt、不导入 numpy,也可以画图,但你会发现数据准备阶段处处别扭。

导入这一步通常不会报错。如果真的报ModuleNotFoundError: No module named 'matplotlib',大概率是环境搞混了——你安装到了系统 Python,但 Jupyter 用的是虚拟环境。这时候先pip list | grep matplotlib看看装在哪,再检查当前解释器路径,比盲目重装更有效。

2.2 第一张折线图:看似简单其实信息量不小

打开编辑器,写下这段代码,这是 Matplotlib 里最小可用的绘图程序:

import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 100) y = np.sin(x) plt.plot(x, y) plt.show()

跑起来你会看到一个正弦曲线窗口。就这三行绘图代码,其实已经发生了很多事情:

  • np.linspace(0, 10, 100)在 0 到 10 之间均匀生成 100 个点,让曲线足够平滑;
  • plt.plot(x, y)创建一条折线,自动根据数据生成坐标范围;
  • plt.show()把图形渲染到屏幕上。在 Jupyter 里,这一步经常被省略,因为 Notebook 会自动显示;但在纯脚本里不调用show(),你大概率什么都看不到。

很多人第一次画图时只写了前两行,然后跑来问"为什么没图"——就是少了个show()。这个坑值得先说清楚。

如果你在 Jupyter Notebook 里使用,建议加上%matplotlib inline(旧版本约定)或直接依赖 Notebook 默认的自动展示机制。新版 Jupyter 一般不需要额外设置,但如果你发现图形不显示,可以在 Notebook 第一个单元格加一行:

%matplotlib inline

这行魔法命令的意思是:把 Matplotlib 输出的图形直接嵌入到 Notebook 的输出区,而不是弹出一个独立窗口。

2.3 一张表记住高频参数:画图别老查文档

plt.plot()可以接收的参数很多,但日常高频的就那么几个。我把它们整理成一张速查表,新手直接对着用就行:

参数作用示例
fmt线型、标记、颜色一起指定'r--o'表示红色虚线加圆点标记
color线条颜色color='#FF5722'
linewidth/lw线宽lw=2.5
linestyle/ls线型ls='--'
marker数据点标记样式marker='o'
markersize/ms标记大小ms=8
alpha透明度(0到1)alpha=0.6
label图例标签label='sin(x)'
xlabel/ylabel坐标轴标签plt.xlabel('时间')
title标题plt.title('正弦曲线')
grid网格开关plt.grid(True)
legend显示图例plt.legend()

颜色参数有个小细节:Matplotlib 支持英文颜色名(red)、缩写(r)、十六进制(#FF5722)、RGB 元组((0.1, 0.2, 0.5))等好几种写法。日常用十六进制最精确,学术论文里你完全可以统一用#2E86AB这类主题色,比默认配色更耐看。

fmt这种缩写写法平时很方便,比如'go--'就是绿色(g)、圆形标记(o)、虚线(--)。但如果你要精细控制,建议还是用color、marker、linestyle分开写,代码可读性更好,以后改起来也不容易误伤。

3. 拆穿 Matplotlib 的底层逻辑:Figure、Axes 和坐标系

3.1 一套三层结构,搞懂它你就赢了八成

Matplotlib 的绘图模型可以简化为三层:

  • 最外层 Figure:整张画布。它包含所有元素,比如标题、坐标轴、图例,实际上都挂在 Figure 下面。你在plt.subplots()里设置的figsize(画布尺寸)、dpi(分辨率),都是 Figure 的属性。
  • 中间层 Axes:一个坐标系。一个 Figure 可以有多个 Axes(比如并排的子图),每个 Axes 拥有自己的 x 轴、y 轴、数据区、刻度。注意这里的Axes是复数的 Axes,不是坐标轴 Axis,初学者特别容易搞混。
  • 内层 Artist:画布上每一个具体元素,比如一条线、一个文本标签、一个图例框,都是 Artist 对象。

用房间来类比:Figure 是整个房间,Axes 是墙上挂的一幅画框,画框里的线条、文字、坐标刻度就是 Artist。你往画框里画了什么,就决定了这幅画的内容。

绝大多数新手用plt.plot()画图时,其实是在操作默认的"当前 Axes"。如果你只画一张图,一点问题没有;一旦你想并排画两张图、设置不同子图的大小、精确控制每个子图的坐标轴,就必须显式地拿到 Axes 对象来操作。

3.2 从 plt.plot 到 fig, ax:两种写法的关系

先看两种常见的写法:

第一种,纯 pyplot 状态机写法:

plt.figure(figsize=(8, 4)) plt.plot(x, y) plt.xlabel('x') plt.title('demo') plt.show()

第二种,面向对象写法:

fig, ax = plt.subplots(figsize=(8, 4)) ax.plot(x, y) ax.set_xlabel('x') ax.set_title('demo') plt.show()

这两段代码生成的图形几乎一模一样,区别在于:第一种是"让 Matplotlib 自己去管理当前画布/坐标系",第二种是"我手动拿到画布和坐标系,明确告诉它该做什么"。

单图场景下,第一种更简短。但为什么越来越多的推荐用第二种?因为当你的图表变复杂——比如绘制双 y 轴、在一张图画里插入局部放大图、创建多个子图并共享坐标轴——状态机写法会变得很难维护。你不知道plt目前"停留在"哪个坐标系上,一旦顺序错了,元素就可能画到意想不到的地方。

我个人建议:刚开始学,先用第一种把概念跑通,不丢人;一旦要画第二张子图,立刻切换到第二种。你会在后面读到非常多使用ax.开头的代码,比如ax.set_xlabel()、ax.scatter()、ax.legend()。它们和plt.xlabel()、plt.scatter()、plt.legend()一一对应,只是前缀不同。

3.3 坐标范围、刻度标签怎么控

控制坐标范围,常用有这几种方式:

ax.set_xlim(0, 8) # x 轴范围 0~8 ax.set_ylim(-1.5, 1.5) # y 轴范围 -1.5~1.5 # 或者用 pyplot plt.xlim(0, 8) plt.ylim(-1.5, 1.5)

设置刻度,用set_xticks指定位置,用set_xticklabels指定显示文本:

ax.set_xticks([0, 2, 4, 6, 8, 10]) ax.set_xticklabels(['0', '2s', '4s', '6s', '8s', '10s'])

这里有另一个常见疑惑:为什么我用plt.plot(x, y)之后,坐标轴范围是自动的?因为 Matplotlib 会遍历你的数据,自动计算数据的最大值最小值,再加一点点边距作为坐标轴范围。这个设计对人很友好,但也导致了一个问题:当你更新数据时,坐标轴范围不会自动跟随,除非调用ax.relim()和ax.autoscale_view()手动刷新。

关于刻度,一个非常实用的经验是:当你画时间序列数据,x 轴默认可能会挤成一团,特别是日期格式。这时候先别急着调刻度间距,而是检查数据是不是真正的日期时间类型;如果只是字符串形式的日期,Matplotlib 无法识别,自然无法自动均匀排布。要么转换成datetime.datetime,要么用plt.xticks(rotation=45)把文字旋转 45 度,立刻舒服很多。

4. 画得更好看:样式、颜色与细节控制

4.1 默认风格太丑?用 style 和颜色体系解决

Matplotlib 默认的蓝橙色风格在学术圈用了很多年,确实有点"教科书感"。但好消息是它内置了多套现成风格,你只需要一行:

plt.style.use('seaborn-v0_8-whitegrid')

或者:

plt.style.use('ggplot') plt.style.use('fivethirtyeight')

我推荐在数据分析阶段用seaborn-v0_8-whitegrid,浅色背景 + 网格线,看趋势特别清楚。作图阶段再根据期刊要求或者 PPT 风格换成无网格的干净风格。

如果你希望整个项目统一风格,可以在 Python 脚本最顶部全局设置:

plt.style.use('default') plt.rcParams['font.family'] = 'sans-serif'

rcParams是 Matplotlib 的全局配置字典,几乎所有默认参数都能在这里改。比如字体、字号、线宽、坐标轴颜色。这是个很强大的机制,我在下面的中文字体小节还会用到它。

配色方面,我强烈建议不要每个图都自己乱挑颜色。你可以用 Matplotlib 内置的tab10、Set2、Dark2这类颜色循环:

import matplotlib.pyplot as plt import numpy as np plt.style.use('seaborn-v0_8-darkgrid') colors = plt.cm.Set2.colors # 一组柔和配色 x = np.linspace(0, 10, 100) fig, ax = plt.subplots() for i in range(3): ax.plot(x, np.sin(x + i), color=colors[i], label=f'shift {i}') ax.legend() plt.show()

用颜色循环的代码以后扩展起来特别省事,比如从 3 条线增加到 7 条线,不需要重设颜色。你只要确保colors列表长度足够。

4.2 中文乱码和负号显示:最劝退新手的两个问题

这是中文用户最容易崩溃的场景:你高高兴兴画了个图,标题写的是"销售趋势",结果显示一堆方框。原因很简单:Matplotlib 默认字体里没有中文字符。

解决办法也很简单,三行代码:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体,或者 ['Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题

第一行指定了无衬线字体优先用黑体;第二行是另一个经典坑:Matplotlib 默认用 Unicode 字符显示负号,但在某些字体下负号会变成一个横线缺失的小方块。设置axes.unicode_minus = False后,负号会用 ASCII 的短横线正常显示。

注意一点,不同系统支持的字体名不一样。Windows 常见SimHei、Microsoft YaHei;macOS 常见PingFang SC、Heiti SC;Linux 常见WenQuanYi Zen Hei。如果你设置SimHei没有生效,先检查系统是不是没有这个字体,用fc-list :lang=zh(Linux / macOS)查看可用中文字体,或者换成'sans-serif'加通用配置。

字体问题在保存成 PDF 或 SVG 时还有个隐患:如果用 PDF 后端,中文字体需要完整嵌入,否则换台电脑打开可能字体错乱。我的建议是,如果只是屏幕查看,用 PNG 格式足够;需要打印或投稿时,再导出 PDF,并确认字体已经嵌入。

4.3 一图多子图:subplots 的标准姿势

当你需要在一张画布里排列多个子图,plt.subplots是最高频的函数。它同时返回Figure和Axes,而且在多子图时返回的ax是一个二维数组:

fig, axes = plt.subplots(2, 2, figsize=(10, 6))

这里axes是 2 行 2 列的数组。使用的时候:

axes[0, 0].plot(x, y1) axes[0, 1].plot(x, y2) axes[1, 0].plot(x, y3) axes[1, 1].plot(x, y4)

如果你嫌一行行写太长,可以循环遍历:

for row in range(2): for col in range(2): axes[row, col].plot(x, np.sin(x + row + col))

共享坐标轴是另一个常用参数。把你的数据画在上下两个子图里,如果 x 轴相同,设置sharex=True可以让两个子图共用 x 轴刻度,避免重复显示占空间:

fig, axes = plt.subplots(2, 1, sharex=True)

多子图之间还经常遇到一个问题:子图标签重叠。这时可以用plt.tight_layout()自动调整间距,或者更严格地用fig.subplots_adjust(hspace=0.3)控制子图之间的垂直距离。tight_layout是万能急救包,画完图发现元素重叠,先跑一下它十有八九能缓解。

4.4 保存图片:这些细节决定你的图能不能用

保存图片看似简单,plt.savefig()一句话搞定,但有几个细节直接影响成品质量。

fig, ax = plt.subplots() ax.plot(x, y) plt.savefig('output.png', dpi=300, bbox_inches='tight')
  • dpi=300:导出高分辨率图,适合打印或投稿;屏幕查看 150 足够。
  • bbox_inches='tight':自动裁剪空白边距,让图形紧凑。不加的话,图片四周会留一圈多余的空白,放进文档里特别突兀。
  • transparent=True:导出透明背景 PNG,适合叠加到 PPT 或其他图片上。

这里有个容易踩的 bug:如果你先调用了plt.show()把图形窗口关闭了,再调用plt.savefig(),保存出来的图片可能是空的。原因在于show()会清空当前 Figure。正确做法是先savefig,再show,或者尽早把要保存的内容画完。

另外,如果你同时开了多个 Figure 窗口,savefig默认保存的是"当前 Figure",不是你上次画的那个。稳妥做法是用fig.savefig(),面向对象写法在这里优势很明显。

5. 新手最容易踩的坑:常见问题与排查实录

5.1 为什么 show 了却还是看不到图

常见环境有两种:一种是在普通 Python 脚本里跑,一种是在 Jupyter 里跑。

普通脚本里,如果代码顺序是:

plt.plot(x, y)

然后脚本结束了,没有plt.show(),那图就不会显示。在命令行直接跑这种脚本尤其明显,窗口一闪而过或者根本不出现。解决:在绘图代码最后加上plt.show(),并且确保show()之后没有其他耗时阻塞的操作。

Jupyter 里不显示图,最常见的原因是没开 inline 模式。虽然新版本默认支持,但如果你使用了某些自定义后端或者老版本,还是建议在顶部加%matplotlib inline。还有一种情况:你多次运行同一个单元格,旧图被覆盖了,新图没有出现。这时候重启内核,或者用plt.close('all')把之前的图形窗口全部关掉再重画。

5.2 字符串数据画折线出现的神秘警告

新手经常这么写:

categories = ['A', 'B', 'C', 'D'] values = [10, 20, 15, 30] plt.plot(categories, values) plt.show()

结果发现图能画出来,但横轴不是 0、1、2、3 而是 A、B、C、D,而且控制台可能会有一条警告:posx and posy should be finite values,或者提示字符串无法直接作为数值坐标。

Matplotlib 对待字符串 x 轴的策略是用数字 0、1、2、3... 作为实际坐标,把 A、B、C、D 显示成刻度标签。这在条形图里没问题,但在折线图里会误导人:它暗示 A 到 B 的间距和 B 到 C 的间距一样,都是 1 个单位。如果分类数据的间距并没有实际意义,折线图会把无关类别强行连成趋势。

我建议:画分类数据优先用条形图或点图;如果确实要画折线,用数值型 x 轴,再手动设置刻度标签。比如:

x = np.arange(len(categories)) plt.plot(x, values) plt.xticks(x, categories)

这样既保留了折线的形状,又不让 Matplotlib 的默认行为误导观众。

5.3 点数一多,交互和保存都卡成 PPT

当你画十万、百万个点时,你会发现 Matplotlib 的反应速度明显下降。原因很简单:Matplotlib 的渲染方式决定了每个数据点都要作为独立图形元素处理,点数一多,CPU 和内存开销自然飙升。

如果只是静态查看,有两个方向:

  • 用rasterized=True参数,把图形元素栅格化(PNG 像素化),矢量质量降低但渲染速度快。
  • 对数据进行降采样,比如只画每隔 10 个点抽 1 个,或者用scatter配合s尺寸参数来表达密度。

如果需要动态更新数据,比如实时监控曲线,我建议别每次plot新线条,而是创建一次Line2D对象,之后只更新它的set_data:

import matplotlib.pyplot as plt import numpy as np fig, ax = plt.subplots() line, = ax.plot([], []) for t in range(100): x_new = np.linspace(0, 2*np.pi, 100) + t * 0.01 y_new = np.sin(x_new) line.set_data(x_new, y_new) ax.relim() ax.autoscale_view() plt.pause(0.05)

核心思路就是:创建一个绘图对象,只更新数据,不创建新对象。窗口刷新一次,速度能快一个数量级。

5.4 从 Pandas 过来的同学要留个心眼

很多同学习惯df.plot(),确实省事。但它的默认参数和直接plt.plot有些差异。比如:

  • df.plot()默认会把你索引列当作 x 轴;如果索引是日期时间,自动生成时间轴;
  • 多列数据会一次性画成多条折线,还会自动调用legend;
  • kind参数能切换到条形图、散点图、饼图等,其实底层都是 Matplotlib 在忙活。

当你用df.plot()画完,又想改标题、改图例位置时,你会遇到一个问题:df.plot()返回的ax对象需要接住才能继续操作:

ax = df.plot(kind='bar', figsize=(10, 4)) ax.set_title('每月销售额') ax.set_ylabel('金额')

如果直接df.plot()不接收返回值,后面想操作坐标轴就很麻烦。所以记住:任何绘图函数,都尽量把返回的 Axes 对象接住,命名成 ax,后面所有设置都通过 ax 来完成。这对你的代码可维护性意义很大。

6. 一些使用 Matplotlib 的长期习惯建议

写到这里,第一章的核心内容已经差不多了。最后分享几个我在实际项目中长期收益的习惯,不一定在文档里看到,但对提升出图效率很有帮助。

第一,建立一个全局配置文件。每次画图都写plt.rcParams['font.sans-serif'] = ['SimHei']确实很烦。我更推荐在项目入口处集中设置一次,或者放在一个单独的工具模块里,其他脚本导入即可。

第二,画任何图之前先问自己:这个图要表达什么?趋势、分布、对比、构成?选错图表类型,细节调得再好也白搭。折线图表达趋势,散点图看相关性,条形图做对比,饼图很少推荐。Matplotlib 本身不限制你用哪种图,但你要有图表选择的基本判断。

第三,保持"最小必要装饰"原则。新手容易一张图堆满标题、副标题、图例、注释、网格、背景色。复杂的装饰并不会让图更有说服力。我出图前都会删掉一版不必要的内容,比如把默认的图例边框去掉、把多余网格线去掉,整个图立刻清爽很多。

第四,尽快脱离"复制代码改数据"的阶段。刚开始抄代码没问题,但你要趁热把plot、scatter、subplots、savefig这几个函数的参数过一遍,哪怕只是把每个参数改到报错一次,也比单纯看文档记得牢。

Matplotlib 的难点从来不在某个神秘参数,而在于你脑子里有没有那张"Figure-Axes-Artist"的地图。一旦这张地图建立了,后续的东西都只是查字典的问题。下一章我会把plot和scatter这两个最常用的绘图函数掰开揉碎讲清楚,顺带聊聊折线图到底怎么选样式才不显得业余。先别急着往下看,把你今天的第一张图画出来再说。

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

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

立即咨询