根据类/类型选择列,兼容dplyr::select。

4

实际问题

如何定义一个选择器助手,根据它们的类/类型选择列,并且与 dplyr 的架构兼容?

尽职调查

我已经查看了https://cran.r-project.org/web/packages/dplyr/vignettes/introduction.html以及dplyr::select_helpers的帮助文档,但是没有找到任何允许我基于类/类型进行选择的内容。

示例

根据类/类型引入一些变化:

dat <- mtcars
dat <- dat %>% mutate(
  mpg = as.character(mpg),
  wt = as.factor(wt),
  vs = as.character(vs)
)

简而言之,我希望将这个方法推广到R中所有可能的类/类型(以及它们的组合):
dat[ , sapply(dat, is.character)]
# mpg    wt vs
# 1    21  2.62  0
# 2    21 2.875  0
# 3  22.8  2.32  1
# 4  21.4 3.215  1

根据Subset variables in data frame based on column type,我可以这样做:

select_on_class <- function(.data, cls = "numeric") {
  dat[ , names(.data)[sapply(.data,
    function(vec, clss) class(vec) %in% clss, clss = cls)]]
}
dat %>% select_on_class(c("character", "factor"))
# mpg    wt vs
# 1    21  2.62  0
# 2    21 2.875  0
# 3  22.8  2.32  1
# 4  21.4 3.215  1

我希望能在调用dplyr::select函数时使用这个变量,所以我尝试了以下方法:

has_class <- function(.data, cls = "numeric") {
  nms <- names(.data)[sapply(.data,
    function(vec, clss) class(vec) %in% clss, clss = cls)]
  sapply(nms, as.name)
}
dat %>% has_class(c("character", "factor"))
# $mpg
# mpg
# 
# $wt
# wt
# 
# $vs
# vs

问题在于sapply(nms, as.name)返回一个list,而这与select的内部机制不兼容(顺便说一句,我还没有完全理解它的内部机制):
dat %>% select(has_class(c("character", "factor")))
# Error: All select() inputs must resolve to integer column positions.
# The following do not:
#   *  has_class("character")

dat %>% select_(has_class(c("character", "factor")))
# Error in UseMethod("as.lazy") : 
#   no applicable method for 'as.lazy' applied to an object of class "list"

编辑

根据使用 select_if 的答案,我试图进行泛化,但遇到了困难:

has_class <- function(.data, cls) {
  sapply(.data, function(vec, clss) class(vec) %in% clss, clss = cls)
}
dat %>% has_class(c("character", "factor"))
# mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
# TRUE FALSE FALSE FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE FALSE
dat %>% select_if(has_class, c("character", "factor"))
# Error in vapply(tbl, p, logical(1), ...) : values must be length 1,
# but FUN(X[[1]]) result is length 32

AFAIU,.predicate函数只需要返回一个逻辑向量(就像has_class一样),我可以通过...传递额外的参数给.predicate函数(我已经这样做了)。那我还错在哪里呢?

2
dat %>% select(which(sapply(., is.character))) 可能是另一种选项。 - talat
@docendodiscimus:不错!我还没有想到这个选项。虽然不是完全符合dplyr选择助手的方式,但至少是一个不错的解决方法。感谢分享! - Rappster
4个回答

13

我认为dplyr::select_if()可能是你正在寻找的。例如

dat <- mtcars %>% 
       mutate(mpg = as.character(mpg),
              wt = as.character(wt),
              vs = as.character(vs)
       ) %>% 
       select_if(is.character)

酷,还不知道 select_if!谢谢! - Rappster
仍然不完全满意呢;-) 当我试图将 is.character 推广到自定义的 .predicate 函数时,你能告诉我我做错了什么吗(请参见编辑,并注意我已将 wt 的类更改为 factor)? - Rappster
1
@Rappster 请考虑如何使用 is.characterselect_if - 数据集中的每一列都会被 select_if 分别传递给 is.character。因此,类比地说,has_class 必须是一个接受列(向量)而不是数据集的函数。所以你需要让 has_class 也适用于单个列。因此,请使用 has_class <- function(vec, clss){ class(vec) %in% clss} - wjchulme

3
如果我们的自定义函数可以返回字符向量而不是列表,那么我们可以使用one_of
has_class_v1 <- function(.data, cls = "numeric") {
  names(.data)[sapply(.data,
                      function(vec, clss) class(vec) %in% clss, clss = cls)]
  }

has_class_v1(dat, "character")
# [1] "mpg" "wt"  "vs" 

# use one_of
dat %>%
  select(one_of(has_class_v1(.,"character"))) %>% 
  head
#    mpg    wt vs
# 1   21  2.62  0
# 2   21 2.875  0
# 3 22.8  2.32  1
# 4 21.4 3.215  1
# 5 18.7  3.44  0
# 6 18.1  3.46  1

以前从未使用过 one_of,这是一个不错的解决方法!谢谢! - Rappster

2

dplyr包在2018年之后添加了where()函数。这是where()文档链接

dat |> 
    select(where(is.factor) | where(is.character))

2
我认为实现这一点最简化和通用的方法是利用dplyr,使用dplyr::select_if更加直接,而不是像@wjchulme建议的那样(虽然是个不错的技巧):
dat %>%
select_if(sapply(., class) %in% c("numeric", "character"))

如果需要更多的类,请按照相同的格式添加。希望这能够帮助你。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接