首页 / 资讯中心 / 文章详情

五数概括与盒图:用IQR法识别异常值的Pandas实战指南

五数概括与盒图:用IQR法识别异常值的Pandas实战指南 ★ FEATURED ARTICLE
数据里最会撒谎的往往是那几个离群点五数概括就是用来治它的我最早做数据分析的时候拿到一份数千行的销售流水第一件事就是算平均值。算出个日均销售额觉得差不多了交上去被打回来三次。后来导师跟我说了一句话“你report里的那个数字被十几个大促订单抬高了真实的日常水平根本不是那样。”我才意识到平均值是个很容易被骗的统计量而五数概括Five-Number Summary和盒图Box Plot这套东西才是看清数据真实面目的基本功。这篇文章就围绕一个非常经典的数据分析入门话题展开五数概括是什么、盒图怎么读、异常值怎么用 IQR 方法检测。全部用 Pandas 实战演示代码放在文章里可以直接跑。适合刚学完 Pandas 基础、想进入真正“分析”阶段的初学者也适合已经写了几个月 SQL 但没系统接触过描述性统计的同学。我一直觉得数据分析入门最容易被忽略的不是模型不是算法而是“你手里这份数据到底长什么样”。五数概括和盒图就是回答这个问题最锋利的两把刀。1. 为什么第一个要学的描述性统计是五数概括而不是平均值很多人学统计学的第一课就是均值、中位数、众数然后做数据分析的时候也习惯性先看均值。这个习惯不能说错但在真实数据面前均值往往带偏你的直觉。均值的本质是把所有数据加在一起除以个数。它非常敏感只要数据里混入哪怕一个极端值均值就会被“拽”过去。举个最简单的例子一条街上住了五户人家年收入分别是 10 万、12 万、11 万、13 万、200 万。均值算下来是 49.2 万你拿这个数字去描述这条街的普通家庭收入完全失真。但中位数是 12 万明显更贴近真实情况。而五数概括比中位数更进一步它用五个数字直接“素描”出整组数据的轮廓最小值Minimum第一四分位数Q125% 分位中位数MedianQ250% 分位第三四分位数Q375% 分位最大值Maximum如果你把一组数据按从小到大排序Q1 表示“前 25% 的数据都小于等于这个值”Q3 表示“前 75% 的数据都小于等于这个值”。也就是说中间那 50% 的数据全部落在 Q1 和 Q3 之间。这一段的宽度我们叫四分位距IQRInterquartile Range等于 Q3 - Q1。那为什么这五个数字比均值更适合做数据探索的第一步原因有三点。第一抗极端值干扰。最大值和最小值会受离群点影响但 Q1、Q2、Q3 这三个分位数非常稳定。哪怕数据里有一百个极端大值只要它们集中在顶部那一小撮中位数和四分位数几乎不动。第二能看出分布形状。如果中位数在 Q1 和 Q3 之间偏左说明数据整体偏右分布右尾长如果偏右说明左尾长。你只看均值和标准差是看不出这种偏态的。第三它就是盒图的内核。五数概括学会之后盒图你拿起就能读异常值检测也能无缝衔接。可以说五数概括是整个描述性分析的地基。在 Pandas 里看五数概括特别简单一行describe()就能输出大部分内容import pandas as pd import numpy as np # 构造一份模拟数据包含正常值和少量离群点 rng np.random.default_rng(42) data pd.DataFrame({ 销售额: np.concatenate([ rng.normal(500, 50, 200), # 200个正常值均值500标准差50 np.array([900, 950, 1100]) # 3个明显离群点 ]) }) print(data[销售额].describe())输出结果大概是count 203.000000 mean 517.625616 std 89.452859 min 365.489534 25% 468.830002 50% 498.214458 75% 530.263361 max 1100.00000025%、50%、75%那三行就是 Q1、中位数、Q3min和max是首尾。这五个数字一出来你对这组数据的认知就不再是“均值 517”这么单薄了中间一半的销售额集中在 468 到 530 之间但最大到 1100说明尾部有非常突出的高值——均值 517 明显是被这几个大值抬上去的。用describe()是偷懒做法如果只想拿分位数quantile()更精确q1 data[销售额].quantile(0.25) q2 data[销售额].quantile(0.50) q3 data[销售额].quantile(0.75) print(fQ1{q1:.2f}, 中位数{q2:.2f}, Q3{q3:.2f})Pandas 默认的quantile()方法是线性插值法这意味着它会根据相邻两个排序位置的值估算分位点。这个方法来自 NumPy 的linear模式对应统计学里常见的第 7 型分位数定义也是 R 语言里type7的默认算法。绝大多数工程场景用这个就够了不需要去折腾其他插值方式。到这里你会发现一个很有意思的事情五数概括其实没有用到任何复杂公式就是排序、找位置、取值。但它描述数据的能力远超一个均值和标准差组合。2. 盒图的结构拆解从四个线段到离群点的视觉逻辑盒图我第一次看的时候觉得它画得神神秘秘的中间一个矩形上下伸出两根线线上偶尔还挂着几个零散的小点。后来真正读懂之后才发现这图就是把五数概括画成了一张图信息密度极高。盒图的核心结构是这样拆的盒子的下边是 Q1上边是 Q3盒子内部那条横线是中位数。盒子高度就是 IQR也就是 Q3 - Q1它表示中间 50% 数据的跨度。盒子上下各有一条“须”whisker上须顶端的位置一般是 Q3 1.5 * IQR 和数据实际最大值中较小的那个下须底端是 Q1 - 1.5 * IQR 和数据实际最小值中较大的那个。须之外的点就是超过上下须范围的数字也就是我们常用的盒图法定义的异常值。这里要注意一个细节上须的顶端并不是“最大值”。很多人看盒图以为须的端点就是 min 和 max其实不是。当数据里有特别极端的大值上须会在 1.5 倍 IQR 的限制位置停住极端点则单独画出来。这么设计的原因是为了让“正常数据范围”和“异常数据”在视觉上一眼分开。1.5 这个系数如果你翻统计学的书会发现它只是一个经验常数不是严格的数学定理。它的原理是当数据来自正态分布时理论上大约有 99.3% 的数据落在 Q1 - 1.5 * IQR 和 Q3 1.5 * IQR 之间。换句话说如果数据完全服从正态分布超过这个范围的点非常罕见概率不到 0.7%。因此用这个标准来标异常具备基本的统计学依据。但如果你的数据不是正态分布呢那 1.5 倍 IQR 就只是一个工程默认值不代表数学真理。在偏态分布、长尾分布的数据集上你完全可以根据业务场景调整系数。比如金融反欺诈领域有人用 3 倍 IQR 来避免把高净值客户误判为异常。这个我们后面实战再展开。用 Matplotlib 画盒图非常简单import matplotlib.pyplot as plt plt.figure(figsize(8, 4)) plt.boxplot(data[销售额], vertTrue, patch_artistTrue, showfliersTrue, flierpropsdict(markero, markerfacecolorred, markersize6)) plt.title(销售额盒图) plt.ylabel(销售额) plt.grid(axisy, linestyle--, alpha0.4) plt.show()跑完之后你会看到盒子主体在 468 到 530 之间中位数线接近盒子中间略微偏上上须拉着三个小红点900、950、1100这就是盒图法识别出来的离群值。还有一个常用组合是seaborn.boxplot尤其在需要按分组画图的时候Seaborn 的表达力更强。Pandas 本身也提供了df.plot.box()方法本质上是对 Matplotlib 盒图的一层封装。入门阶段我建议三种都试一遍选一个自己顺手的方式固定下来。这里额外说一个经验小样本数据画盒图容易翻车。当数据量只有四五个的时候Pandas 画盒图有时会报RuntimeWarning提示没有足够的数据确定须的位置数据量少于 3 个时四分位数本身就不稳定画出来的盒子几乎失去意义。所以盒图适合样本量 20 的场景样本量太小时直接用排序后的五数概括列表反而更准确。读盒图还有一个高级技巧把多个盒图并排画对比组间差异。比如对比各销售区域的业绩分布、各时段的响应延迟并排盒图比柱状图加误差线直观得多。误差线只告诉你均值和波动范围盒图能告诉你每个组的中位数差异、尾部厚度、离群点分布信息量完全不在一个层级。# 分组盒图示例 df_group pd.DataFrame({ 区域: np.repeat([A区, B区, C区], 100), 销售额: np.concatenate([ rng.normal(500, 50, 100), rng.normal(600, 80, 100), rng.normal(450, 30, 100) ]) }) df_group.boxplot(column销售额, by区域, figsize(8, 5)) plt.suptitle() # 去掉自动生成的标题 plt.title(各区域销售额盒图对比) plt.show()盒图一旦会读你对一组数据的感知能力会明显提升一个台阶。你不光知道“大概是什么水平”还能说出“数据散布在什么范围、中间一半有多集中、有没有极端个体”这在探索性数据分析里是最关键的一步。3. 用 IQR 方法检测异常值公式背后的边界逻辑盒图法检测异常值的底层逻辑就是 IQR 法也叫 Tukey 法得名于提出盒图的统计学家 John Tukey——对就是那个统计学界非常有名的老头他同时还提出了箱线图和快速排序算法里的“Tukey’s hinge”。工程界把这个方法简化为一条规则超过 Q3 1.5 * IQR 或低于 Q1 - 1.5 * IQR 的点记为异常值。先手工推导一遍你才能真正理解代码里的每一行在干什么。假设前面那份销售数据Q1 468.83Q3 530.26IQR 530.26 - 468.83 61.43那么上界upper fence就是530.26 1.5 * 61.43 622.41下界lower fence就是468.83 - 1.5 * 61.43 376.68任何销售额大于 622.41 或小于 376.68 的点都会被标记为异常值。按这个规则900、950、1100 是异常值而数据里那些 400 左右的低值因为还没跌破 376.68不算异常。在 Pandas 里实现这个检测逻辑核心代码只有几行def detect_outliers_iqr(series, k1.5): Q1 series.quantile(0.25) Q3 series.quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - k * IQR upper_bound Q3 k * IQR return (series lower_bound) | (series upper_bound) data[is_outlier] detect_outliers_iqr(data[销售额]) print(data[data[is_outlier]])上面函数里的k是可以调的系数默认 1.5。你把 k 改成 3就得到“极端异常值”的判定标准。很多实际工作中业务上会把 k1.5 判出来的叫“潜在异常值”k3 判出来的叫“强异常值”。前者需要人为复核后者几乎可以直接处理。这里有个很容易踩的坑异常值检测不能只看单列分布要结合数据生成的背景。比如监控系统里的响应时间99% 的请求在 100ms 以内但压测期间的请求冲到 3000ms从分布上看是异常值从业务上看却是真实的高负载信号不能粗暴删除。IQR 方法本身也有局限最明显的是数据量少的时候不稳定样本量低于 10 时四分位距波动非常大检测结果基本没有参考价值。另外如果数据本身是偏态分布单侧异常值会被系统性地识别出来而另一侧几乎没有这时需要结合对数变换或分位数变换后再检测。还有一个很多人忽略的细节IQR 方法默认是“无分布假设”的。跟 3σ 法则不同它不要求数据服从正态分布。这是它的普适性优势但也意味着它没有利用数据的分布形态信息。如果你明确知道数据服从正态分布直接用均值 ± 3 倍标准差效果更稳定。那我们什么时候应该放弃 IQR、改用其他方法我总结过一套最简单的判断逻辑如果数据是对称分布或近似对称用均值 ± 3 标准差如果数据明显偏态、又说不清属于什么分布用 IQR如果数据有强季节性或多模态特征IQR 效果也很差更适合用时间序列的异常检测方法。把公式吃透之后代码只是壳。上面detect_outliers_iqr这个函数我建议直接存进你的工具脚本里以后任何 DataFrame 列都能一行调用。4. Pandas 完整实战构造数据、画图、标记异常的一站式流程理论说完了这部分跑一遍完整流程。为了让你看到真实效果我模拟了一份“某电商店铺每日订单量”数据包含 180 个正常观测和几个促销日拉出来的极端值。import pandas as pd import numpy as np import matplotlib.pyplot as plt # 设置随机种子保证结果可复现 rng np.random.default_rng(2024) # 生成 180 天日常订单量均值 200标准差 30 daily rng.normal(200, 30, 180) # 加入 5 天促销日的大订单量 promotion np.array([420, 510, 385, 468, 550]) df pd.DataFrame({ 订单量: np.concatenate([daily, promotion]) }) # 打乱顺序模拟真实时间序列的随机性 df df.sample(frac1, random_state7).reset_index(dropTrue) # 第一步五数概括 print( 五数概括 ) q1 df[订单量].quantile(0.25) q2 df[订单量].quantile(0.50) q3 df[订单量].quantile(0.75) q0 df[订单量].min() q4 df[订单量].max() iqr q3 - q1 print(fMin {q0:.2f}) print(fQ1 {q1:.2f}) print(fMedian {q2:.2f}) print(fQ3 {q3:.2f}) print(fMax {q4:.2f}) print(fIQR {iqr:.2f})输出会是类似这样Min 131.45 Q1 179.82 Median 200.13 Q3 221.56 Max 550.00 IQR 41.74看到没有中位数 200Q1 和 Q3 的跨度只有 42 左右日常订单量相当稳定但 Max 拉到 550这个尾巴一眼就不正常。第二步画盒图让异常值“现形”。plt.figure(figsize(9, 5)) box plt.boxplot(df[订单量], patch_artistTrue, flierpropsdict(markero, markerfacecolorred, markersize7)) box[boxes][0].set_facecolor(#a0d8ef) plt.axhline(q3 1.5 * iqr, colororange, linestyle--, linewidth1.2, labelUpper fence) plt.axhline(q1 - 1.5 * iqr, colororange, linestyle--, linewidth1.2, labelLower fence) plt.title(每日订单量盒图含异常值标记) plt.ylabel(订单量) plt.legend() plt.grid(axisy, linestyle--, alpha0.4) plt.show()这一步会画出一个盒子和几条横线。红色圆点就是 IQR 规则识别出来的异常值橙色虚线是上下界你能直观看到哪些点越过了边界。第三步用 IQR 方法量化标记异常值并输出一个便于人工核查的表格。def flag_outliers(series, k1.5): q1 series.quantile(0.25) q3 series.quantile(0.75) iqr q3 - q1 lower q1 - k * iqr upper q3 k * iqr return (series lower) | (series upper) df[异常标记] flag_outliers(df[订单量]) df[上界], df[下界] df[订单量].quantile(0.75) 1.5 * (df[订单量].quantile(0.75) - df[订单量].quantile(0.25)), df[订单量].quantile(0.25) - 1.5 * (df[订单量].quantile(0.75) - df[订单量].quantile(0.25)) # 只查看被标记为异常值的记录 outliers df[df[异常标记]] print(f检测到 {len(outliers)} 个异常值) print(outliers.head(10))输出会把这 5 个促销日的订单量捞出来420、510、385、468、550。恭喜这个流程一次跑通你已经完成了一次标准的数据质量审查。第四步也是最容易被忽略的一步回归业务背景做判断。检测出异常值之后不要着急删。我列了一个异常值处理决策清单如果是录入错误、传感器故障、数据清洗遗漏导致的异常直接删除或修正。如果是真实业务事件促销、故障、突发流量保留或在建模时单独打标。如果是建模场景看模型类型。树模型对异常值不敏感线性模型和距离类模型非常敏感。对应到这份订单量数据那 5 个促销日的订单量肯定不能删它们是真实业务行为。但如果你要用这份数据训练一个“预测日常订单量”的模型最好把这 5 天标记为“促销日”单独建模或者干脆在训练时剔除。最后把这套流程封装成一个函数方便复用def iqr_summary(series, k1.5): q1 series.quantile(0.25) q2 series.quantile(0.50) q3 series.quantile(0.75) iqr q3 - q1 lower q1 - k * iqr upper q3 k * iqr outlier_mask (series lower) | (series upper) return { count: len(series), min: series.min(), q1: q1, median: q2, q3: q3, max: series.max(), iqr: iqr, lower_fence: lower, upper_fence: upper, outlier_count: outlier_mask.sum(), outlier_indices: series[outlier_mask].index.tolist() } result iqr_summary(df[订单量]) for key, value in result.items(): print(f{key}: {value})这一整个流程下来你是先理解了统计量的意义再看到盒图的直观展示再通过公式批量检测最后结合业务下结论。这也是我个人认为最合理的分析路径先理解再可视化再量化再决策。反过来先跑代码再看文档往往容易把工具用得很熟练但分析没有灵魂。5. 检测只是第一步异常值的处理策略与取舍经验异常值检测出来之后很多人直接问“该不该删”。这问题没有标准答案答案完全取决于你的分析目标。我按场景分享一些实际操作中的经验。场景一做数据清洗目标是建一个干净的训练集。这时候异常值处理的标准是“区分来源”。如果是明显的数据录入错误——比如负数价格、超长字符串、日期 2 月 30 日——直接删。如果是真实的高波动数据尽量保留然后用裁剪winsorize或者变换的方式来降低影响。我之前在一个图像处理项目里处理像素灰度值就遇到过传感器偶尔打出的全白噪点那批数据直接置为缺失值不参与后续统计。场景二做业务分析报告目标是描述真实情况。这种情况我不建议删除异常值而是建议把它们单独拆出来分析。比如前面那个销售数据正常店铺日均 500碰到大促能到 1000那你的报告应该写“日常销售额中位数 498波动区间 468~530大促日销售额可达 900~1100”。这样写业务方一眼就看明白日常情况和峰值情况比一个总均值 517 有用得多。场景三做算法模型的特征工程。有些模型线性回归、逻辑回归、KMeans、PCA对异常值非常敏感一个极端值就可能让系数偏移很大。这时候有三个常用处理手段删除异常值适合异常占比很小5%且确认是噪声的情况。用上下界截断把超过上界的值拉回上界低于下界的拉回下界这叫 winsorize。做对数变换 / Box-Cox 变换把右偏的长尾压回正态形态。这三种手段没有绝对优劣。实战中我会先看异常值的比例如果占比约 1%~2%而且业务上能解释我会倾向截断而不是删除保留部分信息如果占比超过 10%说明你的“正常范围”定义有问题应该回去重新理解业务而不是机械地用 1.5 倍 IQR 处理。另外补充一个检测和处理的协作技巧永远不要在原数据上原地修改养成“先标记、后决策、再过录日志”的习惯。我的做法是新增一列异常标记再写一个字段记录处理方式比如“删除”“截断”“保留”这样分析流程是可审计的。等报告做完回看数据清理日志你还能知道每一步为什么这么做不会出现“当时为什么删了那 20 条数据”的尴尬。坊间还流行一种偏见觉得“只有回归模型在乎异常值树模型无所谓”。这个说法只对了一半。随机森林、XGBoost 这类树模型确实对单个异常值有很强的鲁棒性但当异常值数量偏多或者碰巧聚集在某些特征区间时树模型在划分节点时依然会被带偏。我的建议是不要因为模型不敏感就不做异常值分析尤其是在探索阶段异常值本身就是情报有时候那个突兀的点背后藏着一个业务漏洞。6. 收尾前再分享两个实战技巧最后说两个我用了很久、非常实用的技巧。第一个技巧在 Pandas 里画多个数值列的盒图一键对比所有特征的离群情况。很多初学者只能单列画图效率太低。你可以这样写# 选择所有数值列 numeric_cols df.select_dtypes(include[np.number]).columns df[numeric_cols].boxplot(figsize(12, 6), rot45) plt.title(所有数值特征的盒图总览) plt.show()如果有几十个特征这样一张总览图能帮你快速定位哪几个字段的异常值比例偏高再做针对性分析。我在早期做用户画像分析的时候就是用这种方式从 30 多个字段里两三眼就锁定了“登录次数”和“消费金额”两个高异常字段。第二个技巧使用seaborn.boxen画增强箱线图。当数据量很大几万行以上且分布比较复杂时普通盒图只有一个盒子和两条须很多细节被压缩掉了。Seaborn 的boxenplot以前叫lvplot在 IQR 基础上做了多层分位分解可以看到更细的尾部结构特别适合探索阶段找规律。import seaborn as sns sns.boxenplot(xdf[订单量]) plt.title(订单量增强箱线图) plt.show()增强箱线图在分析长尾数据、日志耗时、耗时分布时非常好用推荐大家试一下。我做了这些年数据分析回头再看真正拉开分析水平的往往不是会不会用 XGBoost、会不会调参而是面对一份陌生数据时能不能在十分钟内准确地说出它的大致分布、离散程度和可疑点位。五数概括、盒图、IQR 异常值检测就是练这个基本功最直接的工具。这套东西看似简单但每一次实战都能帮你看清数据最真实的那张脸。
阅读完成 · 觉得有帮助?
咨询建站