4. 数据清洗与预处理:缺失值处理、异常值检测与处理、数据对齐与重采样、复权因子计算

数据清洗,说白了就是给原始数据「洗澡」。我见过太多人,因子算得飞起,结果一复盘,发现数据里全是坑。嗯,今天我们就来把这些坑填平。

核心观点:数据清洗不是可选项,是必选项。你花在清洗上的每一分钟,都会在后续建模中十倍回报给你。

4.1 缺失值处理:别让「空」毁了你的因子

缺失值,就是数据表格里那些空着的格子。为什么会空?原因很多:停牌、数据源没更新、或者干脆就是采集时漏了。我个人习惯,拿到数据第一件事就是看缺失率。

我的经验:缺失率低于5%,直接填充问题不大。超过20%,你得想想这个数据源是不是靠谱。

处理缺失值,常用的方法就三种:

  • 删除法:简单粗暴,直接扔掉有缺失的行。适合缺失率极低的情况。
  • 填充法:用均值、中位数、前值或后值填充。我最常用的是前值填充,因为金融数据往往有连续性。
  • 插值法:线性插值、多项式插值。适合时间序列数据。

举个例子,用前值填充停牌日的数据:

import pandas as pd

# 假设df是日频数据,停牌日价格为NaN
df['close'].fillna(method='ffill', inplace=True)
# 或者用limit限制最多填充几天
df['close'].fillna(method='ffill', limit=5, inplace=True)

注意:我曾经在回测中用了均值填充,结果把一只连续跌停的股票填成了平稳下跌,因子信号完全失真。切记,金融数据用前值或后值填充更合理。

4.2 异常值检测与处理:别让「极端值」带偏你的模型

异常值,就是那些明显偏离正常范围的数据点。比如某只股票一天涨了1000%,这明显是数据错误。你想想看,如果不处理,你的因子会被这些异常值拉偏到什么程度?

检测异常值,我常用两种方法:

  • 3σ原则:超出均值±3倍标准差的数据,视为异常。适合正态分布的数据。
  • IQR方法:四分位距法。超出Q1-1.5*IQR或Q3+1.5*IQR的数据,视为异常。更稳健,不受极端值影响。

处理方式呢?

  1. 截尾处理:把异常值替换成边界值。比如把超过99%分位数的值,直接设为99%分位数。
  2. 直接删除:如果异常值明显是数据错误,删掉最干净。
  3. 标记为缺失:先标记,再走缺失值处理流程。

代码示例:

def winsorize_series(s, limits=[0.01, 0.99]):
    """截尾处理"""
    lower = s.quantile(limits[0])
    upper = s.quantile(limits[1])
    return s.clip(lower, upper)

# 对收益率序列做截尾
df['return'] = winsorize_series(df['return'])

避坑指南:我曾经对市值因子做截尾,结果把大市值股票全截没了,因子失效。后来我改用分组截尾——按行业分组,每组分别做截尾。效果好了很多。

4.3 数据对齐与重采样:让不同频率的数据「对上话」

做多因子模型,你手里可能有日频数据、周频数据、月频数据。怎么让它们对齐?说白了就是统一时间轴。

数据对齐,核心是索引对齐。Pandas里用mergejoin就能搞定。但要注意:

  • 左对齐:以某个数据集的时间轴为准,其他数据对齐过来。
  • 内连接:只保留所有数据集都有的时间点。
  • 外连接:保留所有时间点,缺失的填NaN。

重采样,就是把高频数据转成低频,或者反过来。比如把日频数据转成周频:

# 日频转周频,取每周最后一个交易日的数据
df_weekly = df.resample('W').last()

# 日频转月频,取每月均值
df_monthly = df.resample('M').mean()

注意:重采样时,用last()还是mean(),取决于你的因子含义。价格类用last(),收益率类用sum()mean()。我见过有人把日收益率用mean()重采样成月收益率,结果月收益率全偏了。

4.4 复权因子计算:还原真实的交易价格

复权,是量化里绕不开的话题。为什么?因为股票会分红、送股、配股。如果不复权,你的价格序列就是断的,因子计算会出大问题。

复权因子,说白了就是一个乘数,把历史价格调整到当前水平。有两种方式:

  • 前复权:调整历史价格,让它们和当前价格可比。适合回测。
  • 后复权:调整当前价格,让它们和历史价格可比。适合看长期走势。

计算复权因子,核心公式是:

复权因子 = (收盘价 + 分红 + 送股折算) / 前一日收盘价

# 实际计算时,用累计复权因子
cum_factor = (1 + 分红收益率) * (1 + 送股比例) * ...

代码实现:

def calc_adj_factor(df):
    """
    计算复权因子
    df需包含: close, dividend, split_ratio等字段
    """
    # 计算每日调整因子
    df['daily_factor'] = 1.0
    # 分红日调整
    df.loc[df['dividend'] > 0, 'daily_factor'] = \
        (df['close'] + df['dividend']) / df['close'].shift(1)
    # 送股日调整
    df.loc[df['split_ratio'] > 0, 'daily_factor'] *= (1 + df['split_ratio'])
    
    # 累计复权因子
    df['cum_factor'] = df['daily_factor'].cumprod()
    return df

我的经验:很多数据源直接提供复权因子,但我不建议直接拿来用。我习惯自己算一遍,至少验证一下。曾经有个数据源的复权因子算错了,导致我回测多赚了20%,嗯,那是个美丽的误会。

知识体系总览

下面这张图,概括了本章的核心逻辑。你可以把它当作数据清洗的「检查清单」:

数据清洗与预处理知识体系 缺失值处理 异常值检测与处理 数据对齐与重采样 复权因子计算 删除法 填充法 插值法 3σ原则 IQR方法 截尾处理 左对齐 内/外连接 升/降采样 前复权 后复权 累计因子 数据清洗的核心目标:让数据真实、连续、可比 缺失值 → 异常值 → 对齐 → 复权,按顺序执行 数据清洗流程: 原始数据 → 缺失值处理 → 异常值处理 → 数据对齐 → 复权计算 → 干净数据

数据清洗这件事,说白了就是「磨刀不误砍柴工」。你花半天时间把数据洗干净,后面算因子、建模型,一路顺畅。反过来,数据不干净,你后面花一个月都未必能发现问题是出在数据上。

最后说一句:我见过太多人,因子回测漂亮得不行,实盘一跑就崩。查到最后,90%的问题都出在数据清洗上。嗯,希望你不是下一个。

专注资料整理