【问题标题】:Why does !! (bang-bang) combined with as.name() give a different output compared to !! or as.name() alone?为什么 !! (bang-bang) 结合 as.name() 与 !!还是 as.name() 单独?
【发布时间】:2021-03-09 13:13:56
【问题描述】:

我使用动态变量(例如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

干杯!

【问题讨论】:

  • 仅供参考 rlang 与 as.name 非常接近,称为 sym。在这里使用它会更惯用,并且可以节省一些击键。

标签: r dplyr tidyverse tidyeval nse


【解决方案1】:

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

准备工作:

library(tidyverse)
ID <- "birth_year"

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

实验 1:

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

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

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

实验 2:

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

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

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

这是一个基本的 R 函数,它接受一个字符串(或包含字符串的变量)并返回 字符串的内容作为变量名。 因此,如果您在基数 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() 它不应该“按原样”处理事物,而是用实际值替换。

实验 4:

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

现在可以了! :)

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

【讨论】:

    【解决方案2】:

    为方便起见,您还可以按照@Lionel Henry 在此comment 中的建议使用.data[[]]。另见rlang 0.4.0 release notes

    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 日创建

    【讨论】:

    猜你喜欢
    • 2013-02-14
    • 2019-09-16
    • 2016-01-24
    • 2011-06-26
    • 2017-05-14
    • 2022-01-06
    • 2016-04-17
    • 2020-01-18
    相关资源
    最近更新 更多