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

Use of tidyeval based non-standard evaluation in recode in right-hand side of mutate

考虑一个 tibble,其中每一列都是一个可以取很多值的字符向量——假设 "A" 到 "F"。

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

我希望创建一个函数,该函数将列名作为参数,并重新编码该列,以便任何答案 "A" 变为 NA,否则 df 将按原样返回。以这种方式设计它的原因是为了适应更广泛的管道,该管道使用给定的列执行一系列操作。

有很多方法可以做到这一点。但我有兴趣了解什么是最好的惯用 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 on 只是 returns 文字字符串(例如 "q1")。我最终采取了一种感觉像是 hacky 的方式来引用右侧的数据,使用基本 R [[ 运算符并依赖于 dplyr 的 . 构造,并且它有效,所以在感觉我已经解决了我的潜在问题:

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

我有兴趣从非常擅长 tidyeval 的人那里得到反馈,关于是否有更惯用的方法来做到这一点,希望看到一个有效的例子能加深我对 tidyeval 函数集的理解一般来说。有什么想法吗?

这里,在:=的右边,我们可以指定sym转换为symbol,然后求值(!!)

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    

如果您有rlang >= 0.4.0,您现在可以使用"curly curly"方法。

感谢@eipi10 的解释:

这将 quote-then-unquote 的两步过程合二为一,所以 {{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    

您还可以允许将重新编码值的向量作为参数输入,从而使函数更加灵活。例如:

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 是 "unquote-spliced" 和 !!!。你可以看到这个例子是做什么的,改编自 Programming with dplyr vignette(搜索 "splice" 以查看相关示例)。请注意 !!! "splices" 如何将这些值对重新编码到 recode 函数中,以便它们用作 recode 中的 ... 参数。

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")))