【问题标题】:Pass arguments to dplyr functions将参数传递给 dplyr 函数
【发布时间】:2015-03-14 12:52:20
【问题描述】:

我想使用dplyr 对以下计算进行参数化,以找出Sepal.Length 的哪些值与Sepal.Width 的多个值相关联:

library(dplyr)

iris %>%
    group_by(Sepal.Length) %>%
    summarise(n.uniq=n_distinct(Sepal.Width)) %>%
    filter(n.uniq > 1)

通常我会这样写:

not.uniq.per.group <- function(data, group.var, uniq.var) {
    iris %>%
        group_by(group.var) %>%
        summarise(n.uniq=n_distinct(uniq.var)) %>%
        filter(n.uniq > 1)
}

但是,这种方法会引发错误,因为dplyr 使用了non-standard evaluation。这个函数应该怎么写?

【问题讨论】:

  • 作为风格问题,我建议不要在现代 R 中的名称中使用点,S3 泛型除外。这非常令人困惑。 dplyr 使用的(以及其他)命名约定要好得多:names_with_underscores。
  • 我知道Hadley Wickham's style guide 推荐使用下划线表示法,但Google R style guide 提倡使用句点(尽管不是用于函数,我在这里做过)​​。在其他语言中,句点用于成员访问(例如 javascript 中的 myArray.length),R 中是否存在其他冲突?
  • Google 风格指南通常很糟糕。在这个特定的例子中,问题在于它会导致 S3 方法产生歧义:some.class.method 是 some 类 class.method 的方法,还是 some.class 类 method 的方法?此外,当您的部分代码在 C(++) 中实现时,它会导致名称不一致,因为这不支持名称中的点,因此需要将后端函数名称映射到不同的 R 名称。

标签: r dplyr lazy-evaluation


【解决方案1】:

您需要使用dplyr 函数的标准评估版本(只需将“_”附加到函数名称,即group_by_ 和summarise_)并将字符串传递给您的函数,然后您需要变成符号。要参数化 summarise_ 的参数,您需要使用 interp(),它在 lazyeval 包中定义。具体来说:

library(dplyr)
library(lazyeval)

not.uniq.per.group <- function(df, grp.var, uniq.var) {
    df %>%
        group_by_(grp.var) %>%
        summarise_( n_uniq=interp(~n_distinct(v), v=as.name(uniq.var)) ) %>%
        filter(n_uniq > 1)
}

not.uniq.per.group(iris, "Sepal.Length", "Sepal.Width")

请注意,在最新版本的 dplyr 中,dplyr 函数的标准评估版本是 "soft deprecated",以支持非标准评估。

有关使用非标准评估的更多信息,请参阅Programming with dplyr vignette。

【讨论】:

    【解决方案2】:

    与最高 0.5 的旧 dplyr 版本一样,新的 dplyr 具有标准评估 (SE) 和非标准评估 (NSE) 的功能。但它们的表达方式与以前不同。

    如果你想要一个 NSE 函数,你pass bare expressions and use enquo to capture them as quosures。如果你想要一个 SE 函数,只需直接传递 quosures(或符号),然后在 dplyr 调用中取消引用它们。这是该问题的 SE 解决方案:

    library(tidyverse)
    library(rlang)
    
    f1 <- function(df, grp.var, uniq.var) {
       df %>%
           group_by(!!grp.var) %>%
           summarise(n_uniq = n_distinct(!!uniq.var)) %>%
           filter(n_uniq > 1)  
    }
    
    a <- f1(iris, quo(Sepal.Length), quo(Sepal.Width))
    b <- f1(iris, sym("Sepal.Length"), sym("Sepal.Width"))
    identical(a, b)
    #> [1] TRUE
    

    请注意 SE 版本如何让您使用字符串参数 - 只需先使用 sym() 将它们转换为符号。有关详细信息,请参阅 programming with dplyr 小插图。

    【讨论】:

      【解决方案3】:

      在dplyr(即将发布0.6.0)的开发版中,我们还可以使用稍微不同的语法来传递变量。

      f1 <- function(df, grp.var, uniq.var) {
         grp.var <- enquo(grp.var)
         uniq.var <- enquo(uniq.var)
      
         df %>%
             group_by(!!grp.var) %>%
             summarise(n_uniq = n_distinct(!!uniq.var)) %>%
             filter(n_uniq >1)  
      
      
      }
      
      res2 <- f1(iris, Sepal.Length, Sepal.Width) 
      res1 <- not.uniq.per.group(iris, "Sepal.Length", "Sepal.Width")
      identical(res1, res2)
      #[1] TRUE
      

      这里enquo 接受参数并返回值作为quosure(类似于base R 中的替换),通过懒惰地评估函数参数并在摘要中,我们要求它取消引用(!! 或UQ)所以它得到评估。

      【讨论】:

      • 如果我从列表中获取Sepal.Length 和Sepal.Width,它将不起作用,因为它将采用"Sepal.Length" 和"Sepal.Width" 的格式,那我该怎么办?
      • @KillerSnail 您应该发布一个新问题,因为此解决方案特定于 OP 帖子中提到的问题
      • @KillerSnail 的上述问题本质上就是我刚刚在这里提出的问题:stackoverflow.com/questions/46310123/…
      【解决方案4】:

      这是从 rlang 0.4 使用 curly curly {{ 伪运算符的方法:

      library(dplyr)
      
      not.uniq.per.group <- function(data, group.var, uniq.var) {
        data %>%
          group_by({{group.var}}) %>%
          summarise(n.uniq=n_distinct({{uniq.var}})) %>%
          filter(n.uniq > 1)
      }
      
      iris %>% not.uniq.per.group(Sepal.Length, Sepal.Width)
      #> # A tibble: 25 x 2
      #>    Sepal.Length n.uniq
      #>           <dbl>  <int>
      #>  1          4.4      3
      #>  2          4.6      4
      #>  3          4.8      3
      #>  4          4.9      5
      #>  5          5        8
      #>  6          5.1      6
      #>  7          5.2      4
      #>  8          5.4      4
      #>  9          5.5      6
      #> 10          5.6      5
      #> # ... with 15 more rows
      

      【讨论】:

        【解决方案5】:

        在当前版本的 dplyr (0.7.4) 中,不推荐使用标准评估函数版本(在函数名称后附加“_”,例如 group_by_)。 相反,您应该在编写函数时依赖 tidyeval。

        以下是您的函数的外观示例:

        # definition of your function
        not.uniq.per.group <- function(data, group.var, uniq.var) {
          # enquotes variables to be used with dplyr-functions
          group.var <- enquo(group.var)
          uniq.var <- enquo(uniq.var)
        
          # use '!!' before parameter names in dplyr-functions
          data %>%
            group_by(!!group.var) %>%
            summarise(n.uniq=n_distinct(!!uniq.var)) %>%
            filter(n.uniq > 1)
        }
        
        # call of your function
        not.uniq.per.group(iris, Sepal.Length, Sepal.Width)
        

        如果您想了解所有详细信息,dplyr-team 有一个 excellent vignette 来了解其工作原理。

        【讨论】:

          【解决方案6】:

          我过去编写了一个函数,它的功能与您正在做的类似,只是它探索主键之外的所有列并查找每个组的多个唯一值。

          find_dups = function(.table, ...) {
            require(dplyr)
            require(tidyr)
            # get column names of primary key
            pk <- .table %>% select(...) %>% names
            other <- names(.table)[!(names(.table) %in% pk)]
            # group by primary key,
            # get number of rows per unique combo,
            # filter for duplicates,
            # get number of distinct values in each column,
            # gather to get df of 1 row per primary key, other column,
            # filter for where a columns have more than 1 unique value,
            # order table by primary key
            .table %>%
              group_by(...) %>%
              mutate(cnt = n()) %>%
              filter(cnt > 1) %>%
              select(-cnt) %>%
              summarise_each(funs(n_distinct)) %>%
              gather_('column', 'unique_vals', other) %>%
              filter(unique_vals > 1) %>%
              arrange(...) %>%
              return
            # Final dataframe:
            ## One row per primary key and column that creates duplicates.
            ## Last column indicates how many unique values of
            ## the given column exist for each primary key.
          }
          

          此功能也适用于管道运算符:

          dat %>% find_dups(key1, key2)
          

          【讨论】:

            【解决方案7】:

            您可以通过使用do 调用匿名函数然后使用get 来避免lazyeval。该解决方案可以更普遍地用于采用多个聚合。我一般是单独写函数的。

            library(dplyr)
            
            not.uniq.per.group <- function(df, grp.var, uniq.var) {
              df %>%
                group_by_(grp.var) %>%
                do((function(., uniq.var) {
                  with(., data.frame(n_uniq = n_distinct(get(uniq.var))))
                }      
              )(., uniq.var)) %>%
              filter(n_uniq > 1)
            }
            
            not.uniq.per.group(iris, "Sepal.Length", "Sepal.Width")
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2018-01-08
              • 2017-06-06
              • 2015-07-01
              • 1970-01-01
              • 1970-01-01
              • 2017-10-16
              相关资源
              最近更新 更多