Data.table: 数据表的 fcase / case_when 函数

创建于 2019-09-05  ·  29评论  ·  资料来源: Rdatatable/data.table

相关/跟进#3657

case_when是 SQL 中的常用工具,用于例如根据标志条件构建标签,例如切割年龄组:

  case when age < 18 then '0-18'
       when age < 35 then '18-35'
       when age < 65 then '35-65'
       else '65+'
  end as age_label

我们在dplyr同志已将其实现为case_when ,其接口类似于

dplyr::case_when(
  age < 18 ~ '0-18',
  age < 35 ~ '18-35',
  age < 65 ~ '35-65',
  TRUE ~ '65+'
)

使用和插入公式对于 R 来说似乎很自然——我唯一能想到的就是on语法( case_when('age<18' = '0-18', ...) )。

至于后端, dplyr正在 R 级别执行两遍for循环,例如需要为 _all_ 行评估age < 65 (而这对于任何具有标签'0-18''18-35' )。

我想我们可以用 C 实现做得更好。 将适当的惰性实现与并行版本进行对比会很有趣(因为 IINM 并行执行将需要首先评估所有“LHS”值,例如 Jan 最近在frollapply遇到的)。

请注意,通常我使用查找连接来做case_when事情,但是将case_when为连接_in general_ 并不简单——尽管我认为这两者是同构的,支持找出相应的连接应该是什么,我认为这是一个难题。 例如,在此处的示例中,真值的顺序很重要,因为后面的条件中隐含的每个条件x都是(x & !any(y)) ,前面是条件y 。 这种情况很容易被转换为cut(age) ,也许只使用roll ,但是当真值条件中涉及多个变量时,事情可能会复杂得多。 所以我认为这条路线不会有成果。

feature request

最有用的评论

我们不着急。 我认为懒惰应该是至关重要的,否则与使用查找表相比,它不会带来任何新的东西(API 除外)。 不要试图在第一次迭代时解决所有问题。 当你觉得准备好提交 PR 以获得反馈。 最终状态可以进行多次迭代或后续 PR。

所有29条评论

我实际上更喜欢像fcase(test1, value1, test2, value2, ..., default)这样的 Synatx。 当testsvalues非常复杂时,它更容易阅读。

从中编码肯定也更容易。

一种前进的方法是使用 S3 并构建一个(test1, value1, ....)调用来为fcase.formula分派(我个人觉得这个公式可以更自然地阅读)

是的,它更容易阅读,尤其是当 test & value 很短且内联时。 但我不确定使用公式样式是否会产生“显着”的开销,因为它需要额外的修补。 如果没有,我对这两种风格都很好......

问题,这如何通过引用改进(或不同于)子分配? 使用以下内容时,您可以执行与 case 等效的操作:

DT <- data.table(age = 0:100)
DT[, age_label := "65+"]
DT[age < 65, age_label := "35-65"]
DT[age < 35, age_label := "18-35"]
DT[age < 18, age_label := "0-18"]

默认情况下,我们已经获得了自动索引。

休见最后一段。 我写了一个更完整的解释,但当我按下评论时它消失了😢

你的例子是一个特例。 fcase应该更通用,并且不限于在数据表对象中使用。

我认为如果我们不必评估所有“LHS”值会更有效,但我需要考虑如何解决该问题。 你怎么看这样的事情?
fcase(variable, default, test1, value1,...,testN, valeN)

@2005m我不太了解函数签名的直觉?

我在fcase分支上进行了探索性工作,R API 是:

fcase = function(..., default=NA) .External(Cfcase, ..., default)

我的想法是首先实施@shrektan的建议,因为它会非常简单(只需要弄清楚如何在 C 中处理DOTSXP 😅;注意.External必须接受... )。

然后稍后构建逻辑以解释formula可以映射到更简单的实现。

我认为我们可以将一个列表对象传递给 C。例如,

fcase = function(..., default = NA) {
  .Call(Cfcase, list(...), default = NA)
}
SEXP fcase(SEXP x, SEXP default) {
  ...
}

此外, ?.External表示接受的...最多为 65:

...要传递给编译代码的参数。 .Call 最多 65 个。

fcase(变量,默认值,test1,value1,...,testN,valeN)

通过这种结构,可以在 R 级别测试 missing(..1) 然后
递归地评估 fcase。

2019 年 9 月 13 日星期五下午 12:31,Xianying Tan通知@ github.com
写道:

我认为我们可以将一个列表对象传递给 C。例如,

fcase = 函数(...,默认值 = NA){
.Call(Cfcase, list(...), default = NA)
}

SEXP fcase(SEXP x, SEXP 默认) {
...
}


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/Rdatatable/data.table/issues/3823?email_source=notifications&email_token=AB54MDCWWKJSFOVMDDQWBADQJL3PDA5CNFSM4IT6NOD2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXWZHJK5000000000000000000000000000000000000000000000000000000000001
或静音线程
https://github.com/notifications/unsubscribe-auth/AB54MDA2AHRQQRGQFC2FW73QJL3PDANCNFSM4IT6NODQ
.

@MichaelChirico ,我
fcase(age, '65+', '<18', '0-18', '<35', '18-35')

因为它只取决于一个变量age ,也许它适合fswitch()作为fcase()的包装器?

对 R 级别的检查越少越好。 我也更喜欢.Call而不是.External

我们应该首先就 API 达成一致。 正如@shrektan所说fswitch可以是fcase的包装器,类似地fifelse ,如果我们都同意,我们就可以专注于fcase API。
一旦建立了 API,那么也许是 R 原型? 这将用于 1.13.0,因此有足够的时间。

截至目前,我们有

  1. 标准评估界面
fcase(...)
fcase(..., default)
fcase(when1, value1, ..., default)
fcase(age < 18, '0-18', age < 35, '18-35', age < 65, '35-65', '65+')
  1. NSE公式界面
fcase(...)
fcase(..., default)
fcase(x, ..., default)
fcase(age < 18 ~ '0-18', age < 35 ~ '18-35', age < 65 ~ '35-65', TRUE ~ '65+')
  1. “矢量化”
fcase(when, value, default)
fcase(list(age < 18, age < 35, age < 65),
      list('0-18', '18-35', '35-65'),
      '65+')

还有其他建议吗?

该帖子的作者似乎要求提供查找表。 我们通过在加入时更新来解决的问题。 如上述评论中所述,具有某种不同目标的情况。

问题: value1可以与when1长度相同还是总是等于 1?
另外我认为我们需要在编码任何东西之前决定你想要上面的哪个功能接口? 到目前为止,我认为有不同的编码方式。 一些简单的(包括评估每个时间)和一些更复杂的(包括不评估每个时间)......

在 SQL 中,它的长度通常相同(例如value1when1都是列),所以我希望它能够工作。

也许这是相关的。 我刚刚构建了一个版本,它只是构建在您非常快速的fifelse()之上,并在 R 中创建了一些检查和结构,但其他方面允许fifelse()运行该节目。

它可以在https://github.com/TysonStanley/tidyfast 上非常发展的tidyfast包中找到 这种非常简单的方法有用吗?

谢谢@TysonStanley 。 我实际上已经用 C 编写了这个函数。我希望今晚做一个拉取请求。 我只需要完成编写测试。 我会看看你的功能。

@2005m太棒了! 我很高兴看到它推出。 语法类似于dplyr::case_when()吗?

是的,请看一看,让我知道您的想法。 这是一个非常简单的方法,因为我可以依赖fifelse()

这是一个先睹为快:

x = sample(1:100,3e7,replace = TRUE) # 114 Mb
data.table::setDTthreads(1L)

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
),
tidyfast::dt_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: seconds
#                    expr    min    lq  mean  median    uq    max neval
# dplyr::case_when         11.69 11.80 11.83   11.81  11.92 11.94     5
# tidyfast::dt_case_when    2.18  2.23  2.32    2.38   2.39  2.41     5
# data.table::fcase         1.87  1.91  2.02    2.02   2.05  2.26     5

语法与dplyr::case_when不同,但我们可以更改它。
注意我的电脑很旧。 代码使用 gcc 4.9 标志 -02 编译。
该函数将支持 interger64 和 nanotime。
不过,我仍然需要继续努力。 我认为 PR 不会在今晚举行。

感谢您的潜行高峰。 看到这样的表演很有趣。 无论哪种方式,语法看起来都一样友好。 我个人喜欢这些公式,但在大多数情况下,它可能并不重要。

您是否计划在未来支持其他矢量类型?

它是评估每一个案例还是只评估那些需要联系以提供答案的案例?

@jangorecki ,是的,它评估所有情况,这就是我对它不满意的原因。 我也对表演不满意。 如果无法改进,我认为@TysonStanley的方法更好,因为更简单且时间相似。

@TysonStanley ,我也更喜欢公式,但它可能是主观的......关于其他向量类型,我不知道。 这取决于团队。

我们不着急。 我认为懒惰应该是至关重要的,否则与使用查找表相比,它不会带来任何新的东西(API 除外)。 不要试图在第一次迭代时解决所有问题。 当你觉得准备好提交 PR 以获得反馈。 最终状态可以进行多次迭代或后续 PR。

fifelse是否懒惰地评估(即仅需要的情况)? 我需要看一下代码,但我想我会问。

是的,它确实

fifelse(TRUE, 1, stop("a"))
#Error in fifelse(TRUE, 1, stop("a")) : a

@jangorecki ,我希望能够在这个周末分享我的代码。 我需要编写 Rd 文件并添加更多测试。

此页面是否有帮助?
0 / 5 - 0 等级