数据清洗与预处理:金融时间序列的缺失值处理、异常值检测、去极值、标准化与归一化
做量化风控这些年,我踩过最大的坑,就是数据没洗干净就往上怼模型。你想想看,金融时间序列这东西,脏数据比你想的要多得多。停牌、涨跌停、数据源中断、交易所异常……随便哪个都能让你的模型翻车。
今天咱们就聊聊数据清洗与预处理。我个人习惯把这步叫做「数据炼油」——原油不能直接烧,得先提纯。金融数据也一样。
核心观点:数据清洗不是可选项,是必选项。模型再牛,也救不了垃圾数据。
一、缺失值处理:别让空值毁了你的回测
金融时间序列的缺失值,说白了就是某个时间点没数据。原因很多:停牌、节假日、数据源抽风。我在项目中遇到过最离谱的一次,某只股票因为交易所系统升级,连续三天没有成交数据,回测直接崩了。
处理缺失值,常用的方法就这几种:
- 向前填充(ffill):用上一个有效值填充。适合停牌场景,因为停牌期间价格理论上不变。
- 向后填充(bfill):用下一个有效值填充。适合数据延迟到达的情况。
- 线性插值:假设缺失期间数据线性变化。适合短时间缺失。
- 删除:缺失比例太高,直接扔掉。我一般超过20%就考虑删了。
import pandas as pd
import numpy as np
# 模拟一个含缺失值的日收益率序列
dates = pd.date_range('2024-01-01', periods=10, freq='D')
returns = pd.Series([0.01, np.nan, -0.02, np.nan, 0.03, 0.01, np.nan, -0.01, 0.02, np.nan], index=dates)
# 向前填充(最常用)
returns_ffill = returns.ffill()
# 线性插值
returns_interp = returns.interpolate(method='linear')
# 删除缺失值
returns_drop = returns.dropna()
print('原始数据:', returns)
print('向前填充:', returns_ffill)
print('线性插值:', returns_interp)
我的习惯:对于日频数据,缺失1-2天用ffill;缺失3-5天用插值;超过5天直接删除。别问为什么,问就是血的教训。
二、异常值检测:揪出那些「妖股」时刻
金融数据里的异常值,可能是乌龙指、数据录入错误,也可能是真正的极端行情。怎么区分?我教你一个简单粗暴的方法。
常用的检测方法:
- 3σ原则:超过均值±3倍标准差的数据视为异常。适合正态分布的数据。
- IQR方法:低于Q1-1.5×IQR或高于Q3+1.5×IQR的数据视为异常。更稳健,不受极端值影响。
- MAD方法:基于中位数的绝对偏差。比3σ更抗干扰。
def detect_outliers_iqr(data, factor=1.5):
"""使用IQR方法检测异常值"""
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - factor * IQR
upper_bound = Q3 + factor * IQR
return (data < lower_bound) | (data > upper_bound)
# 模拟收益率数据
np.random.seed(42)
returns = np.random.normal(0, 0.02, 1000)
# 人为加入几个异常值
returns[100] = 0.15
returns[500] = -0.12
returns[800] = 0.18
outliers = detect_outliers_iqr(pd.Series(returns))
print(f'检测到异常值数量: {outliers.sum()}')
print(f'异常值索引: {np.where(outliers)[0]}')
注意:别一看到异常值就删。我曾经在2015年股灾期间,把真正的市场崩盘当异常值删了,结果模型完全失效。先搞清楚异常值产生的原因,再决定怎么处理。
三、去极值:给数据「剪剪枝」
去极值和异常值检测不一样。异常值检测是「揪出来」,去极值是「压回去」。说白了,就是把太离谱的数据拉回到合理范围。
常用的去极值方法:
- Winsorize(缩尾处理):把超出上下限的值替换为上下限值。比如把超过99%分位数的值替换为99%分位数。
- 截断处理:直接删除超出范围的数据。简单粗暴,但会损失样本量。
- MAD截断:基于中位数绝对偏差的截断方法。
from scipy.stats.mstats import winsorize
# 模拟因子数据
np.random.seed(42)
factor = np.random.normal(0, 1, 1000)
factor[0] = 10 # 极端值
factor[1] = -8 # 极端值
# Winsorize处理(上下各1%)
factor_winsorized = winsorize(factor, limits=[0.01, 0.01])
print(f'原始数据范围: [{factor.min():.2f}, {factor.max():.2f}]')
print(f'处理后范围: [{factor_winsorized.min():.2f}, {factor_winsorized.max():.2f}]')
我的经验:去极值的阈值别设太死。因子数据一般用1%和99%分位数,收益率数据用0.5%和99.5%。具体调参看你的数据分布。
四、标准化与归一化:让数据站在同一起跑线
标准化和归一化,很多人搞混。我简单解释一下:
- 标准化(Standardization):减去均值,除以标准差。结果均值为0,标准差为1。适合数据近似正态分布的情况。
- 归一化(Normalization):缩放到[0,1]区间。减去最小值,除以最大值减最小值。适合数据有明确上下界的情况。
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 模拟多因子数据
np.random.seed(42)
factors = np.random.randn(100, 3) # 3个因子,100个样本
factors[:, 0] = factors[:, 0] * 10 + 50 # 因子1:均值50,标准差10
factors[:, 1] = factors[:, 1] * 2 + 10 # 因子2:均值10,标准差2
factors[:, 2] = factors[:, 2] * 0.5 + 1 # 因子3:均值1,标准差0.5
# 标准化
scaler_std = StandardScaler()
factors_std = scaler_std.fit_transform(factors)
# 归一化
scaler_norm = MinMaxScaler()
factors_norm = scaler_norm.fit_transform(factors)
print('标准化后均值:', factors_std.mean(axis=0))
print('标准化后标准差:', factors_std.std(axis=0))
print('归一化后范围:', factors_norm.min(axis=0), factors_norm.max(axis=0))
选择建议:
- 做PCA、聚类、回归模型 → 用标准化
- 做神经网络、距离度量 → 用归一化
- 因子数据本身有经济含义(如市盈率)→ 慎用归一化,会丢失量纲信息
五、知识体系总览
下面这张图,是我自己总结的数据清洗流程。你照着做,至少能避开80%的坑。
六、实战中的避坑指南
最后,分享几个我踩过的坑:
- 别在回测里用未来数据:标准化时如果用全样本的均值和标准差,就相当于用了未来信息。一定要用滚动窗口或扩展窗口。
- 因子数据要分行业处理:不同行业的因子分布差异很大,混在一起标准化会出问题。我习惯先分行业,再标准化。
- 收益率数据别轻易去极值:极端收益率可能是真实的市场信息,比如黑天鹅事件。去极值前先看看是不是真的异常。
- 缺失值处理要记录:我每次处理完数据,都会保存一份处理日志。哪天模型出问题了,回头查查是不是数据清洗的锅。
曾经有一次,我因为忘记处理停牌期间的缺失值,导致因子暴露度计算完全错误,回测结果漂亮得不像话。实盘一跑,亏得亲妈都不认识。从那以后,数据清洗成了我建模流程里的第一道关卡,谁也别想跳过。
数据清洗这步,看着简单,其实最考验功底。你想想看,模型再复杂,代码再漂亮,数据是脏的,一切都是白搭。希望今天的内容能帮你少走些弯路。