31  分类变量处理

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)

在前面我们已经提过,分类变量(categorical predictor)是指那些取值为离散类别的变量,例如性别、职业、地区等。处理分类变量是机器学习中的一个重要步骤,因为大多数机器学习算法只能处理数值型数据。因此,我们需要将分类变量转换为数值形式,以便模型能够理解和使用它们。

此外,在某些情况下,对分类变量进行适当的简化和转换可以提高模型的性能和解释性。例如,某些分类变量可能具有大量的类别或出现频率较低时,这可能导致模型过拟合或计算效率低下。通过合并一些类别或者使用嵌入(embeddings)等技术,我们可以有效地处理这些问题。

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()查看哪些分类变量被合并了。
length(levels(hotel_rate_train$agent)) # agent中的类别数量
[1] 174
recipe(avg_price_per_room ~ agent, data = hotel_rate_train) |>
  step_other(agent, threshold = 0.0001, id = "other") |> # 将出现频率低于1%的类别合并为 "other" 类别
  prep() |>
  bake(new_data = NULL) |>
  pluck("agent") |> # 查看处理后的 agent 列
  levels() |>
  length() # 处理后类别数量
[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的文档。


  1. 指在回归模型中,两个或多个自变量之间存在高度线性相关关系。当这种关系涉及两个变量时称为共线性,涉及多个变量时称为多重共线性(multicollinearity)。↩︎

  2. 指在某些分类变量中,可能没有一个类别可以被自然地选为基准组。例如,在一个包含多个不同类型的客户的变量中,没有一个特定的客户类型可以被视为“标准”或“对照”类别。在这种情况下,使用效应编码可以更好地捕捉每个类别相对于总体均值的效应,而不需要选择一个特定的基准类别。↩︎

  3. 在统计分析中,主效应是指一个自变量对因变量的直接影响,而不考虑其他自变量的影响。当我们关注每个类别相对于总体均值的效应时,我们更关心的是主效应,而不是与某个特定类别的对比。在这种情况下,使用效应编码可以更好地捕捉每个类别的主效应。↩︎