在 mutate 的右侧使用基于 tidyeval 的非标准评估来重新编码

13

考虑一个数据框,其中每一列都是一个字符向量,可以取多个值 -- 假设为"A"到"F"。

library(tidyverse)
sample_df <- tibble(q1 = c("A", "B", "C"), q2 = c("B", "B", "A"))

我希望创建一个函数,以列名作为参数,并重新编码该列,使任何答案为“A”的值变成NA,其余情况返回原数据框。设计这种方式的原因是为了适应使用给定列执行一系列操作的更广泛的流程。

有许多方法可以实现这个目标,但我对理解最佳的惯用tidy_eval/tidyverse方法很感兴趣。首先,需要将问题名称放在mutate动词的左侧,因此我们恰当地使用!!和:=运算符。但接下来,在右侧放什么呢?

fix_question <- function(df, question) {
    df %>% mutate(!!question := recode(... something goes here...))
}

fix_question(sample_df, "q1") # should produce a tibble whose first column is (NA, "B", "C")

我的最初想法是这个会起作用:

df %>% mutate(!!question := recode(!!question, "A" = NA_character_))

当然,在函数内部的bang-bang只是返回字面字符字符串(例如"q1")。我最终采取了一种看起来有点hacky的方式来引用右侧的数据,使用基本的R [[运算符并依赖于dplyr中的.构造,它可以工作,因此在某种意义上我已经解决了我的根本问题:

df %>% mutate(!!question := recode(.[[question]], "A" = NA_character_))

我希望向精通tidyeval的人获取反馈,看是否有更习惯的方法来完成这个任务。希望通过实例展示来提高我对tidyeval函数集的理解。有任何想法吗?


谢谢,这是一个聪明的方法--我在我的代码的其他部分中使用函数方法,也可以考虑在这里使用它。我知道有些人不喜欢在SO上谈论代码风格,但是很快看到几种不同的答案对我来说非常有益。 - aaron
1
将这个问题中的几个想法结合起来,我认为这是最简洁的版本,适用于 q1(符号)和 "q1"(字符串):df %>% mutate_at( vars(!!ensym(question)), recode, A = NA_character_) - Artem Sokolov
3个回答

8
如果你使用的是 rlang >= 0.4.0 版本,现在你可以使用“花括号花括号”方法。感谢 @eipi10 的解释,这将两个步骤的引用和取消引用合并为一步,因此 {{question}} 等同于 !!enquo(question)
fix_question <- function(df, question){
  df %>% mutate({{question}} := recode({{question}}, A = NA_character_))
}

fix_question(sample_df, q1)
# # A tibble: 3 x 2
#   q1    q2   
#   <chr> <chr>
# 1 NA    B    
# 2 B     B    
# 3 C     A    

请注意,与 ensym 方法不同的是,这种方法不能处理字符名。更糟糕的是,它会做错误的事情而不是仅仅报错。
fix_question(sample_df, 'q1')

# # A tibble: 3 x 2
#   q1    q2   
#   <chr> <chr>
# 1 q1    B    
# 2 q1    B    
# 3 q1    A    

2
我还没有养成使用“双花括号”的习惯。你知道为什么这个能运行,而OP的看起来一模一样的“叹号叹号”版本不行吗? - camille
感谢您提到了“curly-curly”,我听说它即将推出。但是答案并不适用于我安装的任何版本的rlang/dplyr;我会得到LHS错误的提示。如果我用我的LHS替换LHS并引用q1,则会遇到与上面相同的问题;如果我不对q1进行引用,那么会出现错误。这可能是版本问题。 - aaron
1
是的,rlang 0.4.0在6月底刚刚发布,所以如果您自那时以来没有更新它,这将无法为您工作。 - IceCreamToucan
2
我认为bang-bang没有起作用,因为在dplyr管道中使用之前,question首先需要转换为quosure(question = enquo(question))。 {{question}}等同于!!enquo(question) - eipi10
可能可以,但我尝试了 df %>% mutate(!!question := recode(!! enquo(question), A = NA_character_)),并且遇到了与 !!question 相同的问题。再次强调,这可能是因为我使用的是 rlang 0.3.4 版本。 - aaron
2
你需要在第一个问题实例中也使用enquo才能达到等效的效果。 - IceCreamToucan

7
您可以通过允许输入一个重编码值向量作为参数来使函数更加灵活。例如:
library(tidyverse)
sample_df <- tibble(q1 = c("A", "B", "C"), q2 = c("B", "B", "A"))

fix_question <- function(df, question, recode.vec) {

  df %>% mutate({{question}} := recode({{question}}, !!!recode.vec))

}

fix_question(sample_df, q1, c(A=NA_character_, B="Was B"))
  q1    q2   
1 <NA>  B    
2 Was B B    
3 C     A
请注意,recode.vec是通过使用!!!进行“取消引用拼接”的。通过这个例子可以看出,它会将重新编码的值成对地“拼接”到recode函数中,以便作为...参数在recode中使用。本示例改编自使用dplyr编程指南(搜索“splice”以查看相关示例)。请注意,!!!如何执行此操作。保留了HTML标签。
x = c("A", "B", "C")
args = c(A=NA_character_, B="Was B")

quo(recode(x, !!!args))

<quosure>
expr: ^recode(x, A = <chr: NA>, B = "Was B")
env:  global

如果您想在多个列上潜在地运行重新编码功能,可以将其转换为一个只接受列名称和重新编码向量的函数。这种方法似乎更适合于管道式操作。
fix_question <- function(question, recode.vec) {

  recode({{question}}, !!!recode.vec)

}

sample_df %>% 
  mutate_at(vars(matches("q")), list(~fix_question(., c(A=NA_character_, B="Was B"))))
  q1    q2   
1 <NA>  Was B
2 Was B Was B
3 C     <NA>
或者重新编码单个列:
sample_df %>% 
  mutate(q1 = fix_question(q1, c(A=NA_character_, B="Was B")))

6

在这里,我们可以在:=的右侧指定sym以将其转换为符号并进行求值(!!)。

fix_question <- function(df, question) {
    df %>%
       mutate(!!question := recode(!! rlang::sym(question), "A" = NA_character_))
  }

fix_question(sample_df, "q1") 
# A tibble: 3 x 2
#  q1    q2   
#  <chr> <chr>
#1 <NA>  B    
#2 B     B    
#3 C     A    

更好的方法是使用ensym,它适用于带引号和不带引号的输入。
fix_question <- function(df, question) {
    question <- ensym(question)
    df %>%
       mutate(!!question := recode(!! question, "A" = NA_character_))
  }


fix_question(sample_df, q1)
# A tibble: 3 x 2
#  q1    q2   
#  <chr> <chr>
#1 <NA>  B    
#2 B     B    
#3 C     A    

fix_question(sample_df, "q1")
# A tibble: 3 x 2
#  q1    q2   
#  <chr> <chr>
#1 <NA>  B    
#2 B     B    
#3 C     A    

2
我曾尝试使用一些rlang转换函数,但显然没有选择正确的函数,不过你的方法可行——我认为我只需要在脑海中流程化类型转换。我的!!问题无法工作,因为它会逐字评估字符字符串。你的方法之所以有效,是因为它首先将字符字符串转换为符号,然后评估符号,返回向量。我只是无法理解这是操作顺序。再次感谢。 - aaron

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