☰
Matplotlib堆积图实战:从数据准备到自动化出图的完整指南
2026/10/9 22:39:16 网站建设 项目流程

1. 从一张“糊成一团”的图说起:堆积图到底解决什么问题

很多人第一次接触 Matplotlib 的堆积图,都是被一个很具体的需求逼出来的:手头有几组数据,它们共享同一个横轴,比如一周七天、十二个月、几个渠道,你想同时看到“总量”和“构成”。如果直接画多条折线,线一多就互相穿插,谁压着谁根本看不清;如果画并排柱状图,总量又得靠眼睛去加。堆积图(Stacked Chart)就是为这个场景生的——它把同一根柱子按类别往上叠,柱子的总高度代表合计,每一段的厚度代表该类别在当期的贡献。

我第一次真正用上它,是做一个渠道来源分析。当时有自然流量、活动流量、推荐流量三条线,老板要看每天的总访问量,同时想知道活动到底拉动了多少。折线图画出来三条线缠在一起,汇报时被问“这天总量多少”,我当场心算还差点算错。换成堆积柱状图之后,总高度一眼就是总量,活动那一段的颜色深浅直接反映拉动效果,整个汇报顺畅了很多。这件事让我意识到,堆积图的核心价值不是“好看”,而是把“合计”和“构成”这两个信息压缩进同一个视觉通道。

Matplotlib 里画堆积图,底层其实就一句话:plt.bar()的bottom参数。但真正把它用顺手,需要理解的东西远不止这一句。你得知道数据要怎么组织成“可叠加”的形状,颜色怎么选才不会让相邻段糊在一起,标签怎么放才不重叠,横向堆积和纵向堆积分别适合什么场景,以及当类别多到七八个时该怎么救场。这些细节,官方文档不会一条条告诉你,但实际项目里每一个都会绊你一下。

这篇内容适合三类人:刚学完 Matplotlib 基础、想进阶到“能画出能汇报的图”的新手;做过一些图表但堆积图总是画得别扭、颜色乱、标签挤的中级使用者;以及需要把堆积图嵌进自动化报表、批量出图的工程实践者。我会从数据准备讲到样式调优,再到几个真实踩过的坑,尽量把每个“为什么这么写”都交代清楚。你不需要背 API,跟着思路走一遍,下次遇到类似需求就能直接改参数复用。

2. 堆积图的数据形状:为什么你的第一次尝试总是叠错

2.1 从“宽表”到“可叠加数组”的转换逻辑

堆积图最容易出错的地方不在画图代码,而在数据准备。很多人手里拿到的是一张宽表,长这样:

日期自然流量活动流量推荐流量
周一1200800400
周二1350950420
周三11001500380

你想画堆积柱状图,于是直接plt.bar(日期, 自然流量),然后想再加一层,却发现第二根柱子是从零开始画的,把第一根盖住了。这就是没理解bottom参数的作用。bottom的意思是“这根柱子的底部从哪个高度开始画”,默认是 0。要让活动流量叠在自然流量上面,第二根柱子的bottom必须等于自然流量的值。

所以正确的做法是:先画最底层,记下它的高度数组;画第二层时把bottom设成第一层的高度;画第三层时把bottom设成前两层之和。这个“累加”的过程,就是堆积图的本质。用代码表达:

import matplotlib.pyplot as plt import numpy as np days = ['周一', '周二', '周三', '周四', '周五'] natural = np.array([1200, 1350, 1100, 1400, 1250]) activity = np.array([800, 950, 1500, 1100, 900]) recommend = np.array([400, 420, 380, 450, 500]) plt.bar(days, natural, label='自然流量') plt.bar(days, activity, bottom=natural, label='活动流量') plt.bar(days, recommend, bottom=natural + activity, label='推荐流量') plt.legend() plt.show()

注意第三行的bottom=natural + activity,这里用的是 NumPy 数组的逐元素相加,得到的就是前两层在每个位置上的累计高度。如果你用的是 Python 列表,natural + activity会变成列表拼接而不是逐元素相加,结果完全错误。这是我见过新手最常踩的坑之一,所以强烈建议在画图前先把数据转成np.array。

2.2 横向堆积与纵向堆积的选择依据

plt.bar()画的是纵向柱状图,plt.barh()画的是横向条形图。堆积逻辑完全一样,只是bottom变成了left参数。那什么时候该用横向?

我的经验是看类别标签的长度。如果横轴标签是“周一”“周二”这种短词,纵向没问题;但如果标签是“华东大区直营渠道”“华南大区经销商渠道”这种长文本,纵向排列会挤成一团,旋转 45 度也难看。这时候换成横向堆积,标签在左侧自然横排,可读性提升非常明显。

另一个判断维度是类别数量。纵向堆积适合 3 到 5 个类别,横向堆积在类别稍多时更从容,因为纵向空间比横向空间更容易扩展。不过横向堆积也有代价:它占的垂直空间大,如果类别多、又要放进一页 PPT,可能得压缩高度,反而显得局促。所以我的习惯是,先看标签长度,再看类别数量,两者综合决定方向。

还有一个细节:横向堆积时,left参数的累加逻辑和纵向的bottom一模一样,但很多人会忘记把plt.barh的height参数调一下。默认高度是 0.8,如果柱子太粗,段与段之间的边界会不明显,适当调到 0.6 到 0.7 会让图更透气。

2.3 数据归一化:百分比堆积图的适用边界

有时候你关心的不是绝对量,而是构成比例。比如三个渠道每天各占多少百分比,总量本身波动不重要。这时候要做百分比堆积图,思路是先把每一列归一化成 100%,再按普通堆积图画。

total = natural + activity + recommend natural_pct = natural / total * 100 activity_pct = activity / total * 100 recommend_pct = recommend / total * 100 plt.bar(days, natural_pct, label='自然流量') plt.bar(days, activity_pct, bottom=natural_pct, label='活动流量') plt.bar(days, recommend_pct, bottom=natural_pct + activity_pct, label='推荐流量') plt.ylabel('占比 (%)') plt.legend() plt.show()

百分比堆积图的好处是构成对比极其清晰,缺点是丢失了总量信息。如果总量本身也是关键指标,那百分比堆积就不合适,得回到绝对量堆积,或者在旁边加一条总量折线做辅助。我一般会在汇报时明确告诉听众“这张图只看结构,不看规模”,避免误读。

提示:归一化时务必处理分母为零的情况。如果某天总量为 0,除法会产生nan或inf,画图时柱子会消失甚至报错。稳妥做法是先过滤掉总量为 0 的日期,或者用np.where把无效位置置零。

3. 颜色与堆叠顺序:让相邻色块不再“糊”在一起

3.1 配色方案的取舍:为什么默认色板常常不够用

Matplotlib 默认的tab10色板在折线图里表现不错,但用在堆积图上经常出问题。原因是堆积图的色块是紧挨着的,相邻两段如果明度接近,边界就会模糊。比如tab10里的蓝色和橙色放在一起还行,但蓝色和紫色相邻时,在投影仪上几乎分不清。

我的做法是优先选择明度差异明显的配色。一个简单有效的策略是用同一色系的不同深浅,比如深蓝、中蓝、浅蓝,这样既保持了类别间的关联感,又靠明度拉开层次。Matplotlib 里可以用plt.cm.Blues这类 colormap 采样:

import matplotlib.pyplot as plt import numpy as np colors = plt.cm.Blues(np.linspace(0.4, 0.9, 3))

np.linspace(0.4, 0.9, 3)生成三个从 0.4 到 0.9 的等距值,对应从较浅到较深的蓝色。这样画出来的三段柱子有自然的层次感,不会糊。如果类别之间没有天然关联,那就用对比色,但要注意避开红绿搭配,色觉障碍人群很难区分。

另一个实用技巧是给每段柱子加白色描边:

plt.bar(days, natural, label='自然流量', edgecolor='white', linewidth=1.5)

这条白边就像给每个色块画了条分界线,即使颜色接近也能看清边界。我在做需要打印的黑白报表时,甚至会直接用不同灰度加描边,效果比彩色还稳。

3.2 堆叠顺序的心理学:谁该在最下面

堆叠顺序不是随便排的。人眼对柱子底部的感知最稳定,因为底部有横轴作为参照;越往上,判断某一段的绝对高度就越依赖上下文。所以最重要的、或者最稳定的类别应该放在最下面。

举个例子,如果自然流量是基本盘,每天都有且波动不大,那它放底层最合适,读者一眼就能看出基本盘的规模。活动流量波动大,放在中间或上层,它的变化会通过柱子总高度的起伏体现出来。反过来,如果把波动大的类别放底层,整个柱子的起点就在跳,读者很难判断其他类别。

还有一个约定俗成的习惯:如果类别有逻辑顺序,比如“新客、老客、流失召回”,就按这个顺序从下往上排,不要为了颜色好看打乱。读者会下意识地按顺序理解,顺序乱了会增加认知负担。

3.3 图例位置与色块标注的配合

图例默认在右上角,但堆积图的右上角往往被柱子占据,图例压上去会遮挡数据。我一般会把图例移到图外:

plt.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0)

bbox_to_anchor把图例锚定在坐标轴右侧,(1.02, 1)表示横坐标略超出右边界、纵坐标对齐顶部。这样图例不遮挡任何柱子,适合放进报告。

如果类别不多,直接在色块上标数值比图例更直观。但堆积图的中间段标注很麻烦,因为每段的高度不一,文字容易溢出。我的做法是只标总量在柱子顶部,中间段用图例区分。如果非要标每段,可以用plt.text配合累计高度计算位置,但类别超过四个就别折腾了,视觉上会很乱。

注意:bbox_to_anchor的坐标是相对于坐标轴的比例,不是数据坐标。很多人第一次用会填数据值,结果图例飞到图外老远。记住它是 0 到 1 的比例值,超过 1 就是往右/往上超出。

4. 标签、网格与坐标轴:把图从“能看”做到“能汇报”

4.1 总量标签的自动计算与放置

堆积图最有价值的信息之一是柱子顶部的总量。手动算好再写死数字,数据一更新就全错了。正确做法是用累计高度自动计算:

totals = natural + activity + recommend for i, total in enumerate(totals): plt.text(i, total + 30, str(total), ha='center', va='bottom', fontsize=10)

total + 30是给文字留一点上方空间,避免贴着柱子顶。ha='center'让文字水平居中于柱子,va='bottom'让文字的底部对齐给定坐标。这个偏移量 30 需要根据数据量级调整,如果总量是几万,30 就太小了,文字会压在柱子上。一个更稳的做法是用坐标轴范围的百分比来定偏移:

offset = max(totals) * 0.02

这样无论数据量级如何,偏移都是总量的 2%,视觉上比较协调。

4.2 网格线的取舍:横向网格为什么更适合堆积图

堆积图的读者需要比较不同柱子的同一段高度,横向网格线能提供参照。但默认的网格线是双向的,纵向网格线在堆积图里几乎没用,还会和柱子边界打架。所以我会关掉纵向网格:

plt.grid(axis='y', linestyle='--', alpha=0.4)

axis='y'只保留横向网格,alpha=0.4让网格线变淡,不抢柱子的视觉焦点。linestyle='--'虚线比实线更轻,适合做背景参照。

如果柱子本身颜色较深,网格线要更淡,否则会显得脏。我一般把alpha控制在 0.3 到 0.5 之间,具体看柱子颜色的明度。深色柱子配 0.3,浅色柱子配 0.5。

4.3 坐标轴范围与留白:别让柱子顶到天花板

Matplotlib 默认会自动留一点边距,但堆积图的柱子往往比较高,自动边距可能不够,总量标签会顶到图的上边缘。手动设置 y 轴上限更稳妥:

plt.ylim(0, max(totals) * 1.15)

乘以 1.15 是给标签留出 15% 的顶部空间。这个系数不是固定的,如果标签字号大,可能要 1.2;如果标签在柱子内部,1.05 就够。关键是别让任何元素贴边,贴边会显得局促,汇报时也不好看。

x 轴这边,如果柱子数量少,可以适当加宽:

plt.xlim(-0.5, len(days) - 0.5)

这样第一根和最后一根柱子不会贴着左右边界,视觉上更平衡。

4.4 字体与字号:中文显示与可读性底线

Matplotlib 默认字体不支持中文,直接画会显示成方框。这是新手必踩的坑。解决办法是设置支持中文的字体:

plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False

SimHei是黑体,Windows 系统一般都有。Mac 系统可以用Arial Unicode MS或PingFang SC。axes.unicode_minus = False是为了让负号正常显示,否则负号也会变成方框。

字号方面,标题 14 到 16,坐标轴标签 11 到 12,刻度标签 10 左右,图例 10。这是投影和打印都还能看清的下限。如果图要放进论文或正式报告,可以整体放大 1 到 2 号。别用默认的 10 号字交差,在会议室后排根本看不清。

5. 多子图与批量出图:把堆积图塞进自动化流程

5.1 subplots 布局下的堆积图复用

实际项目里很少只画一张图。更常见的是把几个维度的堆积图拼在一起,比如按地区分四个子图,每个子图展示该地区的渠道构成。这时候用plt.subplots创建网格,然后在每个ax上画堆积图:

fig, axes = plt.subplots(2, 2, figsize=(12, 8)) for ax, region in zip(axes.flat, ['华东', '华南', '华北', '西南']): ax.bar(days, natural, label='自然') ax.bar(days, activity, bottom=natural, label='活动') ax.bar(days, recommend, bottom=natural + activity, label='推荐') ax.set_title(region) ax.grid(axis='y', linestyle='--', alpha=0.4) axes[0, 0].legend() fig.tight_layout()

这里有几个关键点。第一,axes.flat把二维数组拉平成一维,方便用zip遍历。第二,图例只在第一个子图上加一次,避免重复。第三,fig.tight_layout()自动调整子图间距,防止标签重叠。如果不加这一句,子图标题和上方子图的 x 轴标签经常打架。

5.2 把堆积图封装成可复用函数

每次画图都重写一遍bar调用太累,而且容易漏参数。封装成函数是更工程化的做法:

def plot_stacked(ax, x, data_dict, colors=None, show_total=True): bottom = np.zeros(len(x)) for i, (label, values) in enumerate(data_dict.items()): color = colors[i] if colors else None ax.bar(x, values, bottom=bottom, label=label, color=color, edgecolor='white', linewidth=1.2) bottom += np.array(values) if show_total: for i, total in enumerate(bottom): ax.text(i, total * 1.02, f'{total:.0f}', ha='center', va='bottom', fontsize=9) ax.legend() ax.grid(axis='y', linestyle='--', alpha=0.4) return ax

这个函数接收一个ax、横轴标签、一个字典(键是类别名,值是数值列表),自动处理累加、描边、总量标签。bottom初始化为全零数组,每画一层就累加一次,逻辑清晰。show_total开关控制是否显示总量,方便在不同场景复用。

封装之后,画一张图就变成一行调用:

fig, ax = plt.subplots(figsize=(8, 5)) plot_stacked(ax, days, {'自然流量': natural, '活动流量': activity, '推荐流量': recommend}) plt.show()

数据变了只改字典,样式调整只改函数,维护成本大幅降低。

5.3 保存与导出:分辨率、格式与透明背景

画完图要保存,plt.savefig的参数直接影响输出质量:

plt.savefig('stacked.png', dpi=300, bbox_inches='tight', transparent=False)

dpi=300是印刷级分辨率,屏幕展示 150 就够,但报告和论文建议 300。bbox_inches='tight'自动裁掉多余白边,避免图周围一圈空白。transparent=False保持白底,如果要做 PPT 叠加,可以设成True导出透明背景的 PNG。

格式选择上,PNG 适合屏幕和 PPT,PDF 和 SVG 是矢量格式,放大不失真,适合论文和印刷。如果图里元素多、文件大,SVG 可能比 PDF 更小。我一般同时导出 PNG 和 PDF,PNG 用于快速预览,PDF 用于正式场合。

提示:bbox_inches='tight'有时会把图例裁掉一部分,尤其是图例放在图外时。如果发现图例不完整,可以手动指定pad_inches增加边距,或者干脆不用 tight,手动调subplots_adjust。

6. 那些年我踩过的堆积图坑:从数据错位到标签重叠

6.1 列表相加导致的“隐形错误”

前面提过列表相加的问题,这里展开说。Python 里[1, 2] + [3, 4]得到[1, 2, 3, 4],而不是[4, 6]。如果你用列表存数据,bottom=natural + activity会得到一个长度翻倍的列表,plt.bar拿到这个bottom后,柱子位置和高度全乱,但程序不一定报错,图可能只是“看起来怪怪的”。这种错误最危险,因为它不崩溃,只是悄悄画错。

我的习惯是数据一进来就转np.array,从源头杜绝。如果数据来自 pandas,.values或.to_numpy()也能拿到数组。总之,画图前确认数据类型是数组,不是列表。

6.2 类别过多时的“彩虹条”灾难

有一次我拿到八个渠道的数据,想全堆在一张图上。画出来之后,每根柱子被切成八段,每段细得像纸片,颜色倒是很丰富,但根本看不出哪段是哪段。这就是类别过多的典型症状。

堆积图的可读性上限大概是五到六个类别。超过这个数,我的处理方式是:把占比最小的几个类别合并成“其他”,只保留前四到五个主要类别。合并之后柱子清爽很多,主要类别的对比也更突出。如果“其他”本身也重要,可以单独画一张小图或者用表格补充。

另一个思路是换图型。类别多的时候,分组柱状图或者热力图可能比堆积图更合适。堆积图不是万能的,它擅长的是“少量类别 + 总量与构成并重”的场景。

6.3 负值数据的处理:堆积图遇到负数会怎样

如果某个类别出现负值,堆积图会变得很尴尬。因为bottom累加的逻辑假设所有值都是正的,一旦有负数,柱子会往下长,和下面的柱子重叠,视觉上完全乱套。

我遇到负值时的做法是:如果负值代表“减少”或“亏损”,把它单独拿出来用另一种图型表达,比如在堆积图下方加一条负值柱状图,或者用折线标注。硬塞进堆积图只会让读者困惑。如果负值只是数据错误,那当然先修数据。

6.4 标签重叠的三种解法

总量标签重叠通常发生在柱子数量多、数值接近的时候。三种解法,按推荐顺序:

第一种,旋转标签。plt.xticks(rotation=45)让横轴标签倾斜,腾出空间。但旋转后标签可能超出画布,配合bbox_inches='tight'保存能自动裁切。

第二种,隔一个标一个。如果柱子太密,没必要每个都标,隔一个标一次,信息量减半但可读性大增。

第三种,缩小字号。这是下策,因为字号太小就失去意义了。如果前两种都不行,说明柱子数量已经超出这张图的承载能力,该考虑分图或者换图型了。

6.5 图例遮挡数据的动态调整

图例位置没有万能解。我的经验是:先画图,看图例默认位置有没有压到柱子,压到了就往外移。往外移之后如果画布不够宽,就调整figsize加宽。如果图例本身太长(类别名很长),可以改成横向排列:

plt.legend(ncol=3, loc='upper center', bbox_to_anchor=(0.5, -0.15))

ncol=3让图例分三列,bbox_to_anchor=(0.5, -0.15)把图例放到图的下方居中。这样图例不占图内空间,适合类别名长的场景。代价是图的高度要留够,否则图例会被裁掉。

7. 从堆积图延伸出去:几个值得一试的变体

7.1 堆积面积图:时间序列的连续堆积

如果横轴是连续时间,柱子之间没有间隙,堆积柱状图就变成了堆积面积图。Matplotlib 里用plt.stackplot实现:

plt.stackplot(days, natural, activity, recommend, labels=['自然', '活动', '推荐'], colors=plt.cm.Blues(np.linspace(0.4, 0.9, 3)), alpha=0.85) plt.legend(loc='upper left')

stackplot自动处理累加,不用手动算bottom,比bar省事。它适合展示长时间跨度的构成变化趋势,比如一年的月度数据。缺点是面积图的边界不如柱状图清晰,精确比较某一天的具体数值比较困难,更适合看整体趋势。

7.2 百分比堆积与绝对量堆积的组合

有时候既要看构成比例,又要看总量规模。一个折中方案是画两张图:上面一张绝对量堆积,下面一张百分比堆积,共享 x 轴。这样读者既能看总量起伏,又能看结构变化。用plt.subplots(2, 1, sharex=True)实现,两张图对齐,信息互补。

这种组合图在业务汇报里特别实用。上面那张回答“量有多大”,下面那张回答“结构怎么变”,两个问题一次讲清。

7.3 给堆积图加趋势线

如果总量本身有趋势,可以在堆积图上方叠加一条总量折线:

plt.plot(days, totals, color='black', marker='o', linewidth=2, label='总量趋势')

折线用黑色或深灰,和彩色柱子区分开。marker='o'在每天的位置加个圆点,强调具体数值。这样一张图里既有构成,又有总量趋势,信息密度高但不乱。注意折线要画在柱子之后,否则会被柱子盖住。

7.4 动画堆积图:让变化过程动起来

如果数据有时间维度,做成动画比静态图更有冲击力。Matplotlib 的FuncAnimation可以逐帧更新柱子高度:

from matplotlib.animation import FuncAnimation fig, ax = plt.subplots() def update(frame): ax.clear() ax.bar(days, natural[:frame+1], label='自然') ax.bar(days, activity[:frame+1], bottom=natural[:frame+1], label='活动') ax.legend() ani = FuncAnimation(fig, update, frames=len(days), interval=500) ani.save('stacked.gif', writer='pillow')

frames=len(days)表示帧数等于数据点数量,interval=500是每帧间隔 500 毫秒。writer='pillow'导出 GIF。动画适合展示数据逐步累积的过程,但要注意文件大小,帧数多、分辨率高时 GIF 会很大,可以适当降低dpi或减少帧数。

注意:动画里的ax.clear()会清掉所有元素,包括图例和网格,所以每帧都要重新设置。如果图例位置固定,可以在update外面设置一次,但clear之后需要重新添加。这是动画代码里常见的坑。

8. 一些让堆积图更“专业”的细节习惯

画了这么多堆积图,我慢慢养成了一些习惯,这些习惯不会让图“能跑”,但会让图“能看、能讲、能复用”。

第一个习惯是先定数据再定样式。很多人一上来就调颜色、调字体,结果数据一改,所有样式白调。我的顺序是:先把数据组织成正确的数组形状,跑通最朴素的图,确认数据没错位、没漏项,再开始调样式。数据错了,图再好看也是错的。

第二个习惯是给每张图写一句“结论标题”。不要用“各渠道流量堆积图”这种描述性标题,而是用“活动流量在周三达到峰值,占总量的 45%”这种带结论的标题。读者不看图也能抓住重点,看了图能验证结论。这个习惯是从做汇报被追问“所以呢”之后逼出来的。

第三个习惯是颜色不超过五种,字体不超过三种。颜色多了显乱,字体多了显杂。主色调用同一色系深浅,强调色用一个对比色,足够了。字体上,标题一种、正文一种,最多加个数字字体,别搞太花。

第四个习惯是导出前必看缩略图。把图缩到 20% 大小看一眼,如果这时候还能看清主要信息,说明图的层次是清楚的;如果缩略图里只剩一团颜色,说明细节太多、对比不够,得回去简化。这个检查方法特别有效,因为汇报时投影出来的效果,往往就接近缩略图。

最后一个习惯是把常用配置写成模板。中文字体、网格样式、图例位置、保存参数,这些每次都要设的东西,写成一个setup_style()函数或者一个matplotlibrc文件,每次画图前调用一次。省下来的时间可以用来打磨数据,而不是重复调样式。

堆积图本身不复杂,复杂的是让它准确传达信息。数据形状对了,颜色分得开,标签放得下,这张图就及格了。再往上,就是靠这些细节习惯一点点磨出来的。我到现在也不敢说每张堆积图都画得完美,但至少每次踩坑之后,下一次会少踩一个。

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

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

立即咨询