资产定价模型选择:AIC、BIC与交叉验证的R语言实战指南 1. 从两个经典资产定价模型说起为什么模型选择比建模本身更考验人做量化研究或者写实证论文的朋友大概率都绕不开两个模型Fama三因子模型和CAPM模型。前者在市场风险溢价之外加入了规模因子SMB和价值因子HML后者只考虑市场风险溢价这一个解释变量。表面上看Fama三因子比CAPM多两个因子拟合优度肯定更高那是不是无脑选三因子就完事了我一开始也是这么想的直到被一个审稿意见怼回来“你的调整R方确实高了但你怎么证明增加的因子不是过拟合”这个问题直接把我打回原形。后来我才意识到模型选择本质上是在拟合优度和模型复杂度之间做权衡而AIC、BIC、调整R方、十折交叉验证、留一交叉验证LOOCV这些工具就是帮你做这个权衡的。这篇内容适合谁看如果你正在用R语言做资产定价的实证分析或者你手头有一组收益率数据纠结到底该用CAPM还是Fama三因子再或者你只是想知道AIC和BIC到底该怎么选、交叉验证的代码怎么写才不会翻车那这篇东西应该能帮你省下不少查文档和踩坑的时间。我会把整个流程拆开从数据准备到模型拟合从信息准则计算到交叉验证实现每一步都给出可复现的R代码和背后的判断逻辑。先明确一个前提模型选择没有绝对的最优解只有在你当前数据和研究目的下相对更合理的选择。AIC偏向预测BIC偏向解释调整R方看拟合交叉验证看泛化。它们各自有各自的适用场景关键是你得知道什么时候该用哪个以及为什么。2. 数据准备与模型设定先把地基打牢2.1 数据来源与预处理的关键细节做资产定价实证数据是第一步。通常你需要三类数据个股或组合的月度收益率、市场组合收益率、无风险利率。Fama三因子还需要规模因子SMB和账面市值比因子HML。这些数据可以从公开的金融数据库中获取但拿到手之后不能直接往模型里扔。我踩过的第一个坑就是时间对齐。不同来源的数据时间戳格式可能不一样有的是“2020-01”有的是“202001”还有的是“Jan-20”。如果你不统一格式就直接合并R会给你一堆NA然后你还在那纳闷为什么回归结果全是缺失值。我的习惯是统一转成Date类型用as.Date()配合format参数处理。第二个坑是缺失值处理。资产定价数据里个股停牌、退市都会导致缺失。如果你的样本里有个股在某个月没有交易数据直接na.omit()会丢掉整行可能让你的样本量骤减。更合理的做法是先检查缺失比例如果某只股票缺失超过30%考虑剔除如果只是零星缺失可以用前向填充或者行业均值填充。但要注意填充后的数据在交叉验证时可能会引入信息泄露这个后面会细说。第三个坑是异常值。收益率数据里偶尔会出现极端值比如某个月涨了200%可能是数据错误也可能是真实的重组事件。我的做法是先画箱线图看看分布对超过3倍标准差的观测值进行缩尾处理winsorize而不是直接删除。缩尾的代码很简单winsorize - function(x, probs c(0.01, 0.99)) { q - quantile(x, probs probs, na.rm TRUE) x[x q[1]] - q[1] x[x q[2]] - q[2] return(x) }2.2 CAPM与Fama三因子的模型表达式CAPM的回归形式很简洁[ R_i - R_f \alpha_i \beta_i (R_m - R_f) \epsilon_i ]其中 ( R_i - R_f ) 是个股或组合的超额收益率( R_m - R_f ) 是市场风险溢价。在R里写出来就是capm_model - lm(excess_return ~ mkt_excess, data df)Fama三因子在此基础上多了两个因子[ R_i - R_f \alpha_i \beta_1 (R_m - R_f) \beta_2 SMB \beta_3 HML \epsilon_i ]对应的R代码ff3_model - lm(excess_return ~ mkt_excess SMB HML, data df)看起来只是多了两个自变量但背后的经济含义完全不同。CAPM假设市场是唯一的风险来源而Fama三因子认为小盘股和价值股有额外的风险溢价。如果你研究的组合恰好偏向小盘价值风格那三因子的解释力会显著高于CAPM但如果你研究的是大盘成长股三因子的优势可能就没那么明显了。这一点在模型选择时非常重要不能脱离研究对象的特征去谈哪个模型更好。2.3 为什么要同时准备多个候选模型很多人做实证只拟合一个模型然后报告结果。但审稿人或者答辩老师往往会问“你为什么不用另一个模型”所以我的习惯是同时准备至少三个候选模型CAPM、Fama三因子有时候还会加一个Fama五因子或者Carhart四因子作为对照。这样你在后续做模型选择时才有比较的基准。在R里我通常会把所有模型放在一个列表里models - list( CAPM lm(excess_return ~ mkt_excess, data df), FF3 lm(excess_return ~ mkt_excess SMB HML, data df) )这样做的好处是后面计算AIC、BIC、调整R方的时候可以直接用lapply或者purrr::map批量处理代码简洁且不容易出错。3. 信息准则与调整R方快速筛选模型的利器3.1 AIC与BIC的计算逻辑与R实现AIC赤池信息准则和BIC贝叶斯信息准则是模型选择中最常用的两个指标。它们的核心思想都是在拟合优度的基础上惩罚模型复杂度但惩罚力度不同。AIC的公式是[ AIC -2 \ln(L) 2k ]BIC的公式是[ BIC -2 \ln(L) k \ln(n) ]其中 ( L ) 是似然函数值( k ) 是参数个数( n ) 是样本量。关键区别在于惩罚项AIC的惩罚是 ( 2k )BIC的惩罚是 ( k \ln(n) )。当样本量较大时( \ln(n) 2 )所以BIC的惩罚更重倾向于选择更简单的模型。在R里计算这两个指标非常简单aic_values - sapply(models, AIC) bic_values - sapply(models, BIC)但这里有个容易被忽略的细节AIC和BIC的比较只能在同一个数据集上拟合的模型之间进行。如果你对CAPM用了完整样本对Fama三因子用了剔除缺失值后的样本那AIC和BIC的数值就没有可比性。我见过有人因为这个原因得出“CAPM优于三因子”的结论其实是样本不一致导致的假象。另外AIC和BIC的绝对值没有意义只有相对大小才有意义。你不需要关心AIC是1000还是2000只需要看哪个模型的AIC更小。通常来说AIC差值超过10就认为是显著差异了。3.2 调整R方的计算与解读调整R方Adjusted R-squared是另一个常用指标。普通R方有个缺陷只要增加自变量R方一定会增大哪怕这个自变量和因变量毫无关系。调整R方通过引入自由度惩罚解决了这个问题[ \bar{R}^2 1 - \frac{(1 - R^2)(n - 1)}{n - k - 1} ]在R里调整R方可以直接从summary()里提取adj_r2 - sapply(models, function(m) summary(m)$adj.r.squared)解读调整R方时要注意它衡量的是模型对当前样本的解释力而不是对未来数据的预测力。一个调整R方很高的模型可能在样本外表现很差。这就是为什么我们还需要交叉验证。3.3 三个指标的对比与选择建议把AIC、BIC、调整R方放在一起看你会发现它们有时候会给出不同的结论。比如在我最近做的一个项目里CAPM的调整R方是0.65Fama三因子是0.72但BIC却显示CAPM更优。原因就是三因子多出来的两个参数在BIC的惩罚下“得不偿失”。那到底该听谁的我的经验是如果你的目标是解释现有数据的模式优先看调整R方和BIC。BIC倾向于简约模型适合理论驱动的分析。如果你的目标是预测未来收益率优先看AIC和交叉验证。AIC偏向预测交叉验证直接评估泛化能力。如果三个指标一致指向同一个模型那基本可以放心选它。如果分歧很大说明数据里的信号不够强这时候交叉验证的结果更有参考价值。下面是一个简单的对比表格方便你快速判断指标惩罚力度倾向适用场景AIC较轻2k稍复杂的模型预测导向BIC较重k ln n简约模型解释导向调整R方中等平衡初步筛选十折交叉验证无显式惩罚泛化能力强的模型最终决策LOOCV无显式惩罚小样本下的泛化样本量较小时注意AIC和BIC的比较必须基于相同的样本和相同的因变量变换。如果你的模型对因变量做了对数变换而另一个没有那AIC和BIC不可直接比较。4. 交叉验证实战十折与留一法的R代码实现4.1 十折交叉验证的原理与手写实现交叉验证的核心思想是把数据分成训练集和测试集用训练集拟合模型用测试集评估预测误差。十折交叉验证就是把数据分成10份每次用9份训练1份测试重复10次最后取平均误差。在R里caret包提供了现成的train()函数但如果你想真正理解背后的逻辑我建议先手写一遍。下面是我常用的十折交叉验证代码set.seed(123) n - nrow(df) folds - sample(rep(1:10, length.out n)) cv_error - function(model_formula, data, folds) { errors - numeric(10) for (i in 1:10) { train_data - data[folds ! i, ] test_data - data[folds i, ] fit - lm(model_formula, data train_data) pred - predict(fit, newdata test_data) errors[i] - mean((test_data$excess_return - pred)^2) } return(mean(errors)) } mse_capm - cv_error(excess_return ~ mkt_excess, df, folds) mse_ff3 - cv_error(excess_return ~ mkt_excess SMB HML, df, folds)这段代码里有个关键细节set.seed(123)必须放在划分折数之前。如果你不设随机种子每次运行的结果都会不一样别人就无法复现你的结果。我审稿时如果看到交叉验证结果没有设种子会直接质疑其可靠性。另一个细节是折数的选择。十折是默认推荐因为它在偏差和方差之间取得了较好的平衡。折数太少比如3折训练集不够大模型可能欠拟合折数太多比如20折计算成本高而且每折的测试集太小误差估计的方差反而增大。4.2 留一交叉验证LOOCV的适用场景与代码LOOCV是十折的极端情况每次只留一个样本作为测试集其余全部用于训练。它的优点是几乎利用了所有数据进行训练偏差很小缺点是计算量大n个样本就要拟合n次而且误差估计的方差较大因为每次的测试集只有一个样本。在R里实现LOOCVloocv_error - function(model_formula, data) { n - nrow(data) errors - numeric(n) for (i in 1:n) { train_data - data[-i, ] test_data - data[i, ] fit - lm(model_formula, data train_data) pred - predict(fit, newdata test_data) errors[i] - (test_data$excess_return - pred)^2 } return(mean(errors)) }但说实话在资产定价研究里LOOCV用得并不多因为样本量通常不大比如月度数据可能只有几百个观测LOOCV的计算量还能接受但它的方差问题比较突出。我更推荐十折或者五折尤其是在样本量超过200的时候。不过有一种情况LOOCV特别有用样本量非常小比如少于50个观测。这时候十折会导致每折的训练集只有45个样本可能不够稳定而LOOCV每次用49个样本训练能最大化利用数据。所以如果你的研究是事件研究或者小样本实验LOOCV值得考虑。4.3 交叉验证中的信息泄露与避坑指南交叉验证最大的坑是信息泄露。什么叫信息泄露就是测试集的信息在训练过程中被模型“看到”了。常见的信息泄露场景包括在划分折数之前做了标准化如果你先对全样本做标准化再划分训练集和测试集那测试集的均值和标准差就泄露到了训练过程中。正确做法是先划分折数然后在每折的训练集上计算均值和标准差再用这些参数去标准化测试集。在划分折数之前做了缺失值填充同理填充用的统计量应该只从训练集计算。时间序列数据随机划分资产定价数据通常是时间序列随机划分折数会导致“未来数据预测过去”。正确做法是按时间顺序划分比如前80%作为训练集后20%作为测试集或者用滚动窗口交叉验证。我踩过的最大的坑就是第三条。有一次我用随机十折交叉验证评估一个动量策略结果好得离谱后来才发现是因为随机划分让模型“偷看”了未来的收益率。改成时间序列划分后表现直接腰斩。所以做金融数据的交叉验证一定要用时间序列感知的方法。5. 模型选择的完整决策流程与实战案例5.1 从数据到决策一个完整的R脚本框架把前面所有内容串起来一个完整的模型选择流程大概是这样的# 1. 数据准备 df - read.csv(data.csv) df$date - as.Date(df$date) df - na.omit(df) df$excess_return - df$ret - df$rf # 2. 拟合候选模型 models - list( CAPM lm(excess_return ~ mkt_excess, data df), FF3 lm(excess_return ~ mkt_excess SMB HML, data df) ) # 3. 信息准则 aic_vals - sapply(models, AIC) bic_vals - sapply(models, BIC) adj_r2_vals - sapply(models, function(m) summary(m)$adj.r.squared) # 4. 十折交叉验证 set.seed(123) folds - sample(rep(1:10, length.out nrow(df))) cv_mse - sapply(models, function(m) { cv_error(formula(m), df, folds) }) # 5. 汇总结果 result - data.frame( Model names(models), AIC aic_vals, BIC bic_vals, Adj_R2 adj_r2_vals, CV_MSE cv_mse ) print(result)运行这个脚本后你会得到一张汇总表。接下来就是解读结果。如果AIC、BIC、调整R方、CV_MSE都指向Fama三因子那结论很明确。但如果出现分歧比如BIC选CAPM而CV_MSE选FF3那就需要结合研究目的来判断。5.2 结果解读当指标出现分歧时怎么办指标分歧是常态不是异常。我遇到过分歧最严重的一次是调整R方显示FF3比CAPM高0.08但BIC显示CAPM更优而十折CV的MSE两者几乎一样。这种情况下我的判断逻辑是第一步看分歧的来源。BIC选CAPM是因为它惩罚复杂度说明多出来的两个因子带来的拟合提升不足以抵消参数增加的代价。CV_MSE差不多说明两个模型的泛化能力接近。调整R方高但CV_MSE不低说明FF3可能过拟合了训练集。第二步看研究目的。如果这篇论文的核心是“检验规模因子和价值因子是否有定价能力”那即使BIC不占优你也应该报告FF3的结果因为你的理论假设需要这两个因子。如果核心是“预测未来收益率”那CV_MSE接近的情况下选更简单的CAPM更稳妥。第三步看经济显著性。统计指标只是参考最终还要看因子的经济含义。如果SMB和HML的系数在经济上很大且方向符合理论预期那即使BIC略高保留它们也是有道理的。5.3 常见问题速查表问题可能原因解决方法AIC和BIC都选CAPM但调整R方选FF3样本量小BIC惩罚重检查样本量考虑用AIC或CVCV_MSE异常大信息泄露或异常值检查数据划分方式做缩尾处理LOOCV结果不稳定样本量太小或杠杆点改用十折或检查高杠杆观测模型系数符号与理论相反多重共线性或数据错误计算VIF检查因子相关性交叉验证每次结果不同未设随机种子在划分折数前加set.seed()提示如果你的数据是面板数据多个个体多个时间点普通的十折交叉验证不适用需要用按个体或按时间分组的交叉验证。这一点在资产定价的组合研究中特别常见。6. 一些实操心得与容易忽略的细节6.1 关于AIC和BIC的样本量问题AIC和BIC都依赖于似然函数值而似然函数值又依赖于样本量。当样本量很小时比如少于30AIC和BIC的近似效果会变差这时候它们的结果可能不可靠。我一般会在样本量小于50时优先看调整R方和LOOCV而不是AIC/BIC。另外AIC和BIC对模型的误差分布假设很敏感。lm()默认假设误差服从正态分布如果你的收益率数据有明显的尖峰厚尾那AIC和BIC的计算基础就不太牢靠。这时候可以考虑用稳健标准误或者改用基于分位数的模型选择方法。不过对于大多数资产定价研究来说月度收益率的正态性假设还算过得去不用过度纠结。6.2 交叉验证的折数选择经验十折是默认推荐但不是唯一选择。我的经验法则是样本量小于100用LOOCV或5折样本量100到500用10折样本量大于500用5折就够了因为大样本下折数的影响变小5折能省不少计算时间还有一个技巧是重复交叉验证。单次十折的结果可能因为随机划分而波动你可以重复10次十折每次用不同的随机种子然后取平均。这样得到的误差估计更稳定。代码上只需要在外面套一层循环repeated_cv - function(formula, data, n_repeats 10, n_folds 10) { mse - numeric(n_repeats) for (r in 1:n_repeats) { set.seed(r) folds - sample(rep(1:n_folds, length.out nrow(data))) mse[r] - cv_error(formula, data, folds) } return(mean(mse)) }6.3 模型选择不是终点而是起点最后想说一点模型选择只是实证分析的一个环节不是全部。选完模型之后你还需要做残差诊断、稳健性检验、经济解释。我见过太多人把精力全花在“选哪个模型”上结果选完之后发现残差有严重的自相关或者系数在经济上无法解释。所以我的建议是把模型选择当作一个快速筛选工具而不是最终决策工具。先用AIC、BIC、调整R方快速排除明显不合适的模型然后用交叉验证确认泛化能力最后用经济逻辑和稳健性检验做最终判断。整个流程走下来你对数据的理解会比单纯跑一个回归深得多。另外不要迷信任何一个指标。AIC、BIC、调整R方、交叉验证它们都是工具工具的价值在于帮你更好地理解数据而不是替代你的判断。真正重要的是你对研究问题的理解和对数据生成过程的洞察。