问题描述
解决方案
在量化交易中,对多个选股因子进行正交化处理(Orthogonalization)的主要目的是消除因子之间的共线性(Collinearity),确保每个因子提供独立的信息,避免在多因子模型中某些特征被重复计算。
在 JoinQuant 聚宽平台上,处理因子正交化通常有两种场景和方法:
- 回归取残差(Gram-Schmidt 正交化/中性化):通常用于剔除因子中的风格因子(如市值、行业)影响,或者剔除某个主因子对其他因子的影响。聚宽提供了现成的 API
neutralize。 - 对称正交化(Symmetric Orthogonalization):用于处理一组地位平等的 Alpha 因子,使它们两两正交,且尽可能保留原始因子的信息。这需要编写自定义函数。
以下是具体的实现方案和代码。
方法一:使用 neutralize 进行回归正交化 (常用)
这是最常见的方法,即从一个因子中剔除其他因子(通常是风险因子或已知的主因子)的影响,保留残差作为新因子。
场景:你有一个“动量因子”,你想剔除“市值因子”和“行业因子”对它的影响。
# -*- coding: utf-8 -*-
from jqdata import *
from jqfactor import get_factor_values, neutralize
import pandas as pd
import numpy as np
def initialize(context):
set_benchmark('000300.XSHG')
set_option('use_real_price', True)
run_daily(factor_process, '09:30')
def factor_process(context):
# 1. 设定股票池 (例如沪深300)
stocks = get_index_stocks('000300.XSHG')
date = context.previous_date
# 2. 获取因子数据
# 假设我们要处理 'momentum' (动量) 和 'size' (市值)
# 注意:实际策略中建议使用 get_factor_values 获取更丰富的数据
# 这里为了演示方便,使用 get_fundamentals 获取市值,用 close 计算简单动量
# 获取市值 (作为要被剔除的影响因子)
q = query(valuation.code, valuation.market_cap).filter(valuation.code.in_(stocks))
df_fund = get_fundamentals(q, date=date)
df_fund.set_index('code', inplace=True)
factor_size = df_fund['market_cap']
# 获取动量 (作为目标因子)
# 获取过去20天的收盘价计算收益率
prices = get_price(stocks, end_date=date, count=20, fields=['close'])['close']
factor_momentum = (prices.iloc[-1] / prices.iloc[0]) - 1
# 3. 数据预处理 (去极值、标准化、处理缺失值)
# 简单处理:填充缺失值
factor_size = factor_size.fillna(factor_size.mean())
factor_momentum = factor_momentum.fillna(factor_momentum.mean())
# 4. 正交化/中性化处理
# 目标:从 动量因子 中剔除 市值因子 和 行业因子 的影响
# how 参数指定要剔除的因子,支持 'jq_l1' (行业) 和自定义 Series (如市值)
# 将 Series 转换为 DataFrame 以符合 neutralize 的输入要求 (axis=1 表示对行进行操作,即截面处理)
# 注意:neutralize 通常接受 DataFrame,index为股票代码
# 方式 A: 仅剔除行业
orth_momentum_industry = neutralize(factor_momentum, how=['jq_l1'], date=date)
# 方式 B: 剔除行业 和 市值 (Gram-Schmidt 正交化的变体)
# 在 how 中传入 factor_size,表示将 momentum 对 size 和 行业 做回归取残差
orth_momentum_all = neutralize(factor_momentum, how=['jq_l1', factor_size], date=date)
print("原始动量因子(前5):")
print(factor_momentum.head())
print("正交化后动量因子(前5):")
print(orth_momentum_all.head())
方法二:对称正交化 (Symmetric Orthogonalization)
如果你有多个 Alpha 因子(例如 A, B, C),它们地位平等,你希望它们互不相关,同时正交化后的因子 $A', B', C'$ 尽可能接近原始因子。这时需要使用对称正交化(通常基于 SVD 分解)。
聚宽没有直接的 API,需要使用 numpy 实现。
# -*- coding: utf-8 -*-
from jqdata import *
import pandas as pd
import numpy as np
from jqfactor import get_factor_values, standardlize, winsorize
def initialize(context):
set_benchmark('000300.XSHG')
set_option('use_real_price', True)
run_daily(symmetric_orth_process, '09:30')
def symmetric_orthogonalize(df):
"""
对称正交化函数
:param df: pandas.DataFrame, index为股票代码, columns为因子名称
:return: 正交化后的 DataFrame
"""
# 1. 处理缺失值 (简单填充为0,假设已经做过标准化)
df = df.fillna(0)
# 2. 获取因子矩阵 M (N只股票 x K个因子)
M = df.values
# 3. 计算协方差矩阵 (K x K)
# 注意:如果因子已经标准化,M.T @ M 就是相关系数矩阵的变体
D = np.dot(M.T, M)
# 4. 特征值分解
# U 是特征向量矩阵, S 是特征值对角矩阵
eigenvalues, U = np.linalg.eigh(D)
# 5. 计算变换矩阵 W = U * S^(-1/2) * U.T
# 注意处理特征值接近0的情况,避免除零错误
with np.errstate(divide='ignore', invalid='ignore'):
S_inv_sqrt = np.diag(1.0 / np.sqrt(eigenvalues))
# 将无穷大或NaN替换为0 (针对极小特征值)
S_inv_sqrt[np.isinf(S_inv_sqrt)] = 0
S_inv_sqrt[np.isnan(S_inv_sqrt)] = 0
W = np.dot(np.dot(U, S_inv_sqrt), U.T)
# 6. 得到正交化后的因子矩阵 M_orth = M * W
M_orth = np.dot(M, W)
# 7. 转回 DataFrame
df_orth = pd.DataFrame(M_orth, index=df.index, columns=df.columns)
return df_orth
def symmetric_orth_process(context):
stocks = get_index_stocks('000300.XSHG')
date = context.previous_date
# 1. 获取多个因子数据 (例如:市净率、换手率、动量)
# 这里使用 get_factor_values 批量获取
factor_names = ['book_to_price_ratio', 'turnover_volatility', 'momentum']
# 获取数据
factor_data = get_factor_values(stocks, factor_names, end_date=date, count=1)
# 整理成 DataFrame: Index=股票, Columns=因子
df_factors = pd.DataFrame(index=stocks)
for name in factor_names:
# get_factor_values 返回的是 dict,value 是 DataFrame(index=date, col=stocks)
# 我们取最后一天的数据并转置
df_factors[name] = factor_data[name].iloc[-1]
# 2. 预处理 (必须步骤:去极值 -> 标准化)
# 正交化对异常值敏感,且要求数据尺度一致
for col in df_factors.columns:
# 去极值
df_factors[col] = winsorize(df_factors[col], qrange=[0.025, 0.975])
# 标准化 (Z-Score)
df_factors[col] = standardlize(df_factors[col])
# 3. 执行对称正交化
df_orth = symmetric_orthogonalize(df_factors)
# 4. 验证结果 (可选)
# 计算原始相关性矩阵
corr_original = df_factors.corr()
# 计算正交化后相关性矩阵 (应该接近单位矩阵)
corr_orth = df_orth.corr()
print("原始因子相关性矩阵:")
print(corr_original)
print("\n正交化后因子相关性矩阵 (应接近对角阵):")
print(corr_orth)
# 接下来可以将 df_orth 用于打分或回归
总结
- 如果目的是去除风险暴露(如去除市值影响):请直接使用
jqfactor.neutralize(target_factor, how=['market_cap', 'jq_l1'])。这是最标准的做法。 - 如果目的是合并多个 Alpha 因子:建议先进行去极值和标准化,然后使用上述代码中的
symmetric_orthogonalize函数进行对称正交化,这样可以保证每个因子在合成时的独立性,避免重复计算某些特征。
Q&A
Q: 为什么正交化之前必须做标准化(Standardize)?
A: 正交化是基于数据的协方差或相关性进行的。如果因子 A 的量级是 10000(如市值),因子 B 的量级是 0.01(如收益率),不做标准化直接计算,数值大的因子会主导整个矩阵的特征值分解,导致数值小的因子信息丢失或计算误差巨大。
Q: neutralize 函数和 Gram-Schmidt 正交化有什么区别?
A: neutralize 本质上就是一种回归取残差的方法,这在数学上等同于 Gram-Schmidt 正交化的步骤。当你对因子 A 关于因子 B 做 neutralize 时,得到的新因子 A' 与 B 是正交(不相关)的。
Q: 对称正交化后的因子物理含义还在吗?
A: 对称正交化的优点是它最小化了正交后因子与原始因子的距离(最小二乘意义上)。因此,正交化后的因子 $A'$ 仍然最大程度地保留了原始因子 $A$ 的特征,但它不再纯粹是原始的 $A$,而是混合了少量其他因子的反向特征以抵消相关性。解释性会略微下降,但统计特性更好。