
第一次接触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#FF5722linewidth/lw线宽lw2.5linestyle/ls线型ls--marker数据点标记样式markeromarkersize/ms标记大小ms8alpha透明度0到1alpha0.6label图例标签labelsin(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(rotation45)把文字旋转 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-serifrcParams是 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), colorcolors[i], labelfshift {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 YaHeimacOS 常见PingFang SC、Heiti SCLinux 常见WenQuanYi Zen Hei。如果你设置SimHei没有生效先检查系统是不是没有这个字体用fc-list :langzhLinux / 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 轴相同设置sharexTrue可以让两个子图共用 x 轴刻度避免重复显示占空间fig, axes plt.subplots(2, 1, sharexTrue)多子图之间还经常遇到一个问题子图标签重叠。这时可以用plt.tight_layout()自动调整间距或者更严格地用fig.subplots_adjust(hspace0.3)控制子图之间的垂直距离。tight_layout是万能急救包画完图发现元素重叠先跑一下它十有八九能缓解。4.4 保存图片这些细节决定你的图能不能用保存图片看似简单plt.savefig()一句话搞定但有几个细节直接影响成品质量。fig, ax plt.subplots() ax.plot(x, y) plt.savefig(output.png, dpi300, bbox_inchestight)dpi300导出高分辨率图适合打印或投稿屏幕查看 150 足够。bbox_inchestight自动裁剪空白边距让图形紧凑。不加的话图片四周会留一圈多余的空白放进文档里特别突兀。transparentTrue导出透明背景 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 和内存开销自然飙升。如果只是静态查看有两个方向用rasterizedTrue参数把图形元素栅格化PNG 像素化矢量质量降低但渲染速度快。对数据进行降采样比如只画每隔 10 个点抽 1 个或者用scatter配合s尺寸参数来表达密度。如果需要动态更新数据比如实时监控曲线我建议别每次plot新线条而是创建一次Line2D对象之后只更新它的set_dataimport 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 轴如果索引是日期时间自动生成时间轴多列数据会一次性画成多条折线还会自动调用legendkind参数能切换到条形图、散点图、饼图等其实底层都是 Matplotlib 在忙活。当你用df.plot()画完又想改标题、改图例位置时你会遇到一个问题df.plot()返回的ax对象需要接住才能继续操作ax df.plot(kindbar, 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这两个最常用的绘图函数掰开揉碎讲清楚顺带聊聊折线图到底怎么选样式才不显得业余。先别急着往下看把你今天的第一张图画出来再说。