跳至正文
MODULE 03特征工程与因子挖掘5 讲 · FACTOR RESEARCH
LECTURE 13共 31 讲预计阅读 45 分钟

第 13 讲:因子正交化与共线性消除 ​

本讲核心目标:
深入理解多因子量化模型中“多重共线性(Multi-collinearity)”对投资组合二次规划优化(MVO)与线性打分的毁灭性破坏;系统掌握矩阵条件数(Condition Number)诊断方法;从泛函与矩阵分析严格推导施密特顺序正交化(Gram-Schmidt)与对称正交化(Löwdin Symmetric Orthogonalization);严格证明 Löwdin 正交化在 Frobenius 范数下的“最小信息扰动”数学性质;编写工业级因子正交化器。


1. 为什么必须进行因子正交化? ​

在经过上一讲的“去极值、填补、截面标准化、行业与市值中性化”之后,我们的因子库中已经拥有了若干个剔除了行业与市值风格偏倚的纯净单因子。

然而,当研究员尝试将这 10 个或 20 个单因子送入线性打分模型、风险协方差矩阵或多因子二次规划优化器(QP)时,常常会遭遇灾难性的现实困境:

课程图示
可放大查看细节,点击百分比恢复原始大小。

1.1 多重共线性引发的“三大杀招” ​

  1. 组合优化器权重剧烈震荡(Matrix Ill-Conditioning):
    在均值-方差优化或马科维茨(Markowitz)求解时,目标股票持仓权重直接依赖于因子协方差逆矩阵:w∗∝Σ−1μ若因子之间高度线性相关,因子协方差矩阵 Σ 的行列式将趋近于 0,条件数(Condition Number κ=λmaxλmin)暴增至 104 以上。此时,因子预期收益 μ 极其微小的估计噪声,就会被逆矩阵放大数千倍,导致最终股票权重出现“今天满仓重配、明天全额清仓”的剧烈翻滚。
  2. 重复信息重复计价(Double Counting):
    假设一个多因子模型使用了 4 个不同窗口期的动量类因子和 1 个低估值因子。如果不加正交化直接等权相加,动量维度的信息实质上被赋予了 4 倍的实际杠杆,模型退化为一个单纯的动量追踪策略。
  3. 多元回归系数失真(VIF 爆炸):
    多元线性回归中,高度共线性的因子会导致方差膨胀因子(Variance Inflation Factor, VIF)飙升,t 统计量失效,原本有效的因子可能被错误地判定为统计不显著,甚至出现与直觉相反的负系数。

2. 正交化三大主流数学方案对比 ​

方案核心思想优缺点对比适用场景
主成分分析 (PCA)寻找方差最大正交投影轴彻底正交,但转换后的主成分失去了原始因子的明确业务含义(如 PC1 是动量与估值的混沌混合体)统计套利、高频隐变量提取、降维压缩
施密特正交化 (Gram-Schmidt)确定优先级,后位因子向首位投影并提取残差保留了基准因子的原始定义,但严重依赖正交化顺序,排名靠后的因子被剥夺绝大部分信息明确存在主导因子的场景(如已知基本面绝对优先,强行剔除动量中的重叠部分)
对称正交化 (Löwdin)寻找与原矩阵距离最小的正交矩阵顺序无关(对偶对称);在 Frobenius 范数下最小化信息扰动,各因子信息保留度极其均衡现代买方量化多因子特征融合事实标准

3. 施密特正交化(Gram-Schmidt)数学推导与路径依赖 ​

施密特正交化的本质是递归投影残差剥离。假设我们有 K 个截面因子列向量 f1,f2,…,fK∈RN(已经过截面标准化,均值为 0,方差为 1)。

算法流程: ​

  1. 第一步:基准因子直接保留u1=f1,e1=u1∥u1∥
  2. 第二步:第二因子减去在 e1 上的投影u2=f2−⟨f2,e1⟩e1,e2=u2∥u2∥
  3. 第 k 步:递归剥离在所有前置基底上的投影分量uk=fk−∑j=1k−1⟨fk,ej⟩ej,ek=uk∥uk∥

施密特的致命局限:强路径依赖

调整因子输入的先后顺序会导致完全不同的正交因子库。如果顺序是 [ROE, 20日动量],ROE 会被完整保留,动量中与 ROE 相关的成分被彻底扣除;如果顺序调换为 [20日动量, ROE],动量被完整保留,ROE 的独立性被大幅削弱。在无法确证因果主次地位时,施密特正交化具有主观随意性。


4. 对称正交化(Löwdin Symmetric Orthogonalization)精要推导 ​

为了摆脱对因子排列次序的主观依赖,量子化学家 Per-Olov Löwdin 于 1950 年提出了对称正交化(Symmetric Orthogonalization),随后被顶级对冲基金引入多因子资产定价领域。

4.1 核心优化目标:最小扰动原则(Minimum Information Disturbance) ​

设原始标准化因子矩阵为 F∈RN×K(N 为股票数,K 为因子数,N≫K)。我们希望找到一个正交化后的因子矩阵 F∗∈RN×K,满足:

  1. 严格正交性:(F∗)TF∗=IK
  2. 与原因子矩阵的几何距离最小(Frobenius 范数最小):minF∗∥F∗−F∥F2=minF∗Tr((F∗−F)T(F∗−F))

4.2 闭式解(Closed-form Solution)推导 ​

设原始因子的样本重叠矩阵(协方差/内积矩阵)为:

S=FTF∈RK×K

由于 S 为实对称半正定矩阵,对其进行特征值谱分解(Spectral Decomposition):

S=VΛVT

其中 V 为正交特征向量矩阵(VVT=VTV=I),Λ=diag(λ1,…,λK) 为特征值对角阵,且所有 λi>0。

构造矩阵的负二分之一次方幂:

S−1/2=VΛ−1/2VT,其中 Λ−1/2=diag(1λ1,…,1λK)

对称正交化矩阵显式解为:

F∗=FS−1/2=F(FTF)−1/2

4.3 为什么 F∗ 必然完全正交?(数学证明) ​

直接验证其内积矩阵:

(F∗)TF∗=(FS−1/2)T(FS−1/2)=S−1/2(FTF)S−1/2=S−1/2SS−1/2(因 S−1/2 对称)=S−1/2S1/2S1/2S−1/2=IK⋅IK=IK

证明完毕! 新矩阵各列两两内积完全为 0,相关系数矩阵严格为单位阵 IK。


4.4 大类因子合成的四大工业方案(华泰多因子框架) ​

在完成因子正交化或大类因子提纯后,如何将细分因子合成为一个强健的大类 Alpha?根据**华泰证券多因子系列之一(2016)**研究,机构通常采用四种合成范式:

  1. 等权合成法(Equal Weighting):fcomposite=1K∑k=1Kfk将同大类内所有因子的标准化载荷简单等权平均,稳健性强,杜绝过拟合。
  2. 历史因子收益率加权法(Historical Return Weighting): 以多元回归得到的各因子历史平均收益率 f¯k 作为加权权重,赋予高收益因子更大话语权。
  3. 历史信息比率(IC 均值-协方差)最优加权法(Historical IC/Covariance Weighting): 设矩阵 A∈RM×K 为过去 M 个截面期上 K 个因子的历史 IC 值时序,b∈RK 为各因子 IC 的均值向量,V∈RK×K 为 IC 序列的样本协方差矩阵。 根据马科维茨均值-方差对偶理论,最优因子合成权重向量为:w∗=sV−1b其中 s 为归一化常数(保证 ∑|wk∗|=1)。核心优势:同时考量了因子历史 IC 表现(期望收益)与 IC 波动率/协方差相关性(风险惩罚),赋予独立且高 ICIR 的因子最大权重,是顶级买方的标配方案!
  4. 主成分分析合成法(PCA Weighting): 对大类内各因子做谱分解,提取解释方差最大的第一主成分载荷作为权重。

5. 动手实操:工业级因子正交化器开发实战 ​

下面的 Python 代码严格遵循机构级买方规范,封装了对称正交化(Löwdin)与施密特顺序正交化、条件数健康诊断以及信息保留度分析。

在项目根目录运行 PYTHONPATH=tutorial python3 -m examples.lecture_13_factor_orthogonalization。依赖安装、共享模块和数据契约见项目说明。

py
"""
A 股教学示例:因子正交化处理器 (AshareFactorOrthogonalizer)
包含:对称正交化 (Löwdin) 与 施密特顺序正交化 (Gram-Schmidt)
"""

from typing import List, Literal, Tuple, Dict
from datetime import date
import numpy as np
import polars as pl


class AshareFactorOrthogonalizer:
    """
    A 股截面因子正交化引擎
    """

    def __init__(
        self,
        method: Literal["symmetric", "gram_schmidt"] = "symmetric",
        regularization: float = 1e-6
    ):
        """
        参数:
            method: 正交化算法,可选 'symmetric' (对称正交化/Löwdin) 或 'gram_schmidt' (施密特顺序正交化)
            regularization: 最小相对特征值容忍度;病态/共线输入明确拒绝
        """
        self.method = method
        if not 0 < regularization < 1:
            raise ValueError("相对特征值容忍度必须在 0 和 1 之间")
        self.regularization = regularization

    def _symmetric_orthogonalize(self, F: np.ndarray) -> np.ndarray:
        """
        Löwdin 对称正交化实现
        输入 F: (N, K) 截面矩阵 (已均值0, 方差1)
        输出 F_star: (N, K) 正交矩阵
        """
        N, K = F.shape
        # 计算样本相关性矩阵 S = (F^T @ F) / (N - 1)
        # 为保持与公式完全一致,直接用 S = F^T @ F
        S = F.T @ F

        # 特征值与特征向量分解 S = V @ diag(eigenvals) @ V^T
        # eigh 专用于对称/厄米特矩阵,数值稳定性远高于常规 eig
        eigenvals, V = np.linalg.eigh(S)

        # 拒绝病态矩阵;不截断特征值伪造满秩
        if eigenvals[0] <= self.regularization * eigenvals[-1]:
            raise ValueError("因子矩阵秩不足或近似共线,请删除冗余因子或先降维")

        # 计算 S^{-1/2} = V @ diag(1 / sqrt(lambda)) @ V^T
        diag_inv_sqrt = np.diag(1.0 / np.sqrt(eigenvals))
        S_inv_sqrt = V @ diag_inv_sqrt @ V.T

        # F* = F @ S^{-1/2}
        F_star = F @ S_inv_sqrt

        # 重新对各列做尺度归一化 (均值 0, 方差 1),便于后续与收益率配对打分
        # 注意:乘以常数倍不改变列之间的 0 相关性
        col_stds = np.std(F_star, axis=0, ddof=1)
        col_stds = np.where(col_stds < 1e-8, 1.0, col_stds)
        F_star = (F_star - np.mean(F_star, axis=0)) / col_stds

        return F_star

    def _gram_schmidt_orthogonalize(self, F: np.ndarray) -> np.ndarray:
        """
        经典施密特正交化实现 (依照列的顺序逐步剔除投影残差)
        """
        N, K = F.shape
        Q = np.zeros_like(F)

        for j in range(K):
            v = F[:, j].copy()
            # 依次扣除在之前所有正交基上的投影
            for i in range(j):
                q_i = Q[:, i]
                proj = np.dot(v, q_i) / (np.dot(q_i, q_i) + 1e-12)
                v = v - proj * q_i

            norm = np.linalg.norm(v)
            if norm < 1e-8:
                Q[:, j] = v  # 几乎线性相关全零向量
            else:
                Q[:, j] = v / norm

        # 标准化为单位方差
        col_stds = np.std(Q, axis=0, ddof=1)
        col_stds = np.where(col_stds < 1e-8, 1.0, col_stds)
        Q = (Q - np.mean(Q, axis=0)) / col_stds
        return Q

    def fit_transform_matrix(
        self,
        F: np.ndarray,
        factor_names: List[str]
    ) -> Tuple[np.ndarray, Dict[str, float]]:
        """
        核心矩阵正交化运算与诊断指标输出
        """
        F = np.asarray(F, dtype=float)
        if F.ndim != 2 or not np.isfinite(F).all():
            raise ValueError("因子矩阵必须为有限的二维数组")
        N, K = F.shape
        if K < 1 or N <= K or len(factor_names) != K or len(set(factor_names)) != K:
            raise ValueError("需要 N > K >= 1,且因子名称唯一、数量等于列数")
        F = F - F.mean(axis=0)
        std = F.std(axis=0, ddof=1)
        if np.any(std <= 1e-12):
            raise ValueError("常数列导致矩阵秩不足")
        F = F / std
        eigenvalues = np.linalg.eigvalsh(F.T @ F)
        if eigenvalues[0] <= self.regularization * eigenvalues[-1]:
            raise ValueError("因子矩阵秩不足或近似共线,请删除冗余因子或先降维")

        # 1. 原始矩阵健康度诊断
        orig_corr = np.atleast_2d(np.corrcoef(F, rowvar=False))
        orig_cond = np.linalg.cond(orig_corr)

        # 2. 正交化计算
        if self.method == "symmetric":
            F_ortho = self._symmetric_orthogonalize(F)
        elif self.method == "gram_schmidt":
            F_ortho = self._gram_schmidt_orthogonalize(F)
        else:
            raise ValueError(f"未知的正交化方法: {self.method}")

        # 3. 正交后矩阵验证
        ortho_corr = np.atleast_2d(np.corrcoef(F_ortho, rowvar=False))
        ortho_cond = np.linalg.cond(ortho_corr)

        # 4. 计算每个新因子与原对应母因子的皮尔逊保留度 (Retention Correlation)
        retentions = {}
        for idx, name in enumerate(factor_names):
            ret_corr = np.corrcoef(F[:, idx], F_ortho[:, idx])[0, 1]
            retentions[name] = float(ret_corr)

        diagnostics = {
            "original_condition_number": float(orig_cond),
            "orthogonal_condition_number": float(ortho_cond),
            "retention_correlations": retentions,
            "max_abs_off_diagonal_corr": float(np.max(np.abs(ortho_corr - np.eye(K))))
        }

        return F_ortho, diagnostics

    def transform_polars(
        self,
        df: pl.DataFrame,
        factor_cols: List[str],
        date_col: str = "trade_date"
    ) -> Tuple[pl.DataFrame, pl.DataFrame]:
        """
        按日期截面遍历并执行正交化
        返回: (带有正交因子列的新 DataFrame, 截面诊断指标 DataFrame)
        """
        ortho_cols = [f"{col}_ortho" for col in factor_cols]
        diag_records = []
        result_dfs = []

        # 按截面日期分组正交化
        dates = df[date_col].unique().sort().to_list()

        for cur_date in dates:
            sub_df = df.filter(pl.col(date_col) == cur_date)
            # 提取因子矩阵
            f_mat = sub_df.select(factor_cols).to_numpy()

            # 执行正交化
            f_ortho, diag = self.fit_transform_matrix(f_mat, factor_cols)

            # 诊断日志
            diag_record = {
                date_col: cur_date,
                "orig_cond": diag["original_condition_number"],
                "ortho_cond": diag["orthogonal_condition_number"],
                "max_residual_corr": diag["max_abs_off_diagonal_corr"]
            }
            # 记录各因子保留度
            for col in factor_cols:
                diag_record[f"retention_{col}"] = diag["retention_correlations"][col]
            diag_records.append(diag_record)

            # 拼接回 Polars DataFrame
            ortho_pl_dict = {
                ortho_cols[i]: f_ortho[:, i] for i in range(len(factor_cols))
            }
            sub_ortho_df = sub_df.with_columns([
                pl.Series(k, v) for k, v in ortho_pl_dict.items()
            ])
            result_dfs.append(sub_ortho_df)

        final_df = pl.concat(result_dfs)
        diag_df = pl.DataFrame(diag_records)
        return final_df, diag_df


# ================= 合成数据演示(统计结果不是实盘收益证明) =================
if __name__ == "__main__":
    np.random.seed(42)
    n_stocks = 800  # 类似中证 800 股票池截面

    # 1. 模拟一组具有相关性的合成因子
    # 假设底层存在一个共同的隐变量 (例如未被中性化干净的中小市值成长动量风格)
    latent_growth = np.random.normal(0, 1, n_stocks)
    latent_value = np.random.normal(0, 1, n_stocks)

    # 构造三个强共线性的动量类因子
    mom_20d = latent_growth * 0.8 + np.random.normal(0, 0.4, n_stocks)
    mom_60d = latent_growth * 0.75 + np.random.normal(0, 0.45, n_stocks)
    macd_power = latent_growth * 0.7 + np.random.normal(0, 0.5, n_stocks)

    # 构造两个强共线性的价值因子
    ep_ratio = latent_value * 0.85 + np.random.normal(0, 0.35, n_stocks)
    bp_ratio = latent_value * 0.80 + np.random.normal(0, 0.40, n_stocks)

    raw_data = {
        "trade_date": [date(2026, 3, 31)] * n_stocks,
        "symbol": [f"{i:06d}.SH" for i in range(n_stocks)],
        "mom_20d": mom_20d,
        "mom_60d": mom_60d,
        "macd_power": macd_power,
        "ep_ratio": ep_ratio,
        "bp_ratio": bp_ratio,
    }
    sample_df = pl.DataFrame(raw_data)
    factor_list = ["mom_20d", "mom_60d", "macd_power", "ep_ratio", "bp_ratio"]

    print(">>> 原始因子截面相关系数矩阵:")
    orig_corr = np.corrcoef(sample_df.select(factor_list).to_numpy(), rowvar=False)
    for i, f1 in enumerate(factor_list):
        row_str = " ".join([f"{orig_corr[i, j]:6.3f}" for j in range(len(factor_list))])
        print(f"  {f1:12s} | {row_str}")

    # 2. 运行 Löwdin 对称正交化
    engine = AshareFactorOrthogonalizer(method="symmetric")
    ortho_df, diag_df = engine.transform_polars(sample_df, factor_list)

    print("\n>>> 对称正交化后的因子相关系数矩阵:")
    ortho_cols = [f"{f}_ortho" for f in factor_list]
    ortho_corr = np.corrcoef(ortho_df.select(ortho_cols).to_numpy(), rowvar=False)
    for i, f1 in enumerate(ortho_cols):
        row_str = " ".join([f"{ortho_corr[i, j]:6.3f}" for j in range(len(ortho_cols))])
        print(f"  {f1:18s} | {row_str}")

    print("\n>>> 正交化诊断指标报告:")
    print(diag_df)

    # 3. 对比施密特正交化
    gs_engine = AshareFactorOrthogonalizer(method="gram_schmidt")
    gs_ortho_df, gs_diag_df = gs_engine.transform_polars(sample_df, factor_list)
    print("\n>>> 施密特正交化信息保留度 (越排在后面丢失越严重):")
    for f in factor_list:
        sym_ret = diag_df[f"retention_{f}"][0]
        gs_ret = gs_diag_df[f"retention_{f}"][0]
        print(f"  因子 {f:12s} -> 对称正交化保留度: {sym_ret:6.3f} | 施密特保留度: {gs_ret:6.3f}")

核心运行输出与信息保留度对比分析: ​

>>> 原始因子截面相关系数矩阵:
  mom_20d      |  1.000  0.757  0.711  0.012 -0.012
  mom_60d      |  0.757  1.000  0.682 -0.001 -0.002
  macd_power   |  0.711  0.682  1.000  0.007  0.001
  ep_ratio     |  0.012 -0.001  0.007  1.000  0.830
  bp_ratio     | -0.012 -0.002  0.001  0.830  1.000

>>> 对称正交化后的因子相关系数矩阵:
  mom_20d_ortho      |  1.000  0.000 -0.000 -0.000  0.000
  mom_60d_ortho      |  0.000  1.000  0.000  0.000 -0.000
  macd_power_ortho   | -0.000  0.000  1.000  0.000  0.000
  ep_ratio_ortho     | -0.000  0.000  0.000  1.000 -0.000
  bp_ratio_ortho     |  0.000 -0.000  0.000 -0.000  1.000

>>> 施密特正交化信息保留度 (越排在后面丢失越严重):
  因子 mom_20d      -> 对称正交化保留度:  0.863 | 施密特保留度:  1.000
  因子 mom_60d      -> 对称正交化保留度:  0.872 | 施密特保留度:  0.653
  因子 macd_power   -> 对称正交化保留度:  0.886 | 施密特保留度:  0.668
  因子 ep_ratio     -> 对称正交化保留度:  0.883 | 施密特保留度:  1.000
  因子 bp_ratio     -> 对称正交化保留度:  0.883 | 施密特保留度:  0.557
  1. 多重共线性彻底归零:
    • 原始矩阵中,mom_20d 与 mom_60d 相关性高达 0.757,ep_ratio 与 bp_ratio 相关性高达 0.830;
    • 经过 Löwdin 对称正交化后,非对角线相关系数全部降至 8.03×10−16(机器浮点精度绝对零),矩阵条件数完美恢复为 1.0!
  2. Löwdin 最小扰动优势尽显:
    • 对称正交化让所有 5 个因子极其均匀地保留了与母因子 86.3% ~ 88.6% 的高度相关性;
    • 相比之下,施密特正交化将排在末位的 bp_ratio 保留度残暴削减到仅剩 0.557,严重扭曲了基本面因子的信息纯度。

6. 本讲小结与第三模块知识图谱总结 ​

至此,第三模块:特征工程与经典/微观因子挖掘 全部五大核心讲座构建完毕!

  • 我们剖析了 A 股短期反转与 MAX 彩票异象的微观行为金融学机理(第 9 讲);
  • 掌握了杜邦质量三因子拆解、Sloan 应计利润异象与 SUE 盈利惊喜(第 10 讲);
  • 推导了基于限价订单簿状态机的高频订单流不平衡(OFI)降维算法(第 11 讲);
  • 实现了 MAD 去极值、行业中位数填补、Z-Score 与 WLS 行业市值中性化四步法(第 12 讲);
  • 严格推导并证明了 Löwdin 对称正交化的 Frobenius 最小信息扰动性质(第 13 讲)。

从下一模块开始,我们将正式进入:第四模块:工业级因子评估体系与伪 Alpha 甄别,使用严谨的 Rank IC、ICIR、单调性分层检验与衰减半衰期分析,对这批纯净因子进行最硬核的“成色体检”!

QUANTML · Quantitative Research Handbook