数据清洗与预处理:打好建模的地基
做量化金融这些年,我越来越觉得数据清洗比模型本身更重要。你想想看,再牛的Transformer,喂进去一堆脏数据,出来的也是垃圾。今天咱们就聊聊时序数据清洗的三个核心环节:缺失值、异常值,还有对齐与重采样。
核心观点:数据清洗不是体力活,而是决定模型上限的关键步骤。我见过太多团队花80%时间调参,却只花20%时间洗数据,结果模型上线就崩。
一、缺失值处理:别让数据出现「断片」
金融时序数据里,缺失值几乎是家常便饭。节假日停盘、数据源故障、网络延迟,随便哪个都能让你的时间序列出现空洞。我个人习惯是,拿到数据第一件事就是检查缺失率。
小技巧:用 df.isnull().sum() / len(df) 快速查看每列的缺失比例。如果超过30%,我建议你重新考虑这个特征是否值得保留。
1.1 前向填充(Forward Fill)
说白了就是用上一个有效值填充当前缺失值。这在金融数据里特别实用——比如股票价格,如果某分钟没成交,用上一分钟的收盘价是合理的。
import pandas as pd
import numpy as np
# 模拟分钟级股价数据
dates = pd.date_range('2024-01-01', periods=10, freq='min')
prices = [100, 101, np.nan, np.nan, 104, 105, np.nan, 107, 108, 109]
df = pd.DataFrame({'price': prices}, index=dates)
# 前向填充
df['price_ffill'] = df['price'].ffill()
print(df)
我在项目中遇到过一个问题:如果连续缺失太多,前向填充会导致数据「平台化」,也就是一段时间的价格完全不变。这会影响波动率计算。所以我会设置一个阈值,比如最多连续填充5个点,超过就改用插值。
1.2 插值法
插值比前向填充更「聪明」一些。它利用缺失点前后的数据,拟合出一条曲线来估算中间值。常用的有线性插值、多项式插值、样条插值。
# 线性插值
df['price_interp'] = df['price'].interpolate(method='linear')
print(df)
嗯,这里要注意:插值法假设数据是平滑变化的。如果遇到跳空开盘这种场景,插值反而会引入错误。我建议在日频以上数据用插值,分钟级数据用前向填充。
避坑指南:我曾经在回测中用了多项式插值填充收益率数据,结果模型在实盘时表现极差。后来发现多项式插值产生了「过拟合」的填充值,把噪声当成了规律。从此以后,金融时序我基本只用线性插值或前向填充。
二、异常值检测:揪出那些「捣乱」的数据点
异常值在金融数据里太常见了。乌龙指、数据录入错误、极端行情,都会产生离谱的数据点。如果不处理,模型会拼命去拟合这些异常,结果就是泛化能力极差。
2.1 3-sigma 法
这个方法假设数据服从正态分布,超过均值±3倍标准差的值就是异常。简单粗暴,但有效。
def detect_3sigma(series):
mean = series.mean()
std = series.std()
lower = mean - 3 * std
upper = mean + 3 * std
return series[(series < lower) | (series > upper)]
# 示例
returns = np.random.randn(1000) * 0.02 # 模拟收益率
returns[500] = 0.5 # 人为添加异常
outliers = detect_3sigma(pd.Series(returns))
print(f"检测到 {len(outliers)} 个异常值")
但说实话,金融数据很少是正态分布的。收益率有尖峰厚尾特征,3-sigma 法会漏掉很多尾部异常。所以我一般只把它作为初步筛查。
2.2 IQR 法
IQR(四分位距法)不依赖正态分布假设,更稳健。它用 Q1 - 1.5*IQR 和 Q3 + 1.5*IQR 作为边界。
def detect_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[(series < lower) | (series > upper)]
outliers_iqr = detect_iqr(pd.Series(returns))
print(f"IQR法检测到 {len(outliers_iqr)} 个异常值")
我个人更偏爱 IQR 法。它不受极端值影响,而且 1.5 倍这个系数在金融领域经过大量验证,效果不错。
2.3 MAD 法
MAD(中位数绝对偏差)是另一种稳健方法。它用中位数代替均值,用 MAD 代替标准差。
def detect_mad(series, threshold=3.5):
median = series.median()
mad = np.median(np.abs(series - median))
modified_z_scores = 0.6745 * (series - median) / mad
return series[np.abs(modified_z_scores) > threshold]
outliers_mad = detect_mad(pd.Series(returns))
print(f"MAD法检测到 {len(outliers_mad)} 个异常值")
经验之谈:三种方法各有优劣。我通常的做法是:先用 IQR 做快速筛查,再用 MAD 做精细确认。如果两种方法都标记为异常,那基本可以确定是脏数据了。
三、数据对齐与重采样:让不同频率的数据「步调一致」
做多因子模型时,你经常会遇到不同频率的数据。股票价格是日频,宏观经济数据是月频,舆情数据可能是不规则时间间隔。怎么把它们对齐到同一个时间轴上?这就是对齐与重采样的工作。
3.1 时间对齐
对齐的核心是选择一个基准时间轴,然后把所有数据都映射上去。我习惯用交易时间作为基准,因为金融数据天然以交易时间为锚。
# 创建两个不同频率的时间序列
dates1 = pd.date_range('2024-01-01', periods=5, freq='D')
dates2 = pd.date_range('2024-01-01', periods=3, freq='2D')
df1 = pd.DataFrame({'price': [100, 102, 101, 103, 105]}, index=dates1)
df2 = pd.DataFrame({'volume': [1000, 1500, 1200]}, index=dates2)
# 对齐到日频
aligned = pd.merge(df1, df2, left_index=True, right_index=True, how='left')
print(aligned)
你想想看,如果不对齐就直接建模,模型会以为不同时间点的数据是同时发生的,这会导致严重的时序错位问题。
3.2 降采样与升采样
降采样是把高频数据变低频,比如分钟级变日频。升采样则相反,把低频数据变高频。
# 降采样:分钟级 -> 小时级
minute_data = pd.date_range('2024-01-01', periods=60, freq='min')
prices = np.random.randn(60).cumsum() + 100
df_min = pd.DataFrame({'price': prices}, index=minute_data)
hourly = df_min.resample('H').agg({
'price': 'ohlc' # 开盘、最高、最低、收盘
})
print(hourly.head())
# 升采样:日频 -> 小时级(用前向填充)
daily_data = pd.date_range('2024-01-01', periods=5, freq='D')
df_day = pd.DataFrame({'rate': [0.03, 0.035, 0.032, 0.038, 0.036]}, index=daily_data)
hourly_rate = df_day.resample('H').ffill()
print(hourly_rate.head())
注意:升采样时千万别用插值法填充利率、汇率这类数据。它们的变化是离散的,插值会创造出实际上不存在的中间值。我曾经犯过这个错,回测时收益率曲线漂亮得很,实盘直接被打脸。
四、实战建议:清洗流程怎么搭?
说了这么多,到底怎么落地?我分享一个自己常用的清洗流水线:
- 第一步:缺失率检查 —— 超过30%直接剔除特征
- 第二步:异常值检测 —— IQR + MAD 双重验证
- 第三步:异常值处理 —— 用前后有效值替换,或者直接删除
- 第四步:缺失值填充 —— 分钟级用前向填充,日频以上用线性插值
- 第五步:对齐与重采样 —— 统一到目标频率,注意升采样别用插值
最后提醒:清洗完的数据一定要做可视化检查。画个时间序列图,看看有没有明显的「断崖」或「平台」。眼睛有时候比统计指标更管用。
数据清洗这件事,说白了就是「垃圾进,垃圾出」的反面。你花在清洗上的每一分钟,都会在模型性能上得到回报。别嫌麻烦,这是量化交易里最值得投入的环节之一。