如何在 `mutate` 中的 `map` 内部取消引用 (`!!`)。

7

我正在使用map2mutate修改foo中的嵌套数据帧,并且我想根据foo$name在每个嵌套数据帧中命名一个变量。我不确定在这里使用非标准评估(NSE)/tidyeval去引用的正确语法。

library(tidyverse)

foo <- mtcars %>%
  group_by(gear) %>%
  nest %>%
  mutate(name = c("one", "two", "three")) %>%
  mutate(data = map2(data, name, ~
                       mutate(.x, !!(.y) := "anything")))
#> Error in quos(...): object '.y' not found

我希望在嵌套的数据框中,新创建的变量名称分别为“one”,“two”和“three”。基于我平常在普通df上使用的正常语法进行尝试,其中name是一个字符串:

name <- "test"
mtcars %>% mutate(!!name := "anything") # works fine

如果成功,下面这行代码应该返回TRUE:
foo[1,2] %>% unnest %>% names %>% .[11] == "one"

你认为这是重复的吗?有些相似之处,但就我所看到的来说,它们是不同的问题。 - lost
我不是故意重复的。 - akrun
2个回答

7
这似乎是一个特性/bug(不确定,请参见下面链接的GitHub问题)涉及到mutatemap!!的工作方式。解决方案是定义一个自定义函数,这样取消引用就能按预期工作。
library(tidyverse)

custom_mutate <- function(df, name, string = "anything")
    mutate(df, !!name := string)

foo <- mtcars %>%
  group_by(gear) %>%
  nest %>%
  mutate(name = c("one", "two", "three")) %>%
  mutate(data = map2(data, name, ~
      custom_mutate(.x, .y)))

foo[1,2] %>% unnest %>% names %>% .[11] == "one"
#[1] TRUE

您可以在GitHub上的#541问题下找到更多细节:dplyr :: mutate()中的map2()调用错误,而独立的map2()调用起作用;请注意,该问题已在2018年9月关闭,因此我假设这是预期行为。


另一种选择可能是使用group_split而不是nest,在这种情况下,我们避免了未引用的问题。

nms <- c("one", "two", "three")

mtcars %>%
    group_split(gear) %>%
    map2(nms, ~.x %>% mutate(!!.y := "anything"))

顺便提一下,我知道你提供的代码只是一个玩具示例,但是像 mutate(name = c("one", "two", "three")) 这样的东西在 nest 之后如果出现了超过(或少于)3个组,可能会带来相当大的危险。 - Maurits Evers
如果您的向量长度不是1,也不与数据框的长度相同,则会引发错误。 - lost
@lost 没错,所以这并不是非常健壮的。我猜你可以将其包装在 tryCatch 环境中,但最好还是在某个地方进行检查,以确保 nms <- c("one", "two", "three"); length(nms) == length(unique(mtcars$gear)) - Maurits Evers
2
@lost PS:我已经添加了一个使用新的 dplyr::group_split 的备选方法,通过分组拆分数据,然后使用 map2 对各个 list 元素进行操作。 - Maurits Evers
@lost 你没有失去任何数据。与通过 mtcars$gear 生成一个嵌套的 tibble 不同,你有一个由 mtcars$gear 分割的 tibble 列表。根据你下游的数据处理,对于这个列表进行操作和对嵌套的tibble进行操作一样容易。这归结于个人喜好。 - Maurits Evers
显示剩余3条评论

4
这是因为解除引用的时机。嵌套Tidy eval函数可能有点棘手,因为它是第一个处理解除引用运算符的Tidy eval函数。
让我们重新书写一下:
mutate(data = map2(data, name, ~ mutate(.x, !!.y := "anything")))

为了

mutate(data = map2(data, name, function(x, y) mutate(x, !!y := "anything")))
xy绑定只有在由map2()调用函数时才会创建。因此,当第一个mutate()运行时,这些绑定还不存在,会出现对象未找到错误。使用公式可能更难理解,但是公式扩展为一个带有.x.y参数的函数,所以我们有相同的问题。
通常最好避免在代码中使用复杂嵌套逻辑,因为它使阅读更加困难。使用整洁评估更增加了复杂性,因此最好按步骤完成任务。额外的好处是,按步骤完成任务需要创建中间变量,如果命名得当,可以帮助理解函数正在执行的操作。

1
虽然我同意你的“分步骤操作”的一般建议,但我要说这并不是一个复杂的嵌套函数;更甚者,我猜想当我们处理嵌套数据时,这可能是一个相当常见的情况。我肯定已经做过(或希望做)类似的事情。值得指出的是,如果我们首先使用split(或新的dplyr::group_split)拆分data.frame/tibble,然后再对各个list元素进行操作,就可以避免这个问题,详见我的帖子更新。 - Maurits Evers
@lionel 我同意Maurits的观点--我并不认为这种用法比R4DS、vignettes等中涵盖的“典型”tidyeval用法更复杂。它似乎是一种相当自然的使用嵌套和映射的方式。我很想知道您对此操作的偏好步骤。 - lost
我认为嵌套的变异很复杂,但我接受其他人可能不这样认为。无论如何,它使得整洁的评估语义更加棘手。@lost 我建议的步骤是给匿名函数命名。 - Lionel Henry
1
也许我们可以找到一个解决方案,使事情更符合直觉。这一直困扰着整洁的 eval 用户 :/ - Lionel Henry
@LionelHenry,看起来这个解决方案不再适用了...我该如何使用新的{{}}语法使其正常工作? - lost
我在这里没有提供任何解决方案,只是展示了为什么它会失败。解决方案是将函数从 mutate() 调用中拆分出来。不过,下一个主要版本的 rlang 可能会更普遍地修复它:https://github.com/r-lib/rlang/issues/845 - Lionel Henry

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