缘由
艺多不压身
碎碎念
我们继续飞行在云层和雾气之间,飞行在一个无光无影的空洞世界中。 ——《风沙星辰》
Ref
11 Comparing Models with Resampling | Tidy Modeling with R
解读
本文用 Ames 数据集,演示如何用 tidymodels 做预处理/交叉验证/模型比较,并用 tidyposterior + rstanarm 做贝叶斯模型比较,从而得到更稳健且对决策有实际意义的结论。
library(tidymodels)
library(modeldata)
library(tidyposterior)
library(rstanarm) # 用于后验抽样(注意:安装 rstanarm 可能需要额外配置)
library(ggplot2)
library(ggrepel)
library(forcats)
library(dplyr)
library(tidyr)
library(purrr)
library(broom)
tidymodels_prefer()
data(ames)
ames <- mutate(ames, Sale_Price = log10(Sale_Price))
set.seed(502)
ames_split <- initial_split(ames, prop = 0.80, strata = Sale_Price)
ames_train <- training(ames_split)
ames_test <- testing(ames_split)1. 三套预处理(recipes)
我们实现三种预处理方案:basic、interact(加入交互)和 splines(在经纬度上加自然样条)。
basic_rec <-
recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type +
Latitude + Longitude, data = ames_train) %>%
step_log(Gr_Liv_Area, base = 10) %>%
step_other(Neighborhood, threshold = 0.01) %>%
step_dummy(all_nominal_predictors())
interaction_rec <-
basic_rec %>%
step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") )
spline_rec <-
interaction_rec %>%
step_ns(Latitude, Longitude, deg_free = 50)
preproc <- list(basic = basic_rec,
interact = interaction_rec,
splines = spline_rec)2. 线性回归工作流集合与 10 折交叉验证
lm_models <- workflow_set(preproc, list(lm = linear_reg()), cross = FALSE)
set.seed(1001)
ames_folds <- vfold_cv(ames_train, v = 10) # 10 折 CV
keep_pred <- control_resamples(save_pred = TRUE, save_workflow = TRUE)
lm_models <-
lm_models %>%
workflow_map("fit_resamples",
seed = 1101, verbose = TRUE,
resamples = ames_folds, control = keep_pred)3. 随机森林(作为比较)
rf_model <-
rand_forest(trees = 1000) %>%
set_engine("ranger") %>%
set_mode("regression")
rf_wflow <-
workflow() %>%
add_formula(
Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built +
Bldg_Type + Latitude + Longitude) %>%
add_model(rf_model)
set.seed(1003)
rf_res <- rf_wflow %>%
fit_resamples(resamples = ames_folds, control = keep_pred)
four_models <-
as_workflow_set(random_forest = rf_res) %>%
bind_rows(lm_models)4. 收集每一折的 R²(用于后续比较)
rsq_indiv_estimates <-
collect_metrics(four_models, summarize = FALSE) %>%
filter(.metric == "rsq")
# 宽格式:每一行 (id) = 一次 resample,列为不同模型的 R^2
rsq_wider <-
rsq_indiv_estimates %>%
select(wflow_id, .estimate, id) %>%
pivot_wider(id_cols = "id", names_from = "wflow_id", values_from = ".estimate")说明:rsq_indiv_estimates 保留了 每一折(每个 id)上所有模型的 R²,而不是折平均。这样可以做配对比较(paired),因为在同一折上评估各模型,它们的表现是可直接比较的。
5. 频率学检验(相关性、配对 t 检验 / 回归)
tidymodels-与贝叶斯模型比较(深度解读)/img-20250924142422109.webp)
# 相关性(说明:同一折上各模型的 R^2 通常高度相关)
corrr::correlate(rsq_wider %>% select(-id), quiet = TRUE)
# 可视化:每个 resample(id) 上的模型 R^2 路径图
rsq_indiv_estimates %>%
mutate(wflow_id = reorder(wflow_id, .estimate)) %>%
ggplot(aes(x = wflow_id, y = .estimate, group = id, color = id)) +
geom_line(alpha = .5, linewidth = 1.25) +
theme_minimal() + theme(legend.position = "none")
# 配对相关检验(示例:basic_lm vs splines_lm)
rsq_wider %>% with(cor.test(basic_lm, splines_lm)) %>% broom::tidy()
# 配对差值分析(平均差值是否为 0)
compare_lm <- rsq_wider %>% mutate(difference = splines_lm - basic_lm)
# 1) 用 lm 测试差值均值
lm(difference ~ 1, data = compare_lm) %>% tidy(conf.int = TRUE) %>%
select(estimate, p.value, starts_with("conf"))
# 2) 或者用配对 t 检验
rsq_wider %>% with(t.test(splines_lm, basic_lm, paired = TRUE)) %>% tidy() %>%
select(estimate, p.value, starts_with("conf"))解读(频率学):你会得到一个点估计(均值差 ≈ 0.00913)和一个 p 值(非常小),说明在样本上 splines_lm 的均值 R² 显著高于 basic_lm。但这里仍然要问:这个差值在实际上重要吗? —— 这正是我们要用贝叶斯方法深入判断的地方。
6. 贝叶斯方法(tidyposterior + rstanarm):为什么用它?
- 交叉验证产生的每折结果并非独立(同一折上不同模型的结果相关),频率学上的 t 检验/置信区间常常忽略这种配对结构或不能充分建模它,导致对不确定性估计不够保守或不够准确。
- 频率学结果通常给出是否“显著”(p 值),但**无法直接提供“模型 A 比模型 B 更好的概率”**这类对决策更有价值的量化结论。
- 我们还关心“差异是否在实践上可忽略(practical equivalence)”,即 ROPE(Region of Practical Equivalence)。
**tidyposterior :
- 把每个 resample(fold)看作一个观测单元,建立层次贝叶斯模型:模型性能 = workflow 固有效应 + resample 的随机偏差(捕捉 resample 相关性),并对参数引入先验。
- 用
rstanarm做后验抽样,得到每个 workflow 的 R² 的后验分布(不是仅仅一个点估计)。 - 基于后验样本可以直接计算两模型差值的后验分布、
P(difference > 0)、可信区间和 ROPE 中的质量份额(pract_equiv 等)。
7. 用 tidyposterior 拟合并抽样(得到后验)
rsq_anova <-
perf_mod(
four_models,
metric = "rsq",
prior_intercept = rstanarm::student_t(df = 1),
chains = 4,
iter = 5000,
seed = 1102
)
# 抽取后验样本(tidy 格式,列名通常是 model / posterior)
model_post <- rsq_anova %>% tidy(seed = 1103)
glimpse(model_post)
# -> 例如: model (workflow id), posterior (每个后验样本的 R^2 数值)
autoplot(rsq_anova) +
geom_text_repel(aes(label = workflow), nudge_x = 1/8, nudge_y = 1/100) +
theme(legend.position = "none")
注意:perf_mod() 内部构建了一个层次模型(workflow-level 参数 + resample-level随机效应),这可以把相同 resample 上不同 workflow 的相关性纳入模型,从而得到更合理的不确定性估计(比简单 t 检验更稳健)。
8. 可视化:每个模型的后验分布(R²)
(默认 posterior 的范围通常在 0–1 之间,因为我们在建模 R²)
# 密度图 + 标注均值与 95% 区间
model_post %>%
mutate(model = forcats::fct_inorder(model)) %>% mutate(model = forcats::fct_inorder(model)) %>%
ggplot(aes(x = posterior)) +
geom_histogram(bins = 50, color = "white", fill = "blue", alpha = 0.4) +
facet_wrap(~ model, ncol = 1)
说明:横轴 0–1 表示 R² 可能取值范围;纵轴样本计数。后验分布的“位置”表示哪个模型的 R² 更高,分布的宽窄表示不确定性大小。
9. 模型间差值的后验(contrast_models)
rqs_diff <- contrast_models(
rsq_anova,
list_1 = "splines_lm",
list_2 = "basic_lm",
seed = 1104
)这里 rqs_diff 就是 差值的后验分布样本池,每个样本 = 一次 MCMC 抽样下 splines_lm 与 basic_lm 的 R² 差值。
差值直方图可视化
rqs_diff %>%
as_tibble() %>%
ggplot(aes(x = difference)) +
geom_vline(xintercept = 0, lty = 2) +
geom_histogram(bins = 50, color = "white", fill = "red", alpha = 0.4)- 横坐标:差值 (
splines_lm - basic_lm)。 - 纵坐标:每个区间的后验样本数(count)。
- 竖线 0:差值为零的参考线。
如果大多数后验质量在 >0 的区域,说明 splines_lm 的 R² 高于 basic_lm。
summary(rqs_diff) %>% select(-starts_with("pract"))
这是标准的贝叶斯后验摘要:
| contrast | probability | mean | lower | upper | size |
|---|---|---|---|---|---|
| splines_lm vs basic_lm | 1.00 | 0.00913 | 0.00510 | 0.0132 | 0 |
解释:
- mean = 0.00913 → 差值的后验均值。
- lower, upper → 95% 可信区间,大约 [0.005, 0.013]。
- probability = 1.00 → 后验概率 P(diff > 0) ≈ 100%。
- size = 0 → 因为你没指定 ROPE,所以这一列显示默认值。
👉 这是在问:差值是否大于零?差不多是多少?
结论:splines_lm 比 basic_lm 更好,平均高 0.009,几乎肯定 >0。
② summary(rqs_diff, size = 0.02) %>% select(contrast, starts_with("pract"))
这是基于 ROPE 的“实践等效”分析:
| contrast | pract_neg | pract_equiv | pract_pos |
|---|---|---|---|
| splines_lm vs basic_lm | 0 | 1 | 0 |
解释:
- pract_neg = 0 → 没有证据支持 splines_lm 明显更差。
- pract_pos = 0 → 没有证据支持 splines_lm 明显更好(大于 +0.02)。
- pract_equiv = 1 → 100% 的后验质量落在 [-0.02, +0.02] 区间内。
这是在问:差值是否大到值得在意?
结论:虽然差值 >0 的概率是 100%,但提升幅度太小(<0.02),在实践上可以看作等效。
两者的差异(重点)
-
不带 size → “统计角度” → 差值几乎肯定 >0。
-
带 size → “实践角度” → 差值虽然显著,但幅度不足以超出 ±0.02 的等效区间。
-
第一份 summary = splines_lm 确实更好(统计上显著)。
-
第二份 summary = 但更好得不够多,可以忽略(实践上等效)。