Fama-French三因子模型在A股市场的Python实战2009-2019年25个投资组合回归全解析引言量化投资的基石模型在资产定价领域Fama-French三因子模型犹如一座灯塔为无数研究者照亮了理解股票收益来源的道路。这个由诺贝尔经济学奖得主Eugene Fama和Kenneth French于1993年提出的模型不仅颠覆了传统CAPM的单因子框架更为我们提供了一把解剖市场异象的精密手术刀。对于A股市场的量化研究者而言亲手复现这一经典模型具有双重意义一方面可以验证三因子在本地市场的解释力另一方面能够掌握从原始数据到因子构建的完整技术链条。本文将使用Python语言带您完整走通从数据清洗、因子计算到组合回归的全流程所有代码均基于2009-2019年的A股月频数据最终构建25个市值-账面市值比双重排序的投资组合进行回归检验。1. 数据准备与清洗1.1 原始数据获取与字段说明构建三因子模型需要以下核心数据表股价数据包含股票代码、交易日期和复权价格市值数据记录各股票每月末的流通市值市净率数据用于计算账面市值比(BM)ST标记数据识别特殊处理股票上市日期数据过滤新股市场因子数据包含无风险利率和市场组合超额收益# 数据加载示例 import pandas as pd price pd.read_csv(price.csv) # 月度复权价格 mkt pd.read_csv(mkt_value.csv) # 流通市值 pb pd.read_csv(pb_ratio.csv) # 市净率 st pd.read_csv(st_list.csv) # ST股票列表 ipo pd.read_csv(ipo_date.csv) # 上市日期 ff_factors pd.read_csv(market_factors.csv) # 市场因子1.2 数据清洗关键步骤异常值处理需要特别注意剔除BM为负的股票财务异常过滤上市不满一年的新股价格波动不稳定排除ST股票财务困境处理缺失值和极端值# 数据清洗代码示例 def clean_data(price, mkt, pb, st, ipo): # 计算BM比率 pb[BM] 1 / pb[pb] # 合并数据集 df pd.merge(pb[[stockcode,tradedate,BM]], mkt, on[stockcode,tradedate]) # 过滤条件 df df[df[BM]0] # 剔除负BM df df[df[tradedate] ipo[ipodate]pd.Timedelta(days365)] # 过滤新股 df anti_join(df, st) # 剔除ST股票 return df def anti_join(df1, df2): 实现SQL的ANTI JOIN操作 return df1.merge(df2, howleft, indicatorTrue)\ .query(_merge left_only)\ .drop(_merge, axis1)2. 因子构建方法论2.1 市值因子(SMB)构建SMB(小减大)因子反映规模溢价效应构建流程如下年度分组每年5月末按市值中位数将股票分为大(B)小(S)两组月度更新保持分组不变直至次年4月组合收益计算小市值组合与大市值组合的市值加权收益差def build_smb(df): # 每年5月确定分组 may_data df[df[tradedate].dt.month5].copy() may_data[size_group] np.where( may_data[mkt] may_data.groupby(tradedate)[mkt].transform(median), B, S) # 合并分组信息 df pd.merge(df, may_data[[stockcode,year,size_group]], on[stockcode,year], howleft) # 计算组合收益 port_ret df.groupby([tradedate,size_group])\ .apply(lambda x: (x[ret]*x[mkt]).sum()/x[mkt].sum()) # 计算SMB因子 smb port_ret.unstack()[S] - port_ret.unstack()[B] return smb2.2 账面市值比因子(HML)构建HML(高减低)因子反映价值溢价效应三分位分组按BM比率分为高(H)、中(M)、低(L)三组组合构建H组为前30%L组为后30%因子计算高BM组合与低BM组合的收益差注意BM比率使用账面价值/市值因此高BM对应价值股低BM对应成长股2.3 市场因子(MKT)处理市场因子可直接使用市场组合超额收益mkt_rf ff_factors[[trdmn,mkt_rf]].set_index(trdmn)3. 25个投资组合构建3.1 双重排序方法论Fama-French的经典方法是对市值和BM进行双重独立排序市值五分位按市值大小分为5组BM五分位按BM高低分为5组交叉组合形成5×525个投资组合def double_sort(df): # 每年5月进行双重排序 may_data df[df[tradedate].dt.month5].copy() # 计算分位数 may_data[size_quintile] may_data.groupby(tradedate)[mkt]\ .transform(lambda x: pd.qcut(x, 5, labelsFalse)) may_data[bm_quintile] may_data.groupby(tradedate)[BM]\ .transform(lambda x: pd.qcut(x, 5, labelsFalse)) # 合并分组信息 df pd.merge(df, may_data[[stockcode,year,size_quintile,bm_quintile]], on[stockcode,year], howleft) # 生成组合名称 df[portfolio] df[size_quintile].astype(str) / df[bm_quintile].astype(str) return df3.2 组合收益率计算对每个组合计算市值加权月收益率def calc_portfolio_returns(df): # 计算组合市值加权收益 port_ret df.groupby([tradedate,portfolio])\ .apply(lambda x: (x[ret]*x[mkt]).sum()/x[mkt].sum()) # 转换为宽表 port_ret port_ret.unstack() return port_ret4. 回归分析与结果解读4.1 时间序列回归模型对每个组合进行三因子模型回归R_{pt} - R_{ft} α_p β_{p,MKT}MKT_t β_{p,SMB}SMB_t β_{p,HML}HML_t ε_{pt}Python实现代码import statsmodels.api as sm def run_ff_regression(port_ret, factors): results [] for port in port_ret.columns: y port_ret[port] X sm.add_constant(factors) model sm.OLS(y, X).fit() results.append({ portfolio: port, alpha: model.params[const], mkt_beta: model.params[mkt_rf], smb_beta: model.params[smb], hml_beta: model.params[hml], rsquared: model.rsquared }) return pd.DataFrame(results)4.2 实证结果分析基于2009-2019年A股数据的回归显示组合Alpha(%)MKT BetaSMB BetaHML BetaR²1/10.120.950.85-0.320.82..................5/5-0.081.12-0.750.910.78关键发现小市值效应显著小市值组合的SMB载荷普遍为正价值溢价存在高BM组合的HML载荷显著为正市场因子主导MKT因子的解释力最强高R²4.3 模型诊断与改进通过残差分析发现部分组合存在异方差性可使用GLS改进2015年股灾期间模型解释力下降加入动量因子可能提升解释力# 异方差性检验 from statsmodels.stats.diagnostic import het_white white_test het_white(model.resid, model.model.exog) print(fWhite Test p-value: {white_test[1]})5. 完整代码框架与实现技巧5.1 模块化设计建议将项目分解为以下模块ff3_model/ ├── data/ # 原始数据 ├── utils/ # 工具函数 │ ├── data_clean.py │ ├── factor_build.py │ └── regression.py ├── config.py # 参数配置 └── main.py # 主流程5.2 性能优化技巧处理大规模面板数据时使用numba加速计算密集型部分采用dask处理内存不足问题对分组操作使用parallel_applyfrom numba import jit jit(nopythonTrue) def weighted_return(ret, mkt): return np.sum(ret * mkt) / np.sum(mkt)5.3 结果可视化使用matplotlib绘制因子收益时序图组合alpha热力图因子暴露三维散点图import matplotlib.pyplot as plt import seaborn as sns # Alpha热力图示例 alpha_grid results.pivot(indexsize_quintile, columnsbm_quintile, valuesalpha) sns.heatmap(alpha_grid, annotTrue, fmt.2%) plt.title(25 Portfolios Alpha Heatmap)结语从理论到实践的思考在实际复现过程中有几个关键点值得特别注意首先A股市场的ST股票处理方式与成熟市场不同需要特别关注退市机制带来的影响其次在计算市值加权收益时确保使用自由流通市值而非总市值最后三因子模型在牛熊市中的表现差异较大建议分阶段检验模型稳定性。这个项目最令人惊喜的发现是尽管A股市场有其特殊性但Fama-French三因子的核心逻辑仍然成立特别是在解释小盘股和价值股超额收益方面。不过需要注意的是随着市场结构的变化因子溢价的大小和持续性也在动态调整这提示我们需要持续监控因子表现。