【问题标题】:R: Tidyverse selection semantics tidyselect::eval_select appending numbers to duplicatesR:Tidyverse 选择语义 tidyselect::eval_select 将数字附加到重复项
【发布时间】:2021-08-11 10:25:51
【问题描述】:

我正在尝试一段时间来了解 tidyverse 设计以及如何使用它进行编程。我试图编写一个使用 tidyselect 语义的函数,我发现 tidyselect::eval_select 将数字附加到 lhs 表达式。看到此语义用于列重命名也就不足为奇了。不幸的是,我的用于构建数据结构的函数不需要这种行为,它需要表达式的 lhs 中提供的常规名称(根据需要重复多次)。我还没有设法找出这种行为的来源。它似乎是make.unique,但我找不到它的实现位置。如果你知道,我很想知道,如果没有,解决我的问题不应该依赖它。 我想要的只是 lhs 名称没有附加数字,如示例中所示:

library(tidyverse)

# Data
data <- mtcars[, 8:11]

# Example
data %>%
  tidyselect::eval_select(rlang::expr(c(foo = 1, bar = c(2:4), foobar = c(1, "am", "gear", "carb"))), .)
#>     foo    bar1    bar2    bar3 foobar1 foobar2 foobar3 foobar4 
#>       1       2       3       4       1       2       3       4

# Function
test <- function(.data, ...) {
  loc <- tidyselect::eval_select(rlang::expr(c(...)), .data)
  names <- names(.data)
  list(names(loc), names[loc])
}

data %>%
  test(foo = 1, bar = c(2:4), foobar = c(1, "am", "gear", "carb"))
#> [[1]]
#> [1] "foo"     "bar1"    "bar2"    "bar3"    "foobar1" "foobar2" "foobar3"
#> [8] "foobar4"
#> 
#> [[2]]
#> [1] "vs"   "am"   "gear" "carb" "vs"   "am"   "gear" "carb"

reprex package (v2.0.0) 于 2021-05-22 创建

期望的输出:

#> [[1]]
#> [1] "foo"     "bar"    "bar"    "bar"    "foobar" "foobar" "foobar"
#> [8] "foobar"
#> 
#> [[2]]
#> [1] "vs"   "am"   "gear" "carb" "vs"   "am"   "gear" "carb"

非常感谢任何帮助。

【问题讨论】:

    标签: r dplyr tidyverse rlang tidyselect


    【解决方案1】:

    再次感谢@TimTeaFan 的详尽回答。我会将其保留为“正确”答案,因为我发现它非常有用。 我迟到了 tidyverse 的变量重命名规则。外部名称根据以下规则传播到所选元素: (1) 对于数据框,附加数字后缀,因为列必须唯一命名。 (2) 对于法线向量,名称只是简单地分配给所有选定的输入。

    因此,我将其发布为我自己问题的答案,因为它更容易,并且对于我创建简单数据结构的函数的目的可以达到相同的结果。我不确定这是否有任何缺点,但我在测试中看不到任何缺点。

    library(tidyverse)
    
    # Data
    data <- mtcars[, 8:11]
      
    # custom function
    test <- function(.data, ...) {
      data <- as.list(.data)
      loc <- tidyselect::eval_rename(rlang::expr(c(...)), data)
      names <- names(.data)
      list(names(loc), names[loc])
    }
    
    # test
    data %>%
      test(foo = 1, bar = c(2:4), foobar = c(1, "am", "gear", "carb"))
    #> [[1]]
    #> [1] "foo"    "bar"    "bar"    "bar"    "foobar" "foobar" "foobar" "foobar"
    #> 
    #> [[2]]
    #> [1] "vs"   "am"   "gear" "carb" "vs"   "am"   "gear" "carb"
    

    reprex package (v2.0.0) 于 2021 年 6 月 3 日创建

    【讨论】:

      【解决方案2】:

      问题是由一个名为ensure_named 的函数引起的,该函数深深嵌套在eval_selects 实现中。它是vars_select_eval 函数的一部分。

      ensure_named(pos, vars, uniquely_named, allow_rename)

      好消息是我们只需要覆盖uniquely_named 参数,这个参数是从第一个名为eval_select_impl 的实现函数进行的,该函数由eval_select 本身调用。所以我们需要做的就是重写tidyselect::eval_select

      为了得到想要的输出,我们需要做两件事:

      1. 添加uniquely_named = NULL作为参数并在调用函数时用FALSE指定
      2. 指定现有参数name_spec = "{outer}"。除非将uniquely_named 设置为FALSE,否则仅执行此步骤是不够的。

      在实际代码之前,请注意:

      tidyselect::eval_select 故意不允许列名重复。

      对于初学者来说,不可能轻松地创建具有重复列名的tibble

      tibble(a = 1:3, b = 4:6, a = 7:9)
      #> Error: Column name `a` must not be duplicated.
      #> Use .name_repair to specify repair.
      

      一种解决方法是使用带有tibble::new_tibble 的列表:

      tibble::new_tibble(list(a = 1:3, b = 4:6, a = 7:9), nrow = 3)
      #> # A tibble: 3 x 3
      #>       a     b     a
      #>   <int> <int> <int>
      #> 1     1     4     7
      #> 2     2     5     8
      #> 3     3     6     9
      

      对于data.frame,只有当check.names 参数设置为FALSE 时,才能创建非唯一名称:

      data.frame(a = 1:3, b = 4:6, a = 7:9, check.names = FALSE)
      #>   a b a
      #> 1 1 4 7
      #> 2 2 5 8
      #> 3 3 6 9
      

      但是当我们将这个data.frame 与常规的 {dplyr} 动词一起使用时,会抛出一个错误,告诉我们不能转换具有重复名称的数据帧:

      data.frame(a = 1:3, b = 4:6, a = 7:9, check.names = FALSE) %>% 
        mutate(c = 1:3)
      #> Error: Can't transform a data frame with duplicate names.
      

      因此我们可以假设不建议在 {tidyverse} 中使用具有重复名称的data.frames。这可能与整洁数据的概念相矛盾。

      话虽如此,下面是解决此问题的上述方法:

      library(tidyverse)
      
      # Data
      data <- mtcars[, 8:11]
      
      # custom eval_select function
      my_eval_select <- function(expr, data,
                                 env = rlang::caller_env(),
                                 ..., include = NULL, 
                                 exclude = NULL, strict = TRUE,
                                 name_spec = NULL,
                                 uniquely_named = NULL, # this is the new argument
                                 allow_rename = TRUE) {
        ellipsis::check_dots_empty()
        tidyselect:::eval_select_impl(data, names(data), rlang::as_quosure(expr, env), 
                         include = include, exclude = exclude, strict = strict, 
                         name_spec = name_spec, allow_rename = allow_rename,
                         uniquely_named = uniquely_named) # which we also add here
      }
      
      # example 1
      data %>%
        my_eval_select(rlang::expr(c(foo = 1, bar = c(2:4), foobar = c(1, "am", "gear", "carb"))),
                                data = .,
                                name_spec = "{outer}",  # we need to specify this
                                uniquely_named = FALSE) # and this
      #>    foo    bar    bar    bar foobar foobar foobar foobar 
      #>      1      2      3      4      1      2      3      4
      
      # example: custom function
      test <- function(.data, ...) {
        loc <- my_eval_select(rlang::expr(c(...)),
                              data = .data,
                              name_spec = "{outer}",
                              uniquely_named = FALSE)
        names <- names(.data)
        list(names(loc), names[loc])
      }
      
      # test
      data %>%
        test(foo = 1, bar = c(2:4), foobar = c(1, "am", "gear", "carb"))
      #> [[1]]
      #> [1] "foo"    "bar"    "bar"    "bar"    "foobar" "foobar" "foobar" "foobar"
      #> 
      #> [[2]]
      #> [1] "vs"   "am"   "gear" "carb" "vs"   "am"   "gear" "carb"
      

      reprex package (v0.3.0) 于 2021-05-22 创建

      【讨论】:

      • 谢谢你的详细解答,我学到了很多。我同意在数据帧或小标题中重复是一个非常糟糕的想法的原则。您是否认为在构建数据结构的情况下,像您提供的那样作为答案的函数也是不可取的(例如,每个向量都有唯一值的向量列表,但值在向量之间重复 - 值是列索引) ?
      • 视情况而定。如果这个函数和它产生的“不整齐”的数据结构是内部函数的一部分,那没问题。如果其他用户与函数和数据结构交互,那么它可能会造成混淆。一旦你使用了 {tidyeval} 语法,人们可能会期待整洁的输出。如果用户只有您和您的团队/同事,那么这很容易沟通。
      • 是的,“untidy”数据结构将是内部函数的一部分,但 main 函数只会提供整洁的输出。再次感谢您提供有用的回复和指导。
      猜你喜欢
      • 1970-01-01
      • 2011-10-28
      • 1970-01-01
      • 2020-03-23
      • 2018-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-08
      相关资源
      最近更新 更多