🚀 AI 一键生成 joinquant 策略代码

JoinQuant 平台如何对多因子进行正交化处理以消除共线性?

joinquant | 发布时间: 2025/12/31
以下内容由 EasyQuant 生成。

问题描述

如何对多个选股因子进行正交化处理?

解决方案

在量化交易中,对多个选股因子进行正交化处理(Orthogonalization)的主要目的是消除因子之间的共线性(Collinearity),确保每个因子提供独立的信息,避免在多因子模型中某些特征被重复计算。

在 JoinQuant 聚宽平台上,处理因子正交化通常有两种场景和方法:

  1. 回归取残差(Gram-Schmidt 正交化/中性化):通常用于剔除因子中的风格因子(如市值、行业)影响,或者剔除某个主因子对其他因子的影响。聚宽提供了现成的 API neutralize
  2. 对称正交化(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 用于打分或回归

总结

  1. 如果目的是去除风险暴露(如去除市值影响):请直接使用 jqfactor.neutralize(target_factor, how=['market_cap', 'jq_l1'])。这是最标准的做法。
  2. 如果目的是合并多个 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$,而是混合了少量其他因子的反向特征以抵消相关性。解释性会略微下降,但统计特性更好。