因子能否落地,很大程度上取决于原始数据是否干净。停牌、新股、涨跌停、财报披露时滞、未来信息泄露,都会把因子结果带偏。下面按数据处理与中性化的关键步骤展开。
因子数据主要来源于以下三类:
因子数据的获取通常可分为两个步骤:
因子计算应遵循先收盘、后计算、次日使用的原则。即在T日收盘后,使用T日可得数据计算因子,并将其用于T+1日的交易信号。
代码示例,获取行情数据:
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 模拟获取某只股票过去20个交易日的收盘价
def get_price_data(stock_code, start_date, end_date):
# 实际项目中,这里会连接数据库或API
# 这里用随机数据模拟
dates = pd.date_range(start=start_date, end=end_date, freq='B')
prices = np.random.randn(len(dates)).cumsum + 100
return pd.Series(prices, index=dates, name=stock_code)
# 获取数据
price_series = get_price_data('000001.SZ', '2024-01-01', '2024-01-31')
print(price_series.head)
2024-01-01 100.620346 2024-01-02 102.121153 2024-01-03 103.701520 2024-01-04 103.175807 2024-01-05 100.646072 Freq: B, Name: 000001.SZ, dtype: float64
数据获取完成后,首要任务并非直接计算因子,而是对原始数据进行清洗。
异常值是指明显偏离正常范围、且可能由数据错误、口径差异或极端事件造成的观测值。例如,个别股票单日收益率异常放大,或估值指标出现极端偏离,均可能对因子分布造成显著扭曲。若不加处理,异常值往往会影响统计特征,进而削弱因子研究的稳健性。
常见的异常值处理方法主要包括以下三类:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| MAD法(中位数绝对偏差) | 以中位数替代均值,对极端值具有更强鲁棒性 | 因子分布偏态较明显时 |
| 百分位截断法 | 截取上下若干分位的极端值 | 处理方式简单、效率较高时 |
| 3σ原则 | 将超过均值±3倍标准差的观测值视为异常 | 因子分布近似正态时 |
在实际应用中,MAD法通常更具稳健性。原因在于,该方法对极端值不敏感;相较于3σ原则,其识别结果不易受到少数极端观测值的干扰。因此,在因子分布明显偏态或存在厚尾特征的情形下,MAD法通常是更优选择。
异常值处理不宜采用一刀切的方式。更合理的做法通常是先进行行业内处理,再进行全市场处理。由于不同行业的因子分布差异较大,例如银行股与科技股在估值水平和波动特征上存在显著差异,若直接混合处理,可能将行业特征误判为异常值。
代码示例,MAD法处理异常值:
def mad_outlier_removal(factor_series, n=5):
"""
使用MAD法识别并处理异常值
n: 阈值,通常取3-5
"""
median = factor_series.median
mad = np.median(np.abs(factor_series - median))
# 计算修正后的Z-score
modified_z_scores = 0.6745 * (factor_series - median) / mad
# 标记异常值
outliers = np.abs(modified_z_scores) > n
# 将异常值替换为中位数
factor_series_clean = factor_series.copy
factor_series_clean[outliers] = median
return factor_series_clean
# 使用示例
raw_factor = pd.Series(np.random.randn(1000) * 10 + 50)
raw_factor[0] = 5000 # 人为加入一个极端值
clean_factor = mad_outlier_removal(raw_factor)
print(f"处理前最大值: {raw_factor.max:.2f}")
print(f"处理后最大值: {clean_factor.max:.2f}")
处理前最大值: 5000.00 处理后最大值: 82.16
异常值处理完成后,下一步是标准化。
标准化的目的是消除不同因子量纲不一致带来的影响。由于市盈率、换手率、动量等指标的取值范围差异较大,若直接将其纳入模型,数值尺度较大的因子可能对结果产生不成比例的影响,从而削弱模型的可解释性与稳健性。
常见的标准化方法主要包括以下两类:
需要注意的是,Z-score标准化通常隐含因子近似正态分布的假设,但实际中许多因子并不满足这一条件。例如,换手率因子往往呈右偏分布,直接采用Z-score标准化并不理想。此时,可优先考虑排序标准化,或先进行对数变换,再实施标准化处理。
在多因子模型构建中,常见做法是先进行排序标准化,再转换为Z-score。这样既保留了因子的相对排序信息,又能使分布更接近正态。具体而言,可先将因子值转换为百分位排名,再利用正态分布的反函数映射为Z-score。
代码示例,排序标准化:
def rank_standardize(factor_series):
"""
排序标准化:将因子值映射到[0,1]区间
"""
rank = factor_series.rank
standardized = (rank - 1) / (len(rank) - 1)
return standardized
# 使用示例
factor = pd.Series(np.random.randn(1000) * 100)
standardized_factor = rank_standardize(factor)
print(f"原始因子范围: [{factor.min:.2f}, {factor.max:.2f}]")
print(f"标准化后范围: [{standardized_factor.min:.2f}, {standardized_factor.max:.2f}]")
原始因子范围: [-283.47, 352.87] 标准化后范围: [0.00, 1.00]
以下进入因子中性化:在清洗与标准化之后,对行业、市值与风格敞口做剥离。
本节讨论因子的市值、行业与风格中性化:用回归或分组等方式,剥离因子值中与这些已知维度相关的部分,保留残差作为“更纯”的因子信号。
实证资产定价文献中,通常并不把“先对市值/行业做回归中性化”作为默认预处理。更常见的做法是:
因此,学术流程往往在检验设计里控制市值与行业,而不是先改写因子、再假装测的是原始特征。若一上来就中性化,还可能改变问题本身——尤其当信号天然与行业结构纠缠时(例如大市值组大量集中于银行)。
A 股量化与组合管理中,中性化很常见,原因主要是产品约束与归因目标不同:
学术默认回答“全市场里这个特征有没有溢价”;实务中性化更多回答“剥离市值与行业后,还有没有可交易的增量信息”。 二者互补,不是谁取代谁。建议同时报告中性化前后的结果,并检查组合的行业与市值暴露。
中性化的核心目标,是剥离因子中的行业、市值与风格干扰,保留残差部分的独立信息(因子值 ≈ 风格暴露 + 残差)。下面介绍行业中性化、市值与风格中性化,以及双重中性化。
行业中性化的第一步是行业分类;若无分类,则无法确定待剥离的对象。
目前主流的行业分类标准如下:
| 分类标准 | 发布方 | 层级 | 特点 |
|---|---|---|---|
| GICS | MSCI / S&P | 11个行业,24个行业组,69个行业 | 全球通用,机构广泛采用 |
| ICB | FTSE Russell | 11个行业,20个超行业,45个行业 | 欧洲常用,与 GICS 类似 |
| 申万行业 | 申万宏源 | 31个一级行业,134个二级行业 | A 股最常用,贴近国内市场 |
| 中信行业 | 中信证券 | 30个一级行业,110个二级行业 | 机构定制,更新较快 |
A 股实践中通常采用申万一级行业分类,因其逻辑贴近国内市场认知且数据易得。需注意申万行业每年调整一次,应及时更新分类。
若构建全球多市场策略,建议采用 GICS 分类,以便在不同市场间建立统一比较框架。
行业中性化通常不与市值分离处理,实践中常同时对行业和市值两个维度进行中性化,即行业市值中性化。
市值须一并纳入处理,原因在于大市值与小市值股票的行业分布天然不均衡——银行股普遍市值较大,科技股普遍市值较小。若仅作行业中性化,市值效应仍将渗入因子暴露。
具体实现如下(代码示例):
import pandas as pd
import statsmodels.api as sm
# 假设 df 包含:股票代码、因子值、行业哑变量、市值对数
# 行业哑变量:每个行业一列,0/1编码
def industry_market_neutralize(df, factor_col, mkt_cap_col, industry_dummies):
"""
行业市值中性化:用回归剥离行业和市值的影响
"""
# 准备自变量:行业哑变量 + 市值对数
X = df[industry_dummies].copy
X['log_mkt_cap'] = np.log(df[mkt_cap_col])
X = sm.add_constant(X) # 加截距项
# 因变量:原始因子值
y = df[factor_col]
# 回归
model = sm.OLS(y, X).fit
# 残差就是中性化后的因子
df['factor_neutral'] = model.resid
return df, model
这段代码的逻辑很简单:用回归把因子值里能被行业和市值解释的部分去掉,剩下的残差就是干净的因子。
注意: 回归须加入截距项;否则残差均值可能非零,导致中性化不彻底,回测结果可能出现约 0.5 个百分点的偏差。
回归为最常用方法,但并非唯一途径。实践中常见的因子剥离方式包括:
回归法可同时处理多个变量且统计性质清晰,应用最为广泛;若仅需快速验证单个因子,分组调整法更为简便。
分组调整法的代码示例如下:
def group_neutralize(df, factor_col, industry_col):
"""
分组调整法:行业内做标准化
"""
df['factor_neutral'] = df.groupby(industry_col)[factor_col].transform(
lambda x: (x - x.mean) / x.std
)
return df
上述代码简洁高效,但该方法仅消除行业均值差异,未能去除行业内部的市值效应。若需更彻底的中性化结果,仍须采用回归法。
行业中性化中若干细节若处理不当,将导致结果偏离预期。常见误区如下:
核心总结: 行业中性化的本质是控制变量——避免因子收益被行业走势主导,须将行业因素纳入控制。回归法最为通用,同时须控制市值。残差即为经中性化后的真因子。
在实务多因子流水线中,行业中性化往往是最基础的预处理步骤之一。若行业效应剥离不彻底,后续合成与优化容易被行业配置主导。
在完成行业中性化之后,还需进一步处理市值与风格暴露。以下各节介绍更细粒度的中性化方法。
以过去一个月换手率为例,该因子与市值是否存在关联?
关联显著:小盘股换手率通常高于大盘股。若不作处理,选股结果可能偏向小盘股,因子收益中换手率效应与市值效应难以区分。
分不清。
中性化的目的,就是把因子中属于其他风格的那部分剥离掉。只留下属于因子自己的独特信息。
核心思想: 因子值 = 风格暴露部分 + 残差部分。目标为残差部分。
市值中性化,是所有中性化里最常用的。做法也很直接:
实践中常用回归法:将因子值对市值作线性回归,以残差作为新的因子值。
import pandas as pd
import statsmodels.api as sm
# 假设 df 包含因子值 factor 和市值 market_cap
# 市值取对数,效果更好
df['log_mkt'] = np.log(df['market_cap'])
X = sm.add_constant(df['log_mkt'])
y = df['factor']
model = sm.OLS(y, X).fit
df['factor_neutral'] = model.resid
# 做完之后,新因子和市值的相关系数应该接近0
print(df[['factor_neutral', 'log_mkt']].corr)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-6-33aaa14fd86d> in <cell line: 0>() 4 # 假设 df 包含因子值 factor 和市值 market_cap 5 # 市值取对数,效果更好 ----> 6 df['log_mkt'] = np.log(df['market_cap']) 7 X = sm.add_constant(df['log_mkt']) 8 y = df['factor'] NameError: name 'df' is not defined
建议先按市值分组,再在组内标准化,以减弱极端值影响。分组数通常取 10 组或 20 组,视股票数量而定。
市值中性化只处理了市值一个维度。但现实中,因子可能和多个风格都有关系。比如,一个盈利增长因子,可能既和市值有关,又和估值有关,还和动量有关。
这时候就需要风格因子正交化。
做法和市值中性化类似,只是把解释变量从市值换成多个风格因子。
# 假设存在市值、估值、动量三个风格因子
style_factors = ['log_mkt', 'pe_ratio', 'momentum_12m']
X = sm.add_constant(df[style_factors])
y = df['factor']
model = sm.OLS(y, X).fit
df['factor_orthogonal'] = model.resid
# 检查正交化后的因子和风格因子的相关性
print(df[['factor_orthogonal'] + style_factors].corr)
需注意:风格因子并非越多越好,过多将削弱因子的有效信息。通常控制在 3–5 个核心风格因子。
注意事项: 正交化不宜过度。若一次性纳入 10 个风格因子,因子 IC 可能降至 0.01 附近并近乎失效,原因往往是将与原始因子高度相关的变量一并纳入。正交化须保留因子的核心信号,并非越彻底越好。
有时仅作正交化仍不足够。市值影响可能呈非线性——大盘股与小盘股内部因子表现差异显著。此类情形宜采用双重中性化。
做法分两步:
即先分组以隔离市值影响,再在组内去除其他风格因素。经此处理的因子,既不受市值分组差异影响,亦不受组内风格相关性干扰。
def double_neutralization(df, group_col='mkt_group', style_factors=['pe_ratio', 'momentum_12m']):
"""
双重中性化处理
"""
df['factor_double_neutral'] = np.nan
for group in df[group_col].unique:
mask = df[group_col] == group
sub_df = df[mask].copy
# 组内做风格因子正交化
X = sm.add_constant(sub_df[style_factors])
y = sub_df['factor']
model = sm.OLS(y, X).fit
sub_df['factor_neutral'] = model.resid
# 组内标准化
sub_df['factor_double_neutral'] = (sub_df['factor_neutral'] - sub_df['factor_neutral'].mean) / sub_df['factor_neutral'].std
df.loc[mask, 'factor_double_neutral'] = sub_df['factor_double_neutral'].values
return df
| 方法 | 处理维度 | 适用场景 | 复杂度 |
|---|---|---|---|
| 市值中性化 | 仅市值 | 因子和市值高度相关 | 低 |
| 风格因子正交化 | 多个风格 | 因子和多个风格相关 | 中 |
| 双重中性化 | 分组+正交 | 市值影响非线性,且有多风格干扰 | 高 |
建议:因子研究初期先采用市值中性化;若因子与多个风格均相关,再升级至风格因子正交化;双重中性化通常用于最终模型构建阶段。
若中性化后因子 IC 反而下降,不宜立即放弃,应先核查风格因子选取是否恰当。例如,将波动率作为风格因子正交化,可能削弱低波因子本身——二者本质相同。
中性化处理旨在去除因子中不属于其本身的风格暴露,保留独立信息。处理得当的多因子模型更为纯净,组合稳定性亦相应提升。
完成去极值、标准化与中性化后,因子生产流水线的预处理环节告一段落。
数据清洗与预处理虽看似繁琐,却是因子投资体系的重要基石。实务中,研究精力往往过度集中于模型创新,而对数据清洗与预处理投入不足,导致回测表现难以在实盘中兑现。因此,在因子研究与组合构建过程中,应将数据清洗置于足够重要的位置。