ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

箱形图从五数概括到异常检测与生产监控实战

箱形图从五数概括到异常检测与生产监控实战 一个班 60 个人考完试平均分 78标准差 9。另一个班也是平均分 78标准差 9。你要不要直接下结论说这两个班水平一样如果只看均值和标准差你会得出这个结论但它大概率是错的——一个班可能是齐刷刷 70 到 86 分的正态分布另一个班可能是三分之二的人考 70 分、三分之一的人考 95 分。箱形图(box plot)就是为了解决这类汇总指标掩盖真相的问题而生的。它用五个数字把一整列数据的分布形状画出来中位数在哪、数据挤不挤、尾巴往哪边长、有没有离群的点一眼就能扫完。我做数据分析这些年箱形图是我用得最多、也踩坑最多的图。用得好它是多组对比、异常检测、监控基线的利器用得糙它会安静地骗你而且骗得很有说服力。这篇就把箱形图从五根线怎么来的一路讲到生产环境里怎么拿它定告警阈值中间穿插 Python、Excel、ECharts 三套实现以及我自己踩过的那几个坑。不管你是刚学会画柱状图的新手还是天天调 matplotlib 的老手应该都能捞到点东西。1. 先把五根线的来历搞清楚再谈画图很多人学箱形图是从代码开始的plt.boxplot(data)一行下去图就出来了但图上的每条线代表什么、须线为什么止步在那个位置、那些孤立的小点凭什么被判成异常说不清楚。结果就是图画得出来结论不敢下。所以我坚持先把这套几何规则讲透它其实就是 John Tukey 在 1977 年提出的一套五数概括加异常值判定的约定。1.1 五个数字最小值、Q1、中位数、Q3、最大值箱形图的本质是把一列数据压缩成五个分位点。把这列数据从小到大排好队Q1第一四分位数排在 25% 位置的那个值说明有四分之一的样本比它小。Q2中位数排在正中间的值50% 分位。Q3第三四分位数排在 75% 位置的值。箱子的底边画在 Q1顶边画在 Q3箱子中间那条横线就是中位数。箱子的高度叫四分位距IQR也就是IQR Q3 - Q1。这个 IQR 很关键它衡量的是中间那 50% 的数据有多分散比标准差抗折腾——标准差会被几个极端值带跑偏IQR 基本不受影响因为它压根不看两头的数据。生活里打个比方一个班的成绩Q1 是第 25 名的分数中位数是第 30 名的分数Q3 是第 45 名的分数假设 60 人。箱子覆盖的就是从第 15 名到第 45 名这拨中间人群的分数区间。你一眼就能看出中间大多数人的分数挤在哪个范围里。1.2 须线不是画到 1.5 倍 IQR 就停这点最容易错这是新手最普遍的误解。规则是这样的先算出两条围栏下围栏 Q1 - 1.5 × IQR上围栏 Q3 1.5 × IQR。下须的端点 落在下围栏之上的最小真实数据点而不是下围栏本身。上须的端点 落在上围栏之下的最大真实数据点而不是上围栏本身。超出围栏的点一个个画成独立的散点就是异常值。举个例子某组数据 Q120Q340那 IQR20下围栏 20-30 -10上围栏 4030 70。假设这组数据里最小的实际值是 5那下须就画到 5如果最小的实际值是 12那下须就画到 12。注意须线端点上一定有一个真实数据点存在它不会悬空停在一个算出来的数字上。为什么要这么设计因为围栏只是一个统计上的可疑边界真不真实还得看数据本身。如果某个数据点刚好卡在围栏外一点点你可以怀疑它是异常如果围栏外根本没有点那须线自然就停在数据的最边缘。1.3 1.5 这个系数从哪来的能不能改1.5 不是拍脑袋定的。Tukey 选它的理由是如果数据服从正态分布超过 1.5 倍 IQR 围栏的样本大约只占 0.7%。换句话说在正常情况下你要画 140 多个点才可能冒出一个被判为异常的点。这个比例低到值得你去看一眼又高不到满图都是点没法看。当然它能改。有些库允许你把系数调到 3.0那对应的是极端异常值far out比 1.5 那一档更严格。也有的场景干脆不用固定系数直接把须线画到 5% 和 95% 分位——matplotlib 的whis参数就支持传一组百分位数比如whis(5, 95)。这时候须线端点就是第 5 和第 95 百分位上的真实值不再有异常值点单独画出来。我个人的经验做数据清洗、找脏数据的时候用默认的 1.5做业务图表给非技术同事看的时候如果异常点特别多可以调到 3.0避免图上一堆点吓到人。但无论用哪个系数图注里一定要写清楚不然读图的人会按默认值去理解结论就跑偏了。1.4 分位数的算法不止一种小样本时结果会差这一点更隐蔽。同样一列 8 个数字用不同的分位数插值方法算 Q1结果能差出好几分。常见的算法有 linear、lower、higher、midpoint、nearest 几种NumPy 的percentile默认是 linearExcel 的QUARTILE.INC和QUARTILE.EXC也是两套不同的口径。数据量大的时候几百上千条这点差异可以忽略但如果你在做小样本分析比如只有 12 个用户的满意度评分那不同工具算出来的箱子高度可能肉眼可见地不一样。踩过一次坑之后我的做法是只要样本量低于 30就在图注里标明用的哪种分位数算法避免同事拿另一套工具复现时对不上号。2. 拿一份真实数据把图画出来三种实现路径规则讲完了接下来动手。我准备了一份模拟的三班考试成绩数据两个班的均值标准差完全一样但分布形状完全不同——这正是箱形图的主场。下面分别用 Python、Excel、ECharts 走一遍你可以按自己手上的工具挑一条路。2.1 Pythonmatplotlib 与 seaborn 的分工先看 matplotlib它是最底层、控制力最强的那个import numpy as np import matplotlib.pyplot as plt np.random.seed(42) # A班标准正态均值78标准差9 class_a np.random.normal(78, 9, 60) # B班双峰大部分人70分少部分人95分 class_b np.concatenate([np.random.normal(70, 4, 40), np.random.normal(95, 3, 20)]) # C班均值一样但特别分散 class_c np.random.normal(78, 18, 60) fig, ax plt.subplots(figsize(8, 5)) bp ax.boxplot([class_a, class_b, class_c], labels[A班, B班, C班], patch_artistTrue, showmeansTrue, meanprops{marker: D, markerfacecolor: white, markeredgecolor: black, markersize: 6}) for box, color in zip(bp[boxes], [#8ecae6, #ffb703, #fb8500]): box.set_facecolor(color) box.set_alpha(0.7) ax.set_ylabel(考试分数) ax.grid(axisy, linestyle--, alpha0.4) plt.tight_layout() plt.show()几个参数值得单独说patch_artistTrue是让箱子内部能被填充颜色不然它默认是空心的。showmeansTrue会在图上多画一个均值标记。箱形图默认是不显示均值的因为它关心的是中位数。但中位数和均值一起看能立刻判断出偏态方向均值明显高于中位数说明右偏反之左偏。whis参数默认就是 1.5要改须线规则就动它。seaborn 则更适合分组对比的场景尤其是数据本来就躺在 DataFrame 里的时候import pandas as pd import seaborn as sns import matplotlib.pyplot as plt df pd.DataFrame({ score: np.concatenate([class_a, class_b, class_c]), class: [A班]*60 [B班]*60 [C班]*60 }) sns.boxplot(datadf, xclass, yscore, paletteSet2, width0.5, linewidth1.2) sns.stripplot(datadf, xclass, yscore, colorblack, size3, alpha0.4, jitterTrue) plt.show()我在实际项目里几乎都会在箱形图后面叠一层抖动散点stripplot 或 swarmplot见上面第二段。原因很实在箱形图只给你聚合结果看不到真实样本量和分布形状。叠上散点之后如果某一组其实是双峰散点会立刻把它暴露出来如果某组样本量只有 5 个散点也会告诉你这箱子不太可信。这个组合我强烈推荐成本极低收益极高。2.2 Excel2016 版之后自带箱形图但有个开关必须确认Excel 从 2016 版开始内置了箱形图类型路径是选中数据 → 插入 → 图表 → 统计图 → 箱形图。如果你用 WPS 或更老的版本找不到这个类型那就得手动算五数再拼图比较麻烦建议直接上 Python。Excel 版本的关键在于右键图表 → 设置数据系列格式里面有这么几个开关选项建议原因显示异常值勾选不勾就退化成简化的须线图看不见离群点显示平均值标记看需求我一般勾上用于判断偏态显示内部点数据量小于 200 时勾选能看出样本量和分布形状四分位计算包含/排除中位数影响就是前面说的分位数口径问题须线范围1.5 倍 IQR 或自定义与 Python 保持一致再对比我见过最典型的翻车场景同事用 Excel 画了一张箱形图发群里我用 Python 画的同一份数据箱子高度对不上两边吵了半天最后发现是 Excel 的四分位计算口径和 NumPy 默认的 linear 插值不同加上样本量只有二十几条差异被放大了。跨工具协作时分位数口径必须先对齐这是血泪教训。2.3 ECharts五数要自己算它不会替你判异常如果图要挂到网页上ECharts 的boxplot类型是主流选择但它有个反直觉的地方它不帮你计算五数概括和异常值你得把[min, Q1, median, Q3, max]这个数组自己算好喂给它。// 先在 Python 里算好每组的下须、Q1、中位数、Q3、上须 // 这里以 A 班为例实际生产环境由后端接口下发 const boxData [ [58, 72, 78, 85, 96], // A班 [62, 68, 71, 78, 99], // B班 [40, 66, 78, 90, 118] // C班 ]; const outliers [ [35, 118], // A班的异常值列表 [], // B班 [21, 129] // C班 ]; option { tooltip: { trigger: item }, xAxis: { type: category, data: [A班, B班, C班] }, yAxis: { type: value, name: 分数, splitLine: { lineStyle: { type: dashed } } }, series: [ { name: 分数分布, type: boxplot, data: boxData, itemStyle: { color: #e0f3ff, borderColor: #2b7ec1 } }, { name: 异常值, type: scatter, data: outliers, symbolSize: 6, itemStyle: { color: #d62728 } } ] };注意这里min和max填的是去除异常值之后的下须和上须端点异常值单独作为一个 scatter 系列画上去。很多人第一次用 ECharts 画箱形图直接把整列数据的最小值和最大值填进去结果图上的须线特别长、一个异常点都没有图就废了。后端算五数的时候用前面那段 Python 里的five_number逻辑照着写一遍就行def five_number(x, whis1.5): x np.asarray(x) q1, q2, q3 np.percentile(x, [25, 50, 75]) iqr q3 - q1 low_fence, high_fence q1 - whis * iqr, q3 whis * iqr inside x[(x low_fence) (x high_fence)] outliers x[(x low_fence) | (x high_fence)] return [inside.min(), q1, q2, q3, inside.max()], outliers.tolist()这段函数我几乎在每个前端可视化项目里都会复用一遍值得存进自己的代码片段库。3. 箱形图最容易骗人的三个地方图会画了接下来是最重要的部分什么时候这张图在骗你。箱形图的信息压缩率很高但压缩就意味着丢信息丢掉的那部分恰好是判断结论可信度的关键。下面这三条是我在实际项目里被坑过之后总结出来的。3.1 相同的箱子可以是完全不同的两个分布这是箱形图结构性的缺陷。前面那份数据里A班是正态分布B班是双峰分布但如果你把 B 班的数据调到让它的五数概括和 A 班接近两张箱形图会看起来几乎一样。历史上有个著名的演示叫 Anscombe 四重奏四组数据的均值和方差等统计量完全相同但画出来形状天差地别。箱形图也有同样的毛病。具体到业务里一个接口的响应时间如果 90% 的请求是 50 毫秒、10% 的请求是 800 毫秒它的箱形图可能和另一个全部请求都在 60 到 200 毫秒之间的接口长得差不多因为那 10% 的慢请求被当成异常值画成了几个孤立点容易被忽略。但这两个接口的用户体验完全不同——一个有明显的卡顿群体一个没有。应对方法很简单叠散点、叠蜂群图或者干脆并排画一张直方图或密度图。箱形图负责快速对比密度图负责揭示形状两者不冲突。3.2 样本量太小时箱子本身就是噪声Q1 和 Q3 是从数据里估出来的样本越少估计越不稳。样本量低于 10 的时候Q1 和 Q3 基本上就是某两个具体观测值稍微换一个样本箱子高度就大变样。我见过有人拿 8 个用户的反馈评分画箱形图得出满意度分布很集中的结论实际上换个 8 人样本结论可能完全反过来。我的经验阈值是这样的样本量箱形图可用性建议1 ~ 9不可靠直接画点图列出每个值10 ~ 29参考必须叠散点图注标注 n 值30 ~ 100基本可用叠散点更佳100 以上稳定常规使用可配密度图一个必须养成的习惯在任何箱形图下面标注每组的样本量 n。这件事成本极低但能避免大量误读。3.3 上须和 P95、P99 不是一回事做性能分析的人容易混淆这两个概念。箱形图的上须端点是1.5 倍 IQR 围栏内的最大真实值而 P95 是95% 分位上的值两者的计算逻辑完全不同。上须的位置取决于 IQR 的大小IQR 大上须就伸得远IQR 小上须就收得紧。而 P95 只跟分位位置有关跟 IQR 无关。这就导致一个现象在某段时间内响应时间整体变慢、但变慢得很均匀整体平移IQR 几乎不变箱子的高度不变只是整体位置往上移。这时候上须可能还落在合理范围内但 P95 已经突破 SLO 了。反过来如果响应时间的波动突然变大一部分请求快了一部分慢了IQR 会拉大上须会往上蹿看着像出现了异常但 P95 可能没怎么变。结论监控告警该用分位数指标P95、P99就用分位数箱形图是给你看形状和对比用的别拿上须当阈值。这一点我在早期做监控面板时搞混过被自己的告警坑了一晚上。4. 多组对比才是箱形图真正的主场单组箱形图其实用处有限它的价值在两组以上并排对比时才完全释放。因为箱形图把中位数、IQR、极值、异常值压缩到了同一套视觉语言里多张图放一起横向比较几乎不需要动脑。这一节讲几个我常用的对比模式以及排序这类容易被忽略的细节。4.1 横向对比看三件事中位数、箱子高度、异常分布把若干组箱形图并排放好按顺序看三个东西第一看中位线的相对高度这是典型水平的对比。A 组中位数比 B 组高一截说明 A 的典型情况更好或更高、更慢看指标方向。第二看箱子的高度也就是 IQR这是稳定性的对比。箱子矮的组说明中间那 50% 的人或请求、或产品批次表现比较一致箱子高的组说明内部差异大可能藏着分群。第三看须线和异常点的分布。如果一组的下须特别长、下方挂了大量异常点说明这组有个低值长尾——比如某些用户遇到了极端糟糕的体验或者某些生产批次的良品率特别低。这类信号在中位数和均值里是看不到的。这三步扫下来基本上十几秒就能对多组数据的差异形成判断效率比逐个看均值标准差高得多。4.2 按中位数排序让图自己讲故事默认情况下工具会按你传入的顺序排列各组。如果传入顺序恰好是字母序或 ID 序那张图的可读性会大打折扣——读者要自己在脑子里排一遍序才能看出趋势。我的做法是按中位数从高到低或从低到高排序后再画order df.groupby(class)[score].median().sort_values(ascendingFalse).index sns.boxplot(datadf, xclass, yscore, orderorder)排序之后图会自然呈现出一条阶梯状的形态趋势自己就浮出来了。如果是时间序列上的多组比如 12 个月的分布那就按时间顺序排列别排序因为时间的先后本身就是信息。再进阶一点可以按 IQR 排序来找最不稳定的组或者按异常值数量排序来找问题最多的组。不同的排序方式会引导读者关注不同的结论这个选择本身就是分析的一部分。4.3 分组箱形图一个维度不够就加第二个当你有两个分类维度时比如三个班级 × 两种教学方式可以用 hue 参数画出分组箱形图sns.boxplot(datadf, xclass, yscore, huemethod, paletteSet2, width0.6)但这里有个度同一张图上箱子的总数最好不要超过 12 个超过之后视觉会乱成一团。如果维度确实多我的处理方式是拆图——按其中一个维度切成若干张小图faceting每张小图里画 3 到 5 个箱子。用 seaborn 的catplot可以一行代码搞定sns.catplot(datadf, xclass, yscore, colmethod, kindbox, col_wrap3, height4)宽高比也值得调一调。箱子是竖直的太宽会显得笨重太窄又看不清异常点。我一般把每个箱子的宽度控制在整图宽度的 5% 到 8%多组对比时用width0.5到0.6比较舒服。5. 什么时候不该用箱形图和直方图、小提琴图的选型箱形图不是万能的它只是可视化工具箱里的一件。选错图比不会画图更糟因为图表会给人一种结论很确定的错觉。下面这张对比表是我自己总结的选型依据贴出来供参考。图表类型优势劣势适用场景箱形图多组横向对比效率极高异常值清晰隐藏分布形状和样本量组间对比、异常排查、监控基线直方图完整展示分布形状能看出双峰和偏态多组对比时拥挤不便并排单组或两三组的分布探索密度图形状连续平滑多组叠加友好受带宽参数影响大有平滑失真连续变量的分布对比小提琴图兼具箱形图和密度图的优点图面积大视觉负担重组数少但形状重要的场景抖动散点图保留每个原始数据点最真实数据量大时重叠严重样本量小于 200 的探索性分析我自己的默认策略是样本量小于 30直接画散点30 到 500画箱形图叠散点大于 500画箱形图必要时配密度图。需要判断分布是否双峰时箱形图旁边一定再挂一张直方图或密度图。小提琴图这几年挺流行但我用得比想象中少。原因是它的视觉重量太大一张图放五个小提琴就已经很满了而且小提琴的形状依赖核密度估计的带宽参数参数没调好会凭空造出根本不存在的峰。如果你不确定带宽怎么设箱形图加散点反而更诚实。还有一个容易被忽略的替代方案误差棒图。如果只需要展示均值和置信区间一组点和短横线的组合比箱形图更简洁尤其适合放在论文或正式报告里。但前提是你明确知道数据接近正态、用均值描述是合理的——否则还是老老实实用中位数口径的箱形图。6. 落在生产环境里的箱形图我的监控实战与踩坑清单前面讲的都是分析场景最后这部分讲讲我在生产监控里怎么用箱形图以及那段被自己坑过的经历。这套做法不复杂但有几个细节不看清楚就会翻车。6.1 用历史数据做基线而不是用固定阈值早期我们给接口响应时间定告警阈值用的是平均值 3 倍标准差。结果线上只要出现几次超时标准差被拉大阈值跟着抬高后面真正的慢请求反而告警不出来了。这就是均值和标准差不抗异常值的典型症状。换成箱形图思路之后做法变成每天用过去 7 天的响应时间画一次箱形图用 Q3 和 IQR 算出一个基线区间作为第二天的参考。因为 Q1、Q3、IQR 都是分位数口径几个极端值不会把它们带跑。同时把每天的上须端点画在时间轴上看趋势能提前发现波动在慢慢变大这种早期信号——这种信号在均值曲线上是看不出来的。那段排查经历大致是这样的监控面板上有段时间均值曲线很平没有任何异常但用户投诉增多。我把时间段内的原始数据拉出来画箱形图发现中位数没变但 IQR 从 40 毫秒拉到了 120 毫秒。继续叠散点看发现是一部分请求变快了缓存命中率提升另一部分请求变慢了某个下游服务抖动两者抵消之后均值纹丝不动。结论是均值没变但用户体验分化了如果只看均值曲线这个问题可能还要潜伏好几天。从那以后我们的监控面板必带一张箱形图而且要求叠散点。6.2 分位数指标和箱形图要一起用别互相替代前面提过一次这里再强调上须端点和 P95、P99 是两个不同的东西。监控告警用 P95 或 P99 这类明确的分位数指标箱形图负责展示形状和对比。两者可以同框左侧放一条 P99 的折线右侧放一张箱形图用户一眼就能看到分位数在什么位置相对于整体分布来说是正常的还是异常的。如果非要用箱形图做告警我的做法是用**连续 N 个点超过上须**作为触发条件而不是单点超过就告警。因为单点超过上须在正常数据里发生的概率本来就有 0.7% 左右前面算过单点触发会带来大量误报。加上连续 N 个这个条件误报率会降好几个数量级。6.3 踩坑清单这些细节我吃过亏最后把我在实际项目里踩过的坑整理成一份清单都是真金白银换来的图注不写 whis 系数。用默认 1.5 和别人用 3.0 画的图放一起对比异常点数量差一大截结论会打架。图注必须写明须线规则。不标样本量。一张图里某组只有 6 个数据点另一组有 600 个视觉上它们一样大读图的人会默认为同等可信。把箱形图当成数据清洗的最终依据。箱形图标出来的异常值只代表统计上离群不代表数据有问题。很多真实的业务场景里那些离群点恰恰是最有价值的样本——比如电商里的高价值客户或者日志里罕见的错误模式。先分析再决定要不要剔除。时间序列数据不做分组就画箱形图。把一整年 12 个月的数据压成一张箱形图季节性趋势全被抹平了。时间序列要按周期分组后分组画或者干脆用折线图。颜色乱用。多组对比时颜色只用来区分组别就够了别再用颜色去编码数值比如深浅代表均值信息维度叠加会让人误读。也别用红绿配色色觉障碍人群看不出来。动态刷新的箱形图上不做视觉过渡。监控面板上的箱形图每秒刷新箱子跳来跳去会让用户晕。加一个 200 到 300 毫秒的过渡动画体感会好很多。还有一个我自己总结的小技巧给箱形图的箱子加一点轻微的不透明度alpha 0.7 左右然后让网格线透出来。这样即使箱子重叠也能看清底层的网格和相邻的箱子比实心填充舒服很多。ECharts 里对应的是itemStyle里的opacitymatplotlib 里是box.set_alpha(0.7)。我个人在实际操作中的体会是箱形图这个工具的价值密度极高——它只用五根线就承载了中位数、分散度、极值和异常四层信息但也正因为压缩得太狠任何一次使用前都应该问自己一句这张图会不会掩盖掉我真正关心的东西如果答案是可能会那就毫不犹豫地叠一层散点或者补一张密度图。多画一张图的成本永远低于下错一个结论的成本。
返回列表