Fama-French 到神经网络:Python 量化人必看的因子投资进化史

原创 数据科学实战 数据科学实战 数据科学实战 2026年5月25日 09:02 四川 2人

 用 Python 揭秘均值回归策略:你的收益从何而来?

2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!

引言

如果你正在学习 Python 量化投资,那么「因子投资」一定是绕不开的话题。从 1992 年 Fama 和 French 提出三因子模型,到 2020 年 Gu、Kelly、Xiu 用机器学习横扫资产定价领域,再到神经网络自动构造因子,这个领域已经发生了翻天覆地的变化。

最近读到一篇非常硬核的实战梳理文章,作者把 60 年的因子投资学术文献串起来,从 CAPM 一路讲到神经网络,还附带了 Python 实现思路。今天我把核心内容整理成中文版,并配上 Python 代码示例,希望能帮你快速理清这条技术主线。


一、因子投资到底在做什么

CAPM(资本资产定价模型)告诉我们:股票收益只由市场组合驱动,其它都是噪声。但 1981 年 Banz 发现小盘股有超额收益,CAPM 解释不了,这种现象被称为「异象」(anomaly)。

Ross 在 1976 年提出 APT(套利定价理论),把资产收益写成多个因子的线性组合:

后续最有名的实现就是 Fama-French 系列:

模型

因子

提出年份

FF 三因子

市场、SMB(规模)、HML(价值)

1993

Carhart 四因子

加 WML(动量)

1997

FF 五因子

RMW(盈利)、CMA(投资)

2015

数据可以直接从 Kenneth French Data Library 下载,Python 里一行代码就能搞定:

# 使用 pandas_datareader 读取 Fama-French 五因子数据
import pandas_datareader.data as web
import pandas as pd

# 拉取 1963 年至今的月度五因子数据
ff5 = web.DataReader(
    "F-F_Research_Data_5_Factors_2x3",  # 五因子数据集名称
    "famafrench",                        # 数据源
    start="1963-07-01"                   # 起始日期
)[0]

# 单位是百分比,需要除以 100 转成小数
ff5 = ff5 / 100

print(ff5.head())  # 查看前几行:MKT-RF、SMB、HML、RMW、CMA、RF

二、如何发现一个新因子

学术界发现因子主要有两种方法。

方法一:投资组合排序(Portfolio Sorting)

按某个特征把股票分成 5 组或 10 组,跟踪每组未来收益,看头尾两组差异是否显著。

# 一个简化的投资组合排序示例
import numpy as np
import pandas as pd

def sort_portfolios(df, characteristic, n_groups=10):
    """
    按某个特征对股票分组并计算各组平均收益
    df: 包含 stock_id、date、return、characteristic 的 DataFrame
    characteristic: 排序使用的特征列名
    n_groups: 分组数,默认 10 组(十分位数)
    """
    # 每个时间点根据特征分组
    df["group"] = df.groupby("date")[characteristic].transform(
        lambda x: pd.qcut(x, n_groups, labels=False)  # 等频分组
    )
    # 计算各组每期平均收益
    group_returns = df.groupby(["date", "group"])["return"].mean().unstack()
    
    # 多空组合:买入第一组,卖空最后一组
    long_short = group_returns[0] - group_returns[n_groups - 1]
    return long_short

# 多空组合的 t 统计量决定因子是否「显著」

方法二:Fama-MacBeth 回归

这是 1973 年的经典方法,分两步:第一步用时间序列回归估计每只股票的因子载荷 beta;第二步在每个时点做横截面回归,估计因子风险溢价 gamma。

import statsmodels.api as sm
import numpy as np

def fama_macbeth(returns, factors):
    """
    Fama-MacBeth 两步回归
    returns: 各资产收益矩阵,形状为 (T, N)
    factors: 因子收益矩阵,形状为 (T, K)
    """
    T, N = returns.shape
    K = factors.shape[1]
    
    # 第一步:时间序列回归,估计每只股票的 beta
    betas = np.zeros((N, K))
    X = sm.add_constant(factors)  # 加截距项
    for i in range(N):
        model = sm.OLS(returns[:, i], X).fit()
        betas[i, :] = model.params[1:]  # 取因子系数,不要截距
    
    # 第二步:每期横截面回归,估计因子溢价 gamma
    gammas = np.zeros((T, K))
    Xb = sm.add_constant(betas)
    for t in range(T):
        model = sm.OLS(returns[t, :], Xb).fit()
        gammas[t, :] = model.params[1:]
    
    # 风险溢价是 gamma 的时间均值
    risk_premia = gammas.mean(axis=0)
    # t 统计量用于显著性检验
    t_stats = risk_premia / (gammas.std(axis=0) / np.sqrt(T))
    
    return risk_premia, t_stats

Sheppard(2023)用这套方法在 25 个 Fama-French 组合上跑出来:市场溢价约 6.66%、SMB 约 2.87%、HML 约 2.81%(年化)。但 J 检验统计量高达 95.29,说明三因子模型作为完整描述其实是被拒绝的。


三、p 值陷阱:你以为显著的因子可能根本不存在

这是很多 Python 量化新手最容易栽的坑。p 值是 P(D|H),即「假设原假设成立时观察到当前数据的概率」,但我们想知道的是 P(H|D),即「在数据下原假设成立的概率」。

Harvey(2017)提出了贝叶斯化 p 值(Bayesianised p-value):

import numpy as np

def bayesianised_p_value(t_stat, prior_odds):
    """
    Harvey (2017) 提出的贝叶斯化 p 值
    t_stat: 回归得到的 t 统计量
    prior_odds: 你对原假设为真的先验赔率,p/(1-p)
                 比如先验认为 86% 概率原假设成立,则 prior_odds = 0.86/0.14 ≈ 6
    返回值:原假设为真的后验概率
    """
    # 公式:Bpv = exp(-t²/2) * prior / (1 + exp(-t²/2) * prior)
    likelihood_ratio = np.exp(-t_stat ** 2 / 2)
    bpv = likelihood_ratio * prior_odds / (1 + likelihood_ratio * prior_odds)
    return bpv

# 案例:t = 2(约 5% 的传统 p 值),先验赔率 6
bpv = bayesianised_p_value(t_stat=2.0, prior_odds=6)
print(f"贝叶斯化 p 值:{bpv:.3f}")  # 约 0.448
# 也就是说原假设为真的概率仍有 44.8%,远不是「显著」

Harvey、Liu、Zhu(2016)整理了文献中超过 300 个因子,其中很多在样本外都无法复现。Chen 和 Zimmermann(2020)估计已发表收益的「出版偏差」约 12%,也就是说论文里报的 8% 收益,真实可能只有 7%。

💡 教训:看到一个新因子不要急着上车,至少把 t 值门槛提到 3 以上再说。


四、当因子集体翻车:2007 年 Quant 大屠杀

Khandani 和 Lo(2007)记录了一段惨痛历史:2007 年 8 月,一家量化基金因次贷损失被迫平仓。由于所有 quant 基金都「在同一个池塘里钓鱼」(持仓高度相似),强制抛售引发了多米诺骨牌:

  1. 1. 「便宜」股票被进一步砸盘

  2. 2. 「贵」股票被买入回补

  3. 3. 做市商持续亏损停止报价

  4. 4. 流动性蒸发,恶性循环

这告诉我们一个残酷事实:因子投资在正常时期是分散化的,在危机时期是高度相关的。杠杆 + 流动性蒸发是因子策略最大的杀手。


五、机器学习真的有用吗?

Gu、Kelly、Xiu(2020)做了迄今最全面的机器学习对比实验:

  • • 数据:30000 只股票 × 60 年 × 900 多个预测变量

  • • 方法:OLS、Lasso、Ridge、Elastic Net、PCR、PLS、随机森林、GBDT、神经网络(1—5 层)

关键结论:

方法

月度 R²

备注

OLS(900 + 变量)

严重为负

过拟合到爆

Elastic Net

0.11%

正则化救回来了

PCR / PLS

0.26% / 0.27%

降维有效

树模型 / 神经网络

0.33%—0.40%

非线性带来提升

神经网络(最佳层数)

3 层

更深反而下降

# 一个 Gu et al. 风格的浅层神经网络示例
import torch
import torch.nn as nn

class StockReturnNet(nn.Module):
    """预测股票月度收益的浅层神经网络"""
    def __init__(self, n_features, hidden_dims=[32, 16, 8]):
        super().__init__()
        layers = []
        in_dim = n_features
        # 三层隐藏层就够了,再深效果反而变差
        for h in hidden_dims:
            layers.append(nn.Linear(in_dim, h))   # 全连接层
            layers.append(nn.ReLU())              # 激活函数
            layers.append(nn.BatchNorm1d(h))      # 批归一化稳定训练
            in_dim = h
        layers.append(nn.Linear(in_dim, 1))       # 输出层:预测收益
        self.net = nn.Sequential(*layers)
    
    def forward(self, x):
        return self.net(x)

# 注意:金融数据信噪比极低,不要照搬 CV 领域那种 50 层网络
model = StockReturnNet(n_features=94, hidden_dims=[32, 16, 8])

💡 重要发现:线性模型与非线性模型的差距,主要来自变量之间的交互作用,而不是单个变量的非线性变换。


六、神经网络不止能选因子,还能造因子

Fang 等(2020)提出了 NNAFC 框架,思路是用神经网络从原始 OHLCV 数据自动构造因子。

核心创新点:

  1. 1. 目标函数用 Spearman 相关(Rank IC) :对异常值更稳健

  2. 2. rank() 不可微 → 用 sigmoid 核近似:让梯度能反向传播

  3. 3. 预训练注入金融先验:先让网络学会复现 MA、MACD、RSI 等技术指标

import torch

def differentiable_rank_kernel(x, p=1.83):
    """
    可微的排序近似函数
    x: 输入张量
    p: 控制锐度的超参数,p=1.83 时 95% 数据落在 ±2 std 内
    """
    mean = x.mean()
    std = x.std()
    # 用 sigmoid 函数平滑替代不可微的 rank()
    return 1.0 / (1.0 + torch.exp(-p * (x - mean) / (2 * std)))

def rank_ic_loss(factor_values, returns):
    """
    Rank IC 损失函数:用于神经网络因子构造
    factor_values: 网络输出的因子值
    returns: 实际收益
    """
    rank_f = differentiable_rank_kernel(factor_values)
    rank_r = differentiable_rank_kernel(returns)
    # 计算两个 rank 序列的相关系数
    rank_f_centered = rank_f - rank_f.mean()
    rank_r_centered = rank_r - rank_r.mean()
    
    cov = (rank_f_centered * rank_r_centered).mean()
    std_f = rank_f_centered.std()
    std_r = rank_r_centered.std()
    
    # 因为是损失函数所以取负号(要最大化相关性)
    return -cov / (std_f * std_r + 1e-8)

实验结果(中国 A 股数据):

方法

IC

多样性

遗传规划(GP)

0.072

17.53

全连接网络

0.124

22.15

LSTM

0.170

24.47

Transformer

0.111

25.26

把 50 个 NNAFC 因子和 50 个专家因子组合,LSTM 版本年化收益 29.9%、最大回撤 15.0%、夏普比率 3.289,全方位碾压纯专家因子。


七、ESG 因子靠谱吗

简单一句话:目前还说不清

主要原因是不同 ESG 数据商对同一家公司的评分差距巨大(Berg、Koelbel、Rigobon,2020)。在数据标准化之前,相关研究的结论可信度都要打折扣。


总结

读完这些材料,我有几点核心收获分享给学 Python 的朋友:

  1. 1. 因子投资是真实存在的,但「真实」不等于「无风险」「永久有效」。因子会被拥挤、会衰减、会在杠杆与流动性同时枯竭时暴力反转。

  2. 2. 机器学习确实有效,但不是因为它「深」,而是因为它能捕捉变量间的非线性交互。金融信号信噪比极低,浅层架构(3 层左右)通常优于深层网络

  3. 3. 神经网络构造因子是真正的范式升级。NNAFC 比遗传规划在信息含量和多样性上都更胜一筹,预训练注入领域知识的思路很值得借鉴。

  4. 4. 永远保持怀疑。出版偏差普遍存在,p 值经常被误读,很多发表的异象在样本外失效。学会用贝叶斯化 p 值思考,比闭眼相信 5% 显著性靠谱得多。

如果你正在用 Python 做量化研究,建议从 Kenneth French 数据库的因子开始,先把 Fama-MacBeth 回归手撸一遍,再尝试 sklearn 里的 ElasticNet 和 PCA,最后用 PyTorch 实现简单的浅层神经网络做因子预测。这条路径走完,你对量化研究会有完全不同的理解。

更多推荐