缘由

艺多不压身

碎碎念

我们继续飞行在云层和雾气之间,飞行在一个无光无影的空洞世界中。 ——《风沙星辰》

Ref

5 Spending our Data | Tidy Modeling with R
6 Fitting Models with parsnip | Tidy Modeling with R

解读

在机器学习与统计建模中,两个核心步骤不可或缺:

  1. 数据集划分 —— 确保模型训练与评估科学合理。
  2. 建模方法选择 —— 如何高效、统一地调用不同算法。

本文将以房价数据集 Ames Housing 为例,展示如何划分数据集,并比较 传统建模写法 与 parsnip 包 的差异,最后还会带你看看如何在预测结果中加入 不确定性区间。


一、数据集的划分

1. 为什么要划分数据集?

  • 训练集 (training set):用于拟合模型。
  • 验证集 (validation set):用于调参和选择模型。
  • 测试集 (test set):最终检验模型在新数据上的表现。

这样做的目的,是保证模型不仅能“记住”训练数据,还能在新数据上保持良好表现。


2. 分层抽样

如果数据分布不均,单纯随机划分可能导致训练集和测试集分布不一致。
例如房价数据,高价房远少于普通住宅。
这时可以用 分层抽样,确保不同价格区间在各个子集里都有代表性:

set.seed(502)
ames_split <- initial_split(ames, prop = 0.80, strata = Sale_Price)
ames_train <- training(ames_split)
ames_test  <- testing(ames_split)

这里的 strata = Sale_Price 就是告诉函数“按价格分层抽样”。


3. 三份数据集(训练 / 验证 / 测试)

有时我们还需要额外的验证集,可以直接拆成三份:

set.seed(52)
ames_val_split <- initial_validation_split(ames, prop = c(0.6, 0.2))
ames_train <- training(ames_val_split)
ames_val   <- validation(ames_val_split)
ames_test  <- testing(ames_val_split)

比例就是 6:2:2。


4. 独立单元采样

除了常见的分层抽样,还可能按 独立单元 划分,例如:

  • 树的上层 / 下层
  • 不同地理区域
  • 不同时间段

这样能避免同一结构的数据同时出现在训练集和测试集中,保证独立性。


二、parsnip:统一建模语法

1. 传统建模写法

在 R 语言里,不同算法各自有不同接口:

# 线性回归 (lm)
lm_fit <- lm(Sale_Price ~ Longitude + Latitude, data = ames_train)
 
# LASSO 回归 (glmnet)
library(glmnet)
x <- as.matrix(ames_train %>% select(Longitude, Latitude))
y <- ames_train$Sale_Price
glmnet_fit <- glmnet(x, y, alpha = 1)
 
# 随机森林 (ranger)
library(ranger)
rf_fit <- ranger(
  Sale_Price ~ Longitude + Latitude,
  data = ames_train,
  num.trees = 1000,
  min.node.size = 5
)

可以看到:

  • lm() 用公式接口。
  • glmnet() 需要矩阵输入。
  • ranger() 又有自己的参数体系。

每个函数写法都不同,不利于学习和模型切换。


2. parsnip 的写法

parsnip 通过统一语法解决了这个问题:

# 线性回归 (lm 引擎)
lm_model <- linear_reg() %>% set_engine("lm")
lm_fit <- lm_model %>%
  fit(Sale_Price ~ Longitude + Latitude, data = ames_train)
 
# LASSO 回归 (glmnet 引擎)
glmnet_model <- linear_reg(penalty = 0.1, mixture = 1) %>% 
  set_engine("glmnet")
glmnet_fit <- glmnet_model %>%
  fit(Sale_Price ~ Longitude + Latitude, data = ames_train)
 
# 随机森林 (ranger 引擎)
rf_model <- rand_forest(trees = 1000, min_n = 5) %>%
  set_engine("ranger", verbose = TRUE) %>%
  set_mode("regression")
rf_fit <- rf_model %>%
  fit(Sale_Price ~ Longitude + Latitude, data = ames_train)

可以看到,不论是线性回归、LASSO 还是随机森林,整体写法几乎一致。
差别只是“指定的模型函数”和“选择的引擎”,其他流程保持统一。

三、模型预测与区间估计

建好模型后,我们不仅关心预测值,还希望得到 预测区间,直观理解模型不确定性。

示例:对测试集前 5 个样本进行预测:

ames_test_small <- ames_test %>% slice(1:5)
 
ames_test_small %>%
  select(Sale_Price) %>%
  bind_cols(predict(lm_fit, ames_test_small)) %>%
  # 添加 95% 预测区间
  bind_cols(predict(lm_fit, ames_test_small, type = "pred_int"))

结果包含:

  • 真实值 (Sale_Price)
  • 预测值
  • 预测区间上下界