4. 数据准备与预处理:数据源选择、清洗、去极值、标准化

做因子研究这么多年,我越来越觉得:数据准备比因子本身更重要。你想想看,再牛的因子,如果喂进去的是脏数据,出来的结果就是垃圾。这不是危言耸听,我在项目里踩过的坑,十有八九都出在数据预处理这个环节。

这一章,咱们就把数据准备这摊事儿掰开揉碎讲清楚。从选数据源开始,到清洗、去极值、标准化,每一步我都会把实际项目中遇到的坑和解决方案告诉你。

数据准备与预处理 数据源选择 Wind / Bloomberg Tushare / 聚宽 交易所原始数据 数据清洗 缺失值处理 异常值检测 重复数据去重 去极值 MAD方法 百分位截断 3σ原则 标准化 Z-score标准化 Min-Max归一化 Rank标准化

4.1 数据源选择:别在源头就输了

数据源的选择,直接决定了你后面所有工作的质量。我个人习惯把数据源分成三个梯队:

梯队 代表 特点 适用场景
第一梯队 Wind、Bloomberg 权威、完整、贵 机构实盘、正式研究
第二梯队 Tushare、聚宽、RiceQuant 方便、便宜、够用 个人研究、策略回测
第三梯队 交易所原始数据、CSMAR 原始、干净、难获取 学术研究、深度分析
我的建议:刚开始做因子研究,用第二梯队的数据源就够了。Tushare pro 的积分制虽然有点烦,但数据质量在免费数据里算是最好的。等你要上实盘了,再考虑上 Wind。

这里有个坑:不同数据源对同一只股票的同一天数据可能不一样。我曾经对比过 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)
经验值:n 取 5 比较合适。取 3 的话,很多因子会被过度压缩,影响区分度。取 7 以上,去极值效果就不明显了。

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 标准化要注意的坑

嗯,这里要注意:标准化一定要在横截面上做,不要在时间序列上做。什么意思?就是每个交易日,对全市场股票的因子值做一次标准化。而不是对某只股票的历史数据做标准化。

为什么?因为拥挤度检测是横截面概念。我们要看的是今天所有股票在这个因子上的分布情况,而不是某只股票相对于它自己的历史位置。

我曾经犯过的错:刚开始做因子研究时,我在时间序列上做了 Z-score 标准化,结果拥挤度指标完全失效。后来才发现,时间序列标准化抹掉了横截面上的差异,拥挤度信号全没了。这个坑,希望你别再踩。

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