4、异常值检测与处理:基于统计方法的异常检测(Z-score、IQR)、基于机器学习的方法(孤立森林、LOF)、异常值修正与剔除策略
异常值检测,说白了就是在一堆数据里找出那些「不对劲」的家伙。我刚开始做行情数据清洗时,总觉得这步可有可无——直到有一次,因为一个异常值没处理,导致整个回测模型跑偏了30%。嗯,从那以后我再也不敢小看它了。
行情数据里的异常值,通常来自三种情况:交易系统故障、人为录入错误、或者市场极端波动。咱们得把它们揪出来,然后决定是修还是扔。
4.1 基于统计方法的异常检测
统计方法是最直观的。你想想看,大部分正常数据都集中在某个范围内,离群太远的,大概率有问题。
4.1.1 Z-score 方法
Z-score 的核心思想很简单:计算每个数据点偏离均值多少个标准差。如果偏离太多,就标记为异常。
公式长这样:
Z = (x - μ) / σ
其中 μ 是均值,σ 是标准差。通常阈值取 3,也就是 |Z| > 3 就算异常。为什么是3?因为正态分布下,99.7%的数据都在3个标准差内。
我个人习惯在行情数据里用 Z-score 做第一轮筛查。举个例子:
import numpy as np
import pandas as pd
def detect_outliers_zscore(data, threshold=3):
z_scores = np.abs((data - data.mean()) / data.std())
return z_scores > threshold
# 假设有一列收盘价
prices = df['close'].values
outliers = detect_outliers_zscore(prices)
print(f"发现 {outliers.sum()} 个异常值")
4.1.2 IQR 方法
IQR(四分位距法)比 Z-score 更稳健。它不受极端值影响,因为用的是中位数和分位数。
计算步骤:
- 算下四分位数 Q1(25%分位)和上四分位数 Q3(75%分位)
- IQR = Q3 - Q1
- 正常范围:[Q1 - 1.5×IQR, Q3 + 1.5×IQR]
- 超出这个范围的,就是异常
def detect_outliers_iqr(data):
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return (data < lower_bound) | (data > upper_bound)
outliers = detect_outliers_iqr(df['volume'])
print(f"IQR方法发现 {outliers.sum()} 个成交量异常值")
4.2 基于机器学习的方法
统计方法虽然快,但遇到复杂模式就力不从心了。比如,某个价格在正常范围内,但结合成交量、时间等因素看,它就是不对劲。这时候得上机器学习。
4.2.1 孤立森林(Isolation Forest)
孤立森林的思路很巧妙——它不描述正常数据长什么样,而是直接找那些「容易被孤立」的点。你想想看,异常值本来就少,而且特征明显,随机切几刀就能把它单独切出来。
算法核心:
- 随机选一个特征,随机选一个切分值
- 把数据分成左右两半
- 重复切分,直到每个点都被孤立
- 异常值需要的切分次数少,正常值需要的次数多
from sklearn.ensemble import IsolationForest
# 准备特征:收盘价、成交量、涨跌幅
features = df[['close', 'volume', 'pct_change']].values
model = IsolationForest(
contamination=0.05, # 预期异常比例
random_state=42
)
df['anomaly'] = model.fit_predict(features)
# -1 表示异常,1 表示正常
anomalies = df[df['anomaly'] == -1]
print(f"孤立森林发现 {len(anomalies)} 个异常点")
实际项目中的坑:孤立森林的 contamination 参数很关键。设小了漏检,设大了误报。我一般先用 IQR 跑一遍,估算出大概的异常比例,再传给孤立森林。这样准确率高很多。
4.2.2 LOF(局部异常因子)
LOF 关注的是「局部密度」。如果一个点的邻居都很密集,它自己却孤零零的,那它可能就是异常。这个方法特别适合处理密度不均匀的数据。
举个例子:在行情数据里,盘整期的正常波动和突破期的正常波动,密度完全不一样。LOF 能区分这种局部差异。
from sklearn.neighbors import LocalOutlierFactor
lof = LocalOutlierFactor(
n_neighbors=20, # 邻居数量
contamination=0.05
)
df['lof_anomaly'] = lof.fit_predict(features)
# -1 表示异常
lof_anomalies = df[df['lof_anomaly'] == -1]
print(f"LOF发现 {len(lof_anomalies)} 个异常点")
4.3 异常值修正与剔除策略
找到异常值之后,怎么处理?这里没有标准答案,得看业务场景。
4.3.1 剔除策略
直接删掉异常值。适合以下情况:
- 异常值数量很少(< 1%)
- 明显是数据录入错误(比如价格为负数)
- 后续分析对数据完整性要求不高
# 剔除异常值
clean_df = df[df['anomaly'] == 1].copy()
4.3.2 修正策略
用合理值替换异常值。常用方法:
- 均值/中位数替换:简单粗暴,但会降低数据方差
- 前向填充:用上一个正常值填充,适合时间序列
- 插值法:线性插值或样条插值,更平滑
- 模型预测:用正常数据训练模型,预测异常点的值
# 前向填充
df['close_filled'] = df['close'].fillna(method='ffill')
# 线性插值
df['close_interp'] = df['close'].interpolate(method='linear')
# 中位数替换
median_val = df.loc[df['anomaly'] == 1, 'close'].median()
df.loc[df['anomaly'] == -1, 'close'] = median_val
4.3.3 我的实战策略
在实际项目中,我一般用「三级处理法」:
- 第一级:明显错误(价格≤0、成交量≤0)→ 直接剔除
- 第二级:统计方法标记的异常 → 用前向填充修正
- 第三级:机器学习方法标记的异常 → 人工复核,根据业务判断
这样做的好处是,既保证了数据质量,又不会过度清洗导致信息丢失。
4.4 知识体系总览
下面这张图,是我梳理的异常值检测与处理的核心逻辑。你可以把它当作一个决策树来用:
嗯,以上就是异常值检测与处理的完整思路。从统计方法到机器学习,从剔除到修正,每一步都有它的适用场景。我个人建议,别迷信某一种方法,多组合、多验证,才能找到最适合你数据的方案。
公众号:蓝海资料掘金营,微信deep3321