数据清洗与预处理:打好轮动模型的地基

做量化轮动模型这些年,我踩过最大的坑,往往不是策略逻辑本身,而是数据。你想想看,模型再牛,喂进去的是垃圾,出来的只能是更垃圾。今天咱们就聊聊数据清洗与预处理——说白了,就是给数据「洗澡」的过程。

核心观点:数据预处理占整个量化项目60%以上的时间,但决定了模型90%的上限。

数据清洗与预处理 缺失值处理 删除 / 填充 / 插值 异常值检测与处理 3σ / IQR / 分位数截断 标准化与归一化 Z-score / Min-Max 多重共线性检验 VIF / 相关系数矩阵 稳健的轮动模型 → 干净的数据

一、缺失值处理:别让「空」毁了你的策略

金融数据里,缺失值太常见了。停牌、节假日、数据源故障……原因五花八门。我个人习惯,拿到数据第一件事就是检查缺失率。

import pandas as pd
import numpy as np

# 检查缺失率
def check_missing(df):
    missing_ratio = df.isnull().sum() / len(df)
    return missing_ratio[missing_ratio > 0].sort_values(ascending=False)

# 示例:某股票日线数据
data = pd.read_csv('stock_data.csv', index_col=0, parse_dates=True)
print(check_missing(data))

处理缺失值,我一般分三步走:

  1. 缺失率 < 5%:直接删除或前向填充。轮动模型里,前向填充(ffill)最常用——用上一个交易日的数据补上。
  2. 缺失率 5%-20%:用插值法。线性插值或时间插值都行,我个人偏爱时间插值,更贴合金融数据的特性。
  3. 缺失率 > 20%:建议直接剔除该特征。别犹豫,补出来的数据也是噪声。

我的经验:曾经有个轮动策略,用了某只小盘股的数据,缺失率高达30%。我硬着头皮用均值填充,结果回测曲线漂亮得不像话——实盘直接崩了。后来才发现,那些缺失值对应的都是极端行情。嗯,从那以后,缺失率超过20%的特征我直接砍掉。

二、异常值检测:揪出那些「捣乱分子」

金融数据里的异常值,说白了就是「离谱」的数据点。比如某股票一天涨了1000%,或者成交量突然放大100倍。这些点会严重干扰轮动模型的信号。

我常用的方法有两种:

方法1:3σ原则(正态分布假设)

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

# 应用
returns = data['close'].pct_change().dropna()
outliers = detect_outliers_3sigma(returns)
print(f'检测到 {outliers.sum()} 个异常值')

方法2:IQR(四分位距法)——更稳健

def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return (series < lower_bound) | (series > upper_bound)

注意:千万别直接删除异常值!我曾经在回测中删除了某次股灾的异常数据,结果模型在实盘遇到类似行情时完全失效。正确的做法是:用分位数截断(winsorize)或替换为边界值。

from scipy.stats.mstats import winsorize

# 分位数截断:将极端值替换为1%和99%分位数
data['returns_winsorized'] = winsorize(data['returns'], limits=[0.01, 0.01])

三、数据标准化与归一化:让不同量纲的指标「平起平坐」

轮动模型里,我们经常同时使用价格、成交量、波动率等不同量纲的指标。如果不做标准化,波动率大的指标会主导模型,这显然不合理。

方法 公式 适用场景 我的建议
Z-score标准化 (x - μ) / σ 数据近似正态分布 轮动模型首选
Min-Max归一化 (x - min) / (max - min) 数据有明确边界 注意未来信息泄露
Robust标准化 (x - median) / IQR 数据含异常值 稳健性最强
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

# Z-score标准化(我最常用的)
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data[['close', 'volume', 'volatility']])

# 注意:一定要用滚动窗口,避免未来信息!
def rolling_standardize(df, window=252):
    """滚动标准化,防止未来数据泄露"""
    return df.rolling(window).apply(lambda x: (x - x.mean()) / x.std())

关键提醒:标准化时一定要用滚动窗口或扩展窗口,千万别用全样本的均值和标准差。否则你的模型在回测时已经「偷看」了未来的数据,实盘必死。

四、多重共线性检验:别让指标「互相打架」

轮动模型里,我们经常加入很多相关性高的指标。比如市盈率和市净率,波动率和振幅……这些指标之间如果高度相关,会导致模型系数不稳定,轮动信号忽东忽西。

我一般用VIF(方差膨胀因子)来检测:

from statsmodels.stats.outliers_influence import variance_inflation_factor

def calculate_vif(df):
    """计算VIF,通常VIF > 10表示存在严重共线性"""
    vif_data = pd.DataFrame()
    vif_data['feature'] = df.columns
    vif_data['VIF'] = [variance_inflation_factor(df.values, i) 
                       for i in range(df.shape[1])]
    return vif_data.sort_values('VIF', ascending=False)

# 示例
features = data[['pe_ratio', 'pb_ratio', 'volatility', 'turnover']]
vif_result = calculate_vif(features)
print(vif_result)

处理共线性的方法:

  • 删除高VIF的特征:最简单粗暴,但可能丢失信息
  • PCA降维:把相关指标合成主成分,我常用在因子数量超过20个时
  • L1正则化(Lasso):自动筛选特征,轮动模型里效果不错

避坑指南:我曾经在构建行业轮动模型时,加入了5个相关性极高的动量指标。结果VIF高达50+,模型在样本外完全失效。后来用PCA降维到3个主成分,效果反而更好。记住:不是指标越多越好,干净、独立的信号才值钱。

总结一下

数据清洗与预处理,说白了就是给模型「喂好料」。我做了这么多年量化,最大的感悟就是:花80%的时间处理数据,用20%的时间调模型。数据干净了,轮动模型自然稳健。

嗯,这一章的内容就到这里。记住:别偷懒,数据预处理做扎实了,后面的路就好走了。