第三章 因子数据处理:数据清洗、中性化与合成降维
说实话,因子数据处理这活儿,看着不起眼,但坑特别多。
我刚开始做量化那会儿,以为选到好因子就万事大吉了。结果回测曲线漂亮得不行,一上实盘就崩。后来才发现,问题出在数据预处理上——极端值没处理、因子之间多重共线性严重、行业暴露没控制好。嗯,从那以后,我再也不敢小看这一步了。
3.1 数据清洗:去极值与标准化
原始因子数据,说白了就是一堆"脏数据"。你想想看,A股市场经常有涨停跌停、财报数据异常、新股上市暴涨暴跌……这些都会让因子值出现离谱的极端值。
3.1.1 去极值(Winsorization)
去极值的方法,我个人习惯用两种:
- 百分位法:把上下1%或5%的数据直接拉到边界值
- 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 本章知识体系
下面这张图,是我自己梳理的因子数据处理流程。你照着这个顺序做,基本不会出大问题。
嗯,整个流程走下来,你会发现:数据预处理花的时间,往往比因子挖掘本身还多。但这是值得的——干净的数据,是量化策略稳定性的基石。
最后说一句:我见过太多人花大量时间挖新因子,却不愿意花半小时做数据清洗。结果呢?因子回测漂亮,实盘一塌糊涂。记住:垃圾进,垃圾出。数据预处理,值得你认真对待。