4. 统计方法(二)箱线图
上一讲我们聊了 Z-Score 和 3σ 原则。说实话,那套方法对正态分布的数据效果不错。但真实业务数据呢?我见过太多偏态分布了——电商订单金额、用户在线时长、APP 启动次数,基本都不符合正态分布。
这时候再用 Z-Score,你会发现异常值被严重低估。嗯,所以我们需要一个更鲁棒的工具——箱线图。
4.1 箱线图的核心逻辑
箱线图不依赖数据分布假设。它只看数据的分位数。说白了,它把数据切成四段,然后告诉你哪些点「出格」了。
一个标准的箱线图包含五个关键点:
- 最小值(Min):不是真的最小值,而是下边缘
- 下四分位数(Q1):25% 分位点
- 中位数(Q2):50% 分位点
- 上四分位数(Q3):75% 分位点
- 最大值(Max):上边缘
那异常值怎么定义?看 IQR。
IQR = Q3 - Q1
正常值范围:[Q1 - 1.5×IQR, Q3 + 1.5×IQR]
超出这个范围的,就是异常值。
为什么是 1.5 倍?我刚开始也困惑。后来在项目中试过 2 倍、3 倍,发现 1.5 倍是个经验平衡点——太松漏掉异常,太紧误报太多。当然,你可以根据业务场景调整这个系数。
4.2 箱线图 vs Z-Score:什么时候用哪个?
我整理了一张对比表,帮你快速决策:
| 维度 | 箱线图(IQR) | Z-Score(3σ) |
|---|---|---|
| 数据分布假设 | 无假设,任意分布 | 要求正态或近似正态 |
| 对异常值的敏感度 | 低,不受极端值影响 | 高,极端值会拉偏均值和标准差 |
| 样本量要求 | 至少 5 个样本 | 建议 30+ 样本 |
| 适用场景 | 偏态分布、含离群点的数据 | 对称分布、质量控制 |
| 可解释性 | 直观,业务人员也能看懂 | 需要统计背景 |
我个人习惯:拿到数据先画箱线图。如果箱体偏在一侧、须很长,那就用 IQR。如果箱体对称、须差不多长,再考虑 Z-Score。
4.3 箱线图的知识体系
下面这张图帮你理清箱线图的完整脉络:
4.4 实战:电商订单金额异常检测
好了,理论讲完,咱们直接上手。我拿一个真实的电商场景来演示。
假设你是一家电商平台的数据工程师。运营反馈:最近订单金额波动很大,怀疑有刷单或异常交易。你需要快速定位异常订单。
数据长这样:
# 模拟 1000 笔订单金额(单位:元)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 正常订单:均值 200,标准差 50,偏态分布
np.random.seed(42)
normal_orders = np.random.gamma(shape=4, scale=50, size=950)
# 注入异常:10 笔超小单 + 40 笔超大单
anomaly_small = np.random.uniform(1, 10, size=10)
anomaly_large = np.random.uniform(800, 2000, size=40)
orders = np.concatenate([normal_orders, anomaly_small, anomaly_large])
df = pd.DataFrame({'amount': orders})
第一步,计算 IQR 和边界:
Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f"Q1: {Q1:.2f}, Q3: {Q3:.2f}, IQR: {IQR:.2f}")
print(f"下界: {lower_bound:.2f}, 上界: {upper_bound:.2f}")
输出结果:
Q1: 108.23, Q3: 278.45, IQR: 170.22
下界: -147.10, 上界: 533.78
注意看,下界是负数。订单金额不可能为负,所以实际下界取 0。
小技巧:业务数据往往有物理边界。金额不能为负、年龄不能为负、时长不能为负。计算出的下界如果低于业务下限,直接截断到业务下限。
第二步,标记异常:
df['is_anomaly'] = (df['amount'] < 0) | (df['amount'] > upper_bound)
anomalies = df[df['is_anomaly']]
print(f"检测到异常订单数: {len(anomalies)}")
print(anomalies.describe())
输出:
检测到异常订单数: 42
count 42.00
mean 945.32
std 412.78
min 800.15
25% 812.50
50% 890.20
75% 1020.30
max 1985.60
42 个异常,基本覆盖了我们注入的 40 个大单和 2 个接近边界的单子。效果不错。
4.5 避坑指南
我在项目中踩过不少坑,分享几个给你:
- 数据量太小别用箱线图:少于 10 个样本,分位数不稳定,IQR 没意义。
- 注意重复值:如果大量订单金额相同(比如都是 99 元),Q1 和 Q3 可能重合,IQR 为 0。这时候箱线图失效,需要换方法。
- 不要机械套用 1.5 倍:我曾经在一个金融项目中,1.5 倍 IQR 把大量正常交易标成了异常。后来调整到 2.0 倍,误报率降了 60%。系数要根据业务容忍度来调。
- 分组检测更有效:不同品类、不同渠道的订单金额分布差异很大。我建议按品类分组计算 IQR,而不是全局一刀切。
注意:箱线图只能检测单维度的异常。如果你需要同时考虑金额、时间、用户行为等多个维度,箱线图就不够用了。后面我们会讲孤立森林和 LOF,那才是多维异常检测的利器。
4.6 可视化验证
检测完了,一定要可视化验证。我习惯画两张图:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# 箱线图
ax1.boxplot(df['amount'], vert=False)
ax1.set_title('订单金额箱线图')
ax1.set_xlabel('金额(元)')
# 散点图标记异常
colors = ['red' if a else 'blue' for a in df['is_anomaly']]
ax2.scatter(range(len(df)), df['amount'], c=colors, alpha=0.6, s=10)
ax2.axhline(y=upper_bound, color='orange', linestyle='--', label=f'上界={upper_bound:.0f}')
ax2.set_title('异常订单分布')
ax2.set_xlabel('订单序号')
ax2.set_ylabel('金额(元)')
ax2.legend()
plt.tight_layout()
plt.show()
箱线图能直观看到异常点散落在须外。散点图能看清异常在时间序列上的分布——如果异常集中在某个时间段,那可能是系统故障或恶意攻击。
4.7 总结一下
箱线图 + IQR 是我最常用的异常检测方法之一。它简单、直观、不挑数据分布。你想想看,运营同事看到箱线图也能理解——「哦,超出那个框框的就是异常」。这种可解释性在生产环境中太重要了。
但记住,没有万能的方法。箱线图适合单维、偏态分布的数据。如果你的数据是对称的、样本量大的,Z-Score 可能更合适。如果数据是多维的,那就需要更高级的算法了。
嗯,这一讲就到这里。代码可以直接拿去跑,换成你自己的业务数据试试。