做数据分析这些年,Matplotlib 是我电脑里存活时间最长的 Python 库。从最开始照着教程画第一张折线图,到后来做月度汇报里的大图、小图、动图,它始终没被换掉。很多人觉得 Matplotlib 难上手、画出来丑、文档又散,但其实你只是没花二十分钟把它那个“三层世界观”搞明白。这篇东西就是把我自己从基础到高级的用法整理一遍,从安装环境、画图思路、配色体系,到子图布局、存高清图和 Gif,再到配合 pandas 完成一份完整的数据汇报图,尽量一次讲透,让你拿来就能用。
1. 画图之前,先把 Matplotlib 的“世界观”搞清楚
1.1 你手里其实是三层结构:Figure、Axes、Artist
Matplotlib 的官方文档虽然写得细,但新手最容易懵的地方,是不知道一张图到底由哪些对象组成。其实它的核心概念就三个:Figure、Axes、Artist。
Figure 是一张空画布,你可以把它理解成一张白纸。Axes 是画布上的一个坐标系区域,也就是真正画线条、柱子的那块地方,一张 Figure 上可以有好几个 Axes。Artist 是画布上所有你能看见的元素,线、点、文字、图例、刻度标签,统统都是 Artist。
我入行第一年犯过一个特别蠢的错误:直接调plt.plot()画图,然后死活不知道怎么把两张图拼在同一个画布上。后来才明白,plt.plot()只是 Matplotlib 给新手开的一扇小门,它内部还是帮你创建了 Figure 和 Axes,只是你没抓住这两个对象的“把手”而已。想从基础进阶,第一件事就是养成“抓对象”的习惯。
import matplotlib.pyplot as plt fig = plt.figure() # 一张空白画布 ax = fig.add_subplot(111) # 画布上创建一个坐标系 ax.plot([1, 2, 3], [4, 5, 6]) # 在坐标系里画线这段代码虽然能用,但你得记住add_subplot(111)这种写法。实际工作中,绝大多数人会用更简洁的plt.subplots(),它一次性把 Figure 和 Axes 都给你,还支持一次性生成多个子图。
1.2 两套接口:pyplot 速写,面向对象精修
Matplotlib 有两套调用方式,很多人学了半年还在混用,最后代码一团乱。
第一套是 pyplot 接口,就是plt.plot()、plt.xlabel()、plt.title()这种。它像一台“傻瓜相机”,适合探索数据、写临时脚本,以及交互式环境里快速查看结果。
第二套是面向对象接口,核心是先拿到 fig 和 ax,然后用ax.plot()、ax.set_xlabel()、ax.set_title()这类方法。一旦开始画子图、调整坐标轴、保存多张一致性风格的图,你会发现面向对象接口才是能控场的那个。
我的建议很直接:哪怕只画一张图,也老老实实用面向对象思路。
fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(x, y, color='#2c3e50', linewidth=2) ax.set_xlabel('时间') ax.set_ylabel('数值') ax.set_title('主标题')这样养成的肌肉记忆,等你需要一次性画六张子图时,会很自然地写出axes[0]、axes[1]这样的代码,而不是到处调用全局plt函数。
1.3 安装与运行环境:别急着跑,先把坑填平
Matplotlib 的安装本身不复杂,一条命令就搞定。
pip install matplotlib但在实际环境里,有几个容易绊倒人的细节。
第一个是环境隔离。如果你电脑上装了 Anaconda,又用系统 Python,还可能用着公司统一的虚拟环境,那就一定要确认装到了你想用的那个环境里。检查方法很简单:
import matplotlib print(matplotlib.__version__)如果版本信息和你想的不一样,说明装岔了。
第二个是 Jupyter Notebook 里的显示问题。默认情况下,Matplotlib 在 Jupyter 里能显示图,但如果你用了%matplotlib inline和%matplotlib notebook,交互能力会不一样。我现在的习惯是顶部直接写%matplotlib inline,让图片内嵌显示,方便记录和共享。
第三个是后端(backend)问题。有些 Windows 用户会碰见“Python is not installed as a framework”之类的报错,这通常出现在 macOS 上,解决方式是把后端切换成 TkAgg:
import matplotlib matplotlib.use('TkAgg')记得这行一定要放在import matplotlib.pyplot之前。否则报了错你都不知道从哪查起。
2. 一张专业的折线图是怎么“养”出来的
2.1 最小可运行案例:从空画布到第一张图
很多人以为画折线图就是把数组丢进plot()就完事,但专业和业余的区别从第一张图就开始了。先看一个最小案例,我拿“某产品一周每日新增用户数”做例子。
import matplotlib.pyplot as plt days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] users = [120, 156, 143, 208, 267, 334, 402] fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(days, users) plt.show()这图画出来,只有一条蓝线,没有任何修饰,放到汇报里就是“业余三件套”之一:无标题、无坐标轴说明、无数据含义。
画图这件事,核心思路是把“图表”当成“叙事工具”。一幅折线图至少要让读者回答三个问题:横轴是什么、纵轴是什么、趋势代表了什么。所以,坐标轴标签和标题不是装饰,是刚需。
fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(days, users, color='#3182bd', linewidth=2, marker='o', markersize=6) ax.set_xlabel('日期', fontsize=12) ax.set_ylabel('新增用户数', fontsize=12) ax.set_title('某产品一周每日新增用户趋势', fontsize=14) ax.grid(True, linestyle='--', alpha=0.6) plt.show()加上了颜色、线宽、数据点和网格,整张图的信息量立刻不同。别小看marker='o',它能在数据点稀疏时让读者一眼看出每个点的具体位置,特别适合样本量少于三十个的折线图。
2.2 坐标轴、刻度和网格里的门道
坐标轴调整是折线图“质感”提升的关键。Matplotlib 默认会自己算刻度,但很多时候默认刻度并不贴近业务。比如一周七天,你通常希望横轴七个刻度一个不少,纵轴刻度间隔也符合同事阅读习惯。
ax.set_xticks(days) # 强制横轴刻度就是这七个日期 ax.set_yticks(range(0, 451, 50)) # 纵轴从0到450,每隔50刻度标签旋转也是一个实操强需求。如果你的横轴是“2025-03-01 08:00”这种长字符串,默认平行放置很容易重叠成黑疙瘩:
ax.set_xticklabels(days, rotation=45, ha='right')ha='right'表示标签右对齐,能让旋转后的文字更整齐。
还有一个坑是网格。网格用好了能辅助读数,用不好会显得脏。我的习惯是只开横向网格,或者横纵都开但把纵轴网格透明度调低,避免对比强烈的黑线把所有视觉注意力拽走。
双坐标轴也是折线图里的高频操作。比如你想在同一张图里比较“新增用户数”和“转化率”,两者量纲完全不同,一个几千一个零点几,共用一个纵轴肯定不行。
fig, ax1 = plt.subplots(figsize=(10, 6)) ax1.plot(days, users, color='#3182bd', label='新增用户数') ax1.set_ylabel('新增用户数', color='#3182bd') ax2 = ax1.twinx() ax2.plot(days, rate, color='#e6550d', linestyle='--', label='转化率') ax2.set_ylabel('转化率', color='#e6550d')用twinx()创建共享横轴的第二个纵轴,两个量级的趋势在同一张图里就能并行展示。唯一要注意的是:双轴图必须用不同颜色并把图例区分清楚,否则读者会把两条线强行关联。
2.3 图例、标注与注释:画龙点睛
图例是图表里的“翻译官”。多序列图如果没有图例,等于让读者猜谜。
ax.plot(days, users, label='新增用户数') ax.plot(days, active, label='活跃用户数') ax.legend(loc='upper left', frameon=True, shadow=False)loc参数控制图例位置,常见的还有upper right、lower right、best。我一般不用best,它虽然自动选位置,但偶尔会遮住数据的关键拐点,不如手动指定来得稳。
数据标注是高阶折线图的重要加分项。比如你想标出“周六增长突然爆发”这个业务结论,单纯靠线形不足以强化记忆,需要加一行注释:
ax.annotate('周末爆发点', xy=(5, 334), xytext=(3, 380), arrowprops=dict(arrowstyle='->', color='#333333'))xy是被注释的数据点坐标,xytext是注释文本所在位置,arrowprops控制箭头样式。这类注释本质上是把图表从“展示数据”升级成“讲述故事”,也是分析报告中真正值钱的那部分能力。
3. Matplotlib 颜色体系:从随手一用到有设计的配色
3.1 指定颜色的五种方式
Matplotlib 里指定颜色特别灵活,但新手容易迷失,我用一张表总结下来:
| 方式 | 示例 | 适用场景 |
|---|---|---|
| 颜色名称 | color='red'、color='tab:blue' | 快速调试 |
| 简写字符 | color='r'、color='g' | 临时脚本 |
| 十六进制 | color='#2c3e50' | 正式图表首选 |
| RGB 元组 | color=(0.1, 0.3, 0.5) | 程序化生成色值 |
| 灰度值 | color='0.6' | 打印件、强调弱化 |
正式汇报的图表,我几乎只用十六进制。因为 Hex 颜色可以从品牌设计规范里直接复制,保证整份报告用色与公司 VI 一致,这种事老板一眼就能看出来。
3.2 用 colormap 处理连续数据与多序列配色
当数据本身是连续的,比如热力图、等高线图、带颜色映射的散点图,你需要用 colormap。
import numpy as np x = np.random.rand(80) y = np.random.rand(80) colors = np.random.rand(80) # 第四维度数据 fig, ax = plt.subplots(figsize=(10, 7)) sc = ax.scatter(x, y, c=colors, cmap='viridis', s=80) fig.colorbar(sc, ax=ax)cmap参数选择色图。Matplotlib 内置了几十种,我强烈建议优先用这几个:viridis、plasma、magma、cividis。它们的亮度变化均匀,对黑白打印也友好,是官方调研后推出的一批感知均匀色图。
多序列图需要不同颜色区分时,别手写一堆颜色名,用 colormap 生成一组颜色更省心:
cmap = plt.get_cmap('tab10') colors = [cmap(i) for i in range(10)] # 得到10个离散颜色tab10是专门为分类数据设计的离散色图,第一眼区分度很高。很多高级图表背后都是这种写法,显得“精准”而不是“随机”。
3.3 配色是否专业的自检标准
做了这么多年图,我总结出三条配色自检标准:
第一,色盲友好。红绿配对是色盲人群最忌讳的。如果你必须在同一张图里用红和绿,就同时改变线型或加上标记点,让“红实线对绿虚线”这种双通道编码降低误读风险。或者干脆把绿色换成蓝色,一劳永逸。
第二,饱和不过度。全图大面积使用#FF0000这种纯红,在屏幕上看久了会刺眼。专业图表常用暗一点的色调,比如酒红#a93226、深蓝#2c3e50,既保留色彩区分度又不扎眼。
第三,全局一致。一个报告里十张图,每张图的“同一含义”必须用同一颜色,比如“预算”永远是蓝色,“实际”永远是橙色。读者看第一张图建立了颜色-含义映射,后九张图就不用重新学。
还有一个我踩过多年的坑:渐变色的误导性。连续 colormap 适合表示连续数值,但如果你把离散类别数据也用彩虹色渐变,读者会误以为“类别A到类别B之间存在过渡关系”。类别数据就老老实实用tab10这类离散色。
4. 中文字体专题:为什么默认设置下中文全是方块
4.1 问题根因:字体世界里缺了中文字形
Matplotlib 默认字体是 DejaVu Sans,这个字体里没有中文字形。所以你在图上写中文标题、中文坐标轴标签时,系统找不到对应字形,只能画出一堆空心方块。这不是 Matplotlib 不能用中文,而是默认配置没指定中文字体。
这个问题我见过太多次,很多人因此放弃 Matplotlib 转去用其他工具。其实解决方案五行代码就能搞定。
4.2 一行 rcParams 解决全局中文
解决方案的核心是修改 Matplotlib 的全局字体配置。
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'PingFang SC', 'Noto Sans CJK SC'] plt.rcParams['axes.unicode_minus'] = False第一行设置无衬线字体列表,优先使用微软雅黑,找不到就依次尝试后面的字体。第二行必须加,否则负号会被渲染成方块。这是因为默认设置的 Unicode 负号字形在部分中文字体里不存在。
在 Windows 上,SimHei是黑体,Microsoft YaHei是微软雅黑,两者都可以;macOS 上用PingFang SC或Heiti SC;Linux 服务器上一般装Noto Sans CJK SC或文泉驿正黑。
4.3 服务器和 Linux 环境的字体注册
服务器上跑图表任务,常见情况是代码在本地正常,一上服务器中文就变方块。根因是服务器系统里根本没装中文字体。
解决步骤也不复杂。先去网上下载一套开源中文字体,比如思源黑体(Noto Sans CJK SC),放到系统字体目录下:
mkdir -p ~/.fonts cp NotoSansCJKsc-Regular.otf ~/.fonts/ fc-cache -f然后再在 Python 代码里查找字体是否被识别:
from matplotlib import font_manager fonts = [f.name for f in font_manager.fontManager.ttflist] print([f for f in fonts if 'Noto' in f or 'CJK' in f])如果列表里出现了Noto Sans CJK SC,说明 Matplotlib 已经能使用这套字体。这种方式比直接改rcParams更治本,因为系统层面有了字体,所有 Python 图形库都能受益。
还有一种临时做法,是直接用字体文件路径创建 FontProperties 对象,不需要全系统安装:
from matplotlib.font_manager import FontProperties font_path = '/path/to/NotoSansCJKsc-Regular.otf' myfont = FontProperties(fname=font_path) ax.set_title('中文标题', fontproperties=myfont)这个方法适合服务器上没法安装字体文件的受限环境。代价是每个文本元素都要手动传fontproperties,比较啰嗦,但对临时渲染足够有效。
5. 从一图到多图:子图布局与组合图表
5.1 subplots 基础与布局调节
单图能表达的信息有限,工作中更常见的是把多张图拼成一个面板,让读者一眼对比多个维度。plt.subplots()支持网格式布局:
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(15, 8))这会生成一个 2 行 3 列的 Axes 网格,axes是二维数组,用axes[0,0]、axes[0,1]这种方式索引。
新手常见问题:生成了六个子图,但只用了五个,剩下一个空白 Axes 也被渲染了出来。处理方式有两种,要么用fig.delaxes(axes[1, 2])删掉它,要么一开始就用其他布局工具避免这个问题。
子图之间的间距也需要调。默认间距经常让标题贴住上面的图,或者横轴标签挤在一起。最省心的是调用:
fig.tight_layout()它会自动调整所有子图的间距,让标签和标题互不遮挡。如果你用的是较新的 Matplotlib 版本,还可以在创建 Figure 时就启用约束布局:
fig, axes = plt.subplots(2, 3, figsize=(15, 8), constrained_layout=True)constrained_layout比tight_layout更智能,尤其适合子图尺寸不一致的情况。
5.2 共享坐标轴:让对比变得公平
比较多个序列时,如果各子图纵轴范围不同,视觉对比就会被误导。比如 A 图纵轴是 0 到 100,B 图纵轴是 0 到 10000,涨幅其实都翻倍,但画出来 B 图显得“猛”得多。
sharex和sharey参数能解决这个问题:
fig, axes = plt.subplots(1, 2, figsize=(12, 5), sharey=True)sharey=True后,两个子图共用同一个纵轴范围,并自动隐藏右侧子图的纵轴标签,减少重复占空间。时间序列对比图里,sharex=True尤其好用,横轴自动对齐后,读者一眼就能看出哪天出现峰值。
还有一种更细腻的共享场景:两张图画的是同一时间范围,但纵轴量纲不同。这时共享纵轴反而会害了数据。正确做法是照常分别设置 y 轴,但通过共享横轴保持时间对齐,这也是sharex=True最大的价值。
5.3 GridSpec 不规则布局:让仪表盘更像仪表盘
纯网格布局有时不够用。比如你想做一张“上面一张大趋势图,下面左右两小块详细图”的汇报版面。这时要用GridSpec:
from matplotlib.gridspec import GridSpec fig = plt.figure(figsize=(12, 8)) gs = GridSpec(2, 3, height_ratios=[2, 1], width_ratios=[2, 1, 1], hspace=0.4, wspace=0.3) ax_main = fig.add_subplot(gs[0, :]) # 第一行占满三列 ax_left = fig.add_subplot(gs[1, 0]) # 第二行左 ax_center = fig.add_subplot(gs[1, 1]) # 第二行中 ax_right = fig.add_subplot(gs[1, 2]) # 第二行右height_ratios和width_ratios控制行高与列宽的相对比例,gs[0, :]表示第一行跨全部三列。这套组合拳打完,你的图表版面就脱离“默认模板”了,更像数据 dashboard 而不是几张图硬拼在一起。
6. 输出与动画:把图表落地成图片和 GIF
6.1 高清静态图:DPI、格式和大小控制
图在屏幕上看着好,不代表打印出来、投到大屏幕上也好。落到文件时,最关键的参数是dpi。
fig.savefig('report.png', dpi=300, bbox_inches='tight')dpi=300保证印刷级的清晰度;bbox_inches='tight'会自动裁剪掉图表周围多余的留白,让导出文件更紧凑。如果你不写bbox_inches,图片四周常常会多出一大圈白边,放进 PPT 里还得手动裁切。
格式选择也有讲究。日常用 PNG,它是点阵图,清晰度上限受 dpi 影响;需要后期编辑的矢量图用 SVG 或 PDF;论文投稿经常要 EPS。在代码里,savefig会根据扩展名自动推断格式,你只需要换后缀就能切换。
还有一个容易忽略的参数是facecolor和edgecolor。如果你的图表背景是深色主题,保存时得把这两个参数也设成对应颜色,否则会出现背景变黑的“变脸”情况。
6.2 从静态到动起来:FuncAnimation 与 GIF 生成
动画图的基础是FuncAnimation,它本质上是一帧一帧地重画图。先看一个我会反复使用的经典例子:动态展示 60 天的数据增长。
import numpy as np import matplotlib.animation as animation fig, ax = plt.subplots(figsize=(10, 6)) x = np.arange(60) y = np.cumsum(np.random.randn(60) + 0.5) line, = ax.plot([], [], lw=2) ax.set_xlim(0, 60) ax.set_ylim(y.min(), y.max()) def update(frame): line.set_data(x[:frame], y[:frame]) return line, anim = animation.FuncAnimation(fig, update, frames=60, interval=100, blit=True) anim.save('growth.gif', writer='pillow', fps=10)这里有几个关键点值得解释。line, = ax.plot([], [], lw=2)后面的逗号很重要,因为plot返回的是一个列表,你解包出第一个元素才是真正要更新的线对象。update函数接收帧号,通过set_data更新线对象的数据,实现逐渐画线的效果。interval=100是每帧间隔 100 毫秒,fps=10决定保存的 GIF 每秒多少帧。
比较关键的坑:保存 GIF 时如果系统没装 ImageMagick,默认 writer 可能会报错。改用writer='pillow'能直接依赖 Pillow 库,免去额外安装麻烦。
6.3 动画的常见误区与性能优化
动画做多了之后,我发现几个高频翻车点。
第一,忘记清理旧内容。如果每帧用同一把 Axes 重新画线而不删除上一帧的线,动画里线会“叠加变粗”。解决方法是统一用set_data更新对象,或者每帧开头用ax.cla()清空坐标轴。ax.cla()虽然干净,但会把坐标轴设置也清掉,所以需要放在每帧重新设置。更推荐第一种方式。
第二,帧数过多导致内存暴涨。60 张图还好,600 张就很吃力了。解决方案有两种:降低像素尺寸让每帧更小,或者把画布调成figsize=(6,4)这种较小的尺寸后再保存。
第三,帧数太少导致 GIF 像幻灯片。一般 GIF 的 fps 在 10 到 15 比较顺滑,如果数据点只有 30 个,可以把每帧推进两个点,而不是一帧一个点。
7. pandas + Matplotlib 实战:从数据到一张能汇报的图表
7.1 一个贴近真实工作的小场景
与其零散地教 API,不如来一个完整场景:有一份某产品近 60 天的每日新增用户数据和每日活跃率数据,想用一张面板图同时展示趋势。
import pandas as pd rng = pd.date_range('2024-01-01', periods=60, freq='D') df = pd.DataFrame({ 'date': rng, 'new_users': np.cumsum(np.random.randint(80, 250, size=60)), 'active_rate': 0.3 + np.random.rand(60) * 0.2 }) df.head()这里用cumsum模拟累计增长趋势,active_rate模拟波动活跃率,数据格式是标准的 DataFrame。现实中你大概率是从 CSV 读:
df = pd.read_csv('data.csv', parse_dates=['date'])7.2 DataFrame 的快速绘图接口与自定义
pandas 自身有plot接口,在探索数据时可以当作快捷方式:
df.set_index('date').plot(figsize=(10, 6))不过默认样式比较简陋,而且一旦需要精细控制,还是要回到 Matplotlib API。所以我的习惯是:数据分析阶段用 pandas 自带plot快速摸数据,做正式汇报时把它拆回fig, ax = plt.subplots()再精修。
fig, axes = plt.subplots(2, 1, figsize=(12, 9), sharex=True) axes[0].plot(df['date'], df['new_users'], color='#3182bd', linewidth=2, label='每日新增用户') axes[0].set_ylabel('新增用户数') axes[0].grid(True, linestyle='--', alpha=0.5) axes[1].plot(df['date'], df['active_rate'], color='#e6550d', linewidth=2, label='活跃率') axes[1].set_ylabel('活跃率') axes[1].set_xlabel('日期') axes[1].grid(True, linestyle='--', alpha=0.5) fig.suptitle('产品核心指标近60天趋势', fontsize=15) fig.tight_layout() fig.savefig('product_dashboard.png', dpi=300, bbox_inches='tight')这段代码就是一个很典型的“业务汇报图”版式:上面新增用户量,下面活跃率,横轴共享时间,两张图共用一个大标题。信息完整,观感清爽。
7.3 进一步:把“图表”升级成“数据故事”
在真实汇报里,最后一张图往往不只是数据本身,而是含有分析结论的图。比如我想强调“新增用户的增长伴随着活跃率波动”,可以在图上加入一个关键事件标注。
用我之前讲的annotate,可以标出某个转折点:
important_idx = 40 axes[0].annotate('关键节点', xy=(df['date'][important_idx], df['new_users'][important_idx]), xytext=(0.2, 0.8), textcoords='axes fraction', arrowprops=dict(arrowstyle='->'))textcoords='axes fraction'的作用是让 xytext 坐标不再基于数据值,而是基于坐标轴的相对位置,这样不管纵轴数值范围怎么变,注释文字都能固定在图中某个百分比的位置,不跑偏。
8. 最后,我的实操心得和进阶方向
画图的功夫不在 API 背得多少,而在“站在读者角度想问题”这件事上。我见过太多图表,配色花哨、元素堆砌,结果核心趋势被淹没。一张好图的标准,是读者三秒钟内能说出结论,十秒钟内能确认细节。为此我形成了三条铁律:坐标轴必须有清晰的标签、同一图表内的颜色含义必须前后一致、关键点必须配注释。
进阶方向有两条路可走。一条是多画,把 Matplotlib 的数据集、官方 Gallery 里的案例挨个抄一遍,然后把每张图改成自己的数据。另一条是速学插件,在 Matplotlib 基础上掌握 seaborn 的统计图表和 pandas 的快捷入口,但核心画布逻辑不变,框架依然吃 Figure、Axes、Artist 这一套。
我个人建议别急着堆太多库。Matplotlib 是底层地基,地基稳了,以后上 pandas 绘图接口、seaborn、甚至画动画都会顺很多。你每掌握一个库的“世界观”,再学下一个库就是在往已有框架上添砖。我在实际项目中最大的感受就是:一旦你熟练掌握了面向对象接口、统一配色逻辑和中文环境配置,Matplotlib 真的能陪你很久很久。