dplyr中case_when的data.table替代方案

22

不久前,他们在dplyr中引入了一种很好的类SQL替代方案ifelse,即case_when

是否存在一个等效的方法在data.table中允许您在一个[]语句中指定不同的条件,而无需加载其他包?

示例:

library(dplyr)

df <- data.frame(a = c("a", "b", "a"), b = c("b", "a", "a"))

df <- df %>% mutate(
    new = case_when(
    a == "a" & b == "b" ~ "c",
    a == "b" & b == "a" ~ "d",
    TRUE ~ "e")
    )

  a b new
1 a b   c
2 b a   d
3 a a   e

这绝对会非常有用,并且会使代码更易读(这也是我在这些情况下一直使用 dplyr 的原因之一)。

4个回答

36

供参考,对于那些在2019年之后看到这篇文章的人,版本号高于1.13.0的data.table有可用的fcase函数。请注意,它的语法与dplyr::case_when不同,不能直接替换使用,但可以作为一种“本地”的data.table计算方式。

# Lazy evaluation
x = 1:10
data.table::fcase(
    x < 5L, 1L,
    x >= 5L, 3L,
    x == 5L, stop("provided value is an unexpected one!")
)
# [1] 1 1 1 1 3 3 3 3 3 3

dplyr::case_when(
    x < 5L ~ 1L,
    x >= 5L ~ 3L,
    x == 5L ~ stop("provided value is an unexpected one!")
)
# Error in eval_tidy(pair$rhs, env = default_env) :
#  provided value is an unexpected one!

# Benchmark
x = sample(1:100, 3e7, replace = TRUE) # 114 MB
microbenchmark::microbenchmark(
dplyr::case_when(
  x < 10L ~ 0L,
  x < 20L ~ 10L,
  x < 30L ~ 20L,
  x < 40L ~ 30L,
  x < 50L ~ 40L,
  x < 60L ~ 50L,
  x > 60L ~ 60L
),
data.table::fcase(
  x < 10L, 0L,
  x < 20L, 10L,
  x < 30L, 20L,
  x < 40L, 30L,
  x < 50L, 40L,
  x < 60L, 50L,
  x > 60L, 60L
),
times = 5L,
unit = "s")
# Unit: seconds
#               expr   min    lq  mean   median    uq    max neval
# dplyr::case_when   11.57 11.71 12.22    11.82 12.00  14.02     5
# data.table::fcase   1.49  1.55  1.67     1.71  1.73   1.86     5

源代码,data.table 1.13.0版本的新闻,发布于2020年7月24日。


1
从data.table中如何弹出来自tidy eval的错误消息?你在复制粘贴过程中犯了错误吗? - jangorecki
1
是的,Jan,我想我做到了。我直接从新闻部分复制了它。我已经纠正了。谢谢你发现了这个问题。 - skedaddle_waznook

21

1) 如果条件是互斥的,并且如果所有条件都为假,则使用默认值,那么这将起作用:

library(data.table)
DT <- as.data.table(df) # df is from question

DT[, new := c("e", "c", "d")[1 +
                             1 * (a == "a" & b == "b") + 
                             2 * (a == "b" & b == "a")]
]

给予:

> DT
   a b new
1: a b   c
2: b a   d
3: a a   e

2) 如果条件的结果是数字,则更容易处理。例如,假设我们想要使用默认值3而不是cd,并且希望将它们替换为10和17,则可以按照以下方式进行操作:

library(data.table)
DT <- as.data.table(df) # df is from question

DT[, new := 3 + 
            (10 - 3) * (a == "a" & b == "b") + 
            (17 - 3) * (a == "b" & b == "a")]

3)请注意,加入一行代码就足以实现这个功能。它假设每一行至少有一个TRUE的选项。

when <- function(...) names(match.call()[-1])[apply(cbind(...), 1, which.max)]

# test
DT[, new := when(c = a == 'a' & b == 'b', 
                 d = a == 'b' & b == 'a', 
                 e = TRUE)]

2
用户只需使用软件包,无需查看其中的代码。这是由开发者/维护者来进行维护。 - G. Grothendieck
@G.Grothendieck,你知道是否有一种方法可以在整数/数字变量上使用 when 函数(而不将它们转换为字符)?例如,我们可以在这里创建 1,2,3 而不是 c,d,e 吗? - arg0naut91
1
对于每一行,如果第一个逻辑条件为TRUE,则返回i。 when.num <- function(...) apply(cbind(...), 1, which.max); DT[, new := when.num(a == 'a' & b == 'b', a == 'b' & b == 'a', TRUE)] - G. Grothendieck
谢谢!这个可以用 - 不过它会自动分配数字。我还在寻找手动分配的可能性,但可能不是一个简单的一行代码解决的问题。 - arg0naut91
2
如果您想手动分配,请使用原始的 when 并将结果转换为数字。 when_num <- function(...) as.numeric(when(...)); DT[, new := when("1" = a == 'a' & b == 'b', "2" = a == 'b' & b == 'a', "99" = TRUE)] - G. Grothendieck
显示剩余3条评论

21

这并不是一个真正的答案,但对于评论来说有点太长了。如果认为不合适,我很乐意删除这篇文章。

RStudio社区上有一篇有趣的帖子讨论了在没有通常的tidyverse依赖项的情况下使用dplyr::case_when的选项。

总结起来,似乎存在三种替代方法:

  1. Stefan Fleckcase_whendplyr中分离出来,构建了一个新的包lest,该包仅依赖于base
  2. yonicd开发了noplyr,它“提供基本的dplyrtidyr功能,而不需要依赖于tidyverse库”。
  3. Bob Rudis (hrbrmstr)freebase的创建者,“这是一个类似于'usethis'的基础R伪等效于'tidyverse'代码的包”,也值得一试。

如果你只需要case_when,我想与data.table结合使用lest可能是一个有吸引力且最小化的选项。


更新 [2019年10月29日]

Tyson Barrett 最近在 GitHub 上发布了软件包 tidyfast (目前版本为 0.1.0),它提供了函数 "dt_case_when,使用 data.table::fifelse() 的速度实现了 dplyr::case_when() 语法。"。

更新 [2020年2月25日]

此外还有 dtplyr,由 Lionel Henry 撰写并由 Hadley Wickham 维护,它 "dplyr 提供了一个 data.table 后端。 dtplyr 的目标是允许您编写自动转换为等效但通常更快的 data.table 代码的 dplyr 代码。"。


2
非常感谢,这很有帮助。我还没有听说过 lest,这确实是我要开始测试的。我绝不认为这个评论不合适,我会让它保留一段时间而不接受,看看其他 data.table 用户可能提供的解决方案。 - arg0naut91
在这里加上 tidyfst 可能是值得的 https://hope-data-science.github.io/tidyfst/index.html - user63230

3
这是@g-grothendieck答案的一个变化,适用于非互斥条件:
DT[, new := c("c", "d", "e")[
  apply(cbind(
    a == "a" & b == "b", 
    a == "b" & b == "a",
    TRUE), 1, which.max)]
  ]

DT
#    a b new
# 1: a b   c
# 2: b a   d
# 3: a a   e

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