4、基于统计的检测方法(二):四分位距法(IQR)、百分位数法、箱线图可视化

上一节我们聊了 Z-score 和 3σ 法则,它们对正态分布的数据很友好。但现实中的金融数据,说实话,很少长得那么「标准」。涨跌幅分布经常是尖峰厚尾的,这时候再用均值±几倍标准差,效果就不太理想了。

那怎么办?我个人的习惯是,遇到非正态分布的数据,优先考虑基于分位数的检测方法。说白了,就是不看数据离均值有多远,而是看它处在整个分布的什么位置。这节我们就来聊聊四分位距法(IQR)、百分位数法,以及怎么用箱线图把它们可视化出来。

4.1 四分位距法(IQR)的核心逻辑

四分位距法,名字听着挺唬人,其实道理很简单。你把所有数据从小到大排好队,切成四等份。中间那 50% 数据的跨度,就是 IQR。

  • Q1(第一四分位数):排在 25% 位置的值
  • Q3(第三四分位数):排在 75% 位置的值
  • IQR = Q3 - Q1

然后我们设定一个「正常范围」:

下界 = Q1 - 1.5 × IQR
上界 = Q3 + 1.5 × IQR

超出这个范围的数据点,就被标记为异常值。

为什么是 1.5 倍?

这个系数是统计学前辈们总结出来的经验值。对于近似正态分布的数据,1.5 倍 IQR 对应的概率大约是 0.7% 的极端值会被标记。你想想看,这个阈值比 3σ 要宽松一些,更适合金融数据那种「偶尔蹦出个大波动」的特性。

我在项目中遇到过一只港股,平时波动很小,突然某天因为财报利好涨了 40%。如果用 Z-score,这个点会被标记为异常。但用 IQR 法,它可能还在正常范围内——因为 IQR 对极端值不敏感,不会因为一个极端值就把整个阈值拉偏。这一点,在金融时间序列里特别实用。

4.2 百分位数法:更灵活的阈值设定

IQR 用的是固定的 1.5 倍系数。但有时候,我们需要根据业务场景自定义阈值。比如,我只想保留 1% 和 99% 之间的数据,把两头最极端的去掉。这时候就用百分位数法。

下界 = P1(第 1 百分位数)
上界 = P99(第 99 百分位数)

你可以根据数据特点调整百分位。比如:

  • 对日收益率,我常用 P0.5 和 P99.5
  • 对成交量,我常用 P5 和 P95
  • 对波动率指标,我常用 P10 和 P90

我的经验:百分位数法特别适合做「分位数截尾」。比如处理股指期货的 tick 数据时,我会把 P0.01 以下和 P99.99 以上的数据直接剔除——这些很可能是撮合错误或者网络延迟导致的异常单。

4.3 箱线图:一眼看穿异常值

箱线图是 IQR 法的可视化呈现。你不需要跑代码,看一眼图就知道数据有没有异常。

箱线图的构成:

  • 箱子:从 Q1 到 Q3,中间那条线是中位数
  • 须(whisker):从箱子两端延伸到 1.5 倍 IQR 的位置
  • :超出须的范围的点,就是异常值

嗯,这里要注意:箱线图默认把超出 1.5 倍 IQR 的点标为异常,但并不是说这些点一定要删除。它只是告诉你:「这些点值得你去看一眼」。

4.4 代码实战:用 Python 实现 IQR 检测

我们拿沪深 300 的日收益率数据来演示。假设你已经有了一个 DataFrame,里面有一列叫 returns

import pandas as pd
import numpy as np

def detect_outliers_iqr(data, column, multiplier=1.5):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    clean_data = data[(data[column] >= lower_bound) & (data[column] <= upper_bound)]
    
    return outliers, clean_data, lower_bound, upper_bound

# 使用示例
outliers, clean, lb, ub = detect_outliers_iqr(df, 'returns')
print(f"检测到 {len(outliers)} 个异常值")
print(f"正常范围: [{lb:.4f}, {ub:.4f}]")

这段代码我用了很多年,基本没改过。唯一的变化是,有时候我会把 multiplier 从 1.5 改成 3——当数据特别「胖尾」的时候。

4.5 百分位数法的代码实现

def detect_outliers_percentile(data, column, lower_p=0.01, upper_p=0.99):
    lower_bound = data[column].quantile(lower_p)
    upper_bound = data[column].quantile(upper_p)
    
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    clean_data = data[(data[column] >= lower_bound) & (data[column] <= upper_bound)]
    
    return outliers, clean_data, lower_bound, upper_bound

# 使用示例
outliers_p, clean_p, lb_p, ub_p = detect_outliers_percentile(df, 'returns', 0.005, 0.995)
print(f"百分位数法检测到 {len(outliers_p)} 个异常值")

避坑指南:我曾经在回测时犯过一个错——用全量数据计算百分位数,然后剔除异常值。这会导致未来数据泄露。正确的做法是:用滚动窗口计算,或者只用历史数据计算阈值。

4.6 箱线图可视化

用 matplotlib 画箱线图很简单:

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
plt.boxplot(df['returns'], vert=False, showfliers=True)
plt.title('沪深300日收益率箱线图')
plt.xlabel('收益率')
plt.grid(True, alpha=0.3)
plt.show()

你会看到箱子的两端、中间的线,以及那些散落在须之外的点。那些点,就是 IQR 法标记的异常值。

4.7 两种方法的对比

方法 优点 缺点 适用场景
IQR 法 对极端值不敏感,稳健 系数固定,不够灵活 金融收益率、波动率
百分位数法 可自定义阈值,灵活 需要业务经验确定百分位 成交量、订单簿数据

我个人更倾向于先用 IQR 法做初步筛查,再用百分位数法做精细化处理。两个方法配合使用,效果比单独用任何一个都好。

4.8 本章知识体系

下面这张图帮你理清这节的核心逻辑:

基于分位数的异常值检测 四分位距法 (IQR) 百分位数法 Q1 - 1.5×IQR 到 Q3 + 1.5×IQR P1 到 P99(可自定义) 箱线图可视化 自定义阈值截尾 共同目标:识别分布尾部的极端值

两种方法的核心思想是一样的:找到数据分布的「正常区间」,把落在区间之外的点标记出来。区别在于,IQR 法用固定的 1.5 倍系数,百分位数法让你自己定阈值。

好了,这节的内容就到这里。记住一点:异常值检测不是要把所有「不一样」的数据都删掉,而是要找到那些「可能有问题」的数据,然后去核实。毕竟,金融市场上,有些异常值其实是真正的信号。


公众号:蓝海资料掘金营,微信deep3321