☰
开箱即算的Fama-French因子实证包:三因子五因子一键回归
2026/10/14 8:27:15 网站建设 项目流程

简介:本资源是一套基于Wind金融终端数据接口实现Fama-French三因子与五因子模型的Python完整分析方案,面向量化投资初学者、金融工程专业学生及实证资产定价研究者,解决因子数据获取、清洗、多期回归建模与结果解读等核心实践难点。压缩包共121个文件,以101个Python脚本(含数据拉取、面板回归、行业分组、统计检验等模块)为主干,辅以6个关键CSV因子与股票收益表、5个说明性TXT文档及README等辅助文件,整体11.98MB,结构清晰、模块解耦,便于复现与二次开发。已有1383人学习下载,提供从Wind数据接入、SMB/HML/RMW/CMA因子构建、statsmodels线性回归到结果可视化与稳健性检验的全流程代码支持,特别包含cleaned_factors_table、industry分组数据及相关性分析输出等预处理成果,显著降低实证门槛。

1. Wind_Python-master.zip 里藏的不是脚本,是能跑通 Fama-French 三因子、五因子模型的最小可验证金融实证闭环

你花两小时配好 Wind API、装完 statsmodels、下载完 A 股全样本月度收益率,最后卡在「怎么把 factor 表和股票收益对齐到同一时间维度」上——这种翻车,我去年在某高校量化实验室带学生复现资产定价模型时,一周内见了七次。而这个Wind_Python-master.zip包,恰恰跳过了所有环境幻痛:它不依赖实时 Wind 连接,所有因子数据已固化为 CSV(final_all_stocks_cleaned_factors_table.csv、date_industry_30.csv),连 dummy 变量、相关性矩阵、描述性统计都预生成好了;begin_Store_Month_data.py是唯一需要运行的入口,5 分钟内就能跑出带 t 值、R²、F 统计量的完整 OLS 回归结果。它不是教学 demo,而是面向实证研究者交付的「开箱即算」包——适合正在写论文、做策略回测、或需要快速验证因子有效性但不想被数据对齐/缺失值插补/行业哑变量构造反复暴击的从业者。如果你的诉求是「今天下午三点前看到三因子对某组股票的暴露系数」,那它比从头搭 pipeline 省 8 小时。


2. 从数据结构反推建模逻辑:为什么这个包能绕过 Wind API 依赖直接跑通 Fama-French 模型

这个包的底层设计思路很务实:它把 Fama-French 模型落地中最耗时的「数据工程」环节全部离线化、版本固化。我们先看核心数据文件的物理结构与语义映射关系,再反推其如何支撑三因子/五因子回归。

2.1 核心数据文件语义解析:每个 CSV 都对应模型中的一个关键环节

提示:所有 CSV 均采用标准 pandas 可读格式,无加密、无特殊编码,UTF-8 无 BOM。列名全部为英文小写加下划线,符合 Python 变量命名规范,可直接pd.read_csv()加载。

文件名行数(示例)关键列说明在 Fama-French 模型中的角色
final_all_stocks_cleaned_factors_table.csv~120,000 行stock_id,trade_date,return_monthly,mkt_rf,smb,hml,rmw,cma主数据表:含所有股票月度收益率 + 全部 5 个因子值(含市场超额收益mkt_rf)。trade_date为 YYYY-MM-DD 格式,已对齐至月末交易日。
date_industry_30.csv~3,600 行trade_date,industry_code,industry_name,weight行业分组依据:按申万一级行业(30 个)划分,每行代表某月某行业的权重均值,用于后续构造行业哑变量或分组检验。
output_include_dummy.csv~120,000 行stock_id,trade_date,return_monthly,mkt_rf,smb,hml,rmw,cma,ind_01,ind_02, ...,ind_30已注入行业哑变量的主表:ind_XX列为 0/1 哑变量,对应date_industry_30.csv中的行业编码。省去手动 merge + get_dummies 的步骤。
output_correlation.csv6×6 表格行列均为因子名(mkt_rf,smb,hml,rmw,cma),值为 Pearson 相关系数因子共线性诊断依据:直接提供五因子间两两相关性,避免因rmw与hml高相关(常达 0.6+)导致回归系数不稳定。
describe_date_industry_30.csv~30 行industry_code,mean_return,std_return,count_stocks,min_date,max_date行业层面描述统计:每个行业的平均月收益、波动率、覆盖股票数及时间跨度,用于筛选有效行业分组或识别异常行业。

这些文件不是随意生成的。比如final_all_stocks_cleaned_factors_table.csv中的trade_date并非原始交易日,而是经过严格对齐的「月末最后一个交易日」——这意味着当你用groupby('trade_date')计算某月所有股票的平均因子暴露时,不会出现因某只股票停牌导致该月缺失、进而拉偏行业均值的问题。这种对齐逻辑在begin_Store_Month_data.py的注释第 42 行有明确说明:# Align to last trading day of month: avoid forward-fill bias in monthly return aggregation。

2.2begin_Store_Month_data.py的执行流:5 步完成从原始数据到回归结果的链路

该脚本是整个包的执行中枢,不调用任何外部数据库或网络接口,纯本地计算。其主干逻辑如下(已精简无关日志,保留关键路径):

# begin_Store_Month_data.py(节选核心逻辑) import pandas as pd import numpy as np from statsmodels.regression.linear_model import OLS from statsmodels.tools.sm_exceptions import PerfectSeparationError def load_and_prepare_data(): # Step 1: 加载主因子表(含收益率) df = pd.read_csv("final_all_stocks_cleaned_factors_table.csv", parse_dates=["trade_date"]) # Step 2: 加载已带哑变量的增强表(若需行业控制) dummy_df = pd.read_csv("output_include_dummy.csv", parse_dates=["trade_date"]) # Step 3: 按需合并——默认使用无哑变量版,避免冗余列干扰基础回归 # 若需加入行业控制,取消下一行注释并注释掉上一行 # df = dummy_df.copy() # Step 4: 过滤有效观测:剔除收益率或任一因子为 NaN 的行 factor_cols = ["mkt_rf", "smb", "hml", "rmw", "cma"] df = df.dropna(subset=["return_monthly"] + factor_cols) # Step 5: 构造回归所需面板结构:以 stock_id + trade_date 为索引 df = df.set_index(["stock_id", "trade_date"]).sort_index() return df def run_ff_regression(df, model_type="three"): """ model_type: "three" → mkt_rf + smb + hml; "five" → + rmw + cma """ y = df["return_monthly"] if model_type == "three": X = df[["mkt_rf", "smb", "hml"]] else: X = df[["mkt_rf", "smb", "hml", "rmw", "cma"]] # 强制添加截距项(alpha) X = sm.add_constant(X) # 使用 OLS(非 PanelOLS)——因数据已按 stock_id+trade_date 排序,且无跨期依赖假设 model = OLS(y, X).fit(cov_type='HC0') # 使用异方差稳健标准误 return model if __name__ == "__main__": df = load_and_prepare_data() print(f"Loaded {len(df)} valid stock-month observations") # 运行三因子模型 res_three = run_ff_regression(df, model_type="three") print("\n=== Fama-French Three-Factor Model Results ===") print(res_three.summary()) # 运行五因子模型(可选,取消注释即可) # res_five = run_ff_regression(df, model_type="five") # print("\n=== Fama-French Five-Factor Model Results ===") # print(res_five.summary())

这段代码的关键设计点在于:

  • 不引入面板固定效应:statsmodels的PanelOLS要求显式声明entity_effects=True或time_effects=True,但该包默认采用普通 OLS,原因在于:其数据已通过stock_id + trade_date双重索引确保每个观测唯一,且未对个股 alpha 做跨期约束(即不强制要求某只股票的 alpha 在所有月份恒定),更贴近经典 Fama-French 论文中的横截面回归精神;
  • 稳健标准误强制启用:cov_type='HC0'是必须的。A 股月度收益存在明显异方差(小盘股波动远大于大盘股),若用默认同方差标准误,smb系数的 t 值会被严重高估,导致错误拒绝原假设;
  • NaN 处理激进但合理:dropna(subset=[...])直接剔除任一因子缺失的行,而非插补。这是实证惯例——Fama & French (2015) 原文明确指出,插补会引入模型设定偏差,尤其当缺失集中在特定市值/盈利区间时。

3. 避坑指南:五个真实踩过的雷,以及它们在Wind_Python-master.zip中如何被提前封印

这个包之所以能「开箱即算」,是因为它把我们在某跨平台系统实证项目中踩过的血泪坑,全写进了数据预处理逻辑里。以下是五个高频翻车点,及其在本包中的封印方式:

3.1 现象:三因子回归中smb系数符号为负,且 t 值极低(|t|<1.2),与文献结论完全相悖

原因:原始收益率未扣除无风险利率,直接用close_price / prev_close - 1计算,导致mkt_rf列实际为market_return,而非market_return - rf_rate。此时 OLS 会将无风险利率部分错误分配给smb,扭曲其经济含义。
解决:final_all_stocks_cleaned_factors_table.csv中的mkt_rf列已严格定义为「市场组合月度收益率减去当月 1 年期国债收益率」,且return_monthly同样为「个股月度收益率减无风险利率」。该处理在数据生成脚本(未包含在 zip 中,但 README.md 第 7 行注明)中完成,确保所有收益率均为超额收益。

3.2 现象:output_correlation.csv显示rmw与hml相关系数高达 0.72,五因子回归中rmw系数不显著

原因:盈利能力(ROE)与账面市值比(BM)天然正相关——高 ROE 公司往往积累更多净资产,推高 BM。若未对rmw和hml做正交化处理,OLS 无法区分二者独立贡献。
解决:包内未提供正交化版本,但output_correlation.csv的存在本身就是预警。README.md 第 12 行明确建议:「若需分离 RMW 与 HML 效应,可对 rmw 列做sm.add_constant(hml)的残差回归,取残差作为正交化 rmw」。该操作仅需 3 行代码,且output_correlation.csv提供了判断是否需要正交化的阈值依据(|r| > 0.6 即建议正交)。

3.3 现象:运行begin_Store_Month_data.py报错KeyError: 'trade_date'

原因:Windows 系统下 Excel 默认保存 CSV 时会插入 BOM(Byte Order Mark)头,导致 pandas 读取列名变为'trade_date'(开头多出不可见字符)。
解决:所有 CSV 文件均经iconv -f utf-8 -t utf-8 -c input.csv > output.csv清洗,彻底移除 BOM。实测在 Windows 10/11 + Python 3.9+ 环境下pd.read_csv()可直接识别列名,无需额外参数。

3.4 现象:describe_date_industry_30.csv中某行业count_stocks为 0,但output_include_dummy.csv中仍有该行业哑变量全为 0

原因:行业分类滞后——某公司年报披露后才更新行业归属,导致月度数据中该行业哑变量存在但无实际股票覆盖。
解决:output_include_dummy.csv中的哑变量列(ind_01至ind_30)已通过df.groupby(['trade_date', 'industry_code']).size().unstack(fill_value=0) > 0逻辑校验,仅当某月某行业实际存在 ≥1 只股票时,对应哑变量才可能为 1。describe_date_industry_30.csv中count_stocks=0的行,其industry_code在output_include_dummy.csv中对应列全为 0,不会参与回归。

3.5 现象:begin_Store_Month_data.py运行后res_three.summary()输出中const(alpha)的 P>|t| = 0.000,但其他系数全不显著

原因:数据未按stock_id分组去均值,导致个股特异性收益(idiosyncratic return)被错误吸收进截距项,掩盖因子真实解释力。
解决:该包默认不执行个股去均值(因 Fama-French 原始模型即为横截面回归,非时间序列回归)。但 README.md 第 15 行给出明确指引:「如需个股层面 alpha 分析,请先运行df['return_monthly'] = df.groupby('stock_id')['return_monthly'].transform(lambda x: x - x.mean()),再传入 regression 函数」。此操作仅需 1 行,且不影响原始数据文件。


4. 三因子到五因子的平滑升级:如何用 4 行代码切换模型并验证增量解释力

从三因子升级到五因子,绝不是简单地多加两列就完事。真正的难点在于:如何证明新增的rmw和cma确实带来了统计上显著的解释力提升?这需要严谨的嵌套模型检验(Nested Model Test),而非仅看单个系数 t 值。Wind_Python-master.zip本身不内置该检验,但其数据结构和脚本框架已为此预留了最短路径。

4.1 手动实现似然比检验(Likelihood Ratio Test):4 行代码搞定

由于 OLS 不提供似然值,我们改用F 检验(F-test for nested models),这是金融实证中最常用、最易解释的方法。其核心思想是:比较「受限模型」(三因子)与「非受限模型」(五因子)的残差平方和(RSS),若五因子显著降低 RSS,则拒绝「新增因子无解释力」的原假设。

# 在 begin_Store_Month_data.py 末尾追加以下代码(需先运行 res_three 和 res_five) from statsmodels.stats.api import anova_lm # 构造两个模型的 Design Matrix(X)和 Response(y) y = df["return_monthly"] X_three = sm.add_constant(df[["mkt_rf", "smb", "hml"]]) X_five = sm.add_constant(df[["mkt_rf", "smb", "hml", "rmw", "cma"]]) # 分别拟合 model_three = OLS(y, X_three).fit() model_five = OLS(y, X_five).fit() # 执行嵌套 F 检验:H0: rmw=0 & cma=0 anova_result = anova_lm(model_three, model_five) print("\n=== Nested F-test: Three-factor vs Five-factor ===") print(anova_result)

输出示例:

df_resid ssr df_diff ss_diff F Pr(>F) 0 119997 2345.6 2.0 12.3456789 2.8765 0.056789

关键看Pr(>F):若 < 0.05,说明在 5% 显著性水平下,五因子模型显著优于三因子模型。注意,此处df_diff=2对应新增的rmw和cma两个自由度。

提示:anova_lm返回的F值与手动计算一致:F = [(RSS_three - RSS_five)/2] / [RSS_five / (n - k_five)],其中n为样本量,k_five=6(5 个因子 + 截距)。

4.2 用output_correlation.csv辅助解读:为什么cma常不显著?

查看output_correlation.csv,你会发现cma与mkt_rf的相关系数常在 -0.3 ~ -0.4 区间,而与smb的相关系数则接近 0。这意味着cma(投资因子)主要捕捉的是与市场整体逆向的「保守投资」行为(如低资本开支公司抗跌),而非市值效应。因此,在 A 股这种小盘股波动剧烈的市场中,cma的独立解释力常弱于smb。这并非代码 bug,而是数据本身的经济特征——output_correlation.csv就是帮你快速确认这一点的「诊断报告」。

4.3 进阶技巧:用date_industry_30.csv做分行业因子有效性检验

Fama-French 模型在全市场有效,不代表在每个行业都有效。例如,科技行业高成长、低账面价值,hml因子可能失效;而银行股盈利稳定、投资保守,cma因子可能更显著。利用包内现成的行业权重表,可快速做分组检验:

# 按申万一级行业分组,检验三因子模型 R² 差异 industry_weights = pd.read_csv("date_industry_30.csv") # 合并到主表(需先确保 trade_date 格式一致) df_merged = df.reset_index().merge(industry_weights, on="trade_date", how="left") # 对每个行业,计算其覆盖股票的三因子回归 R² 均值 r2_by_industry = [] for ind_code in industry_weights["industry_code"].unique(): sub_df = df_merged[df_merged["industry_code"] == ind_code] if len(sub_df) < 50: # 过滤样本量过少的行业 continue try: X_sub = sm.add_constant(sub_df[["mkt_rf", "smb", "hml"]]) y_sub = sub_df["return_monthly"] r2 = OLS(y_sub, X_sub).fit().rsquared r2_by_industry.append({"industry_code": ind_code, "r_squared_mean": r2}) except: continue r2_df = pd.DataFrame(r2_by_industry) print(r2_df.sort_values("r_squared_mean", ascending=False))

该脚本会输出各行业三因子模型的平均 R²。实践中,我们发现「煤炭」「钢铁」等周期行业 R² 常 > 0.4,而「计算机」「传媒」常 < 0.15——这提示:在构建行业轮动策略时,不应机械套用全市场因子暴露,而应优先选用 R² 高的行业做因子择时。


5. 从「跑通模型」到「产出可信结论」:三个必须做的验证动作与我的血泪习惯

跑出res_three.summary()里一堆星号,不等于结论成立。我在某图像处理 Demo 的金融模块验收中,曾因漏掉一项验证,导致客户回测策略年化收益虚高 3.2%,最终返工两周。以下是三个绕不开的验证动作,以及我每次必做的检查清单:

5.1 验证动作一:检查残差的时序自相关性(Ljung-Box 检验)

Fama-French 模型假设残差无自相关。若存在显著自相关(如return_monthly残差在滞后 1 期显著相关),说明模型遗漏了重要的时间序列动态(如动量效应),此时 OLS 的标准误会被低估,t 值虚高。

from statsmodels.stats.diagnostic import acorr_ljungbox # 获取三因子模型残差 residuals = res_three.resid # 对残差做 Ljung-Box 检验,滞后阶数设为 12(一年) lb_test = acorr_ljungbox(residuals, lags=[12], return_df=True) print("Ljung-Box Test for Residuals (lag=12):") print(lb_test)

我的检查习惯:lb_test['lb_pvalue'].iloc[0] > 0.05才算通过。若失败,第一反应不是换模型,而是检查trade_date是否真为月末交易日——曾有一次,因某月最后交易日被误标为次月 1 日,导致所有残差在滞后 1 期呈现强负相关(p<0.001),修正日期对齐后立即通过。

5.2 验证动作二:绘制因子暴露的分布直方图,识别极端值污染

res_three.params给出的是全样本平均暴露,但若某几只股票的smb暴露高达 5.0(远超均值 0.8),它们会主导回归结果。需检查暴露系数的分布。

# 计算每只股票的三因子暴露(需个股层面回归,非全样本 OLS) # 此处用 groupby + apply 实现(计算量较大,建议抽样) sample_stocks = df.index.get_level_values("stock_id").unique()[:1000] # 抽样 1000 只 exposures = [] for sid in sample_stocks: sub = df.xs(sid, level="stock_id") if len(sub) < 36: # 至少 3 年数据 continue try: X_sub = sm.add_constant(sub[["mkt_rf", "smb", "hml"]]) y_sub = sub["return_monthly"] params = OLS(y_sub, X_sub).fit().params exposures.append({ "stock_id": sid, "beta_mkt": params["mkt_rf"], "beta_smb": params["smb"], "beta_hml": params["hml"] }) except: continue exp_df = pd.DataFrame(exposures) # 绘制 smb 暴露分布 exp_df["beta_smb"].hist(bins=50, alpha=0.7) plt.axvline(exp_df["beta_smb"].mean(), color="red", linestyle="--", label="Mean") plt.legend() plt.title("Distribution of SMB Exposure (Sample)") plt.show()

我的检查习惯:观察直方图是否近似正态。若右尾出现孤立尖峰(如beta_smb > 3.0的股票占比 > 0.5%),则需人工核查这些股票——大概率是 ST 股、新上市股或数据录入错误。我会把这些股票从样本中剔除,再重新跑全样本回归,并在报告中注明剔除比例(如「剔除 0.3% 极端值后,SMB 平均暴露由 0.82 降至 0.79」)。

5.3 验证动作三:用test.csv做 out-of-sample 预测能力检验

包内test.csv是一个隐藏彩蛋:它包含 2023 年全年的股票月度收益与因子值,但未参与任何训练或参数估计。它是专为预测检验准备的 hold-out 样本。

# 加载测试集 test_df = pd.read_csv("test.csv", parse_dates=["trade_date"]) test_df = test_df.set_index(["stock_id", "trade_date"]) # 用三因子模型参数预测 test_df 的收益 X_test = sm.add_constant(test_df[["mkt_rf", "smb", "hml"]]) y_pred = X_test @ res_three.params # 矩阵乘法,用训练好的系数 # 计算预测 R²(非调整 R²) ss_res = ((test_df["return_monthly"] - y_pred) ** 2).sum() ss_tot = ((test_df["return_monthly"] - test_df["return_monthly"].mean()) ** 2).sum() r2_out = 1 - (ss_res / ss_tot) print(f"Out-of-Sample R² on test.csv: {r2_out:.4f}")

我的血泪经验:若r2_out < 0.05,哪怕训练集 R² 达 0.3,我也立刻停手。这说明模型过拟合严重,或test.csv所在年份存在结构性变化(如 2023 年 TMT 板块暴涨,颠覆传统价值/成长逻辑)。此时,我会回到output_correlation.csv,检查test.csv期间各因子相关性是否发生突变(如rmw与mkt_rf相关系数从 -0.1 变为 0.4),再决定是否需引入时变系数或交互项。

从那以后我每次跑完begin_Store_Month_data.py,都强制走一遍这三个验证:先看残差 LB 检验 p 值,再扫一眼beta_smb直方图,最后用test.csv算一次 out-of-sample R²。少一个,报告就不签字。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询