r - 一起使用 data.table 和 tidy eval : why group by does not work as expected, 为什么插入 ~ ?

标签 r data.table rlang tidyeval

我没有紧迫的用例,但想了解 tidy eval 和 data.table 如何协同工作。

我有可用的替代解决方案,所以我最感兴趣的是原因,因为我希望更好地理解整洁的评估,这将在各种用例中帮助我。

如何使 data.table + tidy eval 与 group by 一起使用?

在以下示例中,我使用了 rlang 的开发版本。

更新

我根据 Stefan F 的回答和我的进一步探索更新了我原来的问题:我不再认为插入的 ~ 是问题的重要部分,因为它也存在于 dplyr 代码中,但我有一个特定的代码: data.table + group by + quo 我不明白为什么不起作用。

# setup ------------------------------------

suppressPackageStartupMessages(library("data.table"))
suppressPackageStartupMessages(library("rlang"))
suppressPackageStartupMessages(library("dplyr"))
#> Warning: package 'dplyr' was built under R version 3.5.1

dt <- data.table(
    num_campaign = 1:5,
    id = c(1, 1, 2, 2, 2)
)
df <- as.data.frame(dt)

# original question ------------------------

aggr_expr <- quo(sum(num_campaign))

q <- quo(dt[, aggr := !!aggr_expr][])

e <- quo_get_expr(q)
e
#> dt[, `:=`(aggr, ~sum(num_campaign))][]
dt[, `:=`(aggr, ~sum(num_campaign))][]
#> Error in `[.data.table`(dt, , `:=`(aggr, ~sum(num_campaign))): RHS of assignment is not NULL, not an an atomic vector (see ?is.atomic) and not a list column.
eval_tidy(e, data = dt)
#>    num_campaign id aggr
#> 1:            1  1   15
#> 2:            2  1   15
#> 3:            3  2   15
#> 4:            4  2   15
#> 5:            5  2   15

在这种情况下,使用表达式而不是现状并不好,因为用户提供的表达式中的变量可能无法在良好的环境中进行计算:

# updated question --------------------------------------------------------

aggr_dt_expr <- function(dt, aggr_rule) {
    aggr_expr <- enexpr(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_expr][])
    eval_tidy(q, data = dt)
}

x <- 1L
# expression is evaluated with x = 2
aggr_dt_expr(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1   17
#> 2:            2  1   17
#> 3:            3  2   17
#> 4:            4  2   17
#> 5:            5  2   17

aggr_dt_quo <- function(dt, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo][])
    eval_tidy(q, data = dt)
}

x <- 1L
# expression is evaluated with x = 1
aggr_dt_quo(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1   16
#> 2:            2  1   16
#> 3:            3  2   16
#> 4:            4  2   16
#> 5:            5  2   16

我在使用 group by 时遇到了明显的问题:

# using group by --------------------------------

grouped_aggr_dt_expr <- function(dt, aggr_rule) {
    aggr_quo <- enexpr(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo, by = id][])
    eval_tidy(q, data = dt)
}

# group by has effect but x = 2 is used
grouped_aggr_dt_expr(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1    5
#> 2:            2  1    5
#> 3:            3  2   14
#> 4:            4  2   14
#> 5:            5  2   14

grouped_aggr_dt_quo <- function(dt, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo, by = id][])
    eval_tidy(q, data = dt)
}

# group by has no effect
grouped_aggr_dt_quo(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1   16
#> 2:            2  1   16
#> 3:            3  2   16
#> 4:            4  2   16
#> 5:            5  2   16


# using dplyr works fine ------------------------------------------------------------

grouped_aggr_df_quo <- function(df, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(mutate(group_by(df, id), !!aggr_quo))
    eval_tidy(q)
}
grouped_aggr_df_quo(df, sum(num_campaign) + x)
#> # A tibble: 5 x 3
#> # Groups:   id [2]
#>   num_campaign    id `sum(num_campaign) + x`
#>          <int> <dbl>                   <int>
#> 1            1     1                       4
#> 2            2     1                       4
#> 3            3     2                      13
#> 4            4     2                      13
#> 5            5     2                      13

我知道从 quosures 中提取表达式并不是使用 tidy eval 的方法,但我希望将其用作调试工具:(到目前为止运气不佳)

# returning expression in quo for debugging --------------

grouped_aggr_dt_quo_debug <- function(dt, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo, by = id][])
    quo_get_expr(q)
}

grouped_aggr_dt_quo_debug(dt, sum(num_campaign) + x)
#> dt[, `:=`(aggr, ~sum(num_campaign) + x), by = id][]

grouped_aggr_df_quo_debug <- function(df, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(mutate(group_by(df, id), !!aggr_quo))
    quo_get_expr(q)
}
# ~ is inserted in this case as well so it is not the problem
grouped_aggr_df_quo_debug(df, sum(num_campaign) + x)
#> mutate(group_by(df, id), ~sum(num_campaign) + x)

reprex package于2018年8月12日创建(v0.2.0)。

问题的原始措辞:

为什么要插入 ~ ,如果是基本 eval 的问题并且一切都在全局环境中,那么为什么它不是 tidy eval 的问题?

这个示例源自一个更现实但也更复杂的用例,我得到了意想不到的结果。

最佳答案

TLDR:由于一个影响 3.5.1 之前的所有 R 版本的错误,Quosures 被实现为公式。 ~ 的特殊 rlang 定义仅适用于 eval_tidy()。这就是为什么 quosures 不像我们希望的那样与非 tidyeval 函数兼容。

编辑:也就是说,要使 data.table 等数据屏蔽 API 与 quosures 兼容,可能还存在其他挑战。

<小时/>

Quasures 目前以公式的形式实现:

library("rlang")

q <- quo(cat("eval!\n"))

is.call(q)
#> [1] TRUE

as.list(unclass(q))
#> [[1]]
#> `~`
#>
#> [[2]]
#> cat("eval!\n")
#>
#> attr(,".Environment")
#> <environment: R_GlobalEnv>

与普通公式比较:

f <- ~cat("eval?\n")

is.call(f)
#> [1] TRUE

as.list(unclass(f))
#> [[1]]
#> `~`
#>
#> [[2]]
#> cat("eval?\n")
#>
#> attr(,".Environment")
#> <environment: R_GlobalEnv>

那么求导和公式有什么区别呢?前者评估自身,而后者引用自身,即返回自身。

eval_tidy(q)
#> eval!

eval_tidy(f)
#> ~cat("eval?\n")

自引用机制由~原语实现:

`~`
#> .Primitive("~")

该原语的一个重要任务是记录第一次计算公式时的环境。例如,quote(~foo) 中的公式不会被求值,也不会记录环境,而 eval(quote(~foo)) 则会记录环境。

无论如何,当您评估 ~ 调用时,会以普通方式查找 ~ 的定义,通常会找到 ~ 原语。就像计算 1 + 1 时一样,会查找 + 的定义,通常会找到 .Primitive("+")。使用自求值而不是自引用的原因很简单,eval_tidy() 在其求值环境中为 ~ 创建了一个特殊的定义。您可以使用 eval_tidy(quote(`~`)) 来保留这个特殊定义。

那么我们为什么要以公式的形式实现quosures?

  1. 它的解析和打印效果更好。这个原因现在已经过时了,因为我们有自己的表达式解析器,其中用前导 ^ 而不是前导 ~ 打印定额。

  2. 由于 3.5.1 之前的所有 R 版本中存在错误,带有类的表达式将在递归打印上求值。以下是分类调用的示例:

    x  <- quote(stop("oh no!"))
    x <- structure(x, class = "some_class")
    

    对象本身打印良好:

    x
    #> stop("oh no!")
    #> attr(,"class")
    #> [1] "some_class"
    

    但是如果你把它放在一个列表中,它就会被评估!

    list(x)
    #> [[1]]
    #> Error in print(stop("oh no!")) : oh no!
    

急切求值错误不会影响公式,因为它们是自引用的。将定额作为公式实现可以保护我们免受此错误的影响。

理想情况下,我们将直接在 quosure 中内联一个函数。例如。第一个元素不包含符号 ~ 而是一个函数。以下是创建此类函数的方法:

c <- as.call(list(toupper, "a"))
c
#> (function (x)
#> {
#>     if (!is.character(x))
#>         x <- as.character(x)
#>     .Internal(toupper(x))
#> })("a")

在调用中内联函数的最大优点是可以在任何地方对它们进行求值。即使在空旷的环境中!

eval(c, emptyenv())
#> [1] "A"

如果我们用内联函数实现quosures,它们可以在任何地方类似地被评估。 eval(q) 可以工作,您可以在 data.table 调用中取消引用 quosures,等等。但是您是否注意到由于内联而导致内联调用打印的噪音有多大?要解决这个问题,我们必须为调用提供一个类和一个打印方法。但请记住 R <= 3.5.0 错误。当在控制台打印配额列表时,我们会得到奇怪的急切评估。这就是为什么 quosures 至今仍然以公式的形式实现,并且不像我们希望的那样与非 tidyval 函数兼容。

关于r - 一起使用 data.table 和 tidy eval : why group by does not work as expected, 为什么插入 ~ ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51802967/

相关文章:

r - 在 R 中将字符向量粘贴为逗号分隔的未加引号的列表

r - Knitr和Rmarkdown docx表

r - data.table 上的数学运算(在 R 中)

r - Tidyeval in own functions in own functions inside own functions with the pipe 管道

r - 自动完成并在文本框中选择多个值

R - Shiny - DT : how to update col filters

r - 使用 R,data.table,有条件地求和列

r - 使用 data.table 加速 rollapply

r - 如何检测向量中是否并非所有元素都被引用

r - 如何修复 R 函数中的 'Quosures can only be unquoted within a quasiquotation context' 错误