ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

金融多因子模型回归实战:从OLS到弹性网络与滚动打分

金融多因子模型回归实战:从OLS到弹性网络与滚动打分 简介本资源为金融计量经济学实证项目配套资料面向具备经济金融理论与实证研究方法基础的学生及从业者聚焦多因子资产定价模型的回归分析。内容以Fama-French三因子模型为核心在25个按规模与账面市值比划分的股票组合上展开时间序列回归考察SMB与HML因子的统计特征、相关性及显著性并与单因子CAPM模型进行解释力对比同时延伸至规模与运营盈利能力、规模与投资形成的新50组组合检验三因子模型的适用性。资源包内含1个PDF文件约77KB为项目任务说明与报告撰写指引涵盖数据下载、清洗处理、模型构建与结果评价等环节可帮助读者掌握多因子模型的实操流程与研究报告撰写方法。目前已有72人学习适合作为金融工程与计量经济学课程实证练习的参考材料。1. 多因子模型回归从「跑个 OLS」到「因子到底该放几个」很多人第一次接触金融计量经济学里的多因子模型都是从一句“跑个回归看看”开始的。打开 Python把收益率丢进statsmodelsols(ret ~ f1 f2 f3, data).fit()屏幕上跳出一张回归分析结果表R² 看着还行几个系数也显著于是心满意足地关掉 notebook。等到真正拿这套模型去做组合归因、风险预算或者选股打分时才发现问题一大堆因子之间高度共线系数符号和经济学直觉反着来样本外一塌糊涂换一段时间窗口结果就翻脸。这篇东西就是冲着这些具体问题来的——把金融计量经济学中多因子模型的回归分析从数据准备、因子构造、估计方法到诊断和踩坑按能复现的路径讲一遍。适合已经会写基础回归、但被真实金融数据折磨过的从业者也适合想把这套方法落到量化研究里的新手。2. 因子怎么选、数据怎么对齐多因子回归的地基2.1 从 Fama-French 三因子到你自己该用几个因子教科书里讲多因子模型绕不开 Fama-French 三因子市场、规模、价值。但真到实操没人会只跑这三个。常见做法是在市场因子之外按你研究的资产池补行业因子、动量因子、波动率因子甚至宏观因子。问题在于因子不是越多越好。每加一个因子自由度少一个多重共线性风险高一分解释力的边际提升却可能趋近于零。我一般会先问三个问题第一这个因子有没有明确的经济学含义还是纯粹数据挖掘出来的第二它和已有因子的相关系数有没有超过 0.7超了就考虑替换或正交化第三它在样本外是否稳定不能只在某一段行情里有效。这三个问题过不了因子就不该进模型。因子数量上截面回归常见 5 到 15 个时间序列回归 3 到 8 个比较稳妥。超过这个范围除非你有很强的正则化手段否则系数估计的方差会大到没法用。2.2 收益率和因子的频率对齐别让日频月频打架金融数据最烦人的地方就是频率不统一。股票收益率是日频财务因子是季度频宏观因子是月频。直接 merge 会产生大量缺失值或者用未来数据填充造成前视偏差。常见做法是把高频数据降频到低频而不是把低频插值到高频。比如做月频回归就把日收益率按月末复利成月收益率财务因子按公告日滞后一个月对齐确保 t 月的因子值在 t 月月初就能拿到。下面这段代码演示一个最小可复现的对齐流程用 pandas 处理日频收益率和月频因子import pandas as pd import numpy as np # 假设 ret_df 是日频收益率index 为日期columns 为股票代码 # factor_df 是月频因子index 为月末日期columns 为因子名 # 先把日收益率复利成月收益率 ret_df.index pd.to_datetime(ret_df.index) monthly_ret (1 ret_df).resample(M).prod() - 1 # 因子日期对齐到月末并滞后一期避免前视 factor_df.index pd.to_datetime(factor_df.index) factor_df factor_df.resample(M).last() factor_df factor_df.shift(1) # 关键用上个月已知的因子解释本月收益 # 合并只保留两边都有的月份 merged monthly_ret.join(factor_df, howinner) merged merged.dropna() print(merged.shape)逻辑说明resample(M).prod()把日收益率按自然月复利避免用简单求和导致收益高估。shift(1)是防前视偏差的核心操作保证 t 月回归用的是 t-1 月末就能观测到的因子值。howinner只保留共同月份宁可丢样本也不要用填充值污染回归。参数上resample的频率可以按研究需要改成W或Q但滞后逻辑不变。2.3 缺失值、极值和标准化回归前的三件脏活真实数据里缺失值和极值几乎必然出现。缺失值处理上截面回归常用中位数填充或直接剔除该股票该期时间序列回归则倾向用前值填充但最多填一两个周期填多了就是编数据。极值处理更关键金融收益率厚尾一个 10 倍收益的异常点能把整条回归线拉歪。常见做法是 winsorize 到 1% 和 99% 分位或者用 MAD 方法做稳健缩尾。标准化方面截面回归前通常把因子做 z-score 标准化让不同量纲的因子系数可比。但注意标准化要在每个截面上独立做不能全样本一起做否则会引入时间维度的信息泄露。def winsorize_and_standardize(df, factor_cols, lower0.01, upper0.99): df df.copy() for col in factor_cols: # 按截面日期分组做缩尾和标准化 df[col] df.groupby(level0)[col].transform( lambda x: x.clip(x.quantile(lower), x.quantile(upper)) ) df[col] df.groupby(level0)[col].transform( lambda x: (x - x.mean()) / x.std() ) return df这段代码按日期分组做缩尾和标准化level0假设索引第一层是日期。如果你的数据结构是长表需要先set_index([date, stock])。缩尾比例 1% 和 99% 是常用起点样本少时可以放宽到 5% 和 95%。3. 估计方法OLS、稳健标准误和面板回归怎么选3.1 普通最小二乘什么时候够用什么时候翻车OLS 是多因子回归的默认起点但它有三个经典假设误差同方差、无自相关、误差与解释变量不相关。金融数据几乎必然违反前两条。收益率波动率聚集误差项异方差严重时间序列上误差自相关也常见。这时候 OLS 系数估计仍然无偏但标准误算错t 统计量虚高你会误以为某个因子显著。解决办法是换稳健标准误。statsmodels里fit(cov_typeHC3)可以处理异方差fit(cov_typeHAC, cov_kwds{maxlags: 5})同时处理异方差和自相关。参数maxlags一般取样本量的立方根或按 Newey-West 经验法则选月频数据常用 3 到 6。import statsmodels.api as sm # X 是因子矩阵y 是收益率 X sm.add_constant(factor_matrix) model sm.OLS(y, X) results model.fit(cov_typeHAC, cov_kwds{maxlags: 5}) print(results.summary())逻辑说明add_constant加截距项金融回归里截距代表未被因子解释的超额收益不能省。cov_typeHAC是 Newey-West 稳健标准误maxlags5控制自相关修正的滞后阶数。跑完看results.summary()里的系数、t 值和 p 值但别只看这些后面还要做诊断。3.2 面板数据固定效应控制个体异质性的正确姿势如果你用的是多只股票多期的面板数据OLS 会把所有股票混在一起回归忽略了个体差异。比如不同行业的股票对市场因子的敏感度天然不同混在一起估计出来的系数是平均效应对单只股票没有意义。这时候要用面板回归常见的是固定效应模型。固定效应分个体固定效应和时间固定效应。个体固定效应控制每只股票不随时间变化的特征时间固定效应控制每期共同的市场冲击。金融实证里通常两个都加用linearmodels库实现from linearmodels.panel import PanelOLS # 数据需要 MultiIndex: (entity, time) df df.set_index([stock, date]) # 因变量和自变量 y df[ret] X df[[f1, f2, f3]] # 加入个体和时间固定效应 model PanelOLS(y, X, entity_effectsTrue, time_effectsTrue) results model.fit(cov_typeclustered, cluster_entityTrue) print(results)逻辑说明entity_effectsTrue等价于给每只股票加一个截距time_effectsTrue给每个时间点加一个截距。cov_typeclustered和cluster_entityTrue按股票聚类标准误这是面板回归的标配因为同一只股票的误差项通常相关。参数上如果样本时间维度短、个体多聚类标准误可能高估显著性这时可以考虑 bootstrap。3.3 弹性网络回归当因子多到 OLS 扛不住时因子数量接近甚至超过样本量时OLS 直接失效。这时候正则化方法上场。Lasso 做变量选择Ridge 做系数收缩弹性网络Elastic Net是两者的折中在金融多因子里用得越来越多。它特别适合因子池很大、但真正有效的因子只占少数的场景。弹性网络的目标函数是残差平方和 α * (ρ * L1 (1-ρ) * L2)。α 控制正则化强度ρ 控制 L1 和 L2 的混合比例。ρ1 退化成 Lassoρ0 退化成 Ridge。实操里用交叉验证选这两个参数。from sklearn.linear_model import ElasticNetCV from sklearn.preprocessing import StandardScaler import numpy as np # X_train, y_train 是训练集 scaler StandardScaler() X_scaled scaler.fit_transform(X_train) # 弹性网络交叉验证 enet ElasticNetCV( l1_ratio[0.1, 0.3, 0.5, 0.7, 0.9, 1.0], alphasnp.logspace(-4, 1, 50), cv5, max_iter10000, random_state42 ) enet.fit(X_scaled, y_train) print(最优 l1_ratio:, enet.l1_ratio_) print(最优 alpha:, enet.alpha_) print(非零系数个数:, np.sum(enet.coef_ ! 0))逻辑说明l1_ratio是弹性网络的 ρ 参数候选值从 0.1 到 1.0。alphas是正则化强度的候选集用对数刻度。cv5是五折交叉验证。random_state固定随机种子保证可复现。跑完看非零系数个数如果只剩两三个因子说明正则化太强需要放宽 alpha 范围。注意弹性网络对量纲敏感必须先标准化而且标准化参数只能从训练集算再应用到测试集。4. 回归诊断系数显著不等于模型能用4.1 多重共线性VIF 超过 10 就该动手了多因子模型最隐蔽的坑就是多重共线性。因子之间高度相关时系数估计方差膨胀符号可能反转今天显著明天不显著。检测方法是算方差膨胀因子VIF公式是 VIF 1 / (1 - R²_j)其中 R²_j 是第 j 个因子对其他所有因子回归的 R²。VIF 超过 10 通常认为共线性严重超过 5 就要警惕。from statsmodels.stats.outliers_influence import variance_inflation_factor X_with_const sm.add_constant(factor_matrix) vif_data pd.DataFrame() vif_data[factor] X_with_const.columns vif_data[VIF] [variance_inflation_factor(X_with_const.values, i) for i in range(X_with_const.shape[1])] print(vif_data)如果某个因子 VIF 过高处理方式有三种一是剔除该因子二是对因子做正交化三是改用岭回归或弹性网络。正交化的做法是把高 VIF 因子对剩余因子回归取残差作为新因子但这样会改变因子的经济学解释要谨慎。4.2 残差诊断异方差、自相关和正态性回归跑完不能只看 R² 和 t 值残差诊断是必须的。异方差用 Breusch-Pagan 检验或 White 检验自相关用 Durbin-Watson 统计量或 Ljung-Box 检验正态性用 Jarque-Bera 检验。这些在statsmodels里都有现成函数。from statsmodels.stats.diagnostic import het_breuschpagan from statsmodels.stats.stattools import durbin_watson, jarque_bera residuals results.resid # 异方差检验 bp_test het_breuschpagan(residuals, X_with_const) print(Breusch-Pagan p值:, bp_test[1]) # 自相关检验 dw durbin_watson(residuals) print(Durbin-Watson:, dw) # 正态性检验 jb_test jarque_bera(residuals) print(Jarque-Bera p值:, jb_test[1])逻辑说明Breusch-Pagan 的 p 值小于 0.05 说明存在异方差需要换稳健标准误。Durbin-Watson 接近 2 说明无自相关低于 1.5 或高于 2.5 要警惕。Jarque-Bera 的 p 值小于 0.05 说明残差非正态但这在金融数据里几乎是常态不必强求只要样本量够大系数估计的渐近性质仍然可用。4.3 样本外验证别被样本内 R² 骗了样本内 R² 高不代表模型有用。金融数据噪声大样本内过拟合是常态。必须做样本外验证常见做法是滚动窗口或扩展窗口回归。滚动窗口固定训练集长度扩展窗口训练集不断增长。两种方法各有适用场景滚动窗口适合市场结构变化快的时期扩展窗口适合数据稀缺时。def rolling_regression(df, y_col, x_cols, window60): dates df.index.get_level_values(date).unique() predictions [] for i in range(window, len(dates)): train_dates dates[i-window:i] test_date dates[i] train df.loc[df.index.get_level_values(date).isin(train_dates)] test df.loc[df.index.get_level_values(date) test_date] model sm.OLS(train[y_col], sm.add_constant(train[x_cols])).fit() pred model.predict(sm.add_constant(test[x_cols])) predictions.append(pred) return pd.concat(predictions)逻辑说明window60是训练窗口长度月频数据常用 60 个月。每次用前 60 个月训练预测下一个月然后窗口向前滚动。最后把预测值拼起来算样本外 R² 或 IC。参数上窗口太短模型不稳定太长无法适应市场变化60 到 120 是常见范围。5. 避坑与排查多因子回归里那些血泪经验5.1 前视偏差最致命也最容易被忽略现象样本内 R² 高得离谱样本外一塌糊涂IC 接近零甚至为负。原因因子值用了未来信息。比如财务数据用了公告前的数据或者因子标准化时用了全样本均值和标准差。解决所有因子在 t 期回归时只能用 t 期之前已知的信息。财务因子按公告日滞后标准化按截面独立做时间序列因子做 shift。检查方法很简单把因子整体 shift 一期如果模型效果大幅下降说明原来有前视。5.2 幸存者偏差你的股票池可能已经死了一半现象回测收益漂亮实盘差很远。原因股票池只包含当前还在上市的股票退市的、被并购的都没了。这些消失的股票往往收益差剔除它们等于人为拔高收益。解决用历史成分股每个时间点用当时实际存在的股票池。数据源上Wind、聚源都有历史成分股数据但要注意复权处理。如果实在拿不到至少在报告中说明这一偏差。5.3 因子共线性导致的系数符号反转现象单独回归时因子系数为正放进多因子模型后变成负的且显著性下降。原因因子之间高度相关OLS 把解释力分配给了相关性更高的那个另一个系数被扭曲。解决先算相关矩阵和 VIF超过阈值就做正交化或剔除。正交化时注意正交后的因子经济学含义会变解释时要小心。另一个办法是改用弹性网络让正则化自动处理共线性。5.4 标准误算错导致假显著现象t 值很大p 值很小以为因子有效实盘却不赚钱。原因没用稳健标准误异方差和自相关让标准误被低估。解决时间序列回归用 HAC面板回归用聚类标准误截面回归用 HC3。检查方法对比普通标准误和稳健标准误如果差异超过 30%说明原标准误不可靠。5.5 过度拟合因子越多样本外越差现象不断加因子样本内 R² 从 0.3 涨到 0.6样本外 R² 从 0.05 跌到负值。原因因子数量超过样本能支撑的限度模型记住了噪声。解决控制因子数量用交叉验证选正则化参数做样本外测试。经验法则是每个因子至少需要 20 到 30 个独立样本月频数据 5 年只有 60 个样本因子别超过 3 个。6. 一个可复现的滚动回归打分流程把前面所有东西串起来落到一个具体技巧上用滚动回归做因子打分。这个流程我用了很多次适合月频多因子选股。核心思路是每个月用过去 60 个月的数据估计因子收益然后当月用因子暴露乘以估计的因子收益得到每只股票的预期收益排序打分。def factor_score_pipeline(df, factor_cols, ret_col, window60): df: MultiIndex (date, stock)包含因子列和收益率列 factor_cols: 因子列名列表 ret_col: 收益率列名 window: 滚动窗口月数 dates df.index.get_level_values(date).unique().sort_values() scores [] for i in range(window, len(dates)): train_dates dates[i-window:i] test_date dates[i] # 训练集估计因子收益 train df.loc[df.index.get_level_values(date).isin(train_dates)] X_train sm.add_constant(train[factor_cols]) y_train train[ret_col] model sm.OLS(y_train, X_train).fit(cov_typeHAC, cov_kwds{maxlags: 5}) factor_returns model.params[1:] # 去掉截距 # 测试集用因子暴露乘因子收益打分 test df.loc[df.index.get_level_values(date) test_date] test_scores test[factor_cols].dot(factor_returns) test_scores.name score scores.append(test_scores) return pd.concat(scores) # 使用示例 # scores factor_score_pipeline(merged_df, [f1,f2,f3], ret, window60) # 每月按 score 排序选 top 20% 做多bottom 20% 做空逻辑说明window60是训练窗口HAC稳健标准误处理自相关。model.params[1:]取因子收益截距是市场平均收益不参与打分。test[factor_cols].dot(factor_returns)是矩阵乘法得到每只股票的预期收益。最后按 score 排序分组。参数上窗口长度可以按数据频率调整月频 60周频 120日频 250。因子列必须提前做缩尾和标准化否则量纲大的因子会主导打分。这个流程有几个验证点一是看因子收益的时间序列是否稳定如果某个月因子收益符号反转说明市场风格切换二是看打分的 IC即 score 和实际收益的秩相关IC 均值大于 0.03 且 ICIR 大于 0.3 才算可用三是看分组单调性top 组和 bottom 组的收益差是否显著。我自己踩过最大的坑是忘了做因子正交化三个因子相关性都在 0.8 以上滚动回归出来的因子收益每月剧烈波动打分完全没法用。后来加了 VIF 检查把相关性高的因子合并或剔除IC 才稳定下来。另一个坑是标准化用了全样本样本外 IC 虚高改成截面标准化后回归真实水平。这些细节看着小但每一个都能让模型从可用变成不可用。希望帮到你。本文还有配套的精品资源点击获取
返回列表