4. 数据准备与预处理:数据源选择、清洗、去极值、标准化
做因子研究这么多年,我越来越觉得:数据准备比因子本身更重要。你想想看,再牛的因子,如果喂进去的是脏数据,出来的结果就是垃圾。这不是危言耸听,我在项目里踩过的坑,十有八九都出在数据预处理这个环节。
这一章,咱们就把数据准备这摊事儿掰开揉碎讲清楚。从选数据源开始,到清洗、去极值、标准化,每一步我都会把实际项目中遇到的坑和解决方案告诉你。
4.1 数据源选择:别在源头就输了
数据源的选择,直接决定了你后面所有工作的质量。我个人习惯把数据源分成三个梯队:
| 梯队 | 代表 | 特点 | 适用场景 |
|---|---|---|---|
| 第一梯队 | Wind、Bloomberg | 权威、完整、贵 | 机构实盘、正式研究 |
| 第二梯队 | Tushare、聚宽、RiceQuant | 方便、便宜、够用 | 个人研究、策略回测 |
| 第三梯队 | 交易所原始数据、CSMAR | 原始、干净、难获取 | 学术研究、深度分析 |
这里有个坑:不同数据源对同一只股票的同一天数据可能不一样。我曾经对比过 Wind 和 Tushare 的复权因子,差异最大能到 0.5%。别小看这 0.5%,在拥挤度检测这种精细活里,足以让结果跑偏。
4.2 数据清洗:脏数据是因子研究的头号杀手
数据清洗说白了就三件事:补缺失、去重复、纠错误。听起来简单,做起来全是细节。
4.2.1 缺失值处理
缺失值在因子数据里太常见了。停牌、新股上市、数据源漏传,都会导致缺失。我的处理原则是:
- 横截面缺失 < 20%:用行业均值填充。注意是行业均值,不是全市场均值。不同行业的因子分布差异很大。
- 横截面缺失 > 20%:直接剔除该股票。缺失太多说明数据质量有问题,强行填充只会引入噪声。
- 时间序列缺失:用前向填充(ffill),最多填充 5 个交易日。超过 5 天就标记为缺失。
4.2.2 重复数据与错误数据
重复数据通常发生在数据拼接的时候。比如你从两个数据源分别拉了数据,合并时没注意索引,就会出现重复行。我的做法是:
# 按股票代码和日期去重,保留第一条
df = df.drop_duplicates(subset=['stock_code', 'trade_date'], keep='first')
错误数据更隐蔽。比如某只股票的市盈率突然变成 99999,或者市净率变成负数。这种明显不合理的数据,我一般用 3σ 原则 先筛一遍,再结合业务逻辑判断。
4.3 去极值:别让极端值绑架你的因子
去极值,就是把那些离谱的极端值拉回来。为什么要做?因为拥挤度检测里要用到很多统计量(均值、标准差、相关性),一个极端值就能把这些统计量带偏。
常用的去极值方法有三种,我按推荐程度排序:
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| MAD 方法 | 基于中位数绝对偏差 | 稳健,不受极端值影响 | 计算稍复杂 |
| 百分位截断 | 直接截断上下 1% 或 5% | 简单直观 | 可能丢失信息 |
| 3σ 原则 | 超过均值±3σ的替换为边界值 | 经典,容易理解 | 对非正态分布效果差 |
我个人最常用的是 MAD 方法。为什么?因为因子数据很少是正态分布,用 3σ 原则会误杀很多正常值。MAD 基于中位数,天然对异常值不敏感。
def winsorize_mad(series, n=5):
"""
MAD去极值
n: 阈值,通常取 3-5
"""
median = series.median()
mad = (series - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
4.4 标准化:让不同因子站在同一起跑线
标准化是因子预处理最后一步,也是最重要的一步。目的很简单:让不同量纲的因子可以放在一起比较。
比如市盈率(PE)从 5 到 100,换手率从 0.1% 到 10%,如果不标准化,换手率的波动会完全淹没市盈率的信息。
4.4.1 三种标准化方法
| 方法 | 公式 | 输出范围 | 适用场景 |
|---|---|---|---|
| Z-score | (x - μ) / σ | 理论上无界 | 因子值近似正态分布 |
| Min-Max | (x - min) / (max - min) | [0, 1] | 因子值有明确边界 |
| Rank | 排序后映射到 [0, 1] | [0, 1] | 因子分布偏态严重 |
我个人的经验是:做拥挤度检测,优先用 Rank 标准化。原因很简单——拥挤度检测关注的是因子在横截面上的相对位置,而不是绝对数值。Rank 标准化天然保留了排序信息,而且不受极端值影响。
def rank_standardize(series):
"""
Rank标准化:将因子值转换为百分位排名
"""
rank = series.rank()
return (rank - 1) / (len(rank) - 1)
4.4.2 标准化要注意的坑
嗯,这里要注意:标准化一定要在横截面上做,不要在时间序列上做。什么意思?就是每个交易日,对全市场股票的因子值做一次标准化。而不是对某只股票的历史数据做标准化。
为什么?因为拥挤度检测是横截面概念。我们要看的是今天所有股票在这个因子上的分布情况,而不是某只股票相对于它自己的历史位置。
4.5 完整的预处理流水线
最后,我把整个预处理流程串起来,形成一个可复用的流水线:
def factor_preprocessing(df, factor_name):
"""
因子预处理完整流水线
"""
# 1. 去重
df = df.drop_duplicates(subset=['stock_code', 'trade_date'])
# 2. 缺失值处理(行业均值填充)
df[factor_name] = df.groupby(['trade_date', 'industry'])[factor_name].transform(
lambda x: x.fillna(x.mean())
)
# 3. 去极值(MAD方法)
df[factor_name] = df.groupby('trade_date')[factor_name].transform(
lambda x: winsorize_mad(x, n=5)
)
# 4. 标准化(Rank标准化)
df[factor_name + '_std'] = df.groupby('trade_date')[factor_name].transform(
rank_standardize
)
return df
这个流水线我用了好几年,在多个策略上都验证过效果。当然,具体参数(比如 MAD 的 n 值、缺失值填充方式)可以根据你的因子特性微调。
好了,数据准备这块就讲到这里。记住一句话:好的数据预处理,能让一个平庸的因子发挥出 80 分的水平;差的数据预处理,能让一个 90 分的因子直接废掉。下一章咱们进入正题,开始讲拥挤度检测的核心指标。不过在那之前,建议你把今天讲的这些方法在你的数据上跑一遍,感受一下每一步带来的变化。
公众号:蓝海数据掘金营,微信deep3321