为什么 !! (bang-bang) 结合 as.name() 给出与 !! 不同的输出还是 as.name() 一个人?

Why does !! (bang-bang) combined with as.name() give a different output compared to !! or as.name() alone?

我使用动态变量(例如 ID)作为引用列名的方式,该列名将根据我当时正在处理的基因而改变。然后,我在 mutate 中使用 case_when 创建一个新列,该列的值取决于动态列。

我认为 !! (bang-bang) 是我需要强制评估变量内容的东西;但是,我在新专栏中没有得到预期的输出。只有 !!as.name 给了我预期的输出,我不完全明白为什么。有人可以解释为什么在这种情况下仅使用 !! 是不合适的以及 !!as.name 中发生了什么?

这是我编写的一个简单的可重现示例,用于演示我正在经历的事情:

library(tidyverse)

ID <- "birth_year"

# Correct output
test <- starwars %>%
  mutate(FootballLeague = case_when(
    !!as.name(ID) < 10 ~ "U10",
    !!as.name(ID) >= 10 & !!as.name(ID) < 50 ~ "U50",
    !!as.name(ID) >= 50 & !!as.name(ID) < 100 ~ "U100",
    !!as.name(ID) >= 100 ~ "Senior",
    TRUE ~ "Others"
  ))

# Incorrect output
test2 <- starwars %>%
  mutate(FootballLeague = case_when(
    !!(ID) < 10 ~ "U10",
    !!(ID) >= 10 & !!(ID) < 50 ~ "U50",
    !!(ID) >= 50 & !!(ID) < 100 ~ "U100",
    !!(ID) >= 100 ~ "Senior",
    TRUE ~ "Others"
  ))

# Incorrect output
test3 <- starwars %>%
  mutate(FootballLeague = case_when(
    as.name(ID) < 10 ~ "U10",
    as.name(ID) >= 10 & as.name(ID) < 50 ~ "U50",
    as.name(ID) >= 50 & as.name(ID) < 100 ~ "U100",
    as.name(ID) >= 100 ~ "Senior",
    TRUE ~ "Others"
  ))

identical(test, test2)
# FALSE

identical(test2, test3)
# TRUE

sessionInfo()
#R version 4.0.2 (2020-06-22)
#Platform: x86_64-centos7-linux-gnu (64-bit)
#Running under: CentOS Linux 7 (Core)

# tidyverse_1.3.0
# dplyr_1.0.2

干杯!

您可以将表达式包裹在函数 quo() 中,以在应用 !! 运算符后查看运算结果。为简单起见,我将使用较短的表达式进行演示:

准备工作:

library(tidyverse)
ID <- "birth_year"

## Test without quasiquotation:
starwars %>% 
  filter(birth_year < 50)

实验一:

quo(
  starwars %>% 
    filter(ID < 50)
)
## result: starwars %>% filter(ID < 50)

我们了解到:filter() 不会将 ID 视为变量,而是“按原样”处理。所以我们需要一种机制来告诉 filter() 它应该把 ID 当作变量,并且它应该使用它的值。

--> !! 运算符可用于告诉 filter() 它应该将表达式视为变量并替换其值。

实验二:

quo(
  starwars %>% 
    filter(!!ID < 50)
) 
## result: starwars %>% filter("birth_year" < 50)

我们了解到:!! 运算符确实有效:ID 被替换为它的值。但是:ID 的值是 字符串 "birth_year"。注意结果中的引号。但是您可能知道,tidyverse 函数不将变量名作为字符串,它们需要原始名称,不带引号。与实验 1 比较:filter() 接受所有“原样”,因此它查找名为 "birth_year" 的列(包括引号!)

函数as.name()有什么作用?

这是一个基本的 R 函数,它接受一个字符串(或一个包含字符串的变量)和 returns 字符串的 内容 作为变量名。 所以如果你在 base R 中调用 as.name(ID),结果是 birth_year,这次没有引号——就像 tidyverse 期望的那样。那么让我们来试试吧:

实验 3:

quo(
  starwars %>% 
    filter(as.name(ID) < 50)
) 
## result: starwars %>% filter(as.name(ID) < 50)

我们了解到:这没有用,因为,filter() 再次“按原样”处理所有内容。所以现在它查找名为 as.name(ID) 的列,当然它不存在。

--> 我们需要结合这两件事来让它工作:

  1. 使用as.name()将字符串转换为变量名。
  2. 使用!!告诉filter()它不应该“按原样”接受事物,而是替换真实值。

实验四:

quo(
  starwars %>% 
    filter(!!as.name(ID) < 50)
) 
## result: starwars %>% filter(birth_year < 50)

现在可以了! :)

我在实验中使用了 filter(),但它与 mutate() 和其他 tidyverse 函数完全相同。

为了方便起见,您还可以按照@Lionel Henry 在此 . See also rlang 0.4.0 release notes

中的建议使用 .data[[]]
library(tidyverse)

ID <- "birth_year"

# Correct output
test <- starwars %>%
  mutate(FootballLeague = case_when(
    !!as.name(ID) < 10 ~ "U10",
    !!as.name(ID) >= 10 & !!as.name(ID) < 50 ~ "U50",
    !!as.name(ID) >= 50 & !!as.name(ID) < 100 ~ "U100",
    !!as.name(ID) >= 100 ~ "Senior",
    TRUE ~ "Others"
  ))
test

使用.data

test2 <- starwars %>%
  mutate(FootballLeague = case_when(
    .data[[ID]]   < 10 ~ "U10",
    .data[[ID]]  >= 10 & .data[[ID]]  < 50 ~ "U50",
    .data[[ID]]  >= 50 & .data[[ID]]  < 100 ~ "U100",
    .data[[ID]]  >= 100 ~ "Senior",
    TRUE ~ "Others"
  ))
test2
#> # A tibble: 87 x 15
#>    name               height  mass hair_color    skin_color  eye_color
#>    <chr>               <int> <dbl> <chr>         <chr>       <chr>    
#>  1 Luke Skywalker        172    77 blond         fair        blue     
#>  2 C-3PO                 167    75 <NA>          gold        yellow   
#>  3 R2-D2                  96    32 <NA>          white, blue red      
#>  4 Darth Vader           202   136 none          white       yellow   
#>  5 Leia Organa           150    49 brown         light       brown    
#>  6 Owen Lars             178   120 brown, grey   light       blue     
#>  7 Beru Whitesun lars    165    75 brown         light       blue     
#>  8 R5-D4                  97    32 <NA>          white, red  red      
#>  9 Biggs Darklighter     183    84 black         light       brown    
#> 10 Obi-Wan Kenobi        182    77 auburn, white fair        blue-gray
#> 11 Anakin Skywalker      188    84 blond         fair        blue     
#> 12 Wilhuff Tarkin        180    NA auburn, grey  fair        blue     
#> 13 Chewbacca             228   112 brown         unknown     blue     
#> 14 Han Solo              180    80 brown         fair        brown    
#> 15 Greedo                173    74 <NA>          green       black    
#>    birth_year sex    gender    homeworld species films     vehicles  starships
#>         <dbl> <chr>  <chr>     <chr>     <chr>   <list>    <list>    <list>   
#>  1       19   male   masculine Tatooine  Human   <chr [5]> <chr [2]> <chr [2]>
#>  2      112   none   masculine Tatooine  Droid   <chr [6]> <chr [0]> <chr [0]>
#>  3       33   none   masculine Naboo     Droid   <chr [7]> <chr [0]> <chr [0]>
#>  4       41.9 male   masculine Tatooine  Human   <chr [4]> <chr [0]> <chr [1]>
#>  5       19   female feminine  Alderaan  Human   <chr [5]> <chr [1]> <chr [0]>
#>  6       52   male   masculine Tatooine  Human   <chr [3]> <chr [0]> <chr [0]>
#>  7       47   female feminine  Tatooine  Human   <chr [3]> <chr [0]> <chr [0]>
#>  8       NA   none   masculine Tatooine  Droid   <chr [1]> <chr [0]> <chr [0]>
#>  9       24   male   masculine Tatooine  Human   <chr [1]> <chr [0]> <chr [1]>
#> 10       57   male   masculine Stewjon   Human   <chr [6]> <chr [1]> <chr [5]>
#> 11       41.9 male   masculine Tatooine  Human   <chr [3]> <chr [2]> <chr [3]>
#> 12       64   male   masculine Eriadu    Human   <chr [2]> <chr [0]> <chr [0]>
#> 13      200   male   masculine Kashyyyk  Wookiee <chr [5]> <chr [1]> <chr [2]>
#> 14       29   male   masculine Corellia  Human   <chr [4]> <chr [0]> <chr [2]>
#> 15       44   male   masculine Rodia     Rodian  <chr [1]> <chr [0]> <chr [0]>
#>    FootballLeague
#>    <chr>         
#>  1 U50           
#>  2 Senior        
#>  3 U50           
#>  4 U50           
#>  5 U50           
#>  6 U100          
#>  7 U50           
#>  8 Others        
#>  9 U50           
#> 10 U100          
#> 11 U50           
#> 12 U100          
#> 13 Senior        
#> 14 U50           
#> 15 U50           
#> # ... with 72 more rows

检查它们是否相同

identical(test, test2)
#> [1] TRUE

reprex package (v0.3.0)

于 2020-11-26 创建