ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从Wind数据到因子回归:Fama-French三因子与五因子构建全解析

从Wind数据到因子回归:Fama-French三因子与五因子构建全解析 简介面向金融量化研究者与Python开发者提供了一套基于Wind金融终端的Fama-French三因子与五因子模型完整实现涵盖因子获取、数据清洗、多元回归与结果分析全流程。资源共121个文件以101个Python脚本为核心附带6个CSV因子数据样例、5个TXT说明及Markdown文档等压缩包约11.98MB适合直接参考或二次开发。已有1383人学习受到量化入门与进阶用户的关注。通过该资源可掌握使用pandas处理金融面板数据、调用statsmodels进行多因子回归的方法并理解SMB、HML、RMW、CMA等因子的构造逻辑与解释方式。包内代码结构清晰包含可运行的示例数据与输出结果便于对照验证。1. 这份 Wind_Python 资源到底装了什么从三因子到五因子的一整套可跑代码做资产定价的人都知道Fama-French 三因子和五因子最折腾的不是模型本身而是数据——市值、账面市值比、盈利能力、投资风格这些因子数据要自己从 Wind 终端一层层挖出来挖完还得清洗、对齐、回归。这份 Wind_Python-master 资源正好把这条路走通了从 Wind 的 API 批量落盘股票数据到因子表、行业表、虚拟变量表的加工再到三因子和五因子的回归建模全程用 Python 串起来。适合刚上手 Wind 量化接口的从业者也适合做学术复现需要完整因子表的学生。下面我按自己拆解的思路从数据管线讲到回归细节和踩坑记录照着重跑一遍就能把 Python 量化策略里最繁琐的数据清洗与因子生成流程串起来。2. 数据管线梳理从 Wind 接口到因子表11 个文件的上下游关系2.1 先看清压缩包里的文件分工我拿到压缩包的第一件事不是双击打开某个代码文件而是把所有 csv 和 py 文件的体积、行数、列名过一遍。原因很简单Wind 接口取数、清洗、因子构建、回归这四个环节分散在多个 csv 里它们之间有上下游关系。文件对不上号后面改代码时根本不知道哪一步出了问题。解压之后会看到 11 个文件其中三张表是核心文件典型内容在管线中的位置begin_Store_Month_data.pyWind 取数主脚本批量拉月度行情与财务指标管线起点final_all_stocks_cleaned_factors_table.csv清洗后的全市场因子表包含市值、BM、盈利、投资分组因子组装完成date_industry_30.csv日期与 30 个行业分类的对照表中间映射output_include_dummy.csv加入行业哑变量的回归样本表回归输入output_correlation.csv各因子之间的相关系数矩阵共线性检查test.csv小样本回归的验证结果结果验证describe_date_industry_30.csv分行业市值、收益的描述统计数据体检我的判断是begin_Store_Month_data.py 负责把 Wind 的原始数据落盘date_industry_30.csv 在中间位置充当截面与行业的映射final_all_stocks_cleaned_factors_table.csv 是做完市值、BM 等计算后的主表output_include_dummy.csv 则是在主表基础上加了行业哑变量直接喂给回归。output_correlation.csv 是回归之前的共线性体检test.csv 是拿小样本试跑时的输出。另外两个容易被忽略的文件Wind_Python资料.iml 是 IDE 的工程标记文件和建模逻辑无关可以直接无视仓库里同时存在 README.MD 和 README.md 两个名字相近的文档内容大概率一致但在 Windows 上不建议两个都保留否则某些工具会把它们当成两个独立文件后面换环境跑代码时容易混淆目录结构。2.2 begin_Store_Month_data.py 的取数逻辑与参数这个脚本的核心是 WindPy 的 w.wsd 和 w.wss 两个接口wsd 取时间序列wss 取截面数据。下面是我基于这类场景标准写法的还原骨架和原脚本思路一致字段名可以直接替换成你 Wind 账号权限内的版本。 begin_Store_Month_data.py 取数骨架 从 Wind 拉取 A 股月度收益、总市值、流通市值、PB 等字段 依赖WindPy、pandas from WindPy import w import pandas as pd w.start() # 连接 Wind 终端终端必须已登录 # 1. 取股票池全部 A 股代码参数里带上日期与板块 stock_code w.wset( sectorconstituent, date2020-06-30;sectorida001010100000000 ).Data[1] print(f股票池数量: {len(stock_code)}) # 2. 取月度行情与估值数据 # w.wsd 返回 (错误码, 数据, 时间戳, 统计口径) err, data, times w.wsd( stock_code, pct_chg, mkt_cap_ard, mkt_cap_float, pb_lf, 2016-01-01, 2020-06-30, PeriodM;DaysAlldays;FillPrevious ) if err ! 0: print(fWind 取数错误码: {err}) # 常见做法记录错误代码后断点续传而不是直接退出这里几个参数值得展开。PeriodM 表示按月取数DaysAlldays 是保留全自然日FillPrevious 表示把停牌日数据前值填充。三个参数一起用的时候收益率为 0 的月份可能是停牌造成的也可能是真的没涨没跌后面清洗时我会单独处理而不是一刀切断掉。w.wset 的 sectorid 参数是 Wind 里的板块代码我贴的是全部 A 股第一大类的写法实际使用替换成你权限内的板块即可。err 返回值不等于 0 时Wind 官方建议回调重取。但我的实际经验是长周期、多股票的任务里断连经常发生在第 20 分钟之后单次重试不一定成功。更稳的做法是先按股票代码分段拉每段 50 只落盘到中间 csv 后再拼接。原脚本 begin_Store_Month_data.py 的后半段就是把这个思路落地把月度原始数据整理成时间×股票的宽表。2.3 清洗工序停牌、缺失值与行业映射原始数据落地之后就轮到 final_all_stocks_cleaned_factors_table.csv 这张表了。我的处理顺序固定三步顺序不能反否则后面的因子分组会被脏数据带偏。import numpy as np import pandas as pd def clean_monthly(raw: pd.DataFrame, industry_df: pd.DataFrame) - pd.DataFrame: # 第一步处理停牌导致的收益为 0 # 前值填充会把停牌日收益变成 0参与回归会拉低因子暴露 raw.loc[raw[pct_chg] 0, pct_chg] np.nan # 第二步剔除上市不满 12 个月的股票规避次新股效应 # 需要先算上市天数Wind 代码里用 ipo_date 字段 raw[listed_days] (raw[date] - raw[ipo_date]).dt.days raw raw[raw[listed_days] 365] # 第三步合并行业映射 # date_industry_30.csv 是 日期-行业 对照30 个行业 raw raw.merge(industry_df, on[date, stock_code], howleft) return raw这个函数里最关键的是第一步。很多初学者把 FillPrevious 之后收益为 0 的行当作真实横盘导致回归样本里塞进了大量停牌月份。我的习惯是先全量标记 NaN等到了因子分组那一步再看有没有填充的必要。第二步的 365 天门槛不是固定值如果样本区间里有大量次新股可以放宽到 180 天但要接受 IPO 效应带来的噪声。行业映射用 date_industry_30.csv 就够了不需要重新去 Wind 拉。这一步得到的 cleaned 表再和财务指标合并就生成了 final_all_stocks_cleaned_factors_table.csv。到这一步Wind 的原始数据管线走完下一步才是真正意义上的因子构建。3. 因子构建SMB、HML、RMW、CMA 的分组口径与计算代码3.1 分组口径2x3 为什么是基准选择Fama 和 French 在论文里的标准口径是 2x3 分组先按市值中位数分成小盘Small和大盘Big两组再在每组内部按账面市值比BM的 30%、70% 分位把股票分成低、中、高三组组合起来一共六个组合。SMB 是三个小盘组合市值加权收益的平均减去三个大盘组合的市值加权平均HML 是高 BM 的两个组合平均减去低 BM 的两个组合平均。A 股做这个分组时有一个和美股不一样的地方市值截面分布偏度很大中位数分组后 Big 组通常只有两三百只股票但市值权重大Small 组股票数量多、单只权重小。如果不做市值加权而用等权SMB 会被大量尾部小票拉偏。所以我在构建时一律用市值加权这个选择和输出表里的因子值也是对齐的。更细的 5x5 分组在学术论文里常用来做稳健性检验但月度频率下五组样本量会被摊薄极端市值段经常凑不足股票所以我只在 2x3 和 2x2 之间选。另一个细节是分位点。标准论文里用 30/70 分位但 A 股 BM 为负值的股票不少直接按 30/70 会把负 BM 股票混进低 BM 组。我一般先剔除 BM 小于 0 的样本或者单独开一个 Negative BM 组否则 HML 的符号解释会变得很牵强。虽然这样会损失一部分创业板样本但换来的是因子含义的清晰。3.2 构建 SMB 和 HML 的代码实现有了分组口径实现就顺理成章了。下面这段代码就是先做横截面分组再算六个组合的市值加权收益最后合成 SMB 与 HML。import pandas as pd import numpy as np def compute_ff3_factors(monthly: pd.DataFrame) - pd.DataFrame: 输入列date, stock_code, ret, mkt_cap, bm 输出列date, smb, hml df monthly.copy() # 第一步按月的横截面分组 # 市值中位数分大小盘BM 按 30%/70% 分位分三组 df[size_group] df.groupby(date)[mkt_cap].transform( lambda x: pd.qcut(x, 2, labels[S, B]) ) df[bm_group] df.groupby(date)[bm].transform( lambda x: pd.qcut(x.rank(methodfirst), [0, 0.3, 0.7, 1.0], labels[L, M, H]) ) # 第二步计算 6 个组合的市值加权收益 def weighted_ret(g): return (g[ret] * g[mkt_cap]).sum() / g[mkt_cap].sum() port df.groupby([date, size_group, bm_group]).apply(weighted_ret) port port.reset_index(nameport_ret) port port.pivot_table(indexdate, columns[size_group, bm_group], valuesport_ret) # 第三步合成 SMB 与 HML # SMB 小盘三组合平均 - 大盘三组合平均 # HML 高 BM 两组合平均 - 低 BM 两组合平均 port[SMB] (port[(S, L)] port[(S, M)] port[(S, H)]).div(3) - \ (port[(B, L)] port[(B, M)] port[(B, H)]).div(3) port[HML] (port[(S, H)] port[(B, H)]).div(2) - \ (port[(S, L)] port[(B, L)]).div(2) return port[[SMB, HML]].reset_index()代码里的 qcut 有两个参数值得展开。一个是 labels用于给分组命名另一个是 x.rank(methodfirst)因为 BM 如果存在大量相同值直接用 pd.qcut 会因为分位点冲突报 ValueError先 rank 再 qcut 可以保证分组不报错代价是分位点会略微偏离理论值对最终因子影响很小。关于负 BM 的股票这段代码的处理是保留在样本里让排序自然把它们划到 L 组。如果你更在意 HML 的纯度可以先把 bm0 的行抽出来单独标记但那样样本量会下降很多尤其在中小创股票上。六个组合合成 SMB 和 HML 用的是论文原始公式组合内部市值加权组合之间简单平均两步不要反过来否则单只大盘股会通过市值加权路径再被放一次大。跑完之后我会把每个月六个组合的组合收益打印出来人工抽查一两个月份的数值是否和 Wind 终端的手工计算结果一致。3.3 五因子扩展RMW 与 CMA 的差异点五因子就是在 2x3 分组基础上多两个维度RMW 用的是营业利润率衡量高盈利公司对低盈利公司的超额收益CMA 用的是总资产增长率衡量保守投资公司对激进投资公司的超额收益。这两个因子的合成公式和 HML 完全平行只是分组变量不同。def compute_ff5_factors(monthly: pd.DataFrame) - pd.DataFrame: 输入列date, stock_code, ret, mkt_cap, bm, op_profit, inv_growth 输出列date, smb, hml, rmw, cma df monthly.copy() # 同样的 2x3 分组增加两个分组维度 df[op_group] df.groupby(date)[op_profit].transform( lambda x: pd.qcut(x.rank(methodfirst), [0, 0.3, 0.7, 1.0], labels[W, M, R]) # Weak / Medium / Robust ) df[inv_group] df.groupby(date)[inv_growth].transform( lambda x: pd.qcut(x.rank(methodfirst), [0, 0.3, 0.7, 1.0], labels[C, M, A]) # Conservative / Medium / Aggressive ) # 后续合成与 SMB/HML 一致按组合收益求加权平均 return df其实更常见的 FF5 构建方式是 2x2 分组市值一组、其他因子各一组四个组合就够了。但既然最终因子表里能给出每个股票的因子分组值说明走的是 2x3 这种更细的分组。做学术复现时2x2 容易被审稿人质疑过于简化2x3 是相对稳妥的中间选择。这里真正的坑在 op_profit 和 inv_growth 两个字段的 Wind 口径。营业利润率在 Wind 里有营业利润/净资产和营业利润/总资产两种取值不同版本的表给的不一样。inv_growth 是总资产同比增速但如果拉数时把同比和环比搞混CMA 的符号会直接反转。我的做法是先做一组单变量排序相关性检验拿手工算的两个公司样本和 Wind 结果对一遍确认口径一致再进分组。4. 回归建模用 statsmodels 验证因子暴露与显著性4.1 面板对齐股票收益与因子暴露的合并因子构建完成后的下一步是把每只股票的月度收益和对应的因子值对齐成一个面板。这个步骤看起来简单但列名不统一是最常见的卡点。output_include_dummy.csv 这张表就是对齐完成的状态里面已经带了行业哑变量可以直接喂给回归。import pandas as pd def align_panel(returns: pd.DataFrame, factors: pd.DataFrame) - pd.DataFrame: 输入 returns: date, stock_code, ret factors: date, smb, hml, rmw, cma 输出宽表面板一行是 (date, stock_code) # 因子是市场层面的同一日期所有股票共享同一行因子值 panel returns.merge(factors, ondate, howleft) # 剔除完全没有收益的样本避免回归时引入空行 panel panel.dropna(subset[ret]) return panelmerge(ondate) 是这里的关键。因子是市场层面的序列同一日期所有股票共享同一组因子值所以不用股票代码做匹配。唯一的风险是因子表和收益表日期不对齐比如因子是月末值、收益是月初值会出现一列错位。检查方法是拿任意一只股票的收益和 SMB 序列做一期滞后相关正常情况下当期相关应该明显大于滞后一期相关。面板铺开之后行数通常是股票数乘以月数的量级比如 3000 只股票 60 个月就是 18 万行。这么大的数据在 statsmodels 里直接 OLS 也能跑得动就是内存占用会比较难看。我一般会先对行业做分块每个行业单独跑一批或者直接上 PanelOLS。4.2 单资产时间序列回归OLS Newey-West单只股票的时间序列回归是这套代码里用得最多的形式。公式是 R_i - R_f alpha beta_m * MKT beta_smb * SMB beta_hml * HML epsilon。回归的目标是拿到 alpha 和三个 beta 的估计值以及 t 统计量。import statsmodels.api as sm def run_ts_regression(panel, stock_code): 对单只股票跑三因子时间序列回归 panel 已包含列date, stock_code, ret_excess, mkt_excess, smb, hml df panel[panel[stock_code] stock_code].dropna() X df[[mkt_excess, smb, hml]].copy() X sm.add_constant(X, has_constantadd) # 截距项 alpha y df[ret_excess] model sm.OLS(y, X) # 金融时间序列残差有自相关和异方差用 HAC 稳健标准误 result model.fit(cov_typeHAC, cov_kwds{maxlags: 6}) return { stock_code: stock_code, alpha: result.params[const], beta_mkt: result.params[mkt_excess], beta_smb: result.params[smb], beta_hml: result.params[hml], t_alpha: result.tvalues[const], }cov_typeHAC 和 maxlags6 是我每次都会改的参数。默认的 OLS 标准误在 A 股月度数据上通常偏小回归出来的 t 值偏大会被误判为显著。HAC 是 Newey-West 异方差自相关一致标准误maxlags 的经验值是月度数据取 6 到 12。如果换成日频数据maxlags 得取到 20 以上。alpha 的 t 值大于 2 才算显著这是金融实证的老规矩别只看系数大小。跑完全市场股票的回归每个股票的参数和 t 值会汇总成一张结果表。这里我会顺手做一步异常值筛选beta_smb 超过 3 的股票直接打上标记因为单只股票的因子暴露超过 3在 A 股里基本都是数据错误或者极端小票进回测会带偏组合权重。4.3 面板回归Pooled OLS 与固定效应的选择如果不关心单股票而是想检验整个市场在五因子下的整体表现面板回归更合适。output_include_dummy.csv 里带了行业哑变量就是为了在 Pooled OLS 里控制行业层面的固定效应。import statsmodels.formula.api as smf def run_pooled_ols(panel): 面板回归行业用哑变量控制 panel 列需包含ret_excess, mkt_excess, smb, hml, rmw, cma, industry # C() 语法让 statsmodels 自动展开哑变量并默认留下 K-1 个 model smf.ols( ret_excess ~ mkt_excess smb hml rmw cma C(industry), datapanel ) result model.fit(cov_typecluster, cov_kwds{groups: panel[date]}) return result这里 cov_typecluster 是按时间聚类因为同一月份的残差在截面之间高度相关按日期聚类比 HAC 更贴近面板数据的真实数据结构。如果你希望在行业上也做聚类可以把 groups 参数换成两列的组合。Pooled OLS 的局限是它假设所有股票共享同一个斜率这在全 A 股上显然不成立所以它更适合当作全市场平均效应的快速检验而不是个股 alpha 的判断依据。做学术级结论之前我一般会拿它和逐股回归的平均 alpha 做对照两者一致性好的时候结论才敢往下写。5. 常见问题与排查重跑这套 Wind 因子代码时的六条踩坑记录5.1 数据与分组类的三个坑排序方向、停牌污染、连接中断现象跑完因子构建后SMB 因子的时序均值接近 0但回归里 beta_smb 普遍为负看起来像大盘股反而获得了小市值溢价。原因排序环节的 ascending 参数没有显式指定。pandas 的 sort_values 默认是升序如果按市值降序排列后把索引当成 Big 组小盘股全部被划到 B 组SMB 的方向整体反转。这个问题在输出因子相关系数矩阵时很难发现因为相关性只反映线性关联不反映方向。解决别依赖默认参数逐个字段写 ascending。我的习惯是像 3.2 节代码那样用 groupby transform 做 qcut不经过 sort_values 再手工切分从根本上避免方向错位。如果代码里已经用了 sort_values务必在切分前打印每组均值和市值中位数核对一遍。现象回归样本量比正常预期高出 20% 以上且回归的 R 方普遍偏低因子暴露的标准误也异常小。原因Wind 取数时用了 FillPrevious停牌月份的收益率被填充为 0而清洗环节里判空的条件是 pct_chg 0对于真实涨跌幅恰好为 0 的交易日也被清掉了所以仍有一批停牌行混进样本。解决改用成交量辅助判断把 volume 0 的月份直接置 NaN再叠加收益为 0 的过滤条件。两个条件取并集后停牌月份几乎不会漏网。final_all_stocks_cleaned_factors_table.csv 里成交量字段是存在的直接用这个字段判断即可不必重新拉数。现象拉数任务运行到一半Wind 连接中断脚本直接退出重跑时从头再来耗时重复。原因w.start() 建立的连接在长任务里会被服务端断开尤其是一次性取上百只股票、跨五六年月度数据时单次请求超过一定秒数就会被拒。越是行情软件正忙的时段越容易触发这个限制。解决先按 50 只股票一段分块拉取每段落盘到一个独立 csv全部跑完后用 concat 拼接。这样中断时只需要重跑最后一个失败段。我通常在循环里加一个 try-except捕获 err 后重试三次三次仍失败就结束当前段、跳到下一段保证中间成果不白费。5.2 回归与共线性类的三个坑哑变量共线、因子高度相关、t 值虚高现象对 output_include_dummy.csv 跑 Pooled OLS 时statsmodels 报 LinAlgError: Singular matrix或者聚类标准误的结果里出现超大系数和 NaN 标准误。原因行业哑变量和常数项完全共线。如果哑变量创建时没有做 drop_first30 个行业就产生 30 个哑变量再加上常数项正好构成完全共线性矩阵不可逆。这是 dummy variable trap线性回归的基础坑。解决回归公式里用 C(industry) 让 statsmodels 自动选择基准行业或者手动构造哑变量时带上 drop_firstTrue。注意观察结果里是否出现了 NaN 的标准误有的话几乎可以断定是共线性问题不要强行解释系数。现象output_correlation.csv 里SMB 与 RMW 的相关系数高达 0.8 以上五因子回归中这两个因子的 t 值都变得不显著但模型整体 R 方很高。原因A 股市场里小市值公司和低盈利公司高度重叠这是数据本身的真实结构不是代码错误。多因子模型最怕的不是因子不显著而是因子之间的高度相关导致的解释混乱各自承担的风险贡献无法分离。解决先跑一遍因子相关矩阵体检相关系数超过 0.7 的因子对要选择其一进入模型或者做因子正交化处理。很多复现五因子的作业在这里直接翻车输出一张全是高相关的因子表还硬往回归里塞最后每个因子的符号都解释不通。遇到这种情况先用单因子回归看各自的解释力再决定要不要保留全部五个因子。现象回归的 t 值整体偏大所有因子全部显著替换样本后依然如此结果好得不真实。原因月度收益序列之间存在截面相关普通 OLS 标准误低估了不确定性。同一月所有股票的残差共享宏观经济冲击把它们当成独立样本会导致 t 值虚高。解决用聚类标准误按时间聚类是面板金融数据里的默认选择。如果还想更保守可以按 date 和 industry 双维度聚类。这一步做完很多原本显著的因子会回归理性别怕结果变难看真实的数据就是这样。6. 进阶验证因子模型的三个稳健性检查与滚动回归窗口选择6.1 三个每次必跑的稳健性检查第一等权与市值加权对比。SMB 在等权口径下通常比市值加权更显著如果两种口径下符号不一致说明小盘股的极端尾部在主导因子不是稳定溢价。第二分年度子样本回归。把 60 个月分成前后两个子区间分别跑回归如果 beta_hml 在两个区间符号相反大概率是风格切换而非稳定因子。第三与 Wind 内置的 Fama-French 因子做相关性对比。Wind 终端里直接能取到官方的 SMB、HML 序列手动构建的因子序列与官方序列相关系数在 0.9 以上时构建逻辑才算真正可信。6.2 滚动窗口回归的两个参数滚动窗口的窗口长度是绕不开的玄学。月度数据我习惯取 36 个月短期噪声压不住60 个月又太长、风格切换后反应滞后。步长取 1 个月逐月滚动这样得到的是平滑的因子暴露时序。注意滚动窗口的前 36 个月没有结果回测时要留出预热期。验证完这套流程后我最大的收获是因子模型的价值不在回归系数本身而在分组和清洗环节是否经得起推敲。从那以后我每次做因子模型都强制先跑 output_correlation.csv 的相关系数体检再确认分组方向最后才进回归——这套顺序帮我挡掉了至少三次方向性错误。希望帮到你。本文还有配套的精品资源点击获取
返回列表