同时使用data.table和tidy eval:为什么group by不能按预期工作,为什么要插入?

Ild*_*ler 5 r data.table tidyeval rlang

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

我有替代解决方案,所以我最感兴趣的是为什么,因为我希望能够更好地理解整齐的eval,这将有助于我在各种各样的用例中使用.

如何使用group by进行data.table +整理eval工作?

在以下示例中,我使用了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
Run Code Online (Sandbox Code Playgroud)

在这种情况下,使用表达式而不是quo是不好的,因为在良好环境中可能不会评估用户提供的表达式中的变量:

# 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
Run Code Online (Sandbox Code Playgroud)

我有一个明确的问题使用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
Run Code Online (Sandbox Code Playgroud)

我理解从quosures中提取表达式不是使用整洁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)
Run Code Online (Sandbox Code Playgroud)

由reprex包(v0.2.0)于2018-08-12创建.

问题的原始措辞:

为什么要插入〜如果它是基本评估的问题并且一切都在全球环境中,为什么它不是整齐的eval的问题?

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

Lio*_*nry 8

TLDR:Quosures作为公式实现,因为它会影响3.5.1之前的所有R版本.特殊的rlang定义~仅适用于eval_tidy().这就是为什么quosures与我们想要的非tidyeval函数不兼容的原因.

编辑:也就是说,使数据屏蔽API(如data.table)与quosures兼容可能还有其他挑战.


Quosures目前实现为公式:

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>
Run Code Online (Sandbox Code Playgroud)

与普通公式比较:

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

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

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

那么quosure和公式之间的区别是什么?前者评价自己,而后者引用自己,即它自己返回.

eval_tidy(q)
#> eval!

eval_tidy(f)
#> ~cat("eval?\n")
Run Code Online (Sandbox Code Playgroud)

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

`~`
#> .Primitive("~")
Run Code Online (Sandbox Code Playgroud)

该原语的一个重要任务是在第一次评估公式时记录环境.例如,公式in quote(~foo)不被评估,并且不记录环境eval(quote(~foo)).

无论如何,当您评估一个~调用时,~将以普通方式查找定义,并且通常会找到该~原语.就像你计算时一样1 + 1,+查找定义并通常.Primitive("+")找到它.quouts自我评估而不是自我引用的原因只是在其评估环境中eval_tidy()创建了一个特殊的定义~.您可以使用此特殊定义eval_tidy(quote(`~`)).

那么为什么我们将quosures实现为公式?

  1. 它更好地去除和打印.这个原因现在已经过时,因为我们有自己的表达式deparser,其中quosures打印有一个前导^而不是前导~.

  2. 由于3.5.1之前的所有版本的R中都存在错误,因此在递归打印时会评估带有类的表达式.以下是分类呼叫的示例:

    x  <- quote(stop("oh no!"))
    x <- structure(x, class = "some_class")
    
    Run Code Online (Sandbox Code Playgroud)

    对象本身打印很好:

    x
    #> stop("oh no!")
    #> attr(,"class")
    #> [1] "some_class"
    
    Run Code Online (Sandbox Code Playgroud)

    但如果你把它放在一个列表中就会得到评估!

    list(x)
    #> [[1]]
    #> Error in print(stop("oh no!")) : oh no!
    
    Run Code Online (Sandbox Code Playgroud)

急切的评估错误不会影响公式,因为它们会自我引用.将quosures实现为公式可以保护我们免受此错误的影响.

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

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

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

eval(c, emptyenv())
#> [1] "A"
Run Code Online (Sandbox Code Playgroud)

如果我们使用内联函数实现quosures,它们可以在任何地方进行类似的评估.eval(q)可以工作,你可以在data.table调用等内部取消引用quosures但是你是否注意到内联调用因内联而打印的噪音有多大?要解决这个问题,我们必须给这个调用一个类和一个print方法.但请记住R <= 3.5.0错误.在控制台上打印状态列表时,我们会得到奇怪的热切评估.这就是为什么quosures仍然作为公式实现到今天,并且不像我们想要的那样与非tidyeval函数兼容.