缘由
艺多不压身
碎碎念
我们继续飞行在云层和雾气之间,飞行在一个无光无影的空洞世界中。 ——《风沙星辰》
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` 中拿到中间对象的便捷方式。