尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Matplotlib进阶实战:三层结构、专业配色与动态图表

Matplotlib进阶实战:三层结构、专业配色与动态图表 做数据分析这些年Matplotlib 是我电脑里存活时间最长的 Python 库。从最开始照着教程画第一张折线图到后来做月度汇报里的大图、小图、动图它始终没被换掉。很多人觉得 Matplotlib 难上手、画出来丑、文档又散但其实你只是没花二十分钟把它那个“三层世界观”搞明白。这篇东西就是把我自己从基础到高级的用法整理一遍从安装环境、画图思路、配色体系到子图布局、存高清图和 Gif再到配合 pandas 完成一份完整的数据汇报图尽量一次讲透让你拿来就能用。1. 画图之前先把 Matplotlib 的“世界观”搞清楚1.1 你手里其实是三层结构Figure、Axes、ArtistMatplotlib 的官方文档虽然写得细但新手最容易懵的地方是不知道一张图到底由哪些对象组成。其实它的核心概念就三个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, linewidth2) 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 上解决方式是把后端切换成 TkAggimport 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, linewidth2, markero, markersize6) ax.set_xlabel(日期, fontsize12) ax.set_ylabel(新增用户数, fontsize12) ax.set_title(某产品一周每日新增用户趋势, fontsize14) ax.grid(True, linestyle--, alpha0.6) plt.show()加上了颜色、线宽、数据点和网格整张图的信息量立刻不同。别小看markero它能在数据点稀疏时让读者一眼看出每个点的具体位置特别适合样本量少于三十个的折线图。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, rotation45, haright)haright表示标签右对齐能让旋转后的文字更整齐。还有一个坑是网格。网格用好了能辅助读数用不好会显得脏。我的习惯是只开横向网格或者横纵都开但把纵轴网格透明度调低避免对比强烈的黑线把所有视觉注意力拽走。双坐标轴也是折线图里的高频操作。比如你想在同一张图里比较“新增用户数”和“转化率”两者量纲完全不同一个几千一个零点几共用一个纵轴肯定不行。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(locupper left, frameonTrue, shadowFalse)loc参数控制图例位置常见的还有upper right、lower right、best。我一般不用best它虽然自动选位置但偶尔会遮住数据的关键拐点不如手动指定来得稳。数据标注是高阶折线图的重要加分项。比如你想标出“周六增长突然爆发”这个业务结论单纯靠线形不足以强化记忆需要加一行注释ax.annotate(周末爆发点, xy(5, 334), xytext(3, 380), arrowpropsdict(arrowstyle-, color#333333))xy是被注释的数据点坐标xytext是注释文本所在位置arrowprops控制箭头样式。这类注释本质上是把图表从“展示数据”升级成“讲述故事”也是分析报告中真正值钱的那部分能力。3. Matplotlib 颜色体系从随手一用到有设计的配色3.1 指定颜色的五种方式Matplotlib 里指定颜色特别灵活但新手容易迷失我用一张表总结下来方式示例适用场景颜色名称colorred、colortab:blue快速调试简写字符colorr、colorg临时脚本十六进制color#2c3e50正式图表首选RGB 元组color(0.1, 0.3, 0.5)程序化生成色值灰度值color0.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, ccolors, cmapviridis, s80) fig.colorbar(sc, axax)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 SCLinux 服务器上一般装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(fnamefont_path) ax.set_title(中文标题, fontpropertiesmyfont)这个方法适合服务器上没法安装字体文件的受限环境。代价是每个文本元素都要手动传fontproperties比较啰嗦但对临时渲染足够有效。5. 从一图到多图子图布局与组合图表5.1 subplots 基础与布局调节单图能表达的信息有限工作中更常见的是把多张图拼成一个面板让读者一眼对比多个维度。plt.subplots()支持网格式布局fig, axes plt.subplots(nrows2, ncols3, 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_layoutTrue)constrained_layout比tight_layout更智能尤其适合子图尺寸不一致的情况。5.2 共享坐标轴让对比变得公平比较多个序列时如果各子图纵轴范围不同视觉对比就会被误导。比如 A 图纵轴是 0 到 100B 图纵轴是 0 到 10000涨幅其实都翻倍但画出来 B 图显得“猛”得多。sharex和sharey参数能解决这个问题fig, axes plt.subplots(1, 2, figsize(12, 5), shareyTrue)shareyTrue后两个子图共用同一个纵轴范围并自动隐藏右侧子图的纵轴标签减少重复占空间。时间序列对比图里sharexTrue尤其好用横轴自动对齐后读者一眼就能看出哪天出现峰值。还有一种更细腻的共享场景两张图画的是同一时间范围但纵轴量纲不同。这时共享纵轴反而会害了数据。正确做法是照常分别设置 y 轴但通过共享横轴保持时间对齐这也是sharexTrue最大的价值。5.3 GridSpec 不规则布局让仪表盘更像仪表盘纯网格布局有时不够用。比如你想做一张“上面一张大趋势图下面左右两小块详细图”的汇报版面。这时要用GridSpecfrom matplotlib.gridspec import GridSpec fig plt.figure(figsize(12, 8)) gs GridSpec(2, 3, height_ratios[2, 1], width_ratios[2, 1, 1], hspace0.4, wspace0.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. 输出与动画把图表落地成图片和 GIF6.1 高清静态图DPI、格式和大小控制图在屏幕上看着好不代表打印出来、投到大屏幕上也好。落到文件时最关键的参数是dpi。fig.savefig(report.png, dpi300, bbox_inchestight)dpi300保证印刷级的清晰度bbox_inchestight会自动裁剪掉图表周围多余的留白让导出文件更紧凑。如果你不写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([], [], lw2) 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, frames60, interval100, blitTrue) anim.save(growth.gif, writerpillow, fps10)这里有几个关键点值得解释。line, ax.plot([], [], lw2)后面的逗号很重要因为plot返回的是一个列表你解包出第一个元素才是真正要更新的线对象。update函数接收帧号通过set_data更新线对象的数据实现逐渐画线的效果。interval100是每帧间隔 100 毫秒fps10决定保存的 GIF 每秒多少帧。比较关键的坑保存 GIF 时如果系统没装 ImageMagick默认 writer 可能会报错。改用writerpillow能直接依赖 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, periods60, freqD) df pd.DataFrame({ date: rng, new_users: np.cumsum(np.random.randint(80, 250, size60)), 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), sharexTrue) axes[0].plot(df[date], df[new_users], color#3182bd, linewidth2, label每日新增用户) axes[0].set_ylabel(新增用户数) axes[0].grid(True, linestyle--, alpha0.5) axes[1].plot(df[date], df[active_rate], color#e6550d, linewidth2, label活跃率) axes[1].set_ylabel(活跃率) axes[1].set_xlabel(日期) axes[1].grid(True, linestyle--, alpha0.5) fig.suptitle(产品核心指标近60天趋势, fontsize15) fig.tight_layout() fig.savefig(product_dashboard.png, dpi300, bbox_inchestight)这段代码就是一个很典型的“业务汇报图”版式上面新增用户量下面活跃率横轴共享时间两张图共用一个大标题。信息完整观感清爽。7.3 进一步把“图表”升级成“数据故事”在真实汇报里最后一张图往往不只是数据本身而是含有分析结论的图。比如我想强调“新增用户的增长伴随着活跃率波动”可以在图上加入一个关键事件标注。用我之前讲的annotate可以标出某个转折点important_idx 40 axes[0].annotate(关键节点, xy(df[date][important_idx], df[new_users][important_idx]), xytext(0.2, 0.8), textcoordsaxes fraction, arrowpropsdict(arrowstyle-))textcoordsaxes fraction的作用是让 xytext 坐标不再基于数据值而是基于坐标轴的相对位置这样不管纵轴数值范围怎么变注释文字都能固定在图中某个百分比的位置不跑偏。8. 最后我的实操心得和进阶方向画图的功夫不在 API 背得多少而在“站在读者角度想问题”这件事上。我见过太多图表配色花哨、元素堆砌结果核心趋势被淹没。一张好图的标准是读者三秒钟内能说出结论十秒钟内能确认细节。为此我形成了三条铁律坐标轴必须有清晰的标签、同一图表内的颜色含义必须前后一致、关键点必须配注释。进阶方向有两条路可走。一条是多画把 Matplotlib 的数据集、官方 Gallery 里的案例挨个抄一遍然后把每张图改成自己的数据。另一条是速学插件在 Matplotlib 基础上掌握 seaborn 的统计图表和 pandas 的快捷入口但核心画布逻辑不变框架依然吃 Figure、Axes、Artist 这一套。我个人建议别急着堆太多库。Matplotlib 是底层地基地基稳了以后上 pandas 绘图接口、seaborn、甚至画动画都会顺很多。你每掌握一个库的“世界观”再学下一个库就是在往已有框架上添砖。我在实际项目中最大的感受就是一旦你熟练掌握了面向对象接口、统一配色逻辑和中文环境配置Matplotlib 真的能陪你很久很久。
返回列表