横向堆积柱状图:用Matplotlib清晰呈现峰值分布数据

发布时间:2026/10/18 20:33:04
横向堆积柱状图:用Matplotlib清晰呈现峰值分布数据 数据可视化这块有些需求看着简单真做起来却容易卡壳。前阵子有同事拿了一堆服务监控数据来找我说要把系统一周内每天的访问峰值分布讲清楚。我第一反应是画折线图把每天最高峰那个点标出来结果画完发现根本交代不了问题——峰值出现在哪个时段、不同来源流量各自贡献多少、周末和周一的结构差异一张折线图全糊在一起。最后换成了横向堆积柱状图peak distribution信息一下就立住了。今天就把这个做法的完整思路和实操细节拆开聊一聊算是给自己留个总结。不管你是做数据分析、运维监控还是写报表的开发同学只要手上数据是某类别在多个区间里的分布构成这个方案都值得参考。我会把数据预处理、Matplotlib 实现细节、常见坑和进阶玩法都过一遍尽量让每个步骤都能照着抄。1. 为什么偏偏是横向堆积柱状图1.1 同样一张图横过来到底好在哪纵向柱状图大家太熟了但很少有人去想横过来这个操作背后的逻辑。横向堆积柱状图的核心优势是对长文本标签的友好程度实在太高。比如华东区–移动端–页面浏览这种标签放纵向柱状图的坐标轴底下必然要旋转或者截断而横向版本可以让它一行放平阅读成本直线下降。另一个容易忽略的点在于人对长度的感知精度。我们读图时对水平方向长短的直觉判断其实比对垂直方向高度差更稳定。这一点在比较多个条形的总量时特别明显横向图里的条形并列排开谁长谁短几乎不用注释一眼就能看出来。纵向版本经常要凑近看刻度才能确认差距。更重要的是堆积结构的识读顺序。堆积图里每一段数据都叠在上一个段上纵向堆积需要人视线从上往下、再从底往上反复扫描各段的边界而横向堆积图天然符合我们在屏幕上从左到右的阅读节奏——最底部的类别在最上方每一列从左侧起点向右延伸各段之间的分界只要是同一列里对比识别起来非常舒服。这个细节在实际评审场景里作用很大。1.2 peak distribution 这类需求堆积图是不是最优解先解释下标题里的 peak distribution。我用在这个场景里指的就是系统每天访问量的峰值分布哪个时段并发最高、请求集中在哪几个来源、不同来源占比如何。这个需求有两个维度的信息要同时表达——峰值在哪和构成是什么。堆积柱状图正好能同时承载这两个维度每个横向条形是一个时间段或类别条形的总长度反映该时段总量大小而条形内部的不同颜色段则表示该总量中各来源的贡献比例。有些场景其实并不适合堆积图。比如只是想看某个点是不是最高折线图或者散点图就够了如果只关心占比根本不看总量饼图或环形图更直白。拿我的例子说如果只需要每天下午两点是全天最高峰这一个结论画个最简单的折线图两分钟就完事了。但一旦要追问这个高峰里有百分之多少来自App、多少来自Web、或者周末早高峰和晚高峰的结构差异时堆积图就要比折线图或者饼图强大得多。所以前提是总量 构成 类别三个信息都要出现在同一张图里横向堆积柱状图才有真正优势。它适合的人群也很明确报表开发、数据分析师、站点可靠性工程师以及所有需要向上汇报结构变化的人。如果汇报对象只看总量趋势那还是老老实实选普通柱状图。图形类型核心表达能力适合场景不适合场景普通柱状图单维度数值对比简单总量排名需要看内部结构折线图趋势变化时间序列峰值定位需要表达构成占比饼图/环形图占比关系只看比例不看量总量差异大、类别多横向堆积柱状图总量构成多类别峰值的分布结构与时段对比单点对比不关心结构2. 数据预处理先把峰值分布算明白2.1 原始日志长什么样绘图之前数据基本逃不出两种形态。一类是明细日志也就是一条一条的原始访问记录字段通常包括时间戳、来源渠道、请求耗时这些维度。另一类已经是聚合后的结果形如某小时某渠道的请求量。如果你拿到的是一堆原始日志千万别直接丢进绘图函数那是给自己挖坑。第一步一定是按维度做聚合。我当时手里的数据类似这样一行记录代表一次请求包含三个待聚合字段——小时00点到23点、来源渠道Web端、App端、API调用、请求量。要画峰值分布就需要先按小时分组再按来源渠道分组求和。这个聚合看似简单但容易漏掉空时段——某个小时可能没有任何请求这一行在聚合后直接就消失了如果不做填充画出来的图会少一个格子对比时产生误导。所以聚合之后我习惯用reindex把缺失的时间轴补齐。import pandas as pd # 模拟一张明细日志 raw pd.DataFrame({ hour: [8, 8, 9, 9, 9, 10, 14, 14, 15], source: [Web, App, Web, App, API, Web, App, API, App], requests: [120, 80, 200, 160, 50, 90, 240, 70, 180] }) # 聚合 透视 pivot raw.pivot_table( indexhour, columnssource, valuesrequests, aggfuncsum ).fillna(0) # 补全缺失小时0-23点都必须有 pivot pivot.reindex(range(0, 24), fill_value0)这一步做完得到的pivot就是绘图前最干净的数据结构行是小时列是来源渠道每个单元格是请求量。后面所有绘图逻辑都围绕这个表格展开。这里必须强调一个经常翻车的细节不要让某个来源列为空列比如某一天压根没有 API 调用那这一列全是0画图时会留出一条本不该出现的空隙最好在填数阶段就删掉所有列和不为全零的列。2.2 分箱处理把连续分布切成段如果是时长这类连续数值字段比如每条请求的耗时就不能简单按小时聚合了需要先分箱。分箱的意思是把整个数值范围切成长度相等的区间比如 0~100 毫秒、100~500 毫秒、500~1000 毫秒这样。我用的是pd.cut它的好处是返回的每个箱还有标签可以直接作为柱子顺序使用。下面是实际项目中用过的分箱片段。我习惯把耗时数值先定好边界再把这些边界交给pd.cut处理。需要注意的是边界定义有一定讲究如果边界间隔太均等数据密集的区间会全部扎进同一个箱分布特征反而被抹掉如果间隔太随意又会影响可读性。折中的做法是先跑一遍describe()看分位数按业务上关心的阈值来切。import pandas as pd bins [0, 100, 500, 1000, 5000, 20000] labels [0-100ms, 100-500ms, 500-1s, 1s-5s, 5s-20s] df[bucket] pd.cut( df[耗时], binsbins, labelslabels, rightFalse ) bucket_counts df.groupby([bucket, 来源], observedFalse)[请求量].sum().unstack()分箱的粒度直接决定了后续堆积图的信息密度。切得太细柱子数量暴增横向空间不够用切得太粗又体现不出峰值分布的层次感。我的经验是优先让业务方定义边界其次自己看数据分布找拐点比如p50 在150毫秒、p99 在两秒左右就按这个思路去切。代码里那个rightFalse的意义是让区间左闭右开避免同一个值被算进两个箱。2.3 保留总量视角数值型堆积和百分比型堆积堆积柱状图有个容易被新手忽略的分叉点画的是绝对值的堆积还是每行归一化到 100% 的堆积。这两种图画出来结构一样表达含义却完全不同。绝对值堆积重在展示总量差异——哪个时段请求量最大直观看柱子的总长度百分比堆积重在展示构成比例变化——比如同样是 1000 条请求早高峰和晚高峰的内部来源比例到底差在哪。两个思路之间切换非常快。按行归一化只需要一行代码# 绝对值堆积 absolute pivot # 百分比堆积 percent pivot.div(pivot.sum(axis1), axis0) * 100在实际的 peak distribution 项目里我经常两张图都出。第一张给管理层看峰值和总量第二张给技术团队看结构变化。比例视图对数据量级不敏感如果一个时段只有 10 条请求、另一个时段有 1 万条百分比一归一化两者的构成对比就很可疑看图时需要特别提防。这正是我后来在总结中反复提醒自己的百分比堆积图永远需要搭配一个总量信息否则容易被误读成两个时段情况差不多。3. 用 Matplotlib 实现横向堆积柱状图3.1 核心思路barh加left参数Matplotlib 里画横向柱状图的函数是ax.barh。名字里的 h 就是 horizontal。它和纵向的ax.bar在参数上有个对应关系bar用height控制柱高barh用height控制柱子厚度。横向柱子的长度用width而这个width对应的是纵向柱子的height。语法上的对应关系容易让人犯迷糊我统一按物理方向来记横向柱子的粗细用height长短用width。横向堆积的实现关键就是left参数。每一段的起点不再是零点而是前面所有段的累计值。比如第一段从 0 开始、宽度 300第二段就从 300 开始、宽度 500第三段从 800 开始、宽度 200。这样视觉上三个颜色段首尾相接正好组成一个完整的条形。3.2 完整代码走一遍下面这段代码是把前面的pivot表画成横向堆积图。数据行是小时列是来源逐列循环画出堆积段。import matplotlib.pyplot as plt import numpy as np # 假设 pivot 已准备好index小时0-23columns来源 fig, ax plt.subplots(figsize(10, 7)) categories pivot.index.tolist() left np.zeros(len(pivot)) # 每一行的起始位置一开始全部为0 for source in pivot.columns: ax.barh( ycategories, widthpivot[source], leftleft, labelsource, height0.65 ) left left pivot[source].values # 累加作为下一段的起始点 ax.set_xlabel(请求量次) ax.set_ylabel(小时) ax.set_title(全天请求量峰值分布按来源堆积) ax.legend(loclower right) ax.grid(axisx, linestyle--, alpha0.4) plt.tight_layout() plt.show()这里left的更新方式非常直白但有几个隐性坑要提醒大家。第一left left pivot[source].values是在原地更新数组如果在 Jupyter 里反复执行这一段left会被持续累加导致图形越堆越长。稳妥做法是用一个独立的局部数组名或每次画图前重新赋零。第二循环画的时候pivot.columns的顺序就是图例顺序后面讲图例时还会展开。第三ax.barh的y参数传的是类别列表Matplotlib 默认会把列表的第一项画在最上面如果希望第一个类别在最顶反而要把这个顺序反转这算是个经典反直觉点。3.3 颜色、图例和顺序的细节颜色这块Matplotlib 默认的颜色循环在类别少于十列时勉强可用类别一多就必须换colormap。我通常用plt.cm.tab10这种感知上比较统一的调色板或者直接给每个来源指定稳定颜色。注意堆积图里不同来源段的颜色必须是区分度极高的因为人眼要在相邻堆段间快速切换颜色近似的话图就废了。图例顺序有个关键讲究。堆积柱的视觉顺序是从左到右第一列是最底层第二列叠加在上面以此类推。而图例如果是默认的legend()顺序是按画入顺序来的——正好也是第一列、第二列的顺序。但人的阅读习惯是先看顶部或第一个图例这会导致脑海里建立的对应关系容易错位。我的做法是手动反转图例的顺序让第一个图例对应最底层的堆段。具体只要handles, labels ax.get_legend_handles_labels()然后反向传入ax.legend(handles[::-1], labels[::-1])。柱子的粗细由height参数控制。类别的数量少柱子可以画粗一些比如 0.7类别非常多就要缩到 0.3 以下避免重叠。但要注意如果类别标签比较长太粗的柱子反而会让标签区域拥挤所以要结合figsize和标签长度一起调。我在 24 小时这种场景下height用 0.65标签一字排开效果还不错。# 手动控制图例顺序让最底层段显示在第一位 handles, labels ax.get_legend_handles_labels() ax.legend(handles[::-1], labels[::-1], loclower right, frameonFalse)4. 常见问题与排查技巧实录4.1 标签一长就被截断三步解决横向堆积图最常翻车的场景之一是类别标签太宽比如华东区–移动端–页面浏览这种长字符串。Matplotlib 不会自动把坐标轴区域让给标签结果就是标签前后被吞掉。我实际项目里处理过这类问题根本原因其实就是画布空间没有分配好。第一步调figsize把画布加宽第二步用plt.tight_layout()让 Matplotlib 自动收紧子图边界第三步如果标签依然被截断直接用fig.subplots_adjust(left0.3)这类手动命令给左侧坐标轴区域腾出空间。多数长标签问题到这基本就解决了。另外还有个细节y 轴标签如果实在长可以考虑在传入数据时就把它们拆成两行比如用华东区\n移动端代替华东区-移动端。这样既保住了纵向空间的紧凑性也不会触发截断。反过来如果你发现 y 轴标签太过拥挤每根柱子上的标签相互碰到那就用ax.set_yticks(ticks[::2])做疏密控制只显示一半的刻度宁缺毋滥。4.2 堆积顺序和颜色对应不上我踩过的坑有一次我处理四类来源的数据颜色代码写好后怎么都跟图例对不上。看堆积图第一个色块以为是 A一查数据底层其实是 B排查了半天才发现问题在pivot.columns的排序上。原始数据在聚合时可能输出的是字母序也可能是字典序而我自己心里默认的是业务排序两者不一致时整张图的解读就全乱了。这个问题的解法很简单却容易一直忽略画图前显式给列排序。不管是按总量从大到小排还是按业务优先级排总之要有一个确定性顺序不能把排序交给哈希表。我现在写图之前都会加一行pivot pivot[custom_order_list]其中custom_order_list是手工指定的来源列表。这行代码看似多余实则是防止隐性 Bug 最有效的护身符。还有一个和顺序相关的坑某一行数据里某种来源的数值正好是 0那么这个段画出来是个零宽度的隐形条但图例里仍然会包含它。在堆积图里这种零值段会导致相邻颜色块之间的边界出现细微的错位视觉上像多了一条缝。通常的处理方法是画完图后检查每列的sum()如果某列全为零就直接从pivot.columns中剔除。4.3 量级悬殊一条柱子盖住所有细节堆积图最怕的是数据量级差太大。比如 API 调用一天有 3 万次内部接口某类只有 10 次小数值的那一段几乎看不见跟不存在似的。这种问题不是画图技巧能补救的是数据本身该换表达方式了。我见过一种常见处理套路把小数值的类别单独画一张图或者用断裂的方式把大柱子截断——但断裂轴的可信度比较低评审时不推荐。我更推荐的方案是如果不同来源的量级天然不在一个水平那么考虑把这几类来源拆分改成分组堆积图或者双面板把量级大的类别和量级小的类别分开展示。另一个思路是把大类别换成对数轴。注意对数轴在堆积图上会让最底层段的起点变成 NaN 或负值Matplotlib 会直接报错所以对数轴这里并不建议使用除非你能保证所有值都是正数且完整。实际项目里我最后选择的是百分占比视图 绝对总量标注。百分比的堆积消除了量级差异的基本盘总量信息用每个时段标签后面的(总计: xxx)标注出来。这样既照顾了小类别的可见性又不丢掉总量概念。5. 进阶让堆积柱状图会说话5.1 在柱子里加数值标签数据可视化的终极目标不是画出来而是让人看懂。堆积柱状图如果不加标签读者还得拿尺子对着刻度量距离这种体验太糟糕。我习惯在每一堆段的中间位置加一个数值标签标明该段的具体值。加标签时的位置计算公式很简单当前段的起点加上它自身宽度的一半就是这个段的中心横坐标。纵坐标用该行类别索引的数值下标。如果某一段数值太小标签放里面会挤得不可读我会加个条件仅对超过某个阈值的段标注其他段只在图例里体现。threshold 100 # 只标大于100的段 for row_idx, (_, row) in enumerate(pivot.iterrows()): cumulative 0 for source in pivot.columns: value row[source] if value threshold: ax.text( cumulative value / 2, row_idx, f{int(value)}, hacenter, vacenter, fontsize9, colorwhite ) cumulative value这个写法里row_idx是从数据行顺序来的整数正好对应ax.barh里 y 轴的类别位置。需要特别提醒的是颜色问题标签文字颜色必须和所在堆段的背景色形成对比。浅色堆段上用深色字深色堆段上用白色字。如果你用的调色板里浅色较多写一个简单的亮度判断来自动决定文字颜色效果会稳定很多。5.2 保存出图清晰度和尺寸别忽略博文或汇报里用的图最终都要导出成图片。很多人直接用plt.savefig(xxx.png)存完才发现精度不够。出图设置里最容易被忽略的两个参数是dpi和bbox_inches。前者控制清晰度后者保证边缘不被裁掉。平面展示用 150 dpi 足够打印或高清投屏至少 300 dpi。bbox_inchestight会智能调整留白把图例和标签都包含进画布省得手动改subplots_adjust。如果要在深色背景的 PPT 里使用还可以加transparentTrue导出透明底。fig.savefig(peak_distribution.png, dpi300, bbox_inchestight)5.3 从静态图到交互式一个快速的换装方案有时候静态图不够用尤其需要看某个堆积段具体数值时管理者会希望鼠标悬停就有提示。如果图本身逻辑没有变化只是需要交互没必要重新发明轮子。我一般直接用 Plotly Express把pivot表转成一列一列的堆积条形几行代码就能完成移植。大概逻辑是把pivot用melt转成长表然后用px.bar指定orientationh再把color设置为来源字段barmode设为stack。这套模板基本是通用的。import plotly.express as px long_df pivot.reset_index().melt( id_varshour, var_namesource, value_namerequests ) fig px.bar( long_df, xrequests, yhour, colorsource, orientationh, barmodestack, title全天请求量峰值分布 ) fig.show()交互式的优势不只是悬停提示还能在图例上点选隐藏某个来源方便单独对比某几类流量的分布。当然静态图在正式报告里的可排版性仍然更强。我的做法通常是分析阶段用交互式来回探索最终汇报时导出静态图。尾声这个方案背后的真实工作流把横向堆积柱状图peak distribution用到顺手最关键的其实不是记住某个函数参数而是建立一种先想清楚要表达什么、再决定怎么画的工作习惯。我每次拿到这种分布构成类需求流程都固定成四步先明确三个信息维度总量、构成、类别缺不缺再用pivot_table或pd.cut把数据整理成规整的行列表然后根据读者习惯决定画绝对值还是百分比最后才是调颜色、标签这类视觉细节。有一次我在一个数据分析项目中踩了跟别人一样的坑图例顺序没显式排序导致汇报时把两个来源的数据说反了。虽然当时及时纠正了但这个教训让我此后每次画堆积图都会加一行强制列序。细节决定专业度这种话一点不虚。希望这篇小结里提到的那些踩坑记录和代码片段能让你少消耗几个本该用来喝水的晚上。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询