4. 因子计算基础:单因子计算逻辑与标准化处理
因子计算是量化选股的核心环节。原始因子数据通常包含量价信息、财务指标或另类数据,但直接使用原始值进行分层回测会因量纲差异、极端值、行业风格干扰而产生严重偏差。本章系统讲解单因子计算逻辑、Z-score 标准化以及中性化处理,为后续 XGBoost 因子合成与分层回测奠定数据基础。
4.1 单因子计算逻辑
单因子通常分为动量因子、反转因子、波动率因子、价值因子、质量因子等类别。以下以最常见的动量因子与反转因子为例,说明计算逻辑。
| 因子类别 | 计算逻辑 | 典型窗口 | 经济含义 |
|---|---|---|---|
| 动量因子 | 过去 N 日累计收益率:MOM = (close_t / close_{t-N}) - 1 |
20 日、60 日、120 日 | 强者恒强,趋势延续 |
| 反转因子 | 过去 N 日累计收益率取负值:REV = -MOM |
5 日、10 日、20 日 | 短期超买超卖后价格回归 |
| 波动率因子 | 过去 N 日收益率标准差 | 20 日、60 日 | 低波动异象,低波组合超额收益 |
| 价值因子 | 市盈率倒数(EP)、市净率倒数(BP) | 最新财报 | 估值越低,预期收益越高 |
实战要点:动量因子与反转因子在 A 股市场存在明显的周期切换。短周期(5-10 日)反转效应显著,中长周期(60-120 日)动量效应占优。回测时建议同时计算多个窗口,避免单一窗口过拟合。
4.2 因子标准化:Z-score 方法
不同因子的量纲和分布差异巨大(如动量因子在 [-1, ∞) 区间,市盈率在 (0, ∞) 区间),直接合并或比较会扭曲因子权重。Z-score 标准化将因子转换为均值为 0、标准差为 1 的标准正态分布,公式如下:
# Z-score 标准化实现
import numpy as np
import pandas as pd
def zscore_standardize(factor_series):
"""
对因子序列进行 Z-score 标准化
参数:
factor_series: pd.Series, 原始因子值
返回:
pd.Series, 标准化后的因子值
"""
mean = factor_series.mean()
std = factor_series.std()
# 防止除零
if std == 0:
return pd.Series(np.zeros_like(factor_series), index=factor_series.index)
return (factor_series - mean) / std
# 示例:对动量因子进行标准化
mom_factor = pd.Series([0.05, -0.02, 0.12, 0.03, -0.08, 0.07])
mom_zscore = zscore_standardize(mom_factor)
print(mom_zscore)
# 输出: 0.23, -0.85, 1.37, -0.08, -1.62, 0.95
标准化注意事项:Z-score 假设因子近似正态分布。若因子存在严重偏态或厚尾(如极端收益、小市值因子),建议先进行 MAD(中位数绝对偏差)截尾或 分位数截尾,再执行标准化,避免极端值扭曲均值和标准差。
4.3 因子中性化处理
原始因子往往与市场风格(市值、行业、波动率等)高度相关。例如,小市值股票天然具有更高的动量收益,若不剔除市值影响,动量因子的选股能力会被小市值风格干扰。中性化处理的核心是 回归残差法:
- 确定中性化变量:通常包括行业哑变量(申万一级/二级行业)和市值对数(log_market_cap)。
- 建立回归模型:以原始因子为因变量,中性化变量为自变量,进行横截面回归。
- 提取残差:回归残差即为剔除风格影响后的纯净因子。
# 因子中性化实现(横截面回归)
import statsmodels.api as sm
def neutralize_factor(factor_df, industry_dummies, log_mkt_cap):
"""
对因子进行行业 + 市值中性化
参数:
factor_df: pd.DataFrame, 包含 'factor' 列
industry_dummies: pd.DataFrame, 行业哑变量(one-hot)
log_mkt_cap: pd.Series, 对数市值
返回:
pd.Series, 中性化后的因子残差
"""
# 合并自变量
X = pd.concat([industry_dummies, log_mkt_cap], axis=1)
X = sm.add_constant(X) # 添加截距项
y = factor_df['factor']
# 回归
model = sm.OLS(y, X, missing='drop').fit()
residuals = model.resid
return residuals
# 示例:对动量因子进行行业市值中性化
# factor_df 包含股票代码、因子值、行业、市值等列
# neutralized_factor = neutralize_factor(factor_df, industry_dummies, log_mkt_cap)
中性化陷阱:不要对因子本身进行中性化后再做标准化,或反过来。标准流程是:原始因子 → 去极值 → 中性化 → 标准化。顺序错误会导致因子信息失真。另外,中性化变量不宜过多,避免过度拟合导致因子失去预测能力。
4.4 因子处理流程总览
4.5 实战案例:完整因子处理流水线
以下代码展示从原始数据到标准化因子的完整流水线,包含去极值、中性化、标准化三个核心步骤:
import pandas as pd
import numpy as np
import statsmodels.api as sm
def factor_pipeline(factor_raw, industry_dummies, log_mkt_cap,
method='mad', mad_thresh=5):
"""
因子处理完整流水线
参数:
factor_raw: pd.Series, 原始因子值
industry_dummies: pd.DataFrame, 行业哑变量
log_mkt_cap: pd.Series, 对数市值
method: str, 去极值方法 ('mad' 或 'quantile')
mad_thresh: float, MAD 截尾倍数
返回:
pd.Series, 处理后的标准因子
"""
# 1. 去极值
if method == 'mad':
median = factor_raw.median()
mad = np.median(np.abs(factor_raw - median))
upper = median + mad_thresh * mad
lower = median - mad_thresh * mad
factor_clipped = factor_raw.clip(lower, upper)
elif method == 'quantile':
q1 = factor_raw.quantile(0.01)
q99 = factor_raw.quantile(0.99)
factor_clipped = factor_raw.clip(q1, q99)
else:
raise ValueError("method must be 'mad' or 'quantile'")
# 2. 中性化(行业 + 市值)
X = pd.concat([industry_dummies, log_mkt_cap], axis=1)
X = sm.add_constant(X)
y = factor_clipped
model = sm.OLS(y, X, missing='drop').fit()
factor_neutral = model.resid
# 3. Z-score 标准化
mean = factor_neutral.mean()
std = factor_neutral.std()
if std == 0:
factor_standard = pd.Series(np.zeros_like(factor_neutral),
index=factor_neutral.index)
else:
factor_standard = (factor_neutral - mean) / std
return factor_standard
# 使用示例
# factor_clean = factor_pipeline(mom_factor, ind_dummies, log_mkt_cap, method='mad')
流水线验证:处理后的因子应满足:① 无极端值(MAD 截尾后);② 与行业和市值相关系数接近 0(中性化效果);③ 均值为 0,标准差为 1(标准化效果)。建议在回测前绘制因子分布直方图与 Q-Q 图,确认处理质量。
4.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 标准化后因子仍有偏态 | 原始因子分布严重偏斜,Z-score 假设不成立 | 先做 Box-Cox 变换或 rank 标准化(百分位排序) |
| 中性化后因子预测能力下降 | 中性化变量过多,剔除了有效信息 | 仅保留行业和市值,避免加入过多风格因子 |
| 不同截面期因子分布差异大 | 市场环境变化导致因子均值/标准差漂移 | 每个截面期独立进行标准化,不使用全局参数 |
| 去极值后因子信息损失 | 截尾阈值过小,误删有效极端信号 | MAD 阈值设为 5-7 倍,分位数截尾设为 0.5%-99.5% |