缘由
艺多不压身
碎碎念
我们继续飞行在云层和雾气之间,飞行在一个无光无影的空洞世界中。 ——《风沙星辰》
Ref
17 Encoding Categorical Data | Tidy Modeling with R
解读
在房价预测中,如何把分类变量(比如 Neighborhood 小区名)转成模型能用的数值特征,是建模的关键。这里展示三种思路:
- 目标编码(step_lencode_glm)
- 部分池化的混合效应编码(step_lencode_mixed)
- 哈希编码(step_dummy_hash)
1. 数据准备
library(embed)
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)2. GLM 目标编码
ames_glm <-
recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built +
Bldg_Type + Latitude + Longitude, data = ames_train) %>%
step_log(Gr_Liv_Area, base = 10) %>%
step_lencode_glm(Neighborhood, outcome = vars(Sale_Price)) %>%
step_dummy(all_nominal_predictors()) %>%
step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") ) %>%
step_ns(Latitude, Longitude, deg_free = 20)- 核心:
step_lencode_glm用小模型来估计 Neighborhood 对 Sale_Price 的贡献,把类别变成连续数值。 - 好处:信息量大,直接和目标变量相关。
- 风险:过拟合(尤其是样本量少的小区)。
3. 部分池化编码(Mixed Encoding)
ames_mixed <-
recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built +
Bldg_Type + Latitude + Longitude, data = ames_train) %>%
step_log(Gr_Liv_Area, base = 10) %>%
step_lencode_mixed(Neighborhood, outcome = vars(Sale_Price)) %>%
step_dummy(all_nominal_predictors()) %>%
step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") ) %>%
step_ns(Latitude, Longitude, deg_free = 20)- 核心:
step_lencode_mixed用混合效应模型,把 Neighborhood 当作随机效应。 - 效果:不同小区之间会部分共享信息,样本量少的小区不容易过拟合。
我们可以比较两种编码方式的差异:
glm_estimates <- ames_glm %>% prep() %>% tidy(number=2)
mixed_estimates <- ames_mixed %>% prep() %>% tidy(number=2)
glm_estimates %>%
rename(`no pooling` = value) %>%
left_join(
mixed_estimates %>%
rename(`partial pooling` = value), by = "level"
) %>%
left_join(
ames_train %>%
count(Neighborhood) %>%
mutate(level = as.character(Neighborhood))
) %>%
ggplot(aes(`no pooling`, `partial pooling`, size = sqrt(n))) +
geom_abline(color = "gray50", lty = 2) +
geom_point(alpha = 0.7) +
coord_fixed()4. 哈希编码
当类别数量非常多时(比如上千个小区名),虚拟变量会让特征空间爆炸。哈希编码就是解决方案。
4.1 R 内置哈希函数示例
library(rlang)
ames_hashed <-
ames_train %>%
mutate(Hash = map_chr(Neighborhood, hash))
ames_hashed %>%
select(Neighborhood, Hash)输出是一个长字符串哈希值,我们可以手动缩短为数值:
ames_hashed %>%
## 转为 R 能处理的整数
mutate(Hash = strtoi(substr(Hash, 26, 32), base = 16L),
## 取模,压缩到 16 个桶
Hash = Hash %% 16) %>%
select(Neighborhood, Hash)这样每个小区就被随机分配到 0~15 的一个桶里。
4.2 在 Recipe 里用哈希
library(textrecipes)
ames_hash <-
recipe(Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built +
Bldg_Type + Latitude + Longitude, data = ames_train) %>%
step_log(Gr_Liv_Area, base = 10) %>%
step_dummy_hash(Neighborhood, signed = FALSE, num_terms = 16L) %>%
step_dummy(all_nominal_predictors()) %>%
step_interact( ~ Gr_Liv_Area:starts_with("Bldg_Type_") ) %>%
step_ns(Latitude, Longitude, deg_free = 20)num_terms = 16L表示压缩到 16 个特征。signed = FALSE表示不会用正负号抵消冲突,简单地把类别“落桶”。
-
为什么要用哈希?
- 类别太多时,dummy 变量会导致特征空间过大(比如一万维)。
- 哈希能强制压缩到一个固定维度(比如 16 维),节省内存和计算量。
-
哈希冲突是什么?
- 不同类别可能被分到同一个桶 → 信息混淆。
- 这就像宿舍分配时两个人被分到同一间 → 他们的“特征”就重叠了。
-
冲突影响大怎么办?
- 增大桶数(num_terms,比如从 16 提高到 128)。
- 或者引入 signed hashing(正负号抵消一部分冲突)。
-
适用场景
- 类别非常多(上千或更多)。
- 模型本身能容忍一定噪声(比如树模型、神经网络)。
- 如果类别少(几十个),还是直接用目标编码或 dummy 更合适。
常见问题解答
1. 为什么 step_lencode_glm 可以把类别变成数值?只有有序因子才能这样吗?
- 因为它不是简单的“类别编号”,而是用目标变量(Sale_Price)训练得到的一个预测值。
- 所以即使类别无序(比如小区名),也能合理地映射到数值。
- 注意:这种方法用了目标变量信息,所以要小心过拟合,最好配合交叉验证或部分池化。
2. 为什么需要哈希处理?什么时候该用?
- 当类别数量非常多(上千甚至上万),虚拟变量维度会太大,训练速度慢、内存占用高。
- 哈希能强制压缩到固定长度,比如 16、32 或 128 个特征。
- 缺点是可能“撞车”,尤其当类别差异很大但落在同一桶时,模型可能难以区分。
- 如果类别数不多(几十个),没必要用哈希。
3. step_lencode_glm 和哈希是并列关系还是从属关系?
-
它们是并列的编码策略:
- lencode 系列:用目标变量信息 → 更精细,但有过拟合风险。
- 哈希:不看目标变量 → 更粗糙,但能应对超大类别数。
-
具体选哪一个,看数据特点:
- 类别少 → lencode 更合适。
- 类别多 → 哈希更实用。