数据清洗与预处理实战:缺失值处理、异常值检测、标准化与归一化
做量化金融,有一句话我特别认同:垃圾进,垃圾出。你因子挖得再漂亮,模型选得再花哨,原始数据一塌糊涂,结果就是白忙活。今天这一章,咱们就聊聊数据清洗与预处理——说白了,就是给原始金融数据「洗个澡」,让它能上桌见人。
我个人习惯,拿到数据的第一件事不是跑模型,而是先做三件事:处理缺失值、揪出异常值、统一量纲。这三步走完,后面的工作才谈得上靠谱。
核心逻辑:金融数据天生不完美。停牌、涨跌停、节假日、数据源错误……这些都会导致数据缺失或异常。预处理不是可选项,是必选项。
一、缺失值处理:别让「空」坑了你
金融数据里缺失值太常见了。某只股票停牌一天,当天所有行情数据都是NaN。或者某个财务指标只有季报才更新,中间月份就是空的。怎么处理?三种思路。
1. 直接删除
最简单粗暴。如果缺失比例很小(比如低于5%),而且缺失是随机的,直接删掉整行或整列就行。但要注意——金融数据里的缺失往往不是随机的。比如涨跌停时成交量会异常,这时候删掉反而会引入偏差。
我曾经踩过这个坑:做A股因子回测时,直接删除了所有含缺失值的交易日。结果发现回测收益虚高——因为删掉的全是市场波动大的日子。后来我学乖了,先分析缺失原因再决定。
import pandas as pd
import numpy as np
# 删除含缺失值的行
df.dropna(axis=0, how='any', inplace=True)
# 删除缺失比例超过30%的列
df.dropna(axis=1, thresh=len(df) * 0.7, inplace=True)
2. 填充
更常用的方法。金融数据有很强的时序特性,所以填充策略也得讲究。
- 前向填充(ffill):用上一个有效值填充。适合日频数据,比如今天收盘价缺失,用昨天的。我个人最常用这个。
- 后向填充(bfill):用下一个有效值填充。适合某些「事后补录」的场景。
- 均值/中位数填充:用整体统计量填充。但要注意——如果数据有趋势,均值填充会扭曲序列。
# 前向填充:用上一个交易日的数据
df['close'].fillna(method='ffill', inplace=True)
# 按股票分组填充
df['volume'] = df.groupby('stock_id')['volume'].transform(lambda x: x.fillna(method='ffill'))
3. 插值
比简单填充更「聪明」一点。它利用前后数据点,通过数学方法估算缺失值。线性插值最常用,时间序列里也可以用时间权重插值。
# 线性插值
df['close'].interpolate(method='linear', inplace=True)
# 时间插值(适用于时间索引)
df['close'].interpolate(method='time', inplace=True)
我的建议:对于日频价格数据,前向填充 + 线性插值组合使用效果不错。先ffill处理停牌导致的连续缺失,再interpolate平滑处理单点缺失。
二、异常值检测:揪出那些「离谱」的数据
金融数据里经常出现「离谱值」。比如某只股票突然涨了1000%,或者成交量比平时大了100倍。这些可能是数据录入错误,也可能是真实事件(比如借壳上市)。但不管怎样,你得先发现它们。
1. 3-sigma 法则
假设数据服从正态分布,超过均值±3倍标准差的值就是异常。简单,但有个问题——金融数据往往不服从正态分布,厚尾特征明显。所以3-sigma会漏掉很多极端值,也会误伤一些正常值。
def detect_outliers_3sigma(series):
mean = series.mean()
std = series.std()
lower = mean - 3 * std
upper = mean + 3 * std
return (series < lower) | (series > upper)
2. IQR 方法
基于分位数,对分布形态不敏感。用Q1和Q3计算四分位距,超出Q1-1.5*IQR或Q3+1.5*IQR的算异常。我个人更推荐这个,因为它不受极端值本身的影响。
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)
3. MAD 方法
中位数绝对偏差。比标准差更稳健,对异常值不敏感。你想想看,如果数据里已经有异常值了,用标准差去算阈值,阈值本身就被污染了。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
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 3-sigma | 计算简单,直观 | 对非正态分布效果差 | 近似正态的指标(如市值对数) |
| IQR | 稳健,不受极端值影响 | 对尾部信息不敏感 | 收益率、换手率等厚尾分布 |
| MAD | 最稳健,抗污染能力强 | 计算稍复杂 | 含较多异常值的脏数据 |
实战经验:我在做高频因子时,用MAD检测订单流异常。因为高频数据里异常值特别多,3-sigma基本没用,IQR也经常误判。MAD配合合适的阈值,效果最好。
三、标准化与归一化:让不同量纲的数据「对话」
你想想看,股价是几十块,成交量是几百万手,市盈率是十几倍。这三个量纲完全不同的指标,怎么放在一起比较?标准化和归一化就是干这个的。
1. Z-score 标准化
把数据变成均值为0、标准差为1的分布。公式很简单:(x - mean) / std。适合数据大致对称、没有太多极端值的情况。
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
df['zscore_volume'] = scaler.fit_transform(df[['volume']])
但要注意——金融数据经常有极端值。如果直接用Z-score,极端值会被压缩得很厉害,反而失去了信息。我一般先做异常值截断,再做Z-score。
2. Min-Max 归一化
把数据缩放到[0,1]区间。公式:(x - min) / (max - min)。适合有明确上下界的数据,比如涨跌幅限制在±10%的A股。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
df['minmax_return'] = scaler.fit_transform(df[['return']])
我曾经犯过的错:用Min-Max处理全市场市盈率。结果新上市的公司市盈率特别高,把整个区间拉宽了,导致大部分股票被压缩到0.1以下。后来我改用分位数截断后再归一化,效果好多了。
3. Rank 排序归一化
把数据按大小排序,然后映射到[0,1]区间。说白了就是「百分位排名」。这个方法对异常值最不敏感,而且能保留数据的相对顺序信息。做截面因子时我特别喜欢用。
# 按日截面排序
df['rank_factor'] = df.groupby('date')['factor_value'].rank(pct=True)
# 或者用 scipy
from scipy.stats import rankdata
df['rank_factor'] = df.groupby('date')['factor_value'].transform(
lambda x: rankdata(x) / len(x)
)
| 方法 | 输出范围 | 对异常值敏感度 | 保留分布形状 | 推荐场景 |
|---|---|---|---|---|
| Z-score | 无固定范围 | 高 | 是 | 对称分布、无极端值 |
| Min-Max | [0, 1] | 极高 | 是 | 有明确上下界 |
| Rank | [0, 1] | 极低 | 否(仅保留顺序) | 截面因子、多因子组合 |
我的习惯:做多因子模型时,所有因子先做Rank归一化,再转Z-score。这样既消除了量纲差异,又保留了因子间的相对强弱关系。你可以试试看。
嗯,数据清洗和预处理这部分,说白了就是「磨刀不误砍柴工」。你花20%的时间做预处理,后面80%的工作会顺畅很多。反过来,预处理偷懒,后面模型调参、因子筛选全都会出问题。我个人觉得,这是整个因子挖掘流程里最值得花功夫的环节。
公众号:蓝海资料掘金营,微信deep3321