2. 数据清洗基础:处理缺失值、异常值、重复值的基本方法
做因子挖掘这些年,我踩过最大的坑,不是模型选错了,而是数据没洗干净就往上怼。你想想看,一堆脏数据进去,再牛的算法也是白搭。今天咱们就聊聊数据清洗最基础的三件事:缺失值、异常值、重复值。说白了,就是让数据变得「能用」。
核心观点:数据清洗不是可有可无的预处理步骤,而是因子挖掘的基石。脏数据会导致因子失效、过拟合、甚至策略亏损。我个人习惯,拿到数据第一件事不是看分布,而是先做清洗。
2.1 缺失值处理:别让「空」坑了你
缺失值在金融数据里太常见了。停牌、节假日、数据源故障,都会导致空值。我在项目中遇到过,某次回测收益曲线漂亮得不行,结果发现是因为缺失值被自动填充成了0,相当于凭空多了很多「低价买入」的机会——这显然是假的。
2.1.1 缺失值检测
先看看数据到底缺了多少。用Pandas很简单:
import pandas as pd
import numpy as np
# 模拟一份日频因子数据
df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=10, freq='D'),
'factor_a': [1.2, np.nan, 1.5, 1.3, np.nan, 1.7, 1.1, np.nan, 1.4, 1.6],
'factor_b': [0.5, 0.6, np.nan, 0.7, 0.8, np.nan, 0.9, 1.0, np.nan, 1.1],
'return': [0.01, 0.02, -0.01, 0.03, 0.00, 0.02, 0.01, -0.02, 0.03, 0.01]
})
# 检测缺失值
print(df.isnull().sum())
print(f"缺失比例: \n{df.isnull().mean() * 100:.2f}%")
输出结果会告诉你每列缺了多少。我个人习惯,缺失比例超过50%的因子,直接扔掉——补了也是猜,不如不用。
2.1.2 缺失值处理方法
处理缺失值,常用的方法就这几种:
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 删除法 | 缺失比例极低(<5%) | 会损失样本量,小心时间序列断裂 |
| 均值/中位数填充 | 因子分布较稳定 | 会降低方差,影响因子区分度 |
| 前向/后向填充 | 时间序列数据 | 适合停牌、节假日等场景 |
| 插值法 | 数据有趋势性 | 线性插值简单,但别过度拟合 |
| 模型预测 | 缺失值较多且重要 | 计算量大,小心引入偏差 |
举个例子,对于日频因子数据,我一般这样处理:
# 方法1:前向填充(适合停牌场景)
df['factor_a_ffill'] = df['factor_a'].fillna(method='ffill')
# 方法2:线性插值(适合连续缺失)
df['factor_b_interp'] = df['factor_b'].interpolate(method='linear')
# 方法3:行业均值填充(适合截面数据)
# 假设有行业分类
industry_mean = df.groupby('industry')['factor_a'].transform('mean')
df['factor_a_industry'] = df['factor_a'].fillna(industry_mean)
我的经验:金融时间序列数据,优先用前向填充。因为停牌后复牌,因子值通常延续停牌前的状态。千万别用全局均值填充,那会把不同时间点的特征混在一起。
2.2 异常值处理:别让「极端」带偏了
异常值在因子数据里很要命。一个极端值,就能把相关系数从0.3拉到0.6,或者从显著变成不显著。我曾经因为没处理异常值,导致一个因子在回测里表现极好,实盘却一塌糊涂——后来发现是某只股票在数据里出现了100倍市盈率(数据录入错误)。
2.2.1 异常值检测方法
常用的检测方法有这几种:
- Z-score法:假设数据正态分布,|Z|>3视为异常。适合对称分布的数据。
- IQR法:用四分位距,超出Q1-1.5*IQR或Q3+1.5*IQR视为异常。对偏态分布更鲁棒。
- MAD法:中位数绝对偏差,比Z-score更抗异常值干扰。
- 领域知识法:比如市盈率不可能为负,换手率不可能超过100%。
代码实现:
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (series < lower) | (series > upper)
def detect_outliers_zscore(series, threshold=3):
z = np.abs((series - series.mean()) / series.std())
return z > threshold
# 示例
df['factor_a_outlier_iqr'] = detect_outliers_iqr(df['factor_a'])
df['factor_a_outlier_z'] = detect_outliers_zscore(df['factor_a'])
2.2.2 异常值处理方法
检测到异常值后,怎么处理?我一般按这个优先级来:
- 确认数据源:如果是录入错误,直接修正或删除。
- 截尾处理(Winsorize):把异常值替换为边界值。这是我最常用的方法。
- 缺失化处理:把异常值设为NaN,然后用缺失值方法处理。
- 保留但标记:如果异常值本身可能是信号(比如黑天鹅事件),可以单独标记。
from scipy.stats.mstats import winsorize
# 截尾处理:将上下1%的极端值替换为边界值
df['factor_a_winsor'] = winsorize(df['factor_a'], limits=[0.01, 0.01])
# 或者手动实现
def winsorize_series(series, lower=0.01, upper=0.01):
q_low = series.quantile(lower)
q_high = series.quantile(1 - upper)
return series.clip(lower=q_low, upper=q_high)
注意:截尾处理会改变数据分布,尤其是尾部特征。如果你研究的因子本身依赖极端值(比如动量因子中的极端收益),截尾可能会破坏因子逻辑。这种情况下,我建议先保留异常值,单独分析其影响。
2.3 重复值处理:别让「复制」骗了你
重复值在因子数据里相对少见,但一旦出现,后果很严重。重复的样本会放大某些特征的影响,导致因子权重失真。我记得有一次,某只股票因为数据源重复导入,在回测中出现了两次相同的日频数据,结果该股票的因子贡献被高估了一倍。
2.3.1 重复值检测
重复值分两种:完全重复(所有列都一样)和部分重复(关键列一样)。对于因子数据,我们通常关注「时间+股票代码」这个组合是否唯一。
# 检测完全重复行
print(f"完全重复行数: {df.duplicated().sum()}")
# 检测基于关键列的重复
# 假设有股票代码和日期
# df['stock_id'] = ['000001', '000001', '000002', ...]
# df['date'] = ...
# print(f"关键列重复行数: {df.duplicated(subset=['stock_id', 'date']).sum()}")
# 查看重复的具体数据
# df[df.duplicated(subset=['stock_id', 'date'], keep=False)].sort_values(['stock_id', 'date'])
2.3.2 重复值处理方法
处理重复值,原则很简单:保留一条,删除其余。但保留哪条有讲究:
- 保留第一条:适合数据按时间顺序排列,第一条最原始。
- 保留最后一条:适合数据有更新,最后一条是最新版本。
- 聚合处理:如果重复值有细微差异(比如不同数据源),可以取均值或中位数。
# 删除完全重复行
df_clean = df.drop_duplicates()
# 基于关键列删除,保留第一条
df_clean = df.drop_duplicates(subset=['stock_id', 'date'], keep='first')
# 如果有细微差异,聚合处理
# df_agg = df.groupby(['stock_id', 'date']).agg({
# 'factor_a': 'mean',
# 'factor_b': 'median',
# 'return': 'first'
# }).reset_index()
避坑指南:我曾经在处理日频数据时,发现某只股票在同一天出现了两条记录,一条是正常交易数据,一条是复权调整数据。这种情况下不能简单删除,需要根据业务逻辑判断保留哪条。我的建议是:先搞清楚重复的原因,再决定处理方式。
2.4 数据清洗的整体流程
说了这么多,咱们把整个流程串起来。下面这张图是我做数据清洗时的标准流程:
这个流程我用了好几年,基本没出过问题。顺序很重要:先处理缺失值,再处理异常值,最后去重。为什么?因为缺失值填充后可能会产生新的异常值,而异常值处理后又可能产生重复值(比如截尾后多个值变成同一个边界值)。
2.5 实战中的注意事项
最后,分享几个我在实战中总结的经验:
- 别一次性清洗所有数据。因子数据往往是分批获取的,每批数据可能有不同的质量问题。我习惯每获取一批数据,就做一次清洗。
- 保留清洗日志。记录你删了多少行、填充了多少值、截尾了多少异常。这样回测出问题时,能快速定位是不是清洗环节出了问题。
- 清洗参数要可复现。比如截尾的上下限、填充的方法,最好写成配置文件。这样换数据源或更新数据时,能保持一致的处理逻辑。
- 别过度清洗。有些「异常值」其实是真实的市场行为,比如涨跌停导致的极端收益。盲目清洗会丢失重要信息。
总结一下:数据清洗是因子挖掘的第一步,也是最容易被忽视的一步。缺失值、异常值、重复值,这三个问题处理好了,你的因子就成功了一半。剩下的,就是特征工程和模型的事了。