一起使用 data.table 和 tidy eval:为什么 group by 没有按预期工作,为什么要插入 ~?
Using data.table and tidy eval together: why group by does not work as expected, why is ~ inserted?
我没有紧迫的用例,但想了解 tidy eval 和 data.table 如何协同工作。
我有替代解决方案,所以我最感兴趣的是原因,因为我希望总体上更好地理解 tidy eval,这将在各种用例中帮助我。
如何使 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
在这种情况下使用表达式而不是 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
我在使用 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 (v0.2.0) 创建于 2018-08-12。
问题原文:
为什么要插入一个 ~,如果基础 eval 有问题并且一切都在全局环境中,为什么它不是 tidy eval 的问题?
此示例源自一个更现实但也更复杂的用例,在该用例中我得到了意想不到的结果。
您需要使用 expr()
而不是 quo()
expr()
捕获一个表达式,quo()
捕获表达式+应该计算表达式的环境("quosure")。
quosures 是 rlang/tidyeval 特定的事物,因此您需要使用 tidyeval 来评估它们。
至于~
:波浪符用于R中的公式。公式是特殊的R对象,旨在指定R中的模型(例如lm()
),但它们有一些有趣的使它们也可用于其他目的的属性。显然 rlang 使用它们来表示 quosures(但我不太了解这里的内部结构)。
base::eval()
认为您提供了一个公式,但不知道在那种情况下如何处理它,而 eval_tidy()
知道您实际上是在传递一个 quosure。 rlang::expr()
没有这个问题,因为 returns 也基于 R 的对象知道如何处理。
TLDR:由于一个影响 3.5.1 之前的所有 R 版本的错误,Quosures 被实现为公式。 ~
的特殊 rlang 定义仅适用于 eval_tidy()
。这就是为什么 quosures 不像我们希望的那样与非潮汐函数兼容。
编辑:也就是说,要使 data.table 等数据屏蔽 API 与 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>
与普通公式对比:
f <- ~cat("eval?\n")
is.call(f)
#> [1] TRUE
as.list(unclass(f))
#> [[1]]
#> `~`
#>
#> [[2]]
#> cat("eval?\n")
#>
#> attr(,".Environment")
#> <environment: R_GlobalEnv>
那么定数和公式有什么区别呢?前者评价自己,后者引用自己,即returns自己
eval_tidy(q)
#> eval!
eval_tidy(f)
#> ~cat("eval?\n")
自引用机制由~
原语实现:
`~`
#> .Primitive("~")
这个原语的一个重要任务是在第一次计算公式时记录环境。例如,quote(~foo)
中的公式不会被评估,也不会记录环境,而 eval(quote(~foo))
会。
无论如何,当您评估 ~
调用时,~
的定义以普通方式查找并且通常会找到 ~
原语。就像计算 1 + 1
时一样,会查找 +
的定义,通常会找到 .Primitive("+")
。 quosures 自我评估而不是自我引用的原因很简单,eval_tidy()
在其评估环境中为 ~
创建了一个特殊定义。您可以使用 eval_tidy(quote(`~`))
.
来保留这个特殊定义
那么为什么我们将等式实现为公式?
它的解析和打印效果更好。这个原因现在已经过时了,因为我们有自己的表达式解析器,其中使用前导 ^
而不是前导 ~
.
打印 quosures
由于 3.5.1 之前的所有 R 版本中的错误,带有 class 的表达式在递归打印时被计算。这是 classed 调用的示例:
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!
eager evaluation 错误不会影响公式,因为它们是自引用的。将 quosures 作为公式来保护我们免受此错误的影响。
理想情况下,我们将直接在 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 等。但是您是否注意到内联调用打印的噪音有多大?要解决这个问题,我们必须给调用一个 class 和一个打印方法。但请记住 R <= 3.5.0 错误。在控制台打印 quosures 列表时,我们会得到奇怪的急切评估。这就是为什么直到今天 quosures 仍然作为公式实现并且不像我们希望的那样与非潮汐函数兼容。
我没有紧迫的用例,但想了解 tidy eval 和 data.table 如何协同工作。
我有替代解决方案,所以我最感兴趣的是原因,因为我希望总体上更好地理解 tidy eval,这将在各种用例中帮助我。
如何使 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
在这种情况下使用表达式而不是 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
我在使用 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 (v0.2.0) 创建于 2018-08-12。
问题原文:
为什么要插入一个 ~,如果基础 eval 有问题并且一切都在全局环境中,为什么它不是 tidy eval 的问题?
此示例源自一个更现实但也更复杂的用例,在该用例中我得到了意想不到的结果。
您需要使用 expr()
而不是 quo()
expr()
捕获一个表达式,quo()
捕获表达式+应该计算表达式的环境("quosure")。
quosures 是 rlang/tidyeval 特定的事物,因此您需要使用 tidyeval 来评估它们。
至于~
:波浪符用于R中的公式。公式是特殊的R对象,旨在指定R中的模型(例如lm()
),但它们有一些有趣的使它们也可用于其他目的的属性。显然 rlang 使用它们来表示 quosures(但我不太了解这里的内部结构)。
base::eval()
认为您提供了一个公式,但不知道在那种情况下如何处理它,而 eval_tidy()
知道您实际上是在传递一个 quosure。 rlang::expr()
没有这个问题,因为 returns 也基于 R 的对象知道如何处理。
TLDR:由于一个影响 3.5.1 之前的所有 R 版本的错误,Quosures 被实现为公式。 ~
的特殊 rlang 定义仅适用于 eval_tidy()
。这就是为什么 quosures 不像我们希望的那样与非潮汐函数兼容。
编辑:也就是说,要使 data.table 等数据屏蔽 API 与 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>
与普通公式对比:
f <- ~cat("eval?\n")
is.call(f)
#> [1] TRUE
as.list(unclass(f))
#> [[1]]
#> `~`
#>
#> [[2]]
#> cat("eval?\n")
#>
#> attr(,".Environment")
#> <environment: R_GlobalEnv>
那么定数和公式有什么区别呢?前者评价自己,后者引用自己,即returns自己
eval_tidy(q)
#> eval!
eval_tidy(f)
#> ~cat("eval?\n")
自引用机制由~
原语实现:
`~`
#> .Primitive("~")
这个原语的一个重要任务是在第一次计算公式时记录环境。例如,quote(~foo)
中的公式不会被评估,也不会记录环境,而 eval(quote(~foo))
会。
无论如何,当您评估 ~
调用时,~
的定义以普通方式查找并且通常会找到 ~
原语。就像计算 1 + 1
时一样,会查找 +
的定义,通常会找到 .Primitive("+")
。 quosures 自我评估而不是自我引用的原因很简单,eval_tidy()
在其评估环境中为 ~
创建了一个特殊定义。您可以使用 eval_tidy(quote(`~`))
.
那么为什么我们将等式实现为公式?
它的解析和打印效果更好。这个原因现在已经过时了,因为我们有自己的表达式解析器,其中使用前导
^
而不是前导~
. 打印 quosures
由于 3.5.1 之前的所有 R 版本中的错误,带有 class 的表达式在递归打印时被计算。这是 classed 调用的示例:
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!
eager evaluation 错误不会影响公式,因为它们是自引用的。将 quosures 作为公式来保护我们免受此错误的影响。
理想情况下,我们将直接在 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 等。但是您是否注意到内联调用打印的噪音有多大?要解决这个问题,我们必须给调用一个 class 和一个打印方法。但请记住 R <= 3.5.0 错误。在控制台打印 quosures 列表时,我们会得到奇怪的急切评估。这就是为什么直到今天 quosures 仍然作为公式实现并且不像我们希望的那样与非潮汐函数兼容。