第四章:财务数据清洗与预处理
做量化投资的朋友都知道一句话:垃圾进,垃圾出。我干了这么多年,见过太多人拿着漂亮模型往脏数据上一跑,结果惨不忍睹。数据清洗这步,说白了就是给财务数据「洗澡」。洗不干净,后面全是白费功夫。
4.1 缺失值处理:别让「空」坑了你
财务数据里缺失值太常见了。公司停牌、数据源断更、报表调整,都会留下空值。我刚开始做的时候,直接删掉有缺失的行,结果回测曲线漂亮得不行,实盘一跑就崩——后来才发现,那些缺失值往往出现在极端行情里。
⚠️ 注意: 千万别无脑删除缺失值。尤其是时间序列数据,删一行可能破坏整个序列的连续性。
我个人习惯分三步处理:
- 先诊断:看看缺失比例。低于5%的,用均值或中位数填充就行。超过30%的,我建议直接放弃这个字段。
- 再选方法:财务数据用前向填充(ffill)最稳妥。比如某公司一季度没披露营收,用上一季度的值顶上去,比瞎猜靠谱。
- 最后验证:填充完跑个简单统计,看看分布有没有突变。
import pandas as pd
import numpy as np
# 加载财务数据
df = pd.read_csv('financial_data.csv', index_col='date', parse_dates=True)
# 检查缺失比例
missing_ratio = df.isnull().mean()
print('缺失比例:\n', missing_ratio[missing_ratio > 0])
# 前向填充(财务数据常用)
df.fillna(method='ffill', inplace=True)
# 如果开头还有缺失,用后向填充兜底
df.fillna(method='bfill', inplace=True)
💡 我的经验: 遇到那种连续缺失超过3期的数据,我一般直接扔掉。为什么?因为连续缺失说明这家公司信息披露有问题,留着也是隐患。
4.2 异常值检测:揪出「捣乱分子」
异常值是什么?就是那些明显离谱的数据。比如某公司毛利率突然飙到200%,或者营收一夜之间翻了100倍。你想想看,这合理吗?
我曾经在回测一个价值因子时,发现某只股票的市盈率突然变成负数——不是亏损那种负,而是数据录入错误导致的-9999。这个异常值直接拉偏了整个因子的分布,差点让我放弃一个本来有效的策略。
常用的检测方法就三种:
| 方法 | 适用场景 | 阈值 |
|---|---|---|
| Z-score | 正态分布的数据 | |Z| > 3 |
| IQR(四分位距) | 偏态分布的数据 | Q1 - 1.5*IQR 或 Q3 + 1.5*IQR |
| MAD(中位数绝对偏差) | 含较多异常值的数据 | MAD > 3.5 |
from scipy import stats
# Z-score 方法
z_scores = np.abs(stats.zscore(df['pe_ratio']))
df_clean = df[(z_scores < 3)]
# IQR 方法
Q1 = df['pe_ratio'].quantile(0.25)
Q3 = df['pe_ratio'].quantile(0.75)
IQR = Q3 - Q1
df_clean = df[(df['pe_ratio'] >= Q1 - 1.5*IQR) &
(df['pe_ratio'] <= Q3 + 1.5*IQR)]
🔑 核心原则: 异常值不一定要删除。有时候异常值本身就是信号——比如突然的业绩暴雷。我建议先标记出来,单独分析。
4.3 数据标准化与归一化:让不同量纲的数据「对话」
这个问题我踩过坑。早期做多因子模型时,把市盈率(几十倍)和市净率(几倍)直接放一起算综合得分,结果市盈率完全主导了结果。说白了,量纲不同,没法直接比。
标准化和归一化,就是解决这个问题的。我一般这样选:
- 归一化(Min-Max):把数据压到[0,1]区间。适合分布比较均匀的数据,比如换手率、波动率。
- 标准化(Z-score):变成均值为0、标准差为1的分布。适合有异常值的数据,比如市盈率、市净率。
- RobustScaler:用中位数和四分位距做。我个人的最爱,对异常值不敏感。
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# 标准化(Z-score)
scaler_std = StandardScaler()
df['pe_standardized'] = scaler_std.fit_transform(df[['pe_ratio']])
# 归一化(Min-Max)
scaler_mm = MinMaxScaler()
df['turnover_normalized'] = scaler_mm.fit_transform(df[['turnover_rate']])
# RobustScaler(推荐)
scaler_robust = RobustScaler()
df['pb_robust'] = scaler_robust.fit_transform(df[['pb_ratio']])
💡 避坑指南: 我曾经在回测时对整个数据集做了标准化,结果引入了未来信息。正确的做法是:用训练集的参数去标准化测试集。记住,测试集的数据在回测时是「看不到」的。
4.4 时间序列重采样:对齐不同频率的数据
财务数据有个麻烦事:不同指标更新频率不一样。营收数据按季度出,股价数据每天都有,估值指标可能每周变一次。怎么把它们对齐?重采样。
嗯,这里要注意:重采样不是简单的插值。财务数据有它自己的节奏。
- 日频转周频:用收盘价取最后一天,成交量求和。
- 周频转月频:财务指标取月末值,避免月中波动干扰。
- 季频转日频:这个最坑。我建议用前向填充,别用插值。因为季报数据在发布前是未知的,插值等于用了未来数据。
# 日频转周频
df_weekly = df.resample('W').agg({
'close': 'last', # 收盘价取最后一天
'volume': 'sum', # 成交量求和
'pe_ratio': 'last' # 市盈率取周末值
})
# 季频转日频(前向填充)
df_quarterly = df.resample('Q').last()
df_daily = df_quarterly.resample('D').ffill()
⚠️ 重要提醒: 重采样时一定要搞清楚「时间戳」的含义。财务数据的时间戳通常是「报告期」,不是「发布日期」。两者可能差好几个月。我建议用「发布日期」做对齐,否则会引入前瞻偏差。
4.5 本章知识体系
下面这张图是我自己梳理的清洗流程,每次做项目前都会看一眼,防止漏步骤。
这四步走完,数据基本就干净了。我个人习惯在清洗完后,再跑一遍描述性统计,看看每个字段的均值、标准差有没有异常变化。如果有,回头检查一下是不是哪步操作出了问题。
记住一句话:数据清洗花的时间,会在模型效果上十倍地还给你。别嫌麻烦。