缘由

艺多不压身

碎碎念

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

Ref

8 Feature Engineering with recipes | Tidy Modeling with R

解读

在建回归模型之前,把数据预处理写成recipe(配方)可以把所有 feature engineering、编码、采样等步骤串在一起,保证在训练/验证/预测时使用完全一致的变换。workflows 可以把配方和模型合并成一个对象,一次 fit() 即可完成“训练(估计配方参数)+拟合模型”。

一、从最简单的配方开始:一行代码做预处理

加载数据

library(tidymodels)
library(modeldata)
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)
simple_ames <-
  recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type,
         data = ames_train) %>%
  step_log(Gr_Liv_Area, base = 10) %>%
  step_dummy(all_nominal_predictors())

解释每个 step 的作用(按顺序)

  • recipe(...):定义要处理的变量(response ~ predictors)与数据来源,返回一个“配方对象”
  • step_log(Gr_Liv_Area, base = 10):对 Gr_Liv_Area 做 base-10 对数变换(常用于减小右偏分布的影响)
  • step_dummy(all_nominal_predictors()):把所有名义(类别)预测变量转成哑变量(dummy / indicator variables),用于把因子转成模型能接受的数值列

小贴士:step_dummy() 有个 one_hot 参数:

  • 默认 one_hot = FALSE,对有 C 个水平的因子通常会产生 C-1 列(避免与截距列共线)。
  • 若想要完整的 one-hot(C 列),可以设 one_hot = TRUE,但要注意线性模型中可能出现共线性。

把少见水平合并为 other 能减少哑变量的维度并增加稳健性:

simple_ames <-
  recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type,
         data = ames_train) %>%
  step_log(Gr_Liv_Area, base = 10) %>%
  step_other(Neighborhood, threshold = 0.01) %>%
  step_dummy(all_nominal_predictors())
  • step_other(Neighborhood, threshold = 0.01):把训练集中出现频率低于 1% 的 Neighborhood 水平合并为一个新的 “other” 水平。这样能控制虚拟变量数量,并减少极少数类别对模型参数估计的影响。

二、添加交互(step_interact)

如果你怀疑房屋使用面积与建筑类型有乘法效应(不同建筑类型的面积影响不同),可以显式加入交互:

# 在 dummy 之后对 Bldg_Type 哑变量与 Gr_Liv_Area 的交互
simple_ames <-
  recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type,
         data = ames_train) %>%
  step_log(Gr_Liv_Area, base = 10) %>%
  step_other(Neighborhood, threshold = 0.01) %>%
  step_dummy(all_nominal_predictors()) %>%
  step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") )
  • step_interact(~ A:B):创建新的列为变量 A 与变量 B 的逐元素相乘(interaction)。
  • 注意点:step_interact() 通常在已经把分类变量 step_dummy() 转为哑变量后使用(或至少要确保参与交互的列是数值列),否则会出问题或效果不如预期。

三、非线性建模:自然样条(step_ns)与可视化

当变量与响应之间是非线性关系时,自然样条(natural spline)是一种常见选择。

library(patchwork)
library(splines)
 
plot_smoother <- function(deg_free) {
  ggplot(ames_train, aes(x = Latitude, y = 10^Sale_Price)) +
    geom_point(alpha = .2) +
    scale_y_log10() +
    geom_smooth(
      method = lm,
      formula = y ~ ns(x, df = deg_free),
      color = "lightblue",
      se = FALSE
    ) +
    labs(title = paste(deg_free, "Spline Terms"),
         y = "Sale Price (USD)")
}
 
( plot_smoother(2) + plot_smoother(5) ) / ( plot_smoother(20) + plot_smoother(100) )

在配方中引入 step_ns:

recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type + Latitude,
       data = ames_train) %>%
  step_log(Gr_Liv_Area, base = 10) %>%
  step_other(Neighborhood, threshold = 0.01) %>%
  step_dummy(all_nominal_predictors()) %>%
  step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") ) %>%
  step_ns(Latitude, deg_free = 20)  # step_ns 添加自然样条

解释 step_ns(..., deg_free = k):

  • step_ns() 使用 splines::ns() 构造自然样条基(natural spline basis)。
  • deg_free(degrees of freedom)控制基函数的数量——deg_free 越大,拟合越灵活(更能捕捉局部曲线形状),但也更容易过拟合。
  • 在 geom_smooth(..., formula = y ~ ns(x, df = deg_free)) 中,你用 lm + ns() 在散点图上绘制了不同复杂度下的拟合曲线,便于直观比较。

四、把配方和模型绑在一起(workflow),拟合并预测

lm_model <- linear_reg() %>% set_engine("lm")
 
lm_wflow <-
  workflow() %>%
  add_recipe(simple_ames) %>%
  add_model(lm_model)
 
lm_fit <- fit(lm_wflow, ames_train)
 
# 预测(示例:对测试集前3条预测)
predict(lm_fit, ames_test %>% slice(1:3))

说明:workflow() 的好处是把配方(feature engineering)和模型(parsnip model)合并成一个对象,fit() 时会自动 prep()(估计配方中的参数)然后拟合模型。


五、如何查看“配方”的内部与“处理后的数据”(非常重要)

1) 在准备(prep)之前,查看配方结构:

simple_ames         # 打印配方对象,显示选择器和步骤
tidy(simple_ames)   # 列出每一步(number, operation, type, trained 等)

2) 估计配方并把训练集变换出来(查看训练集上实际的变换结果):

prepped_recipe <- prep(simple_ames, training = ames_train)
processed_data   <- bake(prepped_recipe, new_data = ames_train)  # 或 new_data = NULL
 
# 或者:
juice(prepped_recipe)   # 返回训练集(已 bake)的版本(更短的写法)

常用查看函数:

glimpse(processed_data)
head(processed_data)

六、如何查看模型拟合结果(系数 / 引擎对象)

# 返回 parsnip 对象
lm_fit %>% extract_fit_parsnip() %>% tidy() %>% slice(1:5)
 
# 或者直接拿底层 lm 引擎对象并 tidy
lm_fit %>% extract_fit_engine() %>% broom::tidy()
 
# workflow 的 tidy 也能返回有关的摘要:
lm_fit %>% tidy()

extract_fit_parsnip() 返回的是 parsnip 的拟合对象(便于查看配置信息)。extract_fit_engine() 返回的是底层的 lm 对象(可以用 summary() / broom::tidy() 等一般方法查看系数与残差信息)。

七、关于采样(下采样 / 上采样)

  • 旧版 recipes 曾有 step_downsample(),但现在推荐使用 themis 包中的 step_downsample() / step_upsample() 等(它们专用于处理类别不平衡):
# themis 包里的下采样(示例)
recipe(...) %>% themis::step_downsample(Class)
  • 这些采样步骤通常只应用于训练数据(skip = TRUE),以避免在预测时改变测试集的分布。

八、完整示例 —— 你最后整理的配方(含坐标、交互、样条)

ames_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()) %>%
  step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") ) %>%
  step_ns(Latitude, Longitude, deg_free = 20)
 
lm_model <- linear_reg() %>% set_engine("lm")
 
lm_wflow <-
  workflow() %>%
  add_model(lm_model) %>%
  add_recipe(ames_rec)
 
lm_fit <- fit(lm_wflow, ames_train)

九、警告

* **不要重复变换**:如果已经在原始数据里把 `Sale_Price` 取对数,就不要在 recipe 里再 `step_log(Sale_Price)`(除非你确实想再做一次对数)。
* **交互顺序**:若要对分类变量与数值做交互,先 `step_dummy()` 再 `step_interact()`。交互通常基于已创建的数值列。
* **one-hot vs C-1**:`step_dummy()` 默认会产生 `C-1` 列以避免共线性;若需要完整 one-hot,设置 `one_hot = TRUE`。
* **查看结果**:`prep()` + `bake()`(或 `juice()`)是看“处理后数据”的最直接方法;`tidy()`(配方 / step / 模型)是看步骤细节的工具;`extract_*()` 是从 `workflow` 中拿到中间对象的便捷方式。