资讯动态

Fama-French三因子模型实证全流程:从数据对齐到五因子过渡

发布时间:2026/10/14 11:51:30 来源:尧图企业网站定制
简介这份资源面向具备经济金融基础与实证研究方法的学生和从业者围绕美国普通股票收益率的多因子模型展开是金融计量经济学课程中单因子CAPM向三因子模型扩展的实证项目资料。内容涵盖在25个按规模与账面市值比划分的股票组合上估计三因子回归报告SMB与HML因子的统计特征、相关性及显著性并进一步检验规模与运营盈利能力、规模与投资形成的新50组组合的解释力帮助读者掌握数据下载清洗、模型构建评价与研究报告撰写。资源包为1个PDF文件约77KB内含完整的项目任务说明与实证分析要求可直接作为课程作业或自学练习的参考框架。目前已有72人学习适合希望深入理解Fama-French模型并提升多因子资产定价实证能力的读者。1. 从单因子到五因子一份能跑通 Fama-French 全流程的金融计量实证包很多人做金融计量作业时卡在同一个地方模型公式看得懂但真到动手阶段数据从哪下、怎么对齐、回归表怎么复现全是黑匣子。这份 BU.232.620 Empirical Project 2 的资源包解决的正是这个断层。它围绕 Fama-French 三因子模型展开要求你在 25 个 Size/BM 组合上跑多元线性回归再把这套流程迁移到 Size/OP 和 Size/INV 两组新组合上最后对比三因子模型在不同组合间的解释力差异。适合已经学过 CAPM、会一点 Python 但没独立跑过完整实证流程的人。它不教你金融理论它逼你把理论变成能提交的代码和报告。2. 三因子模型的数据地基从 Ken French 数据库到可回归的面板2.1 为什么数据源选择比模型本身更致命Fama-French 三因子模型的实证起点不是回归代码而是数据对齐。Ken French Data Library 提供的文件看起来简单但每个文件的日期格式、频率、组合排序方式都不一样。25 个 Size/BM 组合的月度收益率、三因子时间序列、无风险利率这三类数据必须先在时间轴上严格对齐才能构造出 R(t)-RF(t) 这个被解释变量。常见做法是直接从 Ken French 网站下载以下文件25_Portfolios_5x5_CSV.zip包含按 Size 和 Book-to-Market 划分的 25 个组合的月度收益率F-F_Research_Data_Factors_CSV.zip包含 Mkt-RF、SMB、HML、RF 四个序列后续任务还需要25_Portfolios_ME_OP_5x5_CSV.zip和25_Portfolios_ME_INV_5x5_CSV.zip这些文件解压后是 CSV 格式但日期列通常是YYYYMM格式的整数而收益率是百分比数值。如果不做转换回归出来的系数会差两个数量级。我一般会先写一个统一的数据加载函数把日期解析、百分比转小数、列名标准化这三件事一次做完。import pandas as pd import numpy as np def load_french_csv(filepath, date_coldate): 加载 Ken French 数据文件统一日期格式和数值单位 参数: filepath: CSV 文件路径 date_col: 日期列名French 数据通常无列名需手动指定 返回: DataFrame索引为 datetime数值为小数收益率 # French 数据前几行常有说明文字skiprows 需根据文件调整 df pd.read_csv(filepath, skiprows3, index_col0) # 日期索引转为 YYYY-MM 格式再解析 df.index pd.to_datetime(df.index.astype(str), format%Y%m) # 百分比转小数 df df / 100.0 # 去掉可能的空行或说明行 df df.dropna(howall) return df # 加载三因子和 25 组合 factors load_french_csv(F-F_Research_Data_Factors.csv) portfolios load_french_csv(25_Portfolios_5x5.csv) # 对齐时间轴取交集 common_idx factors.index.intersection(portfolios.index) factors factors.loc[common_idx] portfolios portfolios.loc[common_idx] # 构造市场超额收益 mkt_excess factors[Mkt-RF] rf factors[RF]这段代码的关键在skiprows3和format%Y%m。French 的 CSV 文件头部通常有版权说明和列名说明不跳过会导致列名错位。日期解析必须指定格式否则 pandas 会把202301误读为整数。百分比转小数这一步看起来简单但漏掉的话回归系数会放大 100 倍t 统计量却不变很容易在报告里写出错误的经济含义。2.2 三因子相关性检验多重共线性的第一道防线在跑回归之前必须先看三个因子之间的相关系数矩阵。Fama and French (1993) 的 Table 2 之所以重要是因为它直接告诉你 SMB 和 HML 之间、以及它们与市场因子之间是否存在严重共线性。如果相关系数超过 0.7回归系数的标准误会膨胀显著性检验就不可靠了。# 计算三因子相关系数矩阵 factor_df factors[[Mkt-RF, SMB, HML]] corr_matrix factor_df.corr() print(三因子相关系数矩阵) print(corr_matrix.round(4)) # 检查方差膨胀因子VIF from statsmodels.stats.outliers_influence import variance_inflation_factor vif_data pd.DataFrame() vif_data[因子] factor_df.columns vif_data[VIF] [variance_inflation_factor(factor_df.values, i) for i in range(factor_df.shape[1])] print(\n方差膨胀因子) print(vif_data)逻辑说明相关系数矩阵给出两两线性关系的强度VIF 则量化每个因子被其他因子解释的程度。VIF 超过 10 通常被认为存在严重共线性。在 Fama-French 三因子框架下Mkt-RF 与 SMB、HML 的相关系数通常较低但 SMB 和 HML 之间偶尔会出现中等相关。如果 VIF 在 1 到 3 之间基本可以放心跑回归如果超过 5就需要在报告中讨论共线性对系数解释的影响。参数说明variance_inflation_factor的第一个参数是特征矩阵第二个是特征索引。注意这个函数要求输入不含常数项因为常数项会导致完全共线性。如果因子矩阵里有缺失值需要先 dropna 或填充。2.3 25 组合的多元线性回归从循环到结果表有了对齐后的数据和因子相关性判断接下来就是对 25 个组合逐个跑时间序列回归。模型形式是R(t) - RF(t) a b[RM(t) - RF(t)] sSMB(t) hHML(t) e(t)每个组合跑一次 OLS提取 alpha、b、s、h 的系数和 t 统计量最后拼成一张类似 Fama-French Table 6 的结果表。import statsmodels.api as sm def run_three_factor_regression(portfolio_returns, factors, portfolio_name): 对单个组合跑三因子回归 参数: portfolio_returns: 组合收益率序列小数 factors: 包含 Mkt-RF, SMB, HML, RF 的 DataFrame portfolio_name: 组合名称用于标记结果 返回: 字典包含系数、t值、R方 # 构造被解释变量组合超额收益 y portfolio_returns - factors[RF] # 构造解释变量矩阵 X factors[[Mkt-RF, SMB, HML]] # 添加常数项 X sm.add_constant(X) # 对齐索引 aligned pd.concat([y, X], axis1).dropna() y_clean aligned.iloc[:, 0] X_clean aligned.iloc[:, 1:] # OLS 回归 model sm.OLS(y_clean, X_clean).fit() return { portfolio: portfolio_name, alpha: model.params[const], alpha_t: model.tvalues[const], beta_mkt: model.params[Mkt-RF], beta_mkt_t: model.tvalues[Mkt-RF], beta_smb: model.params[SMB], beta_smb_t: model.tvalues[SMB], beta_hml: model.params[HML], beta_hml_t: model.tvalues[HML], r_squared: model.rsquared, adj_r_squared: model.rsquared_adj } # 对 25 个组合循环回归 results [] for col in portfolios.columns: res run_three_factor_regression(portfolios[col], factors, col) results.append(res) results_df pd.DataFrame(results) print(results_df[[portfolio, alpha, alpha_t, beta_smb, beta_smb_t, beta_hml, beta_hml_t, adj_r_squared]].round(4))逻辑说明sm.add_constant添加截距项对应模型中的 alpha。被解释变量是组合收益减无风险利率解释变量直接使用 Mkt-RF、SMB、HML 三列。dropna确保时间轴完全对齐后再回归避免因缺失值导致样本量不一致。结果中的beta_smb_t和beta_hml_t就是判断 SMB 和 HML 载荷是否显著的关键指标。参数说明model.params返回系数model.tvalues返回 t 统计量。通常 |t| 1.96 对应 5% 显著性水平。rsquared_adj是调整 R 方用于比较三因子模型和单因子 CAPM 的解释力。如果三因子模型的调整 R 方明显高于 CAPM说明新增因子确实吸收了部分时间序列变异。2.4 假设检验SMB 和 HML 是否真的有必要存在项目要求检验的假设是在市场超额收益之外SMB 和 HML 是否具有显著的联合解释力。这本质上是一个嵌套模型的 F 检验。原假设 H0s 0 且 h 0。备择假设 H1至少有一个不为零。from statsmodels.stats.anova import anova_lm def test_joint_significance(portfolio_returns, factors): 对单个组合检验 SMB 和 HML 的联合显著性 比较三因子模型与单因子 CAPM 模型 y portfolio_returns - factors[RF] # 三因子模型 X_full sm.add_constant(factors[[Mkt-RF, SMB, HML]]) aligned_full pd.concat([y, X_full], axis1).dropna() model_full sm.OLS(aligned_full.iloc[:, 0], aligned_full.iloc[:, 1:]).fit() # 单因子 CAPM 模型 X_reduced sm.add_constant(factors[[Mkt-RF]]) aligned_reduced pd.concat([y, X_reduced], axis1).dropna() model_reduced sm.OLS(aligned_reduced.iloc[:, 0], aligned_reduced.iloc[:, 1:]).fit() # F 检验 from scipy import stats rss_full model_full.ssr rss_reduced model_reduced.ssr df_diff 2 # 两个约束 df_resid model_full.df_resid f_stat ((rss_reduced - rss_full) / df_diff) / (rss_full / df_resid) p_value 1 - stats.f.cdf(f_stat, df_diff, df_resid) return f_stat, p_value # 对第一个组合做检验 f_stat, p_val test_joint_significance(portfolios.iloc[:, 0], factors) print(fF统计量: {f_stat:.4f}, p值: {p_val:.6f})逻辑说明F 检验比较两个嵌套模型的残差平方和。如果三因子模型显著降低了残差平方和说明 SMB 和 HML 提供了市场因子之外的额外解释力。p 值小于 0.05 则拒绝原假设认为两个新因子联合显著。参数说明df_diff是约束数量这里 SMB 和 HML 两个系数被约束为零所以是 2。df_resid是三因子模型的残差自由度。这个检验对 25 个组合逐个做最后统计有多少个组合拒绝了原假设就能回答项目中的核心问题。3. 从 25 到 75把三因子模型迁移到 Size/OP 和 Size/INV 组合3.1 新组合的数据加载与描述性统计差异第二部分的任务是把同样的三因子模型应用到两组新组合上25 个 Size/Operating Profitability 组合和 25 个 Size/Investment 组合。数据同样来自 Ken French 数据库但文件不同组合的排序逻辑也不同。Size/BM 组合反映的是价值溢价Size/OP 组合反映的是盈利能力溢价Size/INV 组合反映的是投资保守程度溢价。这三组组合的收益率分布和因子载荷预期会有明显差异。# 加载新组合数据 portfolios_op load_french_csv(25_Portfolios_ME_OP_5x5.csv) portfolios_inv load_french_csv(25_Portfolios_ME_INV_5x5.csv) # 对齐时间轴 common_idx_new factors.index.intersection(portfolios_op.index).intersection(portfolios_inv.index) factors_new factors.loc[common_idx_new] portfolios_op portfolios_op.loc[common_idx_new] portfolios_inv portfolios_inv.loc[common_idx_new] # 描述性统计对比 def summarize_portfolios(portfolios, label): 输出组合收益率的基本统计量 stats pd.DataFrame({ 均值: portfolios.mean() * 100, # 转回百分比便于阅读 标准差: portfolios.std() * 100, 最小值: portfolios.min() * 100, 最大值: portfolios.max() * 100 }) stats[组别] label return stats summary_bm summarize_portfolios(portfolios, Size/BM) summary_op summarize_portfolios(portfolios_op, Size/OP) summary_inv summarize_portfolios(portfolios_inv, Size/INV) # 对比三组组合的平均收益率范围 print(Size/BM 组合月均收益范围, summary_bm[均值].min(), 到, summary_bm[均值].max()) print(Size/OP 组合月均收益范围, summary_op[均值].min(), 到, summary_op[均值].max()) print(Size/INV 组合月均收益范围, summary_inv[均值].min(), 到, summary_inv[均值].max())逻辑说明描述性统计的目的是在跑回归之前先了解数据的分布特征。Size/BM 组合的收益率通常呈现价值股溢价小市值高 BM 的组合平均收益较高。Size/OP 组合中高盈利能力的公司组合通常表现更好。Size/INV 组合中保守投资的公司组合往往有更高收益。这些差异会直接反映在后续回归的 alpha 和因子载荷上。参数说明mean()和std()默认按列计算乘以 100 是为了把小数转回百分比方便和 French 原始数据对比。如果发现某个组合的均值异常高或异常低需要检查数据加载时是否有列错位。3.2 批量回归与结果对比三因子模型在哪组组合上表现更好对 50 个新组合跑同样的三因子回归然后把三组组合的调整 R 方、alpha 显著性、SMB 和 HML 载荷显著性做横向对比。这一步是项目报告的核心分析内容。def batch_regression(portfolios, factors, label): 对一组组合批量跑三因子回归 results [] for col in portfolios.columns: res run_three_factor_regression(portfolios[col], factors, col) res[group] label results.append(res) return pd.DataFrame(results) # 对三组组合分别回归 results_bm batch_regression(portfolios, factors_new, Size/BM) results_op batch_regression(portfolios_op, factors_new, Size/OP) results_inv batch_regression(portfolios_inv, factors_new, Size/INV) # 合并所有结果 all_results pd.concat([results_bm, results_op, results_inv], ignore_indexTrue) # 对比三组组合的调整 R 方分布 print(各组调整 R 方均值) print(all_results.groupby(group)[adj_r_squared].mean().round(4)) print(\n各组 alpha 显著|t|1.96的组合数量) alpha_sig all_results[all_results[alpha_t].abs() 1.96] print(alpha_sig.groupby(group).size()) print(\n各组 SMB 载荷显著的组合数量) smb_sig all_results[all_results[beta_smb_t].abs() 1.96] print(smb_sig.groupby(group).size()) print(\n各组 HML 载荷显著的组合数量) hml_sig all_results[all_results[beta_hml_t].abs() 1.96] print(hml_sig.groupby(group).size())逻辑说明调整 R 方衡量模型对组合收益时间序列变异的解释比例。如果 Size/BM 组合的调整 R 方普遍高于 Size/OP 和 Size/INV说明三因子模型在价值股组合上拟合更好。alpha 显著的比例反映模型是否存在系统性定价误差。SMB 和 HML 载荷的显著性则说明这两个因子对不同组合的区分能力。参数说明groupby按组别聚合size()计数。abs() 1.96是双侧检验 5% 显著性的常用阈值。如果某个组的 alpha 显著比例很高说明三因子模型在该组组合上遗漏了某些定价因子这正好呼应 Fama-French 五因子模型的改进动机。3.3 从三因子到五因子的过渡读懂 Fama-French (2015) 的增量贡献项目要求阅读 Fama and French (2015) 的 five-factor asset pricing model 论文并基于理解做分析。这篇论文在三个因子之外增加了 RMW盈利能力和 CMA投资两个因子。虽然项目没有要求你跑五因子回归但理解这两个新因子的构造逻辑能帮你解释为什么三因子模型在 Size/OP 和 Size/INV 组合上的表现会有差异。RMW 因子做多高运营盈利能力公司、做空低运营盈利能力公司。CMA 因子做多保守投资公司、做空激进投资公司。在五因子框架下如果三因子模型在 Size/OP 组合上 alpha 显著很可能是因为遗漏了 RMW 因子在 Size/INV 组合上 alpha 显著则可能遗漏了 CMA 因子。# 检查三因子模型在 Size/OP 和 Size/INV 组合上的 alpha 分布 # 如果 alpha 与盈利能力或投资特征相关说明存在遗漏因子 # 以 Size/OP 组合为例提取 alpha 和组合名称 op_alphas results_op[[portfolio, alpha, alpha_t]].copy() op_alphas[size_rank] op_alphas[portfolio].str[0] # 假设列名首字符表示 Size 分组 op_alphas[op_rank] op_alphas[portfolio].str[1] # 第二字符表示 OP 分组 # 按 OP 分组看 alpha 均值 print(Size/OP 组合按 OP 分组的 alpha 均值) print(op_alphas.groupby(op_rank)[alpha].mean().round(4))逻辑说明如果 alpha 随着 OP 分组呈现单调变化说明三因子模型无法完全捕捉盈利能力带来的收益差异这正是 RMW 因子要解决的问题。类似地对 Size/INV 组合按 INV 分组看 alpha 变化可以判断 CMA 因子的必要性。参数说明str[0]和str[1]提取组合名称中的排序标识具体取决于 French 数据的列名格式。如果列名是SMALL LoBM这类格式需要改用正则表达式提取。这一步的目的是把统计结果和经济含义对应起来而不是单纯报告数字。4. 避坑与排查金融计量实证里最容易翻车的五个地方4.1 日期对齐错误导致样本量莫名其妙变少现象回归结果中样本量只有几十个观测值而预期应该有几百个月的数据。原因French 数据文件的日期索引格式不一致有的文件是YYYYMM整数有的文件是YYYY-MM字符串直接合并时 pandas 无法自动匹配。解决统一用pd.to_datetime转换并指定format参数。合并前先用index.intersection取交集再检查交集长度是否合理。4.2 百分比和小数混用导致系数放大一百倍现象回归出来的 beta 系数是 100 左右而正常值应该在 1 附近。原因French 原始数据是百分比形式比如 2.5 表示 2.5%但回归时没有除以 100。解决在数据加载阶段统一除以 100把所有收益率转为小数。如果已经跑完回归才发现把系数除以 100 即可但 t 统计量和 R 方不受影响。4.3 组合列名错位导致回归结果张冠李戴现象某个组合的 alpha 异常大或者 SMB 载荷符号与预期相反。原因French 数据的列名可能包含空格或特殊字符pd.read_csv自动解析时可能产生前后空格导致列名匹配错误。解决加载后立即用df.columns df.columns.str.strip()去除空格并打印列名确认顺序。对 25 组合列名通常按 Size 从大到小、BM 从小到大排列可以用df.columns.tolist()核对。4.4 缺失值处理不当导致回归样本不一致现象不同组合的回归样本量不同导致结果无法横向对比。原因某些组合在特定月份可能没有交易数据French 数据中用 -99 或 -999 表示缺失直接读取会被当成有效数值。解决加载后把 -99 和 -999 替换为 NaN再用dropna统一删除缺失月份。注意要在构造被解释变量之前处理否则 RF 的缺失会导致更多样本丢失。4.5 多重共线性检验被忽略导致系数解释错误现象SMB 和 HML 的系数符号与经济直觉相反或者标准误异常大。原因没有先检查三因子之间的相关性直接跑回归。如果 SMB 和 HML 高度相关系数估计会不稳定。解决跑回归前先输出相关系数矩阵和 VIF。如果 VIF 超过 5在报告中讨论共线性对系数解释的限制或者考虑使用正交化后的因子。5. 从结果表到报告三个让实证分析站得住脚的进阶技巧5.1 用 Newey-West 标准误修正时间序列自相关金融收益率数据通常存在异方差和自相关普通 OLS 标准误会低估真实标准误导致 t 统计量虚高。常见做法是用 Newey-West 调整在 statsmodels 里只需一个参数。# 使用 Newey-West 标准误重新估计 model_nw sm.OLS(y_clean, X_clean).fit(cov_typeHAC, cov_kwds{maxlags: 6}) print(model_nw.summary())maxlags6是月度数据的常用选择对应半年的滞后阶数。如果调整后 SMB 或 HML 的 t 统计量从显著变为不显著说明原来的显著性有一部分来自自相关需要在报告中如实说明。5.2 用 GRS 检验整体评估模型表现逐个看 alpha 的 t 统计量只能判断单个组合GRS 检验能同时检验所有组合的 alpha 是否联合为零。这是 Fama-French 论文中常用的整体拟合优度检验。def grs_test(results_df, factors, portfolios): 简化版 GRS 检验检验所有组合 alpha 联合为零 实际实现需要构造残差协方差矩阵这里给出思路 n_portfolios len(results_df) alphas results_df[alpha].values # 残差协方差矩阵需要从每个回归中提取 # 完整实现参考 Fama-French (2010) 或 Cochrane 教材 # 这里用 alpha 的 t 统计量均值作为粗略指标 avg_alpha_t results_df[alpha_t].abs().mean() print(f平均 |alpha t|: {avg_alpha_t:.4f}) print(falpha 显著比例: {(results_df[alpha_t].abs() 1.96).mean():.2%})完整的 GRS 检验需要构造残差协方差矩阵并计算 F 统计量代码量较大。如果时间有限用 alpha 显著比例和平均 |t| 作为替代指标也能说明问题。关键是不要只报告 R 方高就下结论说模型好alpha 的联合显著性才是定价模型的核心检验。5.3 把回归结果写成可复现的报告表格项目要求提交报告和代码报告中的表格应该能直接从代码输出复制。我一般会写一个格式化函数把回归结果整理成类似论文 Table 6 的格式行是组合列是 alpha、b、s、h 及其 t 值最后加一列调整 R 方。def format_regression_table(results_df): 把回归结果格式化为论文风格表格 table results_df[[portfolio, alpha, alpha_t, beta_mkt, beta_mkt_t, beta_smb, beta_smb_t, beta_hml, beta_hml_t, adj_r_squared]].copy() # 系数保留三位小数t 值保留两位 for col in [alpha, beta_mkt, beta_smb, beta_hml]: table[col] table[col].round(3) for col in [alpha_t, beta_mkt_t, beta_smb_t, beta_hml_t]: table[col] table[col].round(2) table[adj_r_squared] table[adj_r_squared].round(3) return table final_table format_regression_table(results_bm) print(final_table.to_string(indexFalse))从那以后我每次跑完回归都会先把这张表打印出来和 Fama-French 原论文的 Table 6 做肉眼对比。如果系数符号和量级大致吻合说明数据加载和回归流程没有大问题如果某个组合的 SMB 载荷突然变成负的大概率是列名错位或缺失值处理出了问题。这个习惯帮我省下了大量返工时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑