ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Python实现CAPM与Fama-French三因子模型:从原理到代码

用Python实现CAPM与Fama-French三因子模型:从原理到代码 第一次在Jupyter Notebook里跑通CAPM回归的时候我做的是一个特别常见的标的——拿一只消费股的月度收益率对着市场超额收益和无风险利率做普通最小二乘。当时我连回归结果里的alpha具体意味着什么都说不清楚只知道sm.OLS(...).fit()出来的summary表格里有一行const的p值小于0.05心里还挺高兴。后来开始接触Fama French Three Factor model才意识到那天跑出来的东西只是资产定价里最小的一块拼图。这篇内容就是从“只会调包”到“大概看懂”的完整记录适合已经有一定Python基础、但不知道怎么把金融模型和代码结合起来的读者。我会把模型背后的逻辑、数据怎么准备、代码怎么写、结果怎么看都过一遍尽量做到你跟着操作一遍就能自己复现。1. 先从模型原理说起为什么一定是CAPM和FF31.1 CAPM到底回答了一个什么问题CAPM全称Capital Asset Pricing Model资本资产定价模型1960年代由Sharpe、Lintner等人提出。它试图回答一个很朴素的问题一个资产的期望收益到底由什么决定答案在当时很颠覆——只有系统性风险也就是资产对市场整体波动的敏感程度才应该被定价。非系统性风险是可以通过分散化消除的所以市场不会为它支付额外回报。这个“敏感程度”就是beta。beta大于1的股票市场涨1%它可能涨1.5%市场跌1%它可能跌1.5%属于进攻型品种beta小于1的股票相对抗跌属于防御型。CAPM用一条公式把期望收益拆成了两部分无风险利率加上beta乘以市场风险溢价。数学形式很简单但意义很重它第一次给“风险”和“收益”之间建立了线性的、可检验的关系。后来所有多因子模型本质上都是在这条路上往前走。1.2 Fama French三因子模型补上了什么CAPM漂亮归漂亮问题在于实证上经常失灵。从1970年代开始大量研究发现了很多CAPM解释不了的异象——小市值股票的收益率系统性高于大市值股票高账面市值比价值型股票的收益率系统性高于低账面市值比成长型股票。这些现象单靠beta解释不了。1993年Fama和French在论文里正式提出了三因子模型在市场因子的基础上加入两个新因子SMBSmall Minus Big和HMLHigh Minus Low。SMB衡量的是小盘股组合相对大盘股组合的超额收益HML衡量的是高账面市值比组合相对低账面市值比组合的超额收益。模型的思路很直白既然市场上长期存在着这两种风格溢价那就把它们当作独立的“风险因子”放进定价方程里。用公式表达就是股票的超额收益等于alpha加上市场因子的暴露乘以市场超额收益加上规模因子的暴露乘以SMB再加上价值因子的暴露乘以HML最后加一个残差项。这里的alpha是回归截距如果它在统计上显著不为零说明这个资产在三个因子之外还有额外的超额收益——这个逻辑对后来的所有因子投资、业绩归因都有直接启发。1.3 为什么我用Python来学这两个模型原因其实很实在。第一Python处理数据的效率高。这两个模型的输入数据——个股收益、市场收益、无风险利率、因子序列——本质上就是几个时间序列做合并、对齐、回归。用Excel也能做但样本稍微一多就非常痛苦而pandas处理这类结构化的表格式数据几乎是天然契合的。第二回归分析有现成的库。statsmodels一行.fit()就能输出完整的回归表格包括系数、标准误、t值、p值、R方这些都是解读模型结果必需的统计量。第三从这两个模型入门后续往多因子模型、Fama-MacBeth回归、Barra模型方向走代码骨架都是相通的变换的只是因子个数和回归方法。所以我一直觉得CAPM和FF3是金融计量入门与Python数据分析之间最好的一个交汇点。2. 数据准备模型跑起来之前一半时间都花在这里2.1 收益率怎么算用日度还是月度先明确一个基本概念模型里用的收益率是指“相对上一个周期的涨跌幅”也就是ret_t (price_t - price_{t-1}) / price_{t-1}。在pandas里直接用.pct_change()就能算。需要注意的是如果用复权价格算出来的收益率才是真正可投资的回报否则碰到除权除息日会出现假跌。频率的选择上学术研究的标准做法是用月度数据。原因主要有两个一是经典的因子数据比如Fama-French的因子文件都是以月度为频率发布的你用了日度数据反而要处理频率对齐的问题二是月度收益率更接近正态分布假设模型估计的稳定性更好。初学者如果拿日度数据跑会发现回归结果的R方通常低得可怜因子系数的标准误也会比较大这不是模型错了是数据频率本身的特性。当然用日度数据做练习也不是不行我自己早期为了方便就用日度数据跑过FF3结论方向大体一致但要花不少精力在日期对齐上。所以我的建议很明确第一次跑通流程优先用月度数据。2.2 无风险利率、市场收益和因子数据从哪里来数据来源是个绕不开的问题。如果做美国市场很多数据都是公开的。Fama-French三因子数据在Kenneth French的个人网站上可以直接下载里面包含市场超额收益、SMB、HML以及无风险利率格式是按月排列的文本文件。读取的时候有个小技巧import pandas as pd ff pd.read_csv(F-F_Research_Data_Factors.csv, sep,, skiprows3, index_col0) ff.index pd.to_datetime(ff.index, format%Y%m)如果数据列之间是空格分隔就把sep换成\\s。这个文件里的市场超额收益已经扣除了无风险利率所以回归时直接用个股超额收益对它做回归就行。国内的话标准做法是从学术数据库导出因子数据或者在一些量化社区找整理过的数据文件。对只想练手的人来说最省事的方案是先不管数据获取直接找一份已经整理好的CSV包含stock_ret、mkt_rf、smb、hml、rf这几列先把回归流程跑通之后再研究自动拉数。很多人在这一步卡住其实不是模型难而是把太多精力花在了数据采集上。2.3 SMB和HML这两个因子到底是怎么算出来的虽然实操时可以下载现成因子但我还是建议把因子的构造逻辑搞清楚不然你根本不知道回归系数在解释什么。Fama-French的标准做法是每年6月底把所有股票按市值中位数分成小盘S和大盘B两组同时按账面市值比的前30%、中间40%、后30%分成高H、中M、低L三组。交叉后得到六个组合S/H、S/M、S/L、B/H、B/M、B/L然后计算每个组合未来12个月的市值加权收益。SMB就是三个小盘组合的平均收益减去三个大盘组合的平均收益SMB 1/3 * (S/H S/M S/L) - 1/3 * (B/H B/M B/L)HML则是两个高账面市值比组合的平均收益减去两个低账面市值比组合的平均收益HML 1/2 * (S/H B/H) - 1/2 * (S/L B/L)这样构造出来的每一个因子都是一个多空组合的收益率序列。它代表的是“某种风格带来的溢价”。当你把SMB放进回归方程时是在问剔除市场整体涨跌影响之后这只股票的收益在多大程度上跟随小盘风格、多大程度上跟随价值风格。这个解释其实是理解所有因子模型的一把万能钥匙。2.4 数据对齐与清洗光这一步就能卡一天拿到多份数据之后第一件事永远不是跑回归而是对齐。我自己踩过最大的坑就是日期索引不匹配。股票的月度收益可能是月末最后一个交易日而因子数据用的是月末自然日merge之后凭空多出一堆NaN。处理的办法很朴素把日期统一转化成月末或者统一用交易日历对齐然后再做merge。还有一个容易被忽略的问题收益率的缩尾。单只股票偶尔会出现极端涨跌幅可能是数据错误也可能是真实事件。这种极端值对回归系数的影响很大虽然初学阶段不一定要做缩尾但至少应该用describe()扫一眼数据分布看看有没有明显异常。另外股票停牌期间收益率为0是常态但这部分0收益会拉低beta的估计值。要不要剔除取决于你的研究目标但如果只是练习可以先把这些细节放一放专注跑通整个流程。清洗完的数据结构上应该是这样的每一行是一个月份列包括stock_ret、rf、mkt_rf、smb、hml其中stock_ret是股票原始月收益率rf是无风险利率mkt_rf是市场超额收益后两个是因子收益率。有了这张表接下来所有回归都只是几行代码的事。3. Python实操从一份CSV到两张回归表3.1 准备工作装好环境备好依赖实操开始之前先把环境确认好。我用的是Python 3.9以上版本核心库就三个pandas、numpy、statsmodels。这三个库用pip装一下就行pip install pandas numpy statsmodels如果你用的是Anaconda那pandas和numpy应该已经装好了只需要补一个statsmodelsconda install statsmodels我的习惯是在Jupyter Notebook里写这类分析脚本因为每一步输出都能直接看到方便调试。不过用VS Code写.py文件也完全没问题关键是把数据读进来之后先df.head()看一眼确认列名和索引没问题再往下走。3.2 数据加载与预处理假设你已经有一份整理好的月度数据文件monthly_data.csv直接读进来import pandas as pd import numpy as np import statsmodels.api as sm df pd.read_csv(monthly_data.csv, parse_dates[date]) df df.set_index(date).sort_index() # 计算个股超额收益 df[stock_exret] df[stock_ret] - df[rf] # 看一眼数据长什么样 print(df.head()) print(df.describe())这里我把原始收益率减掉了无风险利率得到超额收益。为什么要减因为CAPM和FF3在建模时被解释变量永远是个股超额收益而不是原始收益。无风险利率在这里充当的是一个基准投资者承担风险所获得的补偿必须是在无风险利率之上的那部分。3.3 CAPM回归四行代码跑通CAPM的回归方程是stock_exret alpha beta * mkt_rf epsilon在statsmodels里实现X_capm sm.add_constant(df[mkt_rf]) y df[stock_exret] model_capm sm.OLS(y, X_capm).fit() print(model_capm.summary())一定要记得加常数项也就是sm.add_constant。这个常数项在回归里对应的就是模型的alpha。很多人第一次跑回归忘了加常数项结果alpha被强制为0模型解释力完全失真。跑完之后你会在summary里看到一行是const系数就是这只股票在这个样本期内的alpha另一行是mkt_rf系数就是beta。再看一眼P|t|列就可以判断这两个系数是否统计显著。3.4 FF3回归同样套路再加两个变量FF3就是在CAPM的回归方程右侧加上SMB和HML两个因子stock_exret alpha beta * mkt_rf s * smb h * hml epsilon代码几乎一模一样X_ff3 sm.add_constant(df[[mkt_rf, smb, hml]]) model_ff3 sm.OLS(y, X_ff3).fit() print(model_ff3.summary())多因子模型的优势在读summary的时候特别明显。你会发现有些股票的中小盘因子系数s显著为正——说明它跟小盘风格同步波动有些价值因子系数h显著为正——说明它的收益更随价值风格。而CAPM里只有一个beta所有风险都被塞进了市场敏感度这一个数字里解释力自然差很多。这里有一个很多新手会问的问题三个解释变量之间如果有相关性怎么办确实市场超额收益、SMB、HML之间会有一定的相关性但Fama-French在构造因子时做了正交化的处理思路虽然标准的三因子数据并不是严格正交的在绝大多数情况下多重共线性不会严重到让回归结果失效。如果你发现某个因子的标准误异常大、系数符号跟直觉相反再考虑去检查相关性。3.5 批量跑多只股票从单只到一整个组合单只股票跑通之后下一步自然是批量处理。比如你想知道一个股票池里每只股票在CAPM和FF3下的alpha和因子暴露与其手动一只一只跑不如写个循环。先把所有股票的月度收益率放在一个宽表里每列是一只股票然后循环对每一列做回归# 假设 returns 是一个DataFrameindex是月份columns是股票代码 capm_results [] ff3_results [] for stock in returns.columns: df_stock df[[mkt_rf, smb, hml, rf]].copy() df_stock[stock_exret] returns[stock] - df_stock[rf] df_stock df_stock.dropna() y df_stock[stock_exret] X_capm sm.add_constant(df_stock[mkt_rf]) m_capm sm.OLS(y, X_capm).fit() X_ff3 sm.add_constant(df_stock[[mkt_rf, smb, hml]]) m_ff3 sm.OLS(y, X_ff3).fit() capm_results.append({ stock: stock, alpha: m_capm.params[const], beta: m_capm.params[mkt_rf], r2_capm: m_capm.rsquared }) ff3_results.append({ stock: stock, alpha: m_ff3.params[const], beta: m_ff3.params[mkt_rf], smb: m_ff3.params[smb], hml: m_ff3.params[hml], r2_ff3: m_ff3.rsquared }) capm_df pd.DataFrame(capm_results).set_index(stock) ff3_df pd.DataFrame(ff3_results).set_index(stock)这样一个简洁的结果表就出来了。而且你会发现一个特别直观的现象大部分股票的r2_ff3会比r2_capm高出一截这就是三因子模型解释力更强的直接证据。3.6 两个模型的结果对比把两个模型的回归结果放在一起比较时重点看三个维度alpha的显著性、因子系数的显著性、R方。我自己习惯的做法是把结果汇总成一个对比表方便快速扫一眼。指标CAPMFF3alpha截距项表示市场因子之外的超额收益截距项表示三个因子之外的超额收益解释变量仅有市场超额收益市场超额收益 SMB HMLR方通常较低个股月度数据常见0.1-0.5通常更高模型含义只用系统性风险解释收益加入规模与价值风险溢价我自己跑过一只中盘价值股的月度数据CAPM的R方只有0.23FF3一下子跳到0.51而且HML的系数显著为正。这个结果说明这只股票的收益里有很大一部分是跟着价值风格走的。用CAPM去看你会以为它有很多alpha其实这些超额收益大部分是价值因子暴露带来的。4. 结果解读与避坑清单4.1 summary表格里到底该看什么statsmodels输出的summary内容很多初看会有点懵。我一般只看四样东西系数值coef、标准误std err、t统计量、P值以及右上角的R方。系数告诉你因子暴露的方向和大小标准误和t值告诉你这个系数靠不靠谱P值则是帮你做显著性判断的。一个常见误区是看到alpha很大就觉得发现了超额收益。这不对还得看它显不显著。如果alpha对应的P值大于0.05那这个alpha在统计上和0没有实质区别你看到的“超额收益”很可能只是随机波动。样本期短的时候这种情况尤其常见可能只有两三年数据跑出来的alpha看着挺大加个标准误之后就完全不够看了。4.2 回归结果怎么用到实际判断中如果你在做的是业绩归因FF3的因子系数可以直接用来解释组合收益的来源。比如一个组合的SMB系数是0.3说明组合有30%的小盘风格暴露小盘风格好的年份组合表现会加分小盘风格差的年份组合会拖后腿。HML系数为负则说明组合偏向成长风格。如果你在做选股研究可以反过来用先对全市场股票逐只跑FF3回归筛出alpha显著为正、且因子暴露符合自己预期的股票作为候选池进一步分析。不过要注意这种单变量筛选只是初步信号绝不能当作完整策略。真实世界里还要考虑交易成本、流动性、换手率等各种问题。如果你只是想理解一只基金的表现也可以用相同的方法。把基金的月度收益率当作被解释变量跑FF3回归看它到底在哪些因子上有暴露。很多主动基金在剔除三因子暴露之后alpha并不显著这个结论对投资者判断“基金经理是真的有本事还是运气好”有参考价值。4.3 新手最容易踩的坑附排查表我把自己早期踩过的坑和身边朋友问过的问题整理成了一张速查表希望能帮你省点时间。问题可能原因解决思路merge后数据大量NaN日期索引不对齐统一转为月末日期或统一交易日历后再合并回归结果里没有const行忘了sm.add_constant加上常数项再跑系数符号跟直觉相反因子数据方向搞反检查SMB和HML的构造定义以及数据列名是否对应R方低到难以置信用了日度数据、个股噪声大换月度数据或者用组合收益做被解释变量alpha很大但P值不显著样本期太短延长样本期或换更平稳的标的不同股票结果差异巨大因子暴露本来就不同属于正常现象结合股票风格特征去解释跑回归时报维度不对特征矩阵里有NaN提前dropna()4.4 我的一些后续学习建议把这两个模型跑通之后下一步怎么走我个人的体验是顺着三条线继续深挖最有效率。第一条线是统计细节。比如换用Newey-West调整标准误处理异方差和自相关再比如做滚动窗口回归观察因子系数随时间的变化。这些都不算难但能大幅提高你对回归结果的判断力。第二条线是因子扩展。Fama-French后来又出了五因子模型在三个因子基础上加入了盈利因子RMW和投资因子CMA。代码层面你只需要在特征矩阵里加两列但对“为什么市场会给这些风险溢价”的理解会再深一层。第三条线是应用落地。比如用估计出来的因子暴露做组合优化或者用滚动alpha做简单的股票筛选。这个过程会让你更清楚模型在真实投资场景里的作用和边界。我自己走下来最大的感受是模型本身不复杂复杂的是对数据的理解和对结果的解读这恰恰是只靠看论文学不到的东西。最后分享一个我自己常用的调试小技巧不管跑CAPM还是FF3第一段代码永远是df.info()和df.isna().sum()先确认数据类型和缺失值情况。学这两个模型时踩过的坑中有一半以上其实是数据形态的问题而不是模型本身的问题。先把数据搞干净模型的代码几乎都是顺水推舟的事。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进