2. 数据预处理基础:缺失值处理与异常值检测
在量化因子工程中,原始数据几乎不可能完美无缺。缺失值会破坏时间序列的连续性,异常值则可能扭曲因子与收益之间的真实关系。本章系统讲解缺失值与异常值的标准处理方法,确保后续分箱与建模建立在干净、可靠的数据基础之上。
2.1 缺失值处理
缺失值产生的原因包括:数据采集故障、停牌日无交易、财务数据披露周期不一致等。处理策略分为删除、填充与插值三大类,具体选择取决于缺失比例、数据重要性与业务逻辑。
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 删除 | 缺失比例 < 5%,且随机缺失 | 简单快速,不引入偏差 | 损失样本量,破坏时间序列连续性 |
| 固定值填充 | 行业哑变量、停牌标记等分类特征 | 保留样本量,业务含义清晰 | 可能引入常数偏差 |
| 统计量填充 | 连续型因子(均值/中位数) | 计算成本低,中位数抗异常值 | 降低方差,削弱因子区分度 |
| 前向/后向填充 | 时间序列因子(如动量、波动率) | 保留时序结构,符合业务逻辑 | 无法处理连续缺失段 |
| 线性插值 | 收益率曲线、期限结构等平滑序列 | 利用前后信息,平滑过渡 | 假设线性关系,可能失真 |
实践建议: 对于日频量价因子,优先使用前向填充(ffill)处理盘中缺失;对于财务因子,使用行业截面中位数填充;若某只股票连续缺失超过 20 个交易日,建议直接删除该时间段样本。
2.1.1 代码示例:缺失值处理
import pandas as pd
import numpy as np
# 模拟因子数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=10, freq='D')
data = {
'momentum': [0.05, np.nan, 0.03, 0.07, np.nan, 0.02, 0.06, np.nan, 0.04, 0.01],
'volatility': [0.20, 0.22, np.nan, 0.19, 0.21, np.nan, 0.23, 0.18, np.nan, 0.20],
'turnover': [1.2, 1.5, 1.3, np.nan, 1.1, 1.4, np.nan, 1.6, 1.2, 1.3]
}
df = pd.DataFrame(data, index=dates)
# 1. 删除包含缺失的行
df_drop = df.dropna()
# 2. 中位数填充
df_median = df.fillna(df.median())
# 3. 前向填充(时间序列推荐)
df_ffill = df.fillna(method='ffill')
# 4. 线性插值
df_interp = df.interpolate(method='linear')
print("原始数据缺失情况:\n", df.isnull().sum())
print("\n前向填充结果:\n", df_ffill)
2.2 异常值检测与处理
异常值在金融数据中普遍存在:突发新闻导致单日暴涨暴跌、数据录入错误、除权除息未复权等。若不处理,异常值会主导模型训练,导致因子失效。常用检测方法包括 IQR 与 Z-score。
2.2.1 IQR(四分位距)法
IQR 法基于数据分布的分位数,不假设正态分布,适合偏态分布的金融因子。定义下界 Q1 - 1.5×IQR,上界 Q3 + 1.5×IQR,超出范围即为异常值。
关键参数: 系数 1.5 为常规阈值,对于波动较大的因子(如日内振幅)可放宽至 3.0;对于极值敏感的因子(如市值)建议使用 1.5。
2.2.2 Z-score 法
Z-score 假设数据近似正态分布,计算每个样本与均值的标准差距离。通常以 |Z| > 3 作为异常值判定标准。对于金融数据,若因子分布厚尾严重,建议使用修正 Z-score(基于中位数与 MAD)。
| 方法 | 假设条件 | 阈值 | 金融适用性 |
|---|---|---|---|
| IQR | 无分布假设 | Q1 - 1.5×IQR / Q3 + 1.5×IQR | 高(适合偏态、厚尾) |
| Z-score | 近似正态分布 | |Z| > 3 | 中(对厚尾敏感) |
| 修正 Z-score | 对称分布 | |M| > 3.5 | 较高(抗异常值) |
2.2.3 异常值处理策略
- 截尾(Winsorize): 将异常值替换为边界值(Q1 或 Q3),保留样本量,适合连续型因子。
- 删除: 直接移除异常样本,适合明显的数据录入错误或极端事件。
- 填充: 用中位数或行业均值替换,适合少量异常值。
- 分箱处理: 将异常值归入最边缘分箱,适合后续分箱建模场景。
注意: 异常值处理必须在训练集上计算统计量(Q1、Q3、均值、标准差),然后将相同的边界应用到验证集和测试集,避免数据泄露。
2.2.4 代码示例:异常值检测与截尾
from scipy import stats
# 模拟因子数据
np.random.seed(42)
factor = np.random.randn(1000) * 0.5 + 0.02
# 人为注入异常值
factor[10] = 5.0
factor[50] = -4.0
factor[200] = 6.5
# IQR 检测
Q1 = np.percentile(factor, 25)
Q3 = np.percentile(factor, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
iqr_outliers = (factor < lower_bound) | (factor > upper_bound)
print(f"IQR 异常值数量: {iqr_outliers.sum()}")
# Z-score 检测
z_scores = np.abs(stats.zscore(factor))
z_outliers = z_scores > 3
print(f"Z-score 异常值数量: {z_outliers.sum()}")
# 截尾处理(Winsorize)
factor_winsorized = np.clip(factor, lower_bound, upper_bound)
print(f"截尾后因子范围: [{factor_winsorized.min():.4f}, {factor_winsorized.max():.4f}]")
2.3 缺失值与异常值处理流程
下图展示了从原始数据到干净样本的完整处理流水线,包含缺失值处理与异常值检测两个并行分支,最终合并为可用于建模的清洁数据集。
2.4 实践要点与避坑指南
- 时序数据慎用删除: 金融时间序列删除缺失值会破坏时间结构,导致未来信息泄露或滞后计算错误。
- 截面与时间维度区分: 截面因子(如市盈率)使用行业截面中位数填充;时间序列因子(如动量)使用前向填充或插值。
- 异常值阈值需验证: 不同因子的分布差异大,建议对每个因子单独计算 IQR 或 Z-score 阈值,而非使用全局固定值。
- 避免重复处理: 先处理缺失值,再进行异常值检测;若先截尾再填充,可能掩盖真实异常模式。
- 记录处理日志: 记录每个因子的缺失比例、异常值数量、处理方式,便于后续回溯与模型调优。
量化实践提示: 在回测框架中,建议将缺失值与异常值处理封装为独立的 Transformer 类,支持 fit/transform 接口,确保训练集与测试集使用相同的统计参数。