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()} 个异常值")
我的经验:Z-score 对正态分布的数据效果最好。但行情数据往往有尖峰厚尾特征,这时候阈值可以适当调高到 3.5 或 4。我在处理股指期货数据时,就吃过这个亏——用默认的3,结果把正常的极端行情也标记了。

4.1.2 IQR 方法

IQR(四分位距法)比 Z-score 更稳健。它不受极端值影响,因为用的是中位数和分位数。

计算步骤:

  1. 算下四分位数 Q1(25%分位)和上四分位数 Q3(75%分位)
  2. IQR = Q3 - Q1
  3. 正常范围:[Q1 - 1.5×IQR, Q3 + 1.5×IQR]
  4. 超出这个范围的,就是异常
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()} 个成交量异常值")
注意:我曾经在日内高频数据上用过 IQR,发现1.5倍这个系数太保守了。高频数据波动大,建议用3倍IQR。具体调多少,得看你的数据分布。

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)} 个异常点")
我的建议:n_neighbors 这个参数别用默认值。数据量大就设大一点,比如50;数据量小就设小一点。我处理分钟级行情数据时,通常设30-50之间。

4.3 异常值修正与剔除策略

找到异常值之后,怎么处理?这里没有标准答案,得看业务场景。

4.3.1 剔除策略

直接删掉异常值。适合以下情况:

  • 异常值数量很少(< 1%)
  • 明显是数据录入错误(比如价格为负数)
  • 后续分析对数据完整性要求不高
# 剔除异常值
clean_df = df[df['anomaly'] == 1].copy()
注意:别一股脑全删了。我曾经在回测数据里删了2%的异常值,结果导致回测结果失真——因为那些异常值里,有一部分是真实的极端行情。剔除前一定要人工复核。

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 我的实战策略

在实际项目中,我一般用「三级处理法」:

  1. 第一级:明显错误(价格≤0、成交量≤0)→ 直接剔除
  2. 第二级:统计方法标记的异常 → 用前向填充修正
  3. 第三级:机器学习方法标记的异常 → 人工复核,根据业务判断

这样做的好处是,既保证了数据质量,又不会过度清洗导致信息丢失。

4.4 知识体系总览

下面这张图,是我梳理的异常值检测与处理的核心逻辑。你可以把它当作一个决策树来用:

异常值检测与处理决策流程 原始行情数据 第一级:明显错误检测(价格≤0,成交量≤0) 是/否 直接剔除 第二级:统计方法检测(Z-score / IQR) 异常? 前向填充 / 插值修正 第三级:机器学习检测(孤立森林 / LOF) 异常? 人工复核 + 业务判断 保留

嗯,以上就是异常值检测与处理的完整思路。从统计方法到机器学习,从剔除到修正,每一步都有它的适用场景。我个人建议,别迷信某一种方法,多组合、多验证,才能找到最适合你数据的方案。


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