Fama-French三因子模型在A股市场的Python实战:从数据清洗到25组投资组合回归

引言

在量化投资领域,Fama-French三因子模型已成为解释股票收益率的经典框架。本文将带您用Python完整实现该模型在A股市场的实证分析,从原始数据获取到因子构建,再到25组投资组合的回归检验。不同于理论介绍,我们聚焦 可复现的代码实现 ,提供可直接应用于A股研究的工具性内容。无论您是金融专业学生还是量化研究员,都能通过本文掌握三因子模型的核心实现技术。

我们将使用2009-2019年全A股月频数据,重点解决以下实际问题:

  • 如何正确处理A股特有的ST股票和新股问题
  • SMB(规模因子)和HML(价值因子)的精确构建方法
  • 25组投资组合的市值加权收益率计算
  • 回归结果的经济意义解读

1. 数据准备与清洗

1.1 原始数据获取

我们需要以下核心数据表(假设已从CSV文件读取):

import pandas as pd
import numpy as np

# 基础数据读取
price = pd.read_csv('股价数据.csv')  # 每月收盘价
ST = pd.read_csv('ST标记.csv')     # ST状态记录
pb = pd.read_csv('市净率.csv')     # 每月PB值
mkt = pd.read_csv('市值数据.csv')  # 每月总市值
ipodate = pd.read_csv('上市日期.csv') # 股票IPO日期

1.2 数据预处理关键步骤

市值与账面市值比合并

# 计算账面市值比BM=1/PB
BM = pb.copy()
BM['BM'] = 1/pb.pb  
BM = BM.drop(['pb'], axis=1)

# 合并BM与市值数据
f = pd.merge(BM, mkt, on=['tradedate','stockcode'])
f = f.loc[f.BM > 0].reset_index(drop=True)  # 剔除BM<=0的股票

新股过滤(上市不满一年)

# 计算上市满一年的临界日期
ipodate['entry_date'] = ipodate.ipodate + pd.Timedelta(days=365)  
f = pd.merge(f, ipodate, on='stockcode')
f = f.loc[f.tradedate >= f.entry_date].reset_index(drop=True)

ST股票过滤

res = []
for date in f.tradedate.unique():
    fuse = f.loc[f.tradedate == date]
    st_use = ST.loc[(ST.entry_dt <= date) & 
                   ((ST.remove_dt > date) | ST.remove_dt.isna())]
    valid_codes = set(fuse.stockcode) - set(st_use.stockcode)
    res.append(fuse.set_index('stockcode').loc[valid_codes].reset_index())
    
f = pd.concat(res).reset_index(drop=True)

提示:A股数据清洗需特别注意两点:(1) 上市首月涨跌幅限制不同 (2) ST股票的特殊交易规则

2. 因子构建方法论

2.1 规模因子(SMB)与价值因子(HML)

Fama-French三因子的核心在于 双重排序法 构建因子:

  1. 每年5月末进行分组

    f['ym'] = f.tradedate.dt.year*100 + f.tradedate.dt.month
    f_5 = f.loc[f.ym % 10 == 5].copy()  # 每年5月数据
    
  2. BM分组(高、中、低)

    def split_BM(x):
        x.loc[x.BM >= x.BM.quantile(0.7), 'group_BM'] = 'H'
        x.loc[x.BM < x.BM.quantile(0.3), 'group_BM'] = 'L'
        return x
    
    f_5['group_BM'] = 'M'
    f_5 = f_5.groupby('ym').apply(split_BM)
    
  3. 市值分组(大、小)

    def split_SIZE(x):
        x.loc[x.mkt >= x.mkt.median(), 'group_SIZE'] = 'B'
        return x
    
    f_5['group_SIZE'] = 'S'
    f_5 = f_5.groupby('ym').apply(split_SIZE)
    
  4. 形成6个投资组合

    f_5['portfolio_name'] = f_5.group_SIZE + '/' + f_5.group_BM
    

2.2 因子收益率计算

SMB因子公式

SMB = 1/3(Small/High + Small/Medium + Small/Low) - 
      1/3(Big/High + Big/Medium + Big/Low)

HML因子公式

HML = 1/2(Small/High + Big/High) - 
      1/2(Small/Low + Big/Low)

Python实现:

# 计算组合收益率(市值加权)
port_ret = f.groupby(['tradedate','portfolio_name']).apply(
    lambda x: (x.ret*x.mkt).sum()/x.mkt.sum())

# 转换为宽表
port_ret_pivot = port_ret.unstack()

# 计算因子收益率
SMB = (port_ret_pivot['S/H'] + port_ret_pivot['S/M'] + port_ret_pivot['S/L'])/3 - 
       (port_ret_pivot['B/H'] + port_ret_pivot['B/M'] + port_ret_pivot['B/L'])/3

HML = (port_ret_pivot['S/H'] + port_ret_pivot['B/H'])/2 - 
       (port_ret_pivot['S/L'] + port_ret_pivot['B/L'])/2

3. 25组投资组合构建

3.1 五等分分组方法

将市值和BM分别分为5组,形成5x5矩阵:

groups = 5
f_5 = f.loc[f.ym % 10 == 5, ['stockcode','tradedate','BM','mkt']].copy()

# BM五等分
f_5['g_BM'] = f_5.groupby('tradedate')['BM'].rank(
    pct=True).apply(lambda x: np.ceil(x*groups))

# 市值五等分 
f_5['g_SIZE'] = f_5.groupby('tradedate')['mkt'].rank(
    pct=True).apply(lambda x: np.ceil(x*groups))

3.2 组合收益率计算

# 合并分组信息
f = pd.merge(f, f_5[['stockcode','tradedate','g_BM','g_SIZE']], 
             on=['stockcode','tradedate'])

# 计算25组组合收益率
port_ret_25 = f.groupby(['tradedate','g_BM','g_SIZE']).apply(
    lambda x: (x.ret*x.mkt).sum()/x.mkt.sum())

port_ret_25 = port_ret_25.unstack().reset_index()

4. 回归分析与结果解读

4.1 三因子模型回归

对每个组合进行时间序列回归:

R_i - R_f = α + β1*(Rm-Rf) + β2*SMB + β3*HML + ε

Python实现:

import statsmodels.api as sm

# 准备解释变量
ff3 = pd.concat([SMB, HML, mkt_rf], axis=1)
ff3['Intercept'] = 1
x = ff3[['SMB','HML','mkt_rf','Intercept']].values

# 存储结果
results = []
for i in range(25):
    y = port_ret_25.iloc[:,i+1].values
    model = sm.OLS(y, x).fit()
    results.append({
        'Group': port_ret_25.columns[i+1],
        'Alpha': model.params[-1],
        'Beta_MKT': model.params[2],
        'Beta_SMB': model.params[0], 
        'Beta_HML': model.params[1],
        'R2': model.rsquared
    })

4.2 结果分析示例

典型回归结果呈现:

组合 Alpha β_MKT β_SMB β_HML
1/1 0.003** 0.85 0.62 0.41 0.72
3/3 0.001 1.02 -0.08 0.05 0.88
5/5 -0.002* 1.15 -0.35 -0.82 0.81

关键发现:

  1. 小市值效应 :小市值组合(1/x)对SMB因子暴露显著为正
  2. 价值溢价 :高BM组合(x/5)对HML因子暴露为负(A股特有现象)
  3. 大市值组合 表现出更显著的市场风险暴露

注意:A股市场常出现与美股不同的因子特征,需结合本地市场环境解读

5. 完整代码框架

以下是整合后的Jupyter Notebook代码结构:

# 1. 数据准备
def load_and_clean():
    # 数据读取与清洗代码
    ...

# 2. 因子构建  
def construct_factors():
    # SMB/HML构建代码
    ...

# 3. 组合回归
def portfolio_regression():
    # 25组组合回归代码
    ...

# 主程序
if __name__ == '__main__':
    df = load_and_clean()
    factors = construct_factors(df)
    results = portfolio_regression(df, factors)
    
    # 结果可视化
    plot_results(results)

实际应用中,建议将完整代码封装为类结构:

class FamaFrench3Factor:
    def __init__(self, start_date='2009-05', end_date='2019-12'):
        self.data = None
        self.factors = None
        
    def load_data(self):
        # 实现数据加载
        ...
        
    def build_factors(self):
        # 实现因子构建
        ...
        
    def run_regression(self):
        # 实现回归分析
        ...

通过这种模块化设计,可轻松扩展为Carhart四因子或其他多因子模型。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐