library(tidymodels)
library(extrasteps)
tidymodels_prefer()
data(hotel_rates, package = "modeldata")
# Make the initial split
hotel_rates <- hotel_rates |> arrange(arrival_date)
hotel_rate_split <- initial_time_split(hotel_rates, prop = c(0.75))
hotel_rate_train <- training(hotel_rate_split)
hotel_rate_test <- testing(hotel_rate_split)31 分类变量处理
在前面我们已经提过,分类变量(categorical predictor)是指那些取值为离散类别的变量,例如性别、职业、地区等。处理分类变量是机器学习中的一个重要步骤,因为大多数机器学习算法只能处理数值型数据。因此,我们需要将分类变量转换为数值形式,以便模型能够理解和使用它们。
此外,在某些情况下,对分类变量进行适当的简化和转换可以提高模型的性能和解释性。例如,某些分类变量可能具有大量的类别或出现频率较低时,这可能导致模型过拟合或计算效率低下。通过合并一些类别或者使用嵌入(embeddings)等技术,我们可以有效地处理这些问题。
哑变量:哑变量编码用于将分类变量转换为数值形式,以便纳入回归等模型。如果一个分类变量有 \(k\) 个类别,哑变量编码通常只生成 \(k−1\) 列,将其中一类作为基准组(reference group)。这样做可以避免完全多重共线性(dummy variable trap)1,并且模型系数的解释比较直观 — 每个系数表示该类别相对于基准组的效应差异。
独热编码:独热编码也为分类变量的每个值创建
0/1,但会为所有 \(k\) 个类别都生成对应列。它不设定基准组,因此在线性模型中使用时需要特别注意共线性问题(通常要去掉截距项或去掉一列)。独热编码在树模型、神经网络、KNN 等不依赖线性系数解释的算法中更常见,也常用于深度学习 pipeline 的输入层。独热编码常用于非线性模型,尤其在神经网络和基于树的模型中。目标编码:目标编码(target encoding)是根据分类变量的每个类别与目标变量之间的关系来进行编码的一种方法。它通过计算每个类别的平均目标值(或其他统计量)来为每个类别分配一个数值。这种方法可以捕捉类别与目标变量之间的关系,但需要注意过拟合风险,通常需要结合交叉验证或正则化技术来使用。
针对分类变量的编码技术,本质上都是查找表(lookup table)方法:将每个类别映射到一个数值。不同的编码方法使用不同的映射规则,适用于不同类型的模型和数据特征。
tidymodels中使用step_dummy()可以将分类变量转换为哑变量或独热编码,使用step_other()可以将出现频率较低的类别合并为一个“其他”类别,使用step_unknown()和step_novel()可以处理测试数据中出现的训练数据未见过的类别。对于目标编码,可以使用step_target_encode()函数来实现。
# define a simple df
customer_types <-
hotel_rate_train |>
distinct(customer_type) |>
arrange(customer_type)
# define recipe
ind_rec <- recipe(~customer_type, data = customer_types) |>
step_dummy(all_factor_predictors()) |>
prep()
ind_rec
bake(ind_rec, new_data = customer_types, starts_with("customer_type"))# A tibble: 4 × 3
customer_type_group customer_type_transient customer_type_transient_party
<dbl> <dbl> <dbl>
1 0 0 0
2 1 0 0
3 0 1 0
4 0 0 1
31.1 新类别
我们在处理训练数据中的分类变量时,可能会遇到测试数据中出现了训练数据中未见过的类别(novel categories)。这会导致模型无法正确处理这些新类别,从而影响预测性能。为了应对这种情况,我们可以使用 tidymodels 中的 step_unknown()和step_novel()函数来为这些新类别创建一个特殊的“未知”类别。
recipe(~customer_type, data = hotel_rate_train) |>
step_novel(customer_type) |> # 将测试数据中未见过的类别标记为 "new" 类别
prep() |>
bake(new_data = NULL) |>
pluck("customer_type") |> # 查看处理后的 customer_type 列
levels()[1] "contract" "group" "transient" "transient_party"
[5] "new"
31.2 高基数分类变量
分类变量中类别数量非常多的时,与只有几个类别的变量不同,这类变量若直接用传统的编码方式会产生许多问题。
例如,假设我们有一个包含 1000 个不同类别的变量,如果我们使用独热编码,那么就会生成 1000 个新的二元特征,这不仅会导致模型过拟合,还会增加计算资源的消耗。
面对这种情况,我们介绍以下几种处理高基数分类变量的方法:
31.2.1 Other分类
- 当一个分类变量中存在许多类别时,某些类别可能出现频率较低,这些低频类别可能对模型的预测性能产生负面影响。为了处理这种情况,我们可以使用
step_other()函数将这些低频类别合并为一个新的“其他”类别。 - 对配方使用
tidy()查看哪些分类变量被合并了。
[1] 174
[1] 101
31.2.2 特征哈希
特征哈希是一种将高基数分类变量转换为固定长度数值特征向量的方法。它通过哈希函数将类别名称映射到一个指定大小的特征空间中的索引位置。
特征哈希的优点是它可以处理大量类别,并且不需要事先知道所有可能的类别。然而,它也有一些缺点,例如可能会发生哈希冲突(不同类别被映射到同一索引位置),以及生成的特征向量缺乏可解释性。值得注意的是,哈希转换是不可逆的,我们通常很难根据哈希后的索引还原原始类别。
-
特征哈希经常应用于以下场景:
- 高基数分类变量(万级以上类别,如 IP 地址、用户 ID)。
- 大规模在线学习系统(Vowpal Wabbit 等)。
- 文本特征提取(Bag-of-Words 的哈希版本)。
- 需要流式处理或分布式训练的场景。
tidymodels中使用扩展包textrecipes中的
step_dummy_hash()函数来实现特征哈希。
library(textrecipes)
recipe(avg_price_per_room ~ agent, data = hotel_rate_train) |>
step_dummy_hash(agent, num_terms = 4) |> # 将 agent 变量哈希为 4 个特征
prep() |>
bake(new_data = NULL, contains("agent"))# A tibble: 11,551 × 4
dummyhash_agent_1 dummyhash_agent_2 dummyhash_agent_3 dummyhash_agent_4
<int> <int> <int> <int>
1 0 1 0 0
2 0 0 -1 0
3 0 1 0 0
4 0 1 0 0
5 0 1 0 0
6 0 0 0 1
7 0 0 0 1
8 0 1 0 0
9 0 1 0 0
10 0 0 0 1
# ℹ 11,541 more rows
31.2.3 效应编码
效应编码(effect coding,也叫 sum-to-zero coding)是一种分类变量编码方式。它和 R 默认的处理对比编码(treatment coding,也就是常说的虚拟编码)不同。
对于有 \(k\) 个水平的分类变量,效应编码只会产生 \(k-1\) 列。其核心思想是:
- 截距表示总体均值(grand mean)
- 系数表示该水平相对总体均值的偏离
- 所有水平的系数加总为 0
处理对比 contr.treatment
|
效应编码 contr.sum
|
|
|---|---|---|
| 比较基准 | 与参照类别比较 | 与总均值(grand mean)比较 |
| 系数含义 | 该类别比参照类别高/低多少 | 该类别比总体均值高/低多少 |
| 编码值 | 0 和 1 | 1、0 和 -1 |
| 列的个数 | \(k-1\) 列 | \(k-1\) 列(最后一类由前面各类决定) |
- 有明确对照组(如对照组 vs. 实验组)→ 用处理对比编码(默认)
- 无自然参照 2、做方差分析、关注主效应 3(尤其是 ANOVA 风格分析时)→ 用效应编码
- 序数变量(如低/中/高)→ 用多项式对比编码
下面用一个简单例子看差别。contr.sum 会让各水平的编码和为 0:
trt <- factor(c("A", "B", "C"))
contrasts(trt) <- contr.sum(3)
model.matrix(~trt) (Intercept) trt1 trt2
1 1 1 0
2 1 0 1
3 1 -1 -1
attr(,"assign")
[1] 0 1 1
attr(,"contrasts")
attr(,"contrasts")$trt
[,1] [,2]
A 1 0
B 0 1
C -1 -1
如果你想看效应编码对应的系数解释,可以把因子设置成 contr.sum 后再拟合线性模型。此时截距表示总体均值,分类变量的系数表示相对总体均值的偏离。
31.2.4 监督式类别合并
TODO: https://aml4td.org/chapters/categorical-predictors.html#sec-combining-categories
- 使用embed包的
step_collapse_cart()函数可以根据目标变量的分布来合并类别。它使用 CART(Classification and Regression Trees)算法来确定哪些类别可以合并,从而减少类别数量,同时保留对目标变量的预测能力。 - 以下代码示例展示了如何使用
step_collapse_cart()函数来合并agent变量的类别。我们将使用avg_price_per_room作为目标变量来指导类别的合并。有两个参数可以进行调优:-
cost_complexity:成本复杂度( AKA $C_p $ ),该值通常介于 0 和 1 之间,较小的值会生成更复杂的树,而较大的值会生成更简单的树。 -
min_n:允许继续分裂的分组最小行数,值越大,树越简单,类别合并得越多。
-
library(embed)
collapse_agent_rec <- recipe(
avg_price_per_room ~ agent,
data = hotel_rate_train
) |>
# 使用 CART 算法合并类别
# 用 rpart 树把 agent 的类别合并成更少的节点,减少后续建模时的特征数量。
step_collapse_cart(
agent,
outcome = vars(avg_price_per_room), # 指定使用哪个变量指导类别的合并。
id = "collapse"
) |>
prep()
collapse_agent_rec
tidy(collapse_agent_rec, id = "collapse") |>
count(new)# A tibble: 12 × 2
new n
<chr> <int>
1 agent_01 17
2 agent_02 14
3 agent_03 14
4 agent_04 10
5 agent_05 7
6 agent_06 13
7 agent_07 15
8 agent_08 5
9 agent_09 13
10 agent_10 6
11 agent_11 5
12 agent_12 3
31.3 其他类型编码
更多的编码方法可以参考recipe的文档。
指在回归模型中,两个或多个自变量之间存在高度线性相关关系。当这种关系涉及两个变量时称为共线性,涉及多个变量时称为多重共线性(multicollinearity)。↩︎
指在某些分类变量中,可能没有一个类别可以被自然地选为基准组。例如,在一个包含多个不同类型的客户的变量中,没有一个特定的客户类型可以被视为“标准”或“对照”类别。在这种情况下,使用效应编码可以更好地捕捉每个类别相对于总体均值的效应,而不需要选择一个特定的基准类别。↩︎
在统计分析中,主效应是指一个自变量对因变量的直接影响,而不考虑其他自变量的影响。当我们关注每个类别相对于总体均值的效应时,我们更关心的是主效应,而不是与某个特定类别的对比。在这种情况下,使用效应编码可以更好地捕捉每个类别的主效应。↩︎