第三章 因子数据处理:数据清洗、中性化与合成降维

说实话,因子数据处理这活儿,看着不起眼,但坑特别多。

我刚开始做量化那会儿,以为选到好因子就万事大吉了。结果回测曲线漂亮得不行,一上实盘就崩。后来才发现,问题出在数据预处理上——极端值没处理、因子之间多重共线性严重、行业暴露没控制好。嗯,从那以后,我再也不敢小看这一步了。

3.1 数据清洗:去极值与标准化

原始因子数据,说白了就是一堆"脏数据"。你想想看,A股市场经常有涨停跌停、财报数据异常、新股上市暴涨暴跌……这些都会让因子值出现离谱的极端值。

3.1.1 去极值(Winsorization)

去极值的方法,我个人习惯用两种:

  1. 百分位法:把上下1%或5%的数据直接拉到边界值
  2. MAD法:基于中位数和绝对中位差,更稳健

我在项目中遇到过一只股票,因为某天乌龙指导致换手率因子值飙到正常值的100倍。如果不处理,整个因子的有效性都会被它带偏。

核心原则:去极值不是删除数据,而是把极端值"拉回来"。千万别直接删行,否则会导致样本偏差。

import numpy as np
import pandas as pd

def winsorize_series(s, limits=[0.01, 0.01]):
    """
    百分位法去极值
    limits: [下界百分位, 上界百分位]
    """
    lower = s.quantile(limits[0])
    upper = s.quantile(1 - limits[1])
    return s.clip(lower, upper)

def mad_winsorize(s, n=5):
    """
    MAD法去极值
    n: 中位数上下n倍MAD
    """
    median = s.median()
    mad = np.median(np.abs(s - median))
    lower = median - n * mad
    upper = median + n * mad
    return s.clip(lower, upper)

# 实战用法
factor_data['pe_ratio'] = winsorize_series(factor_data['pe_ratio'], limits=[0.01, 0.01])
factor_data['turnover'] = mad_winsorize(factor_data['turnover'], n=5)

我的经验:对于市值、换手率这类分布偏态的因子,MAD法比百分位法更靠谱。因为百分位法假设数据对称,但金融数据往往不对称。

3.1.2 标准化(Standardization)

标准化说白了就是把不同量纲的因子拉到同一个尺度上。不然你想想,市盈率是几十倍,换手率是百分之几,怎么放一起比较?

常用的标准化方法:

  • Z-score标准化:(x - mean) / std,假设数据正态分布
  • Rank标准化:把数据转为0-1之间的排名分位数,对异常值不敏感
  • Min-Max标准化:(x - min) / (max - min),容易受极端值影响

我个人最常用的是Rank标准化。为什么?因为A股因子数据很少服从正态分布,用Z-score反而会放大尾部风险。

def zscore_standardize(s):
    """Z-score标准化"""
    return (s - s.mean()) / s.std()

def rank_standardize(s):
    """Rank标准化:转为0-1之间的分位数"""
    return s.rank(pct=True)

def minmax_standardize(s):
    """Min-Max标准化"""
    return (s - s.min()) / (s.max() - s.min())

# 实战中,我通常先做去极值,再做标准化
factor_data['pe_clean'] = winsorize_series(factor_data['pe_ratio'])
factor_data['pe_std'] = rank_standardize(factor_data['pe_clean'])

注意:标准化一定要在去极值之后做!我曾经犯过这个错——先标准化再去极值,结果标准化后的极端值被压缩,去极值效果大打折扣。

3.2 中性化处理:剥离干扰因素

中性化处理,是量化选股里非常关键的一步。说白了,就是要把因子中"不该有的东西"剔除掉。

举个例子:你发现小市值股票涨得好,但这不是因为市值因子本身有效,而是因为小市值股票里有很多"壳资源"或者"游资炒作"的噪音。如果不做中性化,你选出来的股票可能只是押对了风格,而不是因子真的有效。

3.2.1 行业中性化

行业中性化,就是让因子在每个行业内的分布尽量均匀。不然你想想,如果某个因子在银行股里天然就高,在科技股里天然就低,那选股结果就会偏向某个行业,而不是因子本身在起作用。

def industry_neutralize(factor, industry_dummies):
    """
    行业中性化:用行业哑变量做回归,取残差
    factor: 待中性化的因子值
    industry_dummies: 行业哑变量矩阵(one-hot编码)
    """
    from sklearn.linear_model import LinearRegression
    
    model = LinearRegression()
    model.fit(industry_dummies, factor)
    residual = factor - model.predict(industry_dummies)
    return residual

# 实战用法
# 假设df包含'pe_std'因子和'industry'列
industry_dummies = pd.get_dummies(df['industry'])
df['pe_neutral'] = industry_neutralize(df['pe_std'], industry_dummies)

3.2.2 市值中性化

市值中性化,道理一样。A股市场有明显的市值效应——小市值股票长期跑赢大市值。但如果你做的是基本面因子(比如ROE、毛利率),你希望因子选股不受市值大小的影响。

我一般把行业和市值一起做中性化:

def market_cap_neutralize(factor, log_market_cap, industry_dummies):
    """
    市值+行业联合中性化
    """
    X = pd.concat([log_market_cap, industry_dummies], axis=1)
    model = LinearRegression()
    model.fit(X, factor)
    residual = factor - model.predict(X)
    return residual

# 实战中,市值取对数后再做回归,效果更好
df['log_mkt_cap'] = np.log(df['market_cap'])
df['pe_final'] = market_cap_neutralize(
    df['pe_std'], 
    df['log_mkt_cap'], 
    pd.get_dummies(df['industry'])
)

避坑指南:我曾经把市值和行业分开做中性化,结果发现效果不如一起做。因为市值和行业之间有相关性(比如银行股市值普遍大),分开做会遗漏交互效应。建议用多元回归一次性搞定。

3.3 因子合成与降维

做完清洗和中性化,你手上可能有几十个甚至上百个因子。但因子之间往往高度相关——比如"市盈率"和"市净率"、"换手率"和"波动率"。如果不做降维,模型会过拟合,而且因子之间的多重共线性会让权重估计不稳定。

3.3.1 因子合成方法

因子合成,就是把多个相关因子合并成一个综合因子。常用的方法:

  • 等权合成:简单粗暴,每个因子权重相同。适合因子质量差不多的情况。
  • IC加权:根据因子IC(信息系数)大小分配权重。IC高的因子权重更大。
  • PCA主成分分析:用统计方法提取公共因子,去掉噪音。

我个人习惯用IC加权,因为它既考虑了因子的预测能力,又比PCA更直观、可解释。

def ic_weighted_composite(factor_df, ic_series):
    """
    IC加权合成因子
    factor_df: 多个因子组成的DataFrame(已标准化)
    ic_series: 每个因子的IC值(Series)
    """
    # IC归一化作为权重
    weights = ic_series / ic_series.sum()
    # 加权求和
    composite = factor_df.dot(weights)
    return composite

# 实战用法
# 假设有3个因子:pe, pb, roe
factor_cols = ['pe_final', 'pb_final', 'roe_final']
ic_values = pd.Series([0.05, 0.03, 0.08], index=factor_cols)
df['composite_factor'] = ic_weighted_composite(df[factor_cols], ic_values)

3.3.2 PCA降维

PCA降维,说白了就是"找主成分"。它能把几十个因子压缩成几个不相关的综合因子,同时保留大部分信息。

但要注意:PCA出来的因子没有经济含义,你没法解释"第一主成分"到底是什么。所以,如果你需要向领导或客户解释选股逻辑,建议用IC加权;如果纯粹追求预测效果,PCA是个好选择。

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

def pca_composite(factor_df, n_components=3):
    """
    PCA降维合成因子
    """
    # 先标准化
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(factor_df)
    
    # PCA
    pca = PCA(n_components=n_components)
    pca.fit(X_scaled)
    
    # 用第一主成分作为综合因子
    composite = pca.transform(X_scaled)[:, 0]
    
    # 查看解释方差比
    explained_ratio = pca.explained_variance_ratio_
    print(f"前{n_components}个主成分解释方差比: {explained_ratio}")
    
    return composite, pca

# 实战用法
composite, pca_model = pca_composite(df[factor_cols], n_components=3)
df['pca_factor'] = composite

我的建议:不要盲目追求降维。如果因子数量不多(比如5-10个),而且相关性不高,直接等权合成或者IC加权就够了。PCA更适合因子数量超过20个的情况。

3.4 本章知识体系

下面这张图,是我自己梳理的因子数据处理流程。你照着这个顺序做,基本不会出大问题。

因子数据处理流程 原始因子数据 数据清洗 去极值(百分位法 / MAD法) 标准化(Z-score / Rank / Min-Max) 中性化处理 行业中性化(回归取残差) 市值中性化(联合回归) 因子合成与降维 等权合成 → IC加权合成 → PCA降维 输出:综合因子(用于选股)

嗯,整个流程走下来,你会发现:数据预处理花的时间,往往比因子挖掘本身还多。但这是值得的——干净的数据,是量化策略稳定性的基石。

最后说一句:我见过太多人花大量时间挖新因子,却不愿意花半小时做数据清洗。结果呢?因子回测漂亮,实盘一塌糊涂。记住:垃圾进,垃圾出。数据预处理,值得你认真对待。

专注资料整理