3、基于统计的异常检测:Z-Score方法、IQR(四分位距)方法、假设检验与p值
大家好,我是老张。今天我们来聊聊量化风控里最基础、也最实用的异常检测方法——基于统计的那一套。
说实话,我刚入行那会儿,觉得异常检测就得用深度学习、神经网络才够酷。后来在真实项目里碰了一鼻子灰,才发现统计方法才是真正的"扫地僧"。简单、可解释、还特别稳。
这一章,我带你过三个经典方法:Z-Score、IQR、还有假设检验与p值。每个我都会结合项目经验来讲,保证你听完就能用。
3.1 Z-Score 方法:标准化后的"离群值"
Z-Score 说白了就是看一个数据点离均值有多远,用标准差做尺子量一下。
公式很简单:
Z = (x - μ) / σ
其中 μ 是均值,σ 是标准差。Z 的绝对值越大,说明这个点越异常。
经验阈值:
- |Z| > 2:值得关注(约 95% 的数据落在这个范围内)
- |Z| > 3:高度异常(约 99.7% 的数据落在这个范围内)
- |Z| > 4:基本可以判定为异常
重要提醒:Z-Score 对正态分布假设比较敏感。如果数据本身偏态严重,Z-Score 的效果会打折扣。
我在项目中遇到过一件事。有一次做信用卡交易金额的异常检测,直接用 Z-Score 发现一堆"异常"。后来一查,原来是双十一当天的正常大额消费。为什么?因为交易金额分布是长尾的,不满足正态假设。
代码示例:
import numpy as np
from scipy import stats
def zscore_detection(data, threshold=3):
z_scores = np.abs(stats.zscore(data))
return np.where(z_scores > threshold)[0]
# 示例
data = [10, 12, 11, 13, 100, 12, 11, 10, 13, 12]
outliers = zscore_detection(data)
print(f"异常索引: {outliers}") # 输出: [4]
我的小技巧:如果数据量小(比如少于30个样本),Z-Score 的可靠性会下降。我一般会结合 IQR 一起看,互相验证。
3.2 IQR(四分位距)方法:稳健的"箱线图"法则
IQR 方法是我个人最喜欢的。它不依赖正态分布假设,对极端值也不敏感,特别适合风控场景里的"脏数据"。
计算步骤:
- 计算第一四分位数 Q1(25% 分位数)
- 计算第三四分位数 Q3(75% 分位数)
- 计算 IQR = Q3 - Q1
- 定义异常边界:
- 下界 = Q1 - 1.5 × IQR
- 上界 = Q3 + 1.5 × IQR
- 超出边界的点即为异常
你想想看,为什么是 1.5 倍?这个系数其实来自经验。我在做信贷审批策略时,用 1.5 倍 IQR 能筛掉大约 0.3% 的极端样本,刚好符合业务上"万分之几"的坏账容忍度。
代码示例:
def iqr_detection(data):
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = np.where((data < lower_bound) | (data > upper_bound))[0]
return outliers, lower_bound, upper_bound
data = [10, 12, 11, 13, 100, 12, 11, 10, 13, 12]
outliers, lb, ub = iqr_detection(data)
print(f"异常索引: {outliers}, 下界: {lb:.2f}, 上界: {ub:.2f}")
避坑指南:我曾经在某个项目中,直接用 IQR 检测用户收入数据,结果把高收入人群全判成了异常。后来发现,收入数据是右偏的,上界被拉得很高。解决办法:对数据做对数变换后再用 IQR。
3.3 假设检验与p值:用统计学做"判决"
假设检验的思路很直接:先假设数据是正常的(原假设 H₀),然后看当前这个点出现的概率有多大。如果概率太小(p 值小于显著性水平 α),就拒绝原假设,认为它是异常。
常用的方法有:
- Grubbs 检验:适用于单变量正态分布,一次检测一个异常点
- Dixon 检验:适用于小样本(n ≤ 30),不要求正态分布
- 卡方检验:适用于分类变量的异常检测
Grubbs 检验示例:
from scipy import stats
def grubbs_test(data, alpha=0.05):
n = len(data)
mean = np.mean(data)
std = np.std(data, ddof=1)
z_scores = np.abs(data - mean) / std
max_z = np.max(z_scores)
# 计算临界值
t_crit = stats.t.ppf(1 - alpha / (2 * n), n - 2)
g_crit = (n - 1) / np.sqrt(n) * np.sqrt(t_crit**2 / (n - 2 + t_crit**2))
return max_z > g_crit, max_z, g_crit
data = [10, 12, 11, 13, 100, 12, 11, 10, 13, 12]
is_outlier, g_stat, g_crit = grubbs_test(data)
print(f"是否异常: {is_outlier}, 统计量: {g_stat:.3f}, 临界值: {g_crit:.3f}")
核心要点:p 值不是"异常的概率",而是"在原假设成立下,观察到当前结果或更极端结果的概率"。p 值越小,越有理由怀疑原假设。
我记得有一次做反欺诈模型,用 Grubbs 检验发现某个用户的交易金额 p 值只有 0.001。按理说应该标记为异常,但业务方说这个用户是 VIP 大客户。后来我们调整了策略:对 VIP 用户单独建模,用不同的阈值。
三种方法对比:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Z-Score | 近似正态分布、大样本 | 计算简单、直观 | 对偏态分布不鲁棒 |
| IQR | 任意分布、含极端值 | 稳健、无需分布假设 | 对尾部数据不敏感 |
| 假设检验 | 需要统计显著性判断 | 可量化置信度、理论严谨 | 计算复杂、需选择检验方法 |
我的建议:实际项目中,我通常先用 IQR 做快速筛查,再用 Z-Score 或假设检验做精细化判断。别迷信单一方法,组合使用才是王道。
嗯,这一章的内容就到这儿。统计方法虽然基础,但用好了能解决 80% 的异常检测问题。下一章我们聊聊基于距离的方法,到时候见。
公众号:蓝海资料掘金营,微信deep3321