缘由
艺多不压身
碎碎念
我们继续飞行在云层和雾气之间,飞行在一个无光无影的空洞世界中。 ——《风沙星辰》
Ref
19 When Should You Trust Your Predictions? | Tidy Modeling with R
解读
本文通过四个部分展示 tidymodels 的建模与诊断流程,重点说明 为什么要引入模糊预测、特征工程、未来数据预测与可解释性分析。
一、二分类模拟 + 等概率区间
模拟数据
我们首先模拟一个带噪声的二分类数据集。这里故意设置特征间有相关性,并在生成标签时引入误差,从而让模型面临“不确定”的情况。
library(tidymodels)
tidymodels_prefer()
simulate_two_classes <-
function (n, error = 0.1, eqn = quote(-1 - 2 * x - 0.2 * x^2 + 2 * y^2)) {
sigma <- matrix(c(1, 0.7, 0.7, 1), nrow = 2, ncol = 2)
dat <- MASS::mvrnorm(n = n, mu = c(0, 0), Sigma = sigma)
colnames(dat) <- c("x", "y")
cls <- paste0("class_", 1:2)
dat <-
as_tibble(dat) %>%
mutate(
linear_pred = !!eqn,
linear_pred = linear_pred + rnorm(n, sd = error),
prob = binomial()$linkinv(linear_pred),
class = ifelse(prob > runif(n), cls[1], cls[2]),
class = factor(class, levels = cls)
)
dplyr::select(dat, x, y, class)
}
set.seed(1901)
training_set <- simulate_two_classes(200)
testing_set <- simulate_two_classes(50)为什么这样做:
- 模拟数据比真实数据更可控,可以演示“分类困难”场景。
- 加入噪声后,模型预测不会百分百准确,这正是现实问题的本质。
拟合逻辑回归模型
使用 tidymodels 拟合一个带二次项的逻辑回归模型。
two_class_mod <-
logistic_reg() %>%
set_engine("stan", seed = 1902) %>%
fit(class ~ . + I(x^2)+ I(y^2), data = training_set)
print(two_class_mod, digits = 3)优缺点:
- 逻辑回归直观、可解释,但对非线性边界的刻画有限。
- 加入多项式项能提高拟合能力,但也容易过拟合。
常规预测 vs 模糊预测
先做常规预测:
test_pred <- augment(two_class_mod, testing_set)
predict(two_class_mod, testing_set)然后引入 probably::make_two_class_pred(),为预测加上 模糊区间 (Equivocal Zone):
library(probably)
training_set$class=factor(training_set$class,level=c("class_1","class_2"))
lvls <- levels(training_set$class)
test_pred <-
test_pred %>%
mutate(.pred_with_eqz = make_two_class_pred(.pred_class_1, lvls, buffer = 0.15))为什么要模糊预测:
- 常规预测:即使模型置信度很低,也会强行给出一个类别 → 误导风险高。
- 模糊预测:引入“不可报告”区间,当概率落在模糊区时提示“结果不确定” → 更符合真实业务决策。
- 缺点是:有时会牺牲预测覆盖率。
对比结果
test_pred %>% conf_mat(class, .pred_class)
test_pred %>% conf_mat(class, .pred_with_eqz)对比发现:
- 常规预测准确率较低,因为强行分类。
- 模糊预测虽然减少了报告样本,但整体准确率更高。
我们还可以系统评估不同 buffer 大小的影响:
eq_zone_results <- function(buffer) {
test_pred <-
test_pred %>%
mutate(.pred_with_eqz = make_two_class_pred(.pred_class_1, lvls, buffer = buffer))
acc <- test_pred %>% accuracy(class, .pred_with_eqz)
rep_rate <- reportable_rate(test_pred$.pred_with_eqz)
tibble(accuracy = acc$.estimate, reportable = rep_rate, buffer = buffer)
}
map(seq(0, .1, length.out = 40), eq_zone_results) %>%
list_rbind() %>%
pivot_longer(c(-buffer), names_to = "statistic", values_to = "value") %>%
ggplot(aes(x = buffer, y = value, lty = statistic)) +
geom_step(linewidth = 1.2, alpha = 0.8) +
labs(y = NULL, lty = NULL)下面输出每个分类的预测结果
test_pred <-
test_pred %>%
bind_cols(
predict(two_class_mod, testing_set, type = "pred_int", std_error = TRUE)
)
#输出
.pred_class .pred_class_1 .pred_class_2 x y class
<fct> <dbl> <dbl> <dbl> <dbl> <fct>
1 class_2 0.0256 0.974 1.12 -0.176 class_2
2 class_1 0.555 0.445 -0.126 -0.582 class_2
3 class_2 0.00620 0.994 1.92 0.615 class_2
4 class_2 0.472 0.528 -0.400 0.252 class_2
5 class_2 0.163 0.837 1.30 1.09 class_1二、Chicago 数据预测
数据准备
真实数据,目标是预测芝加哥地铁的日均客流。
我们选取 ridership(客流量)、date 和各个车站数据。拆分训练集(去掉最后 14 天)和测试集(最后 14 天)。
data(Chicago)
Chicago <- Chicago %>% select(ridership, date, one_of(stations))
n <- nrow(Chicago)
Chicago_train <- Chicago %>% slice(1:(n - 14))
Chicago_test <- Chicago %>% slice((n - 13):n)特征工程 + PLS 降维
使用 recipe 做:
- 日期特征;
- 节假日变量;
- 虚拟变量转换;
- 去掉常量列;
- 对车站客流标准化 + PLS 降维(减少维度,提高效率)。
base_recipe <-
recipe(ridership ~ ., data = Chicago_train) %>%
step_date(date) %>%
step_holiday(date, keep_original_cols = FALSE) %>%
step_dummy(all_nominal()) %>%
step_zv(all_predictors()) %>%
step_normalize(!!!stations)%>%
step_pls(!!!stations, num_comp = 10, outcome = vars(ridership))优缺点:
- 优点:有效压缩高维输入、缓解多重共线性。
- 缺点:PLS 牺牲了解释性(不能直观解释每个车站的贡献)。
建模与预测
lm_spec <-
linear_reg() %>%
set_engine("lm")
lm_wflow <-
workflow() %>%
add_recipe(base_recipe) %>%
add_model(lm_spec)
set.seed(1902)
lm_fit <- fit(lm_wflow, data = Chicago_train)
res_test <-
predict(lm_fit, Chicago_test) %>%
bind_cols(
predict(lm_fit, Chicago_test, type = "pred_int"),
Chicago_test
)
res_test %>% select(date, ridership, starts_with(".pred"))
res_test %>% rmse(ridership, .pred)- RMSE 较低,说明模型预测合理。
- 提供置信区间,能更好支持风险控制。
三、未来数据预测:Chicago_2020
我们人为创建 Chicago_2020,日期设为 2020 年 6 月,保持结构一致。这样可以模拟模型在未来的预测表现。
Chicago_2020 <- Chicago_test %>%
mutate(date = seq(as.Date("2020-06-01"), by = "day", length.out = 14))
res_2020 <-
predict(lm_fit, Chicago_2020) %>%
bind_cols(
predict(lm_fit, Chicago_2020, type = "pred_int"),
Chicago_2020
)
res_2020 %>% select(date, contains(".pred"))
res_2020 %>% rmse(ridership, .pred)- 优点:在业务中常用于“未来场景模拟”。
- 缺点:若新数据分布与训练集差异过大,预测可能失真。
四、模型可解释性诊断:applicable
最后用 applicable 包检验新数据是否与训练数据分布接近。
通过 PCA 投影后计算 马氏距离,数值越大,表示新样本“离群”,预测越可能不可靠。
library(applicable)
pca_stat <- apd_pca(~ ., data = Chicago_train %>% select(one_of(stations)),
threshold = 0.99)
pca_stat
#x 轴显示了距离的值,y 轴展示了分布的百分位数。例如,训练集样本中有一半的距离小于 3.7。autoplot(pca_stat, distance) + labs(x = "distance")
score(pca_stat, Chicago_test) %>% select(starts_with("distance")) #训练集中的得分
score(pca_stat, Chicago_2020) %>% select(starts_with("distance")) #新的预测中的得分 用于衡量结果的可信性为什么要做:
- 优点:帮助判断模型是否在“熟悉的区域”预测。
- 缺点:如果阈值过严,可能把合理的新数据误判为异常。
常见问题
- 如果未来真的出现了“异常趋势”(超出了训练数据的分布,比如疫情期间工作日客流骤降),我们现在做的特征工程和模型是不是就失效了?
未来的模式可能跟过去不同,那么这些特征仍然被模型用,却解释力大大降低。你可以把“异常趋势”理解为 分布外 (out-of-distribution, OOD) 问题,常见应对方式有:
- 异常点监控
- 在预测时,先用
applicable::apd_pca()这类工具检测输入是否偏离训练分布。 - 一旦发现异常,就不要盲目给出预测,而是提示“预测不可靠”
- 在预测时,先用
- 鲁棒方法
- 使用对异常点更不敏感的模型(如基于树的集成模型、分位数回归),它们对极端值的适应性更强。
- 预测区间 + 不确定性估计
- 不仅给出点预测,还要输出预测区间。
- 如果异常点来了,区间会快速变宽,提示“预测不确定”。