第二章:数据清洗与预处理

说实话,做量化研究这几年,我踩过最大的坑,往往不是策略逻辑本身,而是数据。

你想想看,辛辛苦苦从各个渠道扒来的另类数据,要是没洗干净就直接扔进模型,那结果基本就是「垃圾进,垃圾出」。我个人习惯是,拿到任何新数据源,第一件事不是跑因子,而是先做一轮彻底的清洗和预处理。今天咱们就聊聊这块的核心环节。

2.1 缺失值处理

缺失值,说白了就是数据里的「空洞」。卫星图像因为云层遮挡丢了几个像素,新闻情感分析因为爬虫故障少了几天的数据,这都是家常便饭。怎么补?得看场景。

均值/中位数填充

这是最粗暴但最常用的方法。我一般这么选:

  • 均值填充:适合数据分布比较对称、没有极端值的情况。比如某只股票的日均成交量,偶尔缺一天,用过去30天的均值补上,问题不大。
  • 中位数填充:如果数据里有明显的异常值(比如某天成交量突然暴增100倍),均值会被拉偏,这时候中位数更稳健。我在处理供应链数据时经常用中位数,因为供应商的订单量偶尔会有离谱的跳变。

核心原则:别盲目用均值。先看一眼数据分布,再决定用均值还是中位数。

import pandas as pd
import numpy as np

# 示例:对某因子列进行缺失值填充
df['factor'] = df['factor'].fillna(df['factor'].mean())  # 均值填充
# 或者
df['factor'] = df['factor'].fillna(df['factor'].median())  # 中位数填充

插值法

插值比均值更聪明一点。它利用时间序列的前后关系来推测缺失值。我个人比较推荐线性插值,尤其是处理高频数据时。

举个例子:某只股票下午2点、3点、5点有数据,4点缺失了。线性插值会认为4点的值大概在3点和5点中间。嗯,这比直接用全天均值合理多了。

# 线性插值
df['factor'] = df['factor'].interpolate(method='linear')

# 时间序列插值(考虑时间间隔)
df['factor'] = df['factor'].interpolate(method='time')

我的小技巧:如果缺失值连续超过3个,我一般就不插值了,直接删掉这段数据。因为连续缺失太多,插出来的值基本是「猜」的,可信度太低。

2.2 异常值检测

异常值,就是数据里的「刺头」。可能是数据录入错误,也可能是真实的极端事件(比如某公司突然暴雷)。怎么区分?得靠统计方法。

3-Sigma 法则

这个最经典。假设数据服从正态分布,那么99.7%的数据会落在均值±3个标准差之内。之外的,就算异常。

但说实话,金融数据很少是完美正态分布。我遇到过很多次,用3-sigma把真实的市场崩盘点给误杀了。所以,它更适合做初筛。

def detect_outliers_3sigma(series):
    mean = series.mean()
    std = series.std()
    outliers = (series < mean - 3*std) | (series > mean + 3*std)
    return outliers

IQR 方法

IQR(四分位距)不依赖正态分布假设,更稳健。它把数据分成四份,看哪些点跑到了「箱子」外面太远。

公式很简单:下界 = Q1 - 1.5*IQR,上界 = Q3 + 1.5*IQR。超出这个范围的,就是异常。

避坑指南:我曾经用IQR处理一组极度偏态的数据,结果发现Q1和Q3几乎重合,IQR小得可怜,导致大量正常值被标为异常。后来我改用调整后的IQR系数(比如3倍IQR),才解决问题。

def detect_outliers_iqr(series, multiplier=1.5):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - multiplier * IQR
    upper = Q3 + multiplier * IQR
    return (series < lower) | (series > upper)

MAD 方法

MAD(中位数绝对偏差)是我个人最喜欢的方法。它比3-sigma更抗干扰,比IQR更灵敏。尤其适合处理那些「偶尔抽风」的另类数据。

MAD的计算逻辑:先算中位数,再算每个点到中位数的绝对偏差,取这些偏差的中位数。然后设定一个阈值(通常3-5倍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

经验之谈:处理卫星图像衍生的植被指数数据时,MAD方法帮我精准识别出了云层遮挡导致的异常低值,而3-sigma和IQR都漏掉了。所以,多试几种方法,选最适合你数据特征的。

2.3 去极值与标准化

去极值,就是把异常值「拉回来」,而不是直接删掉。标准化,则是让不同量纲的数据能放在一起比较。

去极值(Winsorization)

说白了,就是把超出某个范围的值,强制设为边界值。比如,把超过99%分位数的值,全部替换为99%分位数处的值。

我一般在构建因子组合时用这个。因为我不想因为某一天的极端值,就让整个因子的权重发生剧烈变化。

def winsorize_series(series, lower=0.01, upper=0.99):
    q_low = series.quantile(lower)
    q_high = series.quantile(upper)
    return series.clip(lower=q_low, upper=q_high)

标准化(Z-score / Min-Max)

标准化有两种主流方式:

  • Z-score标准化:减去均值,除以标准差。结果变成均值为0、标准差为1的分布。适合数据大致对称的情况。
  • Min-Max标准化:缩放到[0,1]区间。适合数据有明确上下界的情况,比如概率值。
# Z-score
df['factor_z'] = (df['factor'] - df['factor'].mean()) / df['factor'].std()

# Min-Max
df['factor_mm'] = (df['factor'] - df['factor'].min()) / (df['factor'].max() - df['factor'].min())

注意:标准化一定要在去极值之后做。否则,异常值会把均值和标准差拉偏,导致标准化后的数据失真。这个顺序我吃过亏,现在每次都提醒自己。

2.4 截面与时间序列对齐

这是多源数据融合里最头疼的一步。不同数据源的频率不一样:日频的股价数据、周频的供应链数据、月频的消费者信心指数。怎么对齐?

截面对齐

截面数据,就是同一时间点、不同股票的数据。对齐的关键是确保「时间戳」一致。我习惯用交易日历做对齐,把非交易日的数据剔除或前向填充。

# 假设有两个数据源,按日期和股票代码对齐
df_aligned = pd.merge(df_price, df_supply, on=['date', 'stock_code'], how='inner')

时间序列对齐

时间序列对齐,是把不同频率的数据统一到同一时间轴上。常用的方法:

  • 前向填充(ffill):用上一个有效值填充缺失的时间点。适合低频数据对齐到高频。
  • 后向填充(bfill):用下一个有效值填充。适合高频数据对齐到低频。
  • 重采样(resample):把数据聚合到固定频率,比如把日频数据聚合成周频。
# 前向填充:将周频数据对齐到日频
df_weekly_ffill = df_weekly.resample('D').ffill()

# 重采样:将日频数据聚合成周频(取均值)
df_daily_resampled = df_daily.resample('W').mean()

我的习惯:对齐时,我一般先做时间序列对齐,再做截面对齐。因为时间序列对齐会引入新的缺失值,截面对齐时正好可以一并处理。顺序对了,能少踩很多坑。

本章知识体系

下面这张图,是我自己梳理的本章核心逻辑。你可以把它当作一个检查清单:拿到数据后,按这个流程走一遍,基本不会漏掉关键步骤。

数据清洗与预处理核心流程 原始多源数据 缺失值处理 均值 中位数 插值 异常值检测 3-Sigma IQR MAD 去极值 → 标准化 截面与时间序列对齐

嗯,以上就是本章的全部内容。数据清洗这件事,看似基础,但做得好不好,直接决定了后续因子研究的成败。我个人觉得,花70%的时间在数据准备上,30%的时间在建模上,这个比例是合理的。别急着跑模型,先把数据伺候舒服了。

专注资料整理