资讯动态

matplotlib不同范围箱线图绘制:whis、子图分面与对数坐标实战

发布时间:2026/10/14 18:52:14 来源:尧图企业网站定制
箱线图boxplot是数据分析里最常用的分布可视化方式之一但很多人在用matplotlib绘图时只在同一个坐标系里硬塞所有分组数据遇到“不同范围”的数据就翻车。这里说的不同范围不只是数据数值范围还包括箱线图里须whisker的统计范围、坐标轴的显示范围甚至是子图分面时每个图各自的范围。这篇文章我会把这些情况拆开来讲并给出可以直接运行的matplotlib代码让你能根据数据特点灵活制作不同范围的箱线图。文章面向刚接触matplotlib的读者也适合已经在用箱线图、但被“分组差异太大”“离群点太乱”“子图坐标轴不统一”这些问题折腾过的人。1. 箱线图的“范围”到底指什么1.1 一张箱线图里有哪些和“范围”有关的元素箱线图的核心元素有五个下边缘下须端点、第一四分位数Q1、中位数Q2、第三四分位数Q3、上边缘上须端点之外的点统称离群点。很多教程会把下边缘直接说成“最小值”其实不准确。matplotlib默认的下边缘是Q1 - 1.5 * IQR范围内最小的数据点上边缘是Q3 1.5 * IQR范围内最大的数据点。也就是说箱线图把“正常范围”定义成了统计意义上的范围而不是数据本身的物理范围。所以一张箱线图里至少有三层“范围”概念数据分布范围、须的统计范围、坐标轴显示范围。如果你只是画一个简单的示例比如两组数据可能感觉不到它们之间的区别。但一旦数据量级不同或者你需要在多个子图里做对比这三层范围很容易互相打架。一个最简单的示例import matplotlib.pyplot as plt import numpy as np low_data np.random.normal(50, 10, 100) high_data np.random.normal(100, 20, 100) fig, ax plt.subplots() ax.boxplot([low_data, high_data]) plt.show()这段代码里matplotlib 会按照各自数据范围自动生成y轴范围两组数据因为均值不同箱子位置会错开。但这种“自动”在分组一多、量级一拉大时就不好用了。1.2 为什么不能把所有不同范围的数据强行画在一张图里假设有三组模拟数据第一组在0.1附近波动第二组在50附近波动第三组在5000附近波动。如果直接在同一个坐标系里调用boxploty轴范围会被第三组主导前两组可能被压缩成贴近底部的一条细线连箱体高度都看不清。更麻烦的是当我们使用默认的whis1.5时每一组数据对“离群点”的判定阈值不同低量级组的离群阈值可能是0.5高量级组可能是8000。从视觉上看它们似乎都能容纳各自的离群点但在业务解读上这种“范围”完全不具可比性。于是就有了“制作不同范围的箱线图”的需求。通常的做法有三条路一是调整箱线图自身的须统计范围二是用多个子图分别展示各自的数据范围三是在同一张图里用对数坐标或截断坐标轴来迁就不同量级。我下面分别展开。2. 用whis参数调节须与离群点的显示范围2.1 默认须范围1.5倍IQR的原理与局限默认的whis1.5指的是以四分位距IQR为基准把正常范围定义在Q1 - 1.5*IQR到Q3 1.5*IQR之间。为什么是1.5而不是1或者2因为在近似正态分布下这个范围大约能覆盖99.3%的数据超出部分可以视作潜在异常值。它更像一个经验阈值而不是某个统计学定理。这个默认值在小样本、偏态分布、或者业务上对“异常”有明确定义时往往不合适。比如某些计量指标本来就呈长尾分布1.5倍IQR可能把大量正常值标成离群点导致图上有星星点点的小圆圈干扰阅读。我自己在实际分析时更偏向把whis看作“你希望图里哪些点被单独标记”的控制开关。它并不修改任何数据只是决定须的端点在哪一步停下来并决定哪些点作为fliers被单独画出来。理解这一点后手动改参数就不会心虚了。2.2 用whis参数自定义须范围matplotlib的boxplot里whis有两种传法。第一种是传一个数字比如whis0.5或whis3表示须的长度是IQR的0.5倍或3倍。第二种是传一个[low, high]区间比如whis[5, 95]这时候须会从数据的5%分位延伸到95%分位低于5%或高于95%的点会被当成离群点。两者怎么选如果数据相对对称用whis1.5或whis3就够了如果数据偏态严重我建议用百分位区间这样更接近业务上“只关心两端极端值”的思路。下面这段代码可以直观对比fig, axes plt.subplots(1, 3, figsize(12, 4)) data np.concatenate([np.random.normal(50, 5, 100), [25, 75, 80]]) for ax, w in zip(axes, [0.5, 1.5, [5, 95]]): ax.boxplot(data, whisw) ax.set_title(fwhis{w})运行后你会发现whis0.5时箱子非常宽须很短离群点一大堆whis1.5时比较常见whis[5, 95]时须基本覆盖了绝大多数点只有离中心更远的点被标记出来。2.3 同时控制是否显示离群点showfliersFalse可以把离群点全部隐藏。要注意隐藏不等于删除数据还在只是图上不画。若想同时隐藏离群点并让须的统计范围有意义还是建议保留whis阈值。比如ax.boxplot(data, whis[2, 98], showfliersFalse)这样图中只显示箱体和须但须的端点会落在2%和98%分位上。对于某些正式报告这种“去离群点”的呈现方式更干净。不过隐藏离群点有一个副作用阅读者无法知道数据里是否存在极端值。因此我更推荐在报告文字里注明阈值或者在图上用注释标记离群点数量。3. 用子图分面制作不同数据范围的箱线图3.1 什么时候用子图而不是单图当数据组的量级差异非常大同时你又希望每组内部的细节都清晰可读时单图方案基本没有活路。我处理过一份模拟实验数据三个条件分别落在低、中、高三个量级直接画在一张图里低量级的箱子被压成一条横线别说看四分位数了连箱体颜色都看不出来。这种场景下按组拆分成多个子图是更实用的解法。每个子图拥有独立的y轴范围能自动放大小范围内的分布差距。但要明确一点分面之后“不同范围”是指每个子图各自的数据范围它们之间的坐标刻度不再通用不能用来直接比较具体数值高低。3.2 最基本的subplots分面代码用plt.subplots创建一行三列的子图再循环绘图rng np.random.default_rng(42) low_data rng.normal(0.5, 0.2, 120) middle_data rng.normal(50, 10, 120) high_data rng.normal(5000, 800, 120) groups [low_data, middle_data, high_data] fig, axes plt.subplots(1, 3, figsize(12, 5)) for ax, group in zip(axes, groups): ax.boxplot(group) plt.show()这段代码里三个子图默认shareyFalse所以每个y轴范围各自独立。低量级组的箱体可以放大到整个子图高度高量级组的细节也能看清楚。如果只是快速探索数据分布这个方案已经够用。3.3 共享坐标轴与独立坐标轴怎么权衡如果你希望多个子图之间保持同一把“尺子”那就需要共享坐标轴。最简单的做法是在subplots里加shareyTruefig, axes plt.subplots(1, 3, figsize(12, 5), shareyTrue)这样三个子图共用同一个y轴刻度数值差异一目了然但低量级组的箱体会再次被压缩。因此我建议先想清楚这次分析是要“突出每组分布形状”还是要“严格对比数值高低”。想突出形状用独立坐标轴想对比高低再用共享坐标轴。还有第三种折中方案手动设置统一的y轴范围但不使用sharey。比如先把每组的最小最大值都找出来再设置一个包含所有数据的范围然后让每个子图都使用这个范围。这样每个子图虽然仍然独立但视觉比较基准是一致的。ymin min([np.min(g) for g in groups]) ymax max([np.max(g) for g in groups]) for ax in axes: ax.set_ylim(ymin, ymax)这种方法比较灵活但要注意如果极端离群点把范围拉得特别大其他组的箱体依然会被压扁。所以实际操作时我会先看数据分位数再决定是否手动扩展范围。3.4 分面后的美化细节分面之后第一个容易被忽略的是标题。每个子图顶部都必须有清晰标签否则看的人根本不知道哪张图是哪组数据。我习惯在循环里给每个ax单独设置set_title并在fig上设置总标题。另一个细节是填充颜色。默认的箱线图是空心的线框在黑白打印和快速浏览时都容易糊成一片。设置patch_artistTrue后可以对箱子填充颜色配合boxprops里的facecolor参数每个子图可以有不同的填充色。另外网格线对分面图帮助很大。ax.grid(axisy, linestyle--, alpha0.6)能让你更快读出箱体的纵向位置。这种小技巧在正式汇报图表里尤其管用。4. 同图内不同范围数据的处理技巧4.1 对数坐标让跨数量级数据同图可见有些场景不适合拆成子图或者你希望用一张图讲完整体分布这时候对数坐标是一种非常有效的处理方式。当数据组之间相差几个数量级时只要所有数据都大于0可以给y轴设置log刻度fig, ax plt.subplots(figsize(8, 5)) ax.boxplot(groups, labels[low, middle, high]) ax.set_yscale(log) plt.show()对数坐标的核心价值在于它让“比值”而不是“差值”成为视觉比较的基础。低量级组和高量级组都能在画布上占据合理位置箱子不容易被压扁。这里有两个坑想提醒你。第一数据里不能有0或负值否则log刻度会直接报错或画出无效图形如果数据里有非正数可以考虑先平移比如对所有数据加上一个正的常数。第二boxplot的须和离群点阈值是在原始数值上计算的不是在对数空间里计算所以对数坐标只改变显示方式不改变统计结果。4.2 截断坐标轴显示局部范围有时候你并不想让所有数据都出现在图上只想看某个局部区间的分布。比如一组数据里有个别的极端离群点是1000但绝大多数数据都在0到10之间你很想让箱体放大显示这时直接设置ax.set_ylim(0, 10)就行。但这么做有个副作用超出范围的箱体、须和离群点都会被裁剪掉。如果你只想隐藏离群点最好先设置showfliersFalse再把ylim设置到一个合理范围内避免出现“箱子被切了一半”的尴尬。我建议在设置ylim后再加上一段自动留白ax.set_ylim(0, 10) ax.margins(y0.05)margins会为数据区域四周留出少量空白让箱体不会顶到画布边缘。不过margins和手动ylim同时使用时matplotlib的优先级规则有点绕我通常是在设置ylim之后再微调而不是依赖margins自动计算。4.3 断裂坐标轴broken axis的简易实现如果你既想保留极端离群点又想放大常规范围断裂坐标轴是一个经典思路。它的本质是画两个上下堆叠的子图上子图显示高范围下子图显示低范围然后隐藏中间的边框制造出“同一个图被切断了”的视觉效果。实现上并不复杂核心代码大致是fig, (ax1, ax2) plt.subplots(2, 1, sharexTrue, figsize(6, 6)) ax1.boxplot(groups, showfliersTrue) ax2.boxplot(groups, showfliersFalse) ax1.set_ylim(8000, 10000) # 上部分显示高范围 ax2.set_ylim(0, 1000) # 下部分显示低范围 ax1.spines[bottom].set_visible(False) ax2.spines[top].set_visible(False)但断裂坐标轴的维护成本不低坐标轴刻度、子图间距、斜线标记都要手动调整稍有不慎就会让图显得很业余。我的个人建议是内部探索数据时不用搞这么复杂用分面或对数坐标足够只有在对外汇报且必须展示极端值全貌时再考虑断裂坐标轴。5. 实操案例从一张图到一组“不同范围”箱线图5.1 构造模拟数据为了让案例可复现我用numpy构造三组不同量级的数据每组都掺入一两个离群点rng np.random.default_rng(42) low_data rng.normal(0.5, 0.2, 120) middle_data rng.normal(50, 10, 120) high_data rng.normal(5000, 800, 120) low_data[0] 2.5 middle_data[0] 90 high_data[0] 9000这里的低量级组均值只有0.5高量级组均值达到5000三组数据天然不在一个视觉范围内。加上离群点是为了演示whis和showfliers的作用。5.2 基本信息绘图一张图全塞进去先画出最朴素的一张图看看会发生什么groups [low_data, middle_data, high_data] fig, ax plt.subplots(figsize(8, 5)) ax.boxplot(groups, labels[low, middle, high]) plt.show()运行后你基本会看到高量级组的箱子占满了整个绘图区域低量级组的箱子变成一条贴近零点的细线中间组的细节也被压缩得很难分辨。这个结果非常直观地解释了为什么需要“制作不同范围的箱线图”。5.3 进阶分面独立范围统一外观接下来用三个子图分别展示三组数据每个子图拥有独立的y轴范围并给箱体加上填充色fig, axes plt.subplots(1, 3, figsize(12, 5)) for ax, group, name in zip(axes, groups, [low, middle, high]): bp ax.boxplot(group, patch_artistTrue, boxpropsdict(facecolorlightsteelblue), medianpropsdict(colordarkorange, linewidth2), flierpropsdict(markero, markerfacecolorgray, markersize4, alpha0.6)) ax.set_title(name) ax.set_ylim(np.min(group) - 0.5 * np.std(group), np.max(group) 0.5 * np.std(group)) ax.grid(axisy, linestyle--, alpha0.5) fig.suptitle(Three groups with independent y ranges) fig.tight_layout() plt.show()这里我用0.5 * np.std(group)作为上下扩展量而不是单纯取最小最大值原因很简单最小最大值会把离群点顶到图的最边缘箱体反而显得很小加一点标准差可以让箱子在图中居中一些视觉上更舒服。当然这只是一个经验值你也可以用np.ptp(group) * 0.05之类的比例来自动留白。5.4 保持可比性统一y轴范围内的分面分面之后虽然三个子图能各自看清分布但它们之间不可能直接比大小。如果你需要统一参考可以选择shareyTrue或手动设置公共的ylim。以手动设置为例子代码会变成ymin min([np.min(g) for g in groups]) ymax max([np.max(g) for g in groups]) for ax in axes: ax.set_ylim(ymin, ymax)这个方案的问题在于低量级组数据可能只占y轴高度的1%都不到箱体几乎看不见。所以在实践里如果你既要分面又要可比对数坐标往往是更合适的统一方案。fig, ax plt.subplots(figsize(8, 5)) ax.boxplot(groups, labels[low, middle, high]) ax.set_yscale(log) plt.show()三组数据在log坐标下都能保留箱体形状同时y轴刻度的数量级差距也一目了然。不过要注意log坐标下箱体之间的“高度差”不再代表线性距离解读时要格外小心。6. 常见问题与排查实录6.1 常见问题速查表问题现象可能原因解决办法箱体被y轴切掉一半ylim设置范围过小扩大范围或用ax.margins(y0.05)留白离群点消失showfliersFalse或whis[0, 100]把所有点都纳入须范围按需恢复显示明确统计阈值多个子图y轴刻度不一致未设置sharey各子图根据自身数据自动确定范围设置shareyTrue或手动set_ylimlog坐标下绘图报错或图形异常数据包含0或负值对所有数据做正平移或删掉非正值for循环画子图时只出了一张图忽略axes数组直接用了plt.boxplot循环中传入ax.boxplot(group, axax)或按索引调用中位数线看不清默认中位数线太细设置medianpropsdict(linewidth2)图例或标签被裁剪保存时边距不足使用plt.tight_layout()和savefig(bbox_inchestight)6.2 如何快速定位箱线图的统计起点如果你怀疑某个箱线图画得不对最直接的办法是不要盯着图猜而是用numpy手动算一下分位数和须的端点。比如对某组数据用np.percentile(group, [25, 50, 75])检查Q1、Q2、Q3再用Q1 - 1.5*(Q3-Q1)和Q3 1.5*(Q3-Q1)验算须的边界。或者直接解析boxplot的返回值。bp ax.boxplot(...)返回一个字典其中包含boxes、medians、whiskers、caps、fliers。你可以输出bp[whiskers][0].get_ydata()看看第一条须的y坐标是不是自己期望的下边缘。这个方法在复杂分面场景下特别好用能快速定位到底哪一组数据的范围设置出了问题。6.3 我的几条实操心得先说颜色。默认的箱线图颜色是单一样式数据一多就像一团铁丝网。我几乎每次都会设置patch_artistTrue并给不同组配不同颜色这个是提升可读性的最划算操作。其次是中位数线。中位数是箱线图里最重要的特征我会用medianprops把它的线宽调到2颜色换成橙色或者深色。这样读者第一眼就能找到中心趋势而不是在箱子里找哪根线是哪根。再就是网格线。多子图对比时网格线能帮读者快速对齐位置但也别太醒目用虚线加低透明度适合。还有一个容易踩的坑保存图片时不指定dpi和bbox_inches放到文档里就会出现字体模糊或坐标轴标签被截断。我的习惯是plt.savefig(output.png, dpi200, bbox_inchestight)。如果后续还要继续修改就同时保存一份.pdf或.svg矢量格式在调整尺寸时不会失真。最后分享一个和箱线图配套的小技巧当样本量很小比如每组不到20个点时箱线图可能会掩盖数据的具体分布这时可以在同一个ax上叠加一层散点抖动图。虽然matplotlib没有直接内置stripplot但可以用np.random.normal给每个点添加少量x方向随机偏移来实现。这样既保留了箱线图的分布概览又让读者看到数据的真实落点。我自己在项目里最常用的套路是分组少、量级差异大时直接用子图分面并设置独立y轴范围分组多且量级差异极大时优先考虑对数坐标对外汇报需要强调极端离群值时才会用断裂坐标轴。箱线图本身不复杂但“范围”这个概念确实值得花时间琢磨清楚。希望这一套代码和踩坑经验能让你下次画图时少绕点弯。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑