Tidyeval:将列名列表作为quosure传递给select()函数

5

我想在mutate()中向pmap()传递一堆列。之后,我想选择这些相同的列。

目前,我将一组列名作为quosure传递给pmap(),这样做很好,虽然我不知道这是否是“正确”的方法。但我无法弄清如何在select()中使用相同的quosure/list。

我几乎没有使用tidyeval的经验,只是通过玩耍才做到了这一点。我想象中肯定有一种方法可以同时用于pmap()select(),最好不必将我的每个列名都放在引号中,但我还没有找到它。

library(dplyr)
library(rlang)
library(purrr)

df <- tibble(a = 1:3,
             b = 101:103) %>% 
    print
#> # A tibble: 3 x 2
#>       a     b
#>   <int> <int>
#> 1     1   101
#> 2     2   102
#> 3     3   103

cols_quo <- quo(list(a, b))

df2 <- df %>% 
    mutate(outcome = !!cols_quo %>% 
               pmap_int(function(..., word) {
                   args <- list(...)

                   # just to be clear this isn't what I actually want to do inside pmap
                   return(args[[1]] + args[[2]])
               })) %>% 
    print()
#> # A tibble: 3 x 3
#>       a     b outcome
#>   <int> <int>   <int>
#> 1     1   101     102
#> 2     2   102     104
#> 3     3   103     106

# I get why this doesn't work, but I don't know how to do something like this that does
df2 %>% 
    select(!!cols_quo)
#> Error in .f(.x[[i]], ...): object 'a' not found
2个回答

9
这有点棘手,因为这个问题涉及到语义的混合。`pmap()`接受一个列表,并将每个元素作为自己的参数传递给一个函数(在某种意义上,它类似于`!!!`)。因此,您的引用函数需要引用其参数并以某种方式传递列的列表给`pmap()`。
我们的引用函数可以有两种方法。要么延迟列表的创建(即延迟执行),要么立即创建一个包含引用表达式的实际列表:
quoting_fn1 <- function(...) {
  exprs <- enquos(...)

  # For illustration purposes, return the quoted inputs instead of
  # doing something with them. Normally you'd call `mutate()` here:
  exprs
}

quoting_fn2 <- function(...) {
  expr <- quo(list(!!!enquos(...)))

  expr
}

由于我们的第一个变体仅返回输入的引用列表,因此它实际上等同于 quos()

quoting_fn1(a, b)
#> <list_of<quosure>>
#>
#> [[1]]
#> <quosure>
#> expr: ^a
#> env:  global
#>
#> [[2]]
#> <quosure>
#> expr: ^b
#> env:  global

第二个版本返回一个带引号的表达式,指示R创建一个带引号的输入列表。
quoting_fn2(a, b)
#> <quosure>
#> expr: ^list(^a, ^b)
#> env:  0x7fdb69d9bd20

这两者之间有微妙但重要的区别。第一个版本创建了一个实际的列表对象:

exprs <- quoting_fn1(a, b)
typeof(exprs)
#> [1] "list"

另一方面,第二个版本不返回一个列表,而是返回用于创建列表的表达式:

expr <- quoting_fn2(a, b)
typeof(expr)
#> [1] "language"

让我们找出哪个版本更适合与pmap()接口。但首先,我们将为pmapped函数命名,以使代码更清晰,更易于实验:

myfunction <- function(..., word) {
  args <- list(...)
  # just to be clear this isn't what I actually want to do inside pmap
  args[[1]] + args[[2]]
}

要理解整洁评估是如何工作的,这很困难,部分原因在于我们通常无法观察到取消引用步骤。我们将使用rlang :: qq_show()来使用!!揭示取消引用expr(延迟列表)和exprs(实际列表)的结果:

rlang::qq_show(
  mutate(df, outcome = pmap_int(!!expr, myfunction))
)
#> mutate(df, outcome = pmap_int(^list(^a, ^b), myfunction))

rlang::qq_show(
  mutate(df, outcome = pmap_int(!!exprs, myfunction))
)
#> mutate(df, outcome = pmap_int(<S3: quosures>, myfunction))

当我们取消延迟列表的引用时,mutate()将使用在数据框中计算的list(a,b)调用pmap_int(),这正是我们所需要的内容。
mutate(df, outcome = pmap_int(!!expr, myfunction))
#> # A tibble: 3 x 3
#>       a     b outcome
#>   <int> <int>   <int>
#> 1     1   101     102
#> 2     2   102     104
#> 3     3   103     106

另一方面,如果我们取消引用一个实际上被引用的表达式列表,就会出现错误:

mutate(df, outcome = pmap_int(!!exprs, myfunction))
#> Error in mutate_impl(.data, dots) :
#>   Evaluation error: Element 1 is not a vector (language).

这是因为列表内的引用表达式未在数据帧中进行评估。实际上,它们根本没有被评估。 pmap() 会将引用表达式原样获取,但它并不理解这些表达式。回想一下 qq_show() 所示的内容:

#> mutate(df, outcome = pmap_int(<S3: quosures>, myfunction))

尖括号内的任何内容都按原样传递。 这表明我们应该使用!!!而不是,来内联周围表达式中的quoasure列表的每个元素。 让我们尝试一下:

rlang::qq_show(
  mutate(df, outcome = pmap_int(!!!exprs, myfunction))
)
#> mutate(df, outcome = pmap_int(^a, ^b, myfunction))

嗯...看起来不对。我们应该向pmap_int()传递一个列表,而这里却将每个引用输入作为单独的参数进行处理。事实上,我们会收到类型错误:

mutate(df, outcome = pmap_int(!!!exprs, myfunction))
#> Error in mutate_impl(.data, dots) :
#>   Evaluation error: `.x` is not a list (integer).

很容易解决,只需将list()调用拼接进去即可:
rlang::qq_show(
  mutate(df, outcome = pmap_int(list(!!!exprs), myfunction))
)
#> mutate(df, outcome = pmap_int(list(^a, ^b), myfunction))

完成了!

mutate(df, outcome = pmap_int(list(!!!exprs), myfunction))
#> # A tibble: 3 x 3
#>       a     b outcome
#>   <int> <int>   <int>
#> 1     1   101     102
#> 2     2   102     104
#> 3     3   103     106

2

当有多个元素时,我们可以使用quos并用!!!进行评估。

cols_quo <- quos(a, b)
df2 %>%
    select(!!!cols_quo)

可以使用以下方式创建对象'df2'

df %>%
    mutate(output = list(!!! cols_quo) %>% 
        reduce(`+`))

如果我们想像 OP 帖子中那样使用 quosure
cols_quo <- quo(list(a, b))
df2 %>%
    select(!!! as.list(quo_expr(cols_quo))[-1])
# A tibble: 3 x 2
#      a     b
#  <int> <int>
#1     1   101
#2     2   102
#3     3   103

谢谢,我也尝试过那个方法,在选择步骤中确实有效,但是在使用pmap的中间步骤中该怎么做呢? - Oliver

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