第二章:数据清洗与预处理
说实话,做量化研究这几年,我踩过最大的坑,往往不是策略逻辑本身,而是数据。
你想想看,辛辛苦苦从各个渠道扒来的另类数据,要是没洗干净就直接扔进模型,那结果基本就是「垃圾进,垃圾出」。我个人习惯是,拿到任何新数据源,第一件事不是跑因子,而是先做一轮彻底的清洗和预处理。今天咱们就聊聊这块的核心环节。
2.1 缺失值处理
缺失值,说白了就是数据里的「空洞」。卫星图像因为云层遮挡丢了几个像素,新闻情感分析因为爬虫故障少了几天的数据,这都是家常便饭。怎么补?得看场景。
均值/中位数填充
这是最粗暴但最常用的方法。我一般这么选:
- 均值填充:适合数据分布比较对称、没有极端值的情况。比如某只股票的日均成交量,偶尔缺一天,用过去30天的均值补上,问题不大。
- 中位数填充:如果数据里有明显的异常值(比如某天成交量突然暴增100倍),均值会被拉偏,这时候中位数更稳健。我在处理供应链数据时经常用中位数,因为供应商的订单量偶尔会有离谱的跳变。
核心原则:别盲目用均值。先看一眼数据分布,再决定用均值还是中位数。
import pandas as pd
import numpy as np
# 示例:对某因子列进行缺失值填充
df['factor'] = df['factor'].fillna(df['factor'].mean()) # 均值填充
# 或者
df['factor'] = df['factor'].fillna(df['factor'].median()) # 中位数填充
插值法
插值比均值更聪明一点。它利用时间序列的前后关系来推测缺失值。我个人比较推荐线性插值,尤其是处理高频数据时。
举个例子:某只股票下午2点、3点、5点有数据,4点缺失了。线性插值会认为4点的值大概在3点和5点中间。嗯,这比直接用全天均值合理多了。
# 线性插值
df['factor'] = df['factor'].interpolate(method='linear')
# 时间序列插值(考虑时间间隔)
df['factor'] = df['factor'].interpolate(method='time')
我的小技巧:如果缺失值连续超过3个,我一般就不插值了,直接删掉这段数据。因为连续缺失太多,插出来的值基本是「猜」的,可信度太低。
2.2 异常值检测
异常值,就是数据里的「刺头」。可能是数据录入错误,也可能是真实的极端事件(比如某公司突然暴雷)。怎么区分?得靠统计方法。
3-Sigma 法则
这个最经典。假设数据服从正态分布,那么99.7%的数据会落在均值±3个标准差之内。之外的,就算异常。
但说实话,金融数据很少是完美正态分布。我遇到过很多次,用3-sigma把真实的市场崩盘点给误杀了。所以,它更适合做初筛。
def detect_outliers_3sigma(series):
mean = series.mean()
std = series.std()
outliers = (series < mean - 3*std) | (series > mean + 3*std)
return outliers
IQR 方法
IQR(四分位距)不依赖正态分布假设,更稳健。它把数据分成四份,看哪些点跑到了「箱子」外面太远。
公式很简单:下界 = Q1 - 1.5*IQR,上界 = Q3 + 1.5*IQR。超出这个范围的,就是异常。
避坑指南:我曾经用IQR处理一组极度偏态的数据,结果发现Q1和Q3几乎重合,IQR小得可怜,导致大量正常值被标为异常。后来我改用调整后的IQR系数(比如3倍IQR),才解决问题。
def detect_outliers_iqr(series, multiplier=1.5):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - multiplier * IQR
upper = Q3 + multiplier * IQR
return (series < lower) | (series > upper)
MAD 方法
MAD(中位数绝对偏差)是我个人最喜欢的方法。它比3-sigma更抗干扰,比IQR更灵敏。尤其适合处理那些「偶尔抽风」的另类数据。
MAD的计算逻辑:先算中位数,再算每个点到中位数的绝对偏差,取这些偏差的中位数。然后设定一个阈值(通常3-5倍MAD),超过就算异常。
def detect_outliers_mad(series, threshold=3.5):
median = series.median()
mad = np.median(np.abs(series - median))
modified_z_scores = 0.6745 * (series - median) / mad
return np.abs(modified_z_scores) > threshold
经验之谈:处理卫星图像衍生的植被指数数据时,MAD方法帮我精准识别出了云层遮挡导致的异常低值,而3-sigma和IQR都漏掉了。所以,多试几种方法,选最适合你数据特征的。
2.3 去极值与标准化
去极值,就是把异常值「拉回来」,而不是直接删掉。标准化,则是让不同量纲的数据能放在一起比较。
去极值(Winsorization)
说白了,就是把超出某个范围的值,强制设为边界值。比如,把超过99%分位数的值,全部替换为99%分位数处的值。
我一般在构建因子组合时用这个。因为我不想因为某一天的极端值,就让整个因子的权重发生剧烈变化。
def winsorize_series(series, lower=0.01, upper=0.99):
q_low = series.quantile(lower)
q_high = series.quantile(upper)
return series.clip(lower=q_low, upper=q_high)
标准化(Z-score / Min-Max)
标准化有两种主流方式:
- Z-score标准化:减去均值,除以标准差。结果变成均值为0、标准差为1的分布。适合数据大致对称的情况。
- Min-Max标准化:缩放到[0,1]区间。适合数据有明确上下界的情况,比如概率值。
# Z-score
df['factor_z'] = (df['factor'] - df['factor'].mean()) / df['factor'].std()
# Min-Max
df['factor_mm'] = (df['factor'] - df['factor'].min()) / (df['factor'].max() - df['factor'].min())
注意:标准化一定要在去极值之后做。否则,异常值会把均值和标准差拉偏,导致标准化后的数据失真。这个顺序我吃过亏,现在每次都提醒自己。
2.4 截面与时间序列对齐
这是多源数据融合里最头疼的一步。不同数据源的频率不一样:日频的股价数据、周频的供应链数据、月频的消费者信心指数。怎么对齐?
截面对齐
截面数据,就是同一时间点、不同股票的数据。对齐的关键是确保「时间戳」一致。我习惯用交易日历做对齐,把非交易日的数据剔除或前向填充。
# 假设有两个数据源,按日期和股票代码对齐
df_aligned = pd.merge(df_price, df_supply, on=['date', 'stock_code'], how='inner')
时间序列对齐
时间序列对齐,是把不同频率的数据统一到同一时间轴上。常用的方法:
- 前向填充(ffill):用上一个有效值填充缺失的时间点。适合低频数据对齐到高频。
- 后向填充(bfill):用下一个有效值填充。适合高频数据对齐到低频。
- 重采样(resample):把数据聚合到固定频率,比如把日频数据聚合成周频。
# 前向填充:将周频数据对齐到日频
df_weekly_ffill = df_weekly.resample('D').ffill()
# 重采样:将日频数据聚合成周频(取均值)
df_daily_resampled = df_daily.resample('W').mean()
我的习惯:对齐时,我一般先做时间序列对齐,再做截面对齐。因为时间序列对齐会引入新的缺失值,截面对齐时正好可以一并处理。顺序对了,能少踩很多坑。
本章知识体系
下面这张图,是我自己梳理的本章核心逻辑。你可以把它当作一个检查清单:拿到数据后,按这个流程走一遍,基本不会漏掉关键步骤。
嗯,以上就是本章的全部内容。数据清洗这件事,看似基础,但做得好不好,直接决定了后续因子研究的成败。我个人觉得,花70%的时间在数据准备上,30%的时间在建模上,这个比例是合理的。别急着跑模型,先把数据伺候舒服了。