数据准备与清洗:多源数据获取、缺失值处理、异常值检测与处理、数据对齐与频率统一
做量化投资,圈里有一句老话:「数据决定上限,模型只是逼近这个上限」。我做了这么多年因子挖掘,见过太多人花90%的时间调模型参数,结果数据源里藏着几个异常值,直接把IC曲线干崩了。说白了,数据准备这一步要是没做好,后面全是白忙活。
今天这一章,我就把数据清洗的实战经验掰开揉碎了讲。咱们从多源数据获取开始,一路走到频率统一,每一步我都会告诉你:哪些坑我踩过,哪些坑你千万别踩。
核心观点:数据清洗不是体力活,而是决定因子有效性的第一道关卡。80%的因子失效问题,根源都在数据质量上。
一、多源数据获取:别让数据源成为你的短板
做因子挖掘,数据源通常来自三个地方:
- 行情数据:日线、分钟线、Tick级数据。我习惯用Wind或聚宽,但回测时一定要用复权数据。
- 财务数据:季报、年报、业绩预告。这里有个坑——财务数据有滞后性,你拿到的Q1数据,实际披露日可能是4月底。
- 另类数据:舆情、龙虎榜、产业链数据。这类数据噪声大,但往往能挖出超额收益。
我在项目中遇到过最头疼的事:同一个股票,不同数据源的收盘价差了0.5%。你想想看,这0.5%的误差,算出来的收益率可能就完全变样了。所以我的建议是:固定一个主数据源,其他数据源只做交叉验证。
小技巧:写一个数据源校验函数,每天跑完数据后自动比对关键字段。我一般比对「收盘价」和「成交量」两个字段,偏差超过0.1%就报警。
二、缺失值处理:别一股脑全删掉
拿到数据后第一件事——检查缺失值。很多新手上来就`dropna()`,这其实很危险。为什么?因为缺失值本身可能就包含信息。
举个例子:某只股票突然停牌,那几天的数据全是NaN。如果你直接删掉,那你的因子在停牌期间就「断档」了。但如果你用前值填充,又可能引入未来信息。
我个人习惯分三种情况处理:
- 随机缺失(比如某天数据没采集到):用前后均值填充,或者用同类股票的均值填充。
- 结构性缺失(比如新股上市前没有数据):直接保留NaN,计算因子时跳过。
- 连续缺失(比如停牌超过5天):标记为异常,单独处理。
# 这是我常用的缺失值处理函数
def handle_missing(df, method='ffill', limit=3):
"""
method: 'ffill' 前向填充, 'interpolate' 插值, 'drop' 删除
limit: 连续缺失超过该值则标记为异常
"""
if method == 'ffill':
df = df.fillna(method='ffill', limit=limit)
elif method == 'interpolate':
df = df.interpolate(method='linear', limit_direction='both')
elif method == 'drop':
df = df.dropna()
return df
注意:千万不要用未来数据填充缺失值!比如用后一天的收盘价填充前一天的缺失,这在回测里就是「未来函数」,会严重高估因子表现。
三、异常值检测与处理:别让一个极端值毁掉你的因子
异常值检测,说白了就是找那些「离谱」的数据点。比如某只股票一天涨了1000%,这明显是数据错误。但有些异常值没那么明显——比如某天成交量突然放大10倍,可能是真实事件,也可能是数据录入错误。
我常用的方法有三种:
- 3σ原则:假设数据服从正态分布,超过均值±3倍标准差的值视为异常。简单粗暴,但遇到厚尾分布会误杀。
- IQR方法:用四分位距(Q3-Q1)来界定异常值。比3σ更稳健,我一般用这个。
- MAD方法:中位数绝对偏差。对极端值不敏感,适合金融数据这种有尖峰厚尾特征的。
我曾经遇到过一个案例:某因子在回测中IC高达0.15,但实盘时直接崩了。后来一查,原来是数据里有一个异常值——某只小盘股因为一笔大宗交易,换手率异常高,导致因子值被拉偏。所以我现在做异常值检测,一定会先按行业分组,再分别检测。
# 按行业分组的异常值检测
def detect_outliers_by_group(df, group_col='industry', value_col='factor_value', method='iqr'):
def detect_group(group):
Q1 = group[value_col].quantile(0.25)
Q3 = group[value_col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (group[value_col] >= lower) & (group[value_col] <= upper)
df['is_normal'] = df.groupby(group_col).apply(detect_group).reset_index(level=0, drop=True)
return df
避坑指南:我曾经犯过一个错误——把异常值直接替换成均值。结果导致因子分布被「压缩」,IC反而下降了。后来我改用「截尾处理」:把异常值替换成上下限的边界值,这样既保留了数据分布形态,又消除了极端值的影响。
四、数据对齐与频率统一:时间戳是最大的敌人
多源数据最头疼的问题是什么?时间戳对不上。
比如行情数据是交易日历,财务数据是报告期,另类数据可能是实时推送的。你把这些数据拼在一起,如果不做对齐,算出来的因子就是「四不像」。
我一般分两步走:
- 第一步:统一时间戳格式。把所有时间戳转成`datetime`类型,精确到日。分钟级数据我一般先聚合到日频。
- 第二步:对齐交易日历。用全市场的交易日历作为基准,把非交易日的数据前向填充或后向填充。
这里有个细节:财务数据发布后,通常需要滞后一段时间才能用。我习惯用「财报发布日期+1天」作为可用日期,而不是财报截止日期。这样能避免未来信息泄露。
# 数据对齐示例:将财务数据对齐到交易日历
def align_financial_data(fin_df, trade_calendar):
# 假设fin_df有'publish_date'和'value'两列
# trade_calendar是交易日列表
aligned = pd.DataFrame(index=trade_calendar)
for date in trade_calendar:
# 找到最近一次发布的财务数据
latest = fin_df[fin_df['publish_date'] <= date].iloc[-1]
aligned.loc[date, 'value'] = latest['value']
return aligned
频率统一的原则:低频数据向高频数据对齐时,用前向填充;高频数据向低频数据对齐时,用聚合(比如取均值或求和)。千万别反过来,否则会引入未来信息。
五、实战中的「数据清洗流水线」
说了这么多,我给大家画一张图,把整个数据清洗的流程串起来。这张图是我自己项目里一直在用的框架,你可以直接拿去用。
这张图里,我特别想强调「数据质量校验」这一步。很多人做完前四步就直接开始挖因子了,但我会多花10分钟做一次校验:
- 检查数据范围是否合理(比如PE不能为负,换手率不能超过100%)
- 检查时间序列是否连续(有没有跳空)
- 检查不同数据源之间的交叉验证是否通过
这一步看起来简单,但能帮你省掉后面排查问题的几个小时。我自己的经验是:数据清洗的时间,至少占整个因子挖掘流程的40%。别嫌多,这40%的时间花得值。
最后说一句:数据清洗没有银弹。每个数据集都有自己的「脾气」,你需要花时间去了解它。但只要你把上面这套流程跑熟了,至少能保证你的因子不会因为数据问题而失效。