异常值处理:3σ原则、IQR方法、基于分位数的截断、Winsorize处理
做量化交易的朋友都知道,数据里藏着"脏东西"是家常便饭。尤其是异常值,处理不好,模型直接崩给你看。我这些年踩过的坑,十有八九都跟异常值有关。
今天咱们就把异常值处理这四种方法聊透。3σ原则、IQR方法、分位数截断、Winsorize处理,一个一个来。
一、先说说异常值到底是个啥
异常值,说白了就是那些"不合群"的数据点。比如某只股票平时波动1%,突然某天涨了20%,这大概率是数据录入错误,或者发生了极端事件。
我个人习惯把异常值分成两类:
- 自然异常值:比如金融危机时的暴跌,这是真实的市场行为
- 错误异常值:比如数据采集时多打了个0,或者传感器故障
你想想看,如果混进一个错误数据,模型会拼命去拟合这个"假信号",结果就是实盘时一塌糊涂。所以,处理异常值不是可选项,是必选项。
二、3σ原则:最经典的方法
3σ原则基于正态分布假设。数据落在均值±3个标准差之外的概率只有0.3%,这些点就可以视为异常值。
核心公式:如果 |x - μ| > 3σ,则判定为异常值
代码实现很简单:
import numpy as np
import pandas as pd
def remove_outliers_3sigma(data, col):
mean = data[col].mean()
std = data[col].std()
lower = mean - 3 * std
upper = mean + 3 * std
return data[(data[col] >= lower) & (data[col] <= upper)]
# 使用示例
df_clean = remove_outliers_3sigma(df, 'close_price')
我的经验:3σ原则对金融数据其实不太友好。因为收益率分布通常有"肥尾"特征,3σ会误杀很多真实的市场波动。我一般只在数据质量检查的初步阶段用一下。
三、IQR方法:更稳健的选择
IQR(四分位距)方法不依赖正态分布假设,它用中位数和四分位数来界定异常值。我个人更偏爱这个方法,尤其是在处理非对称分布的数据时。
计算步骤:
- 计算第一四分位数Q1(25%分位数)
- 计算第三四分位数Q3(75%分位数)
- IQR = Q3 - Q1
- 下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR
def remove_outliers_iqr(data, col):
Q1 = data[col].quantile(0.25)
Q3 = data[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return data[(data[col] >= lower) & (data[col] <= upper)]
# 使用示例
df_clean = remove_outliers_iqr(df, 'volume')
注意:1.5这个系数是经验值。对于金融数据,我建议调整到2或2.5,否则会剔除太多有效数据。我曾经在回测时用默认的1.5,结果发现2015年股灾的数据被删掉了一大半,回测结果变得异常漂亮——但那是假的。
四、基于分位数的截断:简单粗暴但有效
这个方法更直接——直接砍掉数据两端一定比例的值。比如去掉最小的1%和最大的1%。
为什么需要这个方法?因为有些场景下,我们不需要判断"是不是异常值",只需要保证数据在合理范围内。
def truncate_by_quantile(data, col, lower_percent=0.01, upper_percent=0.99):
lower = data[col].quantile(lower_percent)
upper = data[col].quantile(upper_percent)
return data[(data[col] >= lower) & (data[col] <= upper)]
# 去掉两端1%的极端值
df_clean = truncate_by_quantile(df, 'return_rate', 0.01, 0.99)
避坑指南:我曾经在构建因子时,直接用分位数截断处理了所有特征。结果发现某个因子的区分度大幅下降。后来才意识到,那个因子本身就有"长尾"特征,截断后信息丢失严重。所以,分位数截断适合做"粗筛",不适合精细处理。
五、Winsorize处理:保留样本,修正极端值
前面三种方法都是"删除"异常值。但有时候,删除样本太奢侈了——尤其是样本量本来就不大的时候。
Winsorize处理的做法是:把异常值替换成边界值。比如把小于1%分位数的值,全部替换成1%分位数本身。
from scipy.stats.mstats import winsorize
def winsorize_data(data, col, limits=[0.01, 0.01]):
# limits参数:左尾和右尾分别截断的比例
data[col] = winsorize(data[col], limits=limits)
return data
# 使用示例
df_clean = winsorize_data(df, 'pe_ratio', limits=[0.05, 0.05])
嗯,这里要注意:Winsorize处理后的数据分布会变得"平坦",方差会减小。如果你做的是波动率相关的策略,这个方法要慎用。
六、四种方法对比:什么时候用哪个?
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 3σ原则 | 近似正态分布的数据 | 计算简单,理论成熟 | 对肥尾数据不友好 |
| IQR方法 | 非对称分布、金融数据 | 稳健,不受极端值影响 | 系数需要调整 |
| 分位数截断 | 数据质量检查、粗筛 | 简单直接,可控性强 | 可能丢失有用信息 |
| Winsorize处理 | 样本量小、不想删除数据 | 保留样本量,修正极端值 | 改变数据分布特征 |
七、实战中的选择策略
我个人在实际项目中,通常这样组合使用:
- 第一步:用分位数截断做快速清洗,去掉明显错误的数据(比如价格为负值)
- 第二步:用IQR方法识别潜在异常值,但系数调大到2.5或3
- 第三步:对于识别出的异常值,用Winsorize处理,而不是直接删除
这样做的好处是:既清理了数据,又最大程度保留了样本量。尤其是在做高频数据时,每一笔数据都很珍贵。
核心原则:不要为了处理异常值而处理。先理解你的数据,再选择合适的方法。金融数据里,有些"异常"其实是信号。
八、本章知识体系
下面这张图总结了四种异常值处理方法的核心逻辑和适用场景:
好了,这四种方法够你应对大部分场景了。记住,没有万能的方法,只有最适合你数据的方案。多试、多对比,慢慢你就会有感觉。