【问题标题】:R: Looping over custom dplyr functionR:循环自定义 dplyr 函数
【发布时间】:2019-08-08 17:38:03
【问题描述】:

我想构建一个自定义的 dplyr 函数并在理想情况下使用 purrr::map 对其进行迭代以保持在 tidyverse 中。

为了让事情尽可能简单,我使用一个非常简单的汇总函数来复制我的问题。

在使用 dplyr 构建自定义函数时,我遇到了非标准评估 (NSE) 的问题。我找到了三种不同的方法来处理它。当直接调用函数时,处理 NSE 的每种方法都可以正常工作,但在循环时则不行。下面你会找到复制我的问题的代码。使我的函数与 purrr::map 一起使用的正确方法是什么?

    # loading libraries
    library(dplyr)
    library(tidyr)
    library(purrr)

    # generate test data
    test_tbl <- rbind(tibble(group = rep(sample(letters[1:4], 150, TRUE), each = 4),
                             score = sample(0:10, size = 600, replace = TRUE)),

                      tibble(group = rep(sample(letters[5:7], 50, TRUE), each = 3),
                             score = sample(0:10, size = 150, replace = TRUE))
    )




    # generate two variables to loop over
    test_tbl$group2 <- test_tbl$group
    vars <- c("group", "group2")


    # summarise function 1 using enquo()
    sum_tbl1 <- function(df, x) {

        x <- dplyr::enquo(x)

        df %>%
            dplyr::group_by(!! x) %>%
            dplyr::summarise(score = mean(score, na.rm =TRUE),
                             n = dplyr::n())

    }

    # summarise function 2 using .dots = lazyeval
    sum_tbl2 <- function(df, x) {

        df %>%
            dplyr::group_by_(.dots = lazyeval::lazy(x)) %>%
            dplyr::summarize(score = mean(score, na.rm =TRUE),
                             n = dplyr::n())

    }

    # summarise function 3 using ensym()
    sum_tbl3 <- function(df, x) {

        df %>%
            dplyr::group_by(!!rlang::ensym(x)) %>%
            dplyr::summarize(score = mean(score, na.rm =TRUE),
                             n = dplyr::n())

    }


    # Looping over the functions with map
    # each variation produces an error no matter which function I choose

    # call within anonymous function without pipe
    map(vars, function(x) sum_tbl1(test_tbl, x))
    map(vars, function(x) sum_tbl2(test_tbl, x))
    map(vars, function(x) sum_tbl3(test_tbl, x))

    # call within anonymous function witin pipe
    map(vars, function(x) test_tbl %>% sum_tbl1(x))
    map(vars, function(x) test_tbl %>% sum_tbl2(x))
    map(vars, function(x) test_tbl %>% sum_tbl3(x))

    # call with formular notation without pipe
    map(vars, ~sum_tbl1(test_tbl, .x))
    map(vars, ~sum_tbl2(test_tbl, .x))
    map(vars, ~sum_tbl3(test_tbl, .x))

    # call with formular notation within pipe
    map(vars,  ~test_tbl %>% sum_tbl1(.x))
    map(vars,  ~test_tbl %>% sum_tbl2(.x))
    map(vars,  ~test_tbl %>% sum_tbl3(.x))

我知道还有其他解决方案可以在循环中生成汇总表,例如直接调用 map 并在 map 中创建匿名函数(参见下面的代码)。但是,我感兴趣的问题是如何处理循环中的 NSE。

# One possibility to create summarize tables in loops with map
 vars %>%
    map(function(x){
        test_tbl %>%
            dplyr::group_by(!!rlang::ensym(x)) %>%
            dplyr::summarize(score = mean(score, na.rm =TRUE),
                             n = dplyr::n())
    })

更新:

下面 akrun 提供了一个解决方案,可以通过 purrr::map() 进行调用。然而,直接调用函数只能通过直接调用分组变量作为字符串来实现

sum_tbl(test_tbl, “group”)

或间接作为

sum_tbl(test_tbl, vars[1])

在此解决方案中,无法以正常的 dplyr 方式将分组变量称为

sum_tbl(test_tbl, group)

最终,在我看来,自定义 dpylr 函数中 NSE 的解决方案可以在函数调用本身的级别解决问题,然后使用 map/lapply 是不可能的,或者 NSE 可以处理迭代,那么变量只能称为“字符串”。

在 akruns answer 的基础上,我构建了一个解决方法函数,该函数允许函数调用中的字符串和普通变量名。但是,肯定有更好的方法可以实现这一点。理想情况下,在自定义 dplyr 函数中处理 NSE 有一种更直接的方法,因此首先不需要像下面这样的解决方法。

sum_tbl <- function(df, x) {

        x_var <- dplyr::enquo(x)

        x_env <- rlang::get_env(x_var)

        if(identical(x_env,empty_env())) {

            # works, when x is a string and in loops via map/lapply
            sum_tbl <- df %>%
                dplyr::group_by(!! rlang::sym(x)) %>%
                dplyr::summarise(score = mean(score, na.rm = TRUE),
                                 n = dplyr::n())

        } else {
            # works, when x is a normal variable name without quotation marks
            x = dplyr::enquo(x)

            sum_tbl <- df %>%
                dplyr::group_by(!! x) %>%
                dplyr::summarise(score = mean(score, na.rm = TRUE),
                                 n = dplyr::n())
        }

        return(sum_tbl)
    }

最终更新/解决方案

在他的答案的更新版本中,akrun 提供了一个解决方案,它解释了调用变量 x 的四种方式:

  1. 作为普通(非字符串)变量名:sum_tbl(test_tbl, group)
  2. 作为字符串名称:sum_tbl(test_tbl, "group")
  3. 作为索引向量:sum_tbl(test_tbl, !!vars[1])
  4. 并作为purr::map()内的向量:map(vars, ~ sum_tbl(test_tbl, !!.x))

在 (3) 和 (4) 中,必须使用 !! 取消引用变量 x。

如果我只为自己使用该功能,这不是问题,但一旦其他团队成员使用该功能,我需要解释,记录该功能。

为了避免这种情况,我现在扩展了 akrun 的解决方案,以在不取消引用的情况下考虑所有四种方式。但是,我不确定此解决方案是否会造成其他陷阱。

sum_tbl <- function(df, x) {

    # if x is a symbol such as group without strings, than turn it into a string    
    if(is.symbol(get_expr(enquo(x))))  {

        x <- quo_name(enquo(x))

    # if x is a language object such as vars[1], evaluate it
    # (this turns it into a symbol), then turn it into a string
    } else if (is.language(get_expr(enquo(x))))  {

        x <- eval(x)
        x <- quo_name(enquo(x))

    } 

      # this part of the function works with normal strings as x
        sum_tbl <- df %>%
            dplyr::group_by(!! rlang::sym(x)) %>%
            dplyr::summarise(score = mean(score, na.rm = TRUE),
                             n = dplyr::n())

    return(sum_tbl)

}

【问题讨论】:

    标签: r loops dplyr purrr nse


    【解决方案1】:

    我们可以使用group_by_at,它可以接受一个字符串作为参数

    sum_tbl1 <- function(df, x) {
    
    
    
                df %>%
                    dplyr::group_by_at(x) %>%
                    dplyr::summarise(score = mean(score, na.rm =TRUE),
                                     n = dplyr::n())
    
            }
    

    然后调用为

    out1 <- map(vars, ~ sum_tbl1(test_tbl, .x))
    

    或者另一种选择是转换为symbol,然后在group_by 内评估(!!

    sum_tbl2 <- function(df, x) {
    
    
    
                df %>%
                    dplyr::group_by(!! rlang::sym(x)) %>%
                    dplyr::summarise(score = mean(score, na.rm =TRUE),
                                     n = dplyr::n())
    
            }
    
    out2 <- map(vars, ~ sum_tbl2(test_tbl, .x))
    
    identical(out1 , out2)
    #[1] TRUE
    

    如果我们指定其中一个参数,我们就不必提供第二个参数,因此也可以在没有匿名调用的情况下运行

    map(vars, sum_tbl2, df = test_tbl)
    

    更新

    如果我们想在更新的 OP 帖子中提到的条件下使用它

    sum_tbl3 <- function(df, x) {
    
               x1 <- enquo(x)
               x2 <- quo_name(x1)
    
                df %>%
                    dplyr::group_by_at(x2) %>%
                    dplyr::summarise(score = mean(score, na.rm =TRUE),
                                     n = dplyr::n())
    
            }
    
    
    sum_tbl3(test_tbl, group)
    # A tibble: 7 x 3
    #  group score     n
    #  <chr> <dbl> <int>
    #1 a      5.43   148
    #2 b      5.01   144
    #3 c      5.35   156
    #4 d      5.19   152
    #5 e      5.65    72
    #6 f      5.31    36
    #7 g      5.24    42
    
    sum_tbl3(test_tbl, "group")
    # A tibble: 7 x 3
    #  group score     n
    #  <chr> <dbl> <int>
    #1 a      5.43   148
    #2 b      5.01   144
    #3 c      5.35   156
    #4 d      5.19   152
    #5 e      5.65    72
    #6 f      5.31    36
    #7 g      5.24    42
    

    或从'vars'调用

    sum_tbl3(test_tbl, !!vars[1])
    # A tibble: 7 x 3
    #  group score     n
    #  <chr> <dbl> <int>
    #1 a      5.43   148
    #2 b      5.01   144
    #3 c      5.35   156
    #4 d      5.19   152
    #5 e      5.65    72
    #6 f      5.31    36
    #7 g      5.24    42
    

    map

    map(vars, ~ sum_tbl3(test_tbl, !!.x))
    #[[1]]
    # A tibble: 7 x 3
    #  group score     n
    #  <chr> <dbl> <int>
    #1 a      5.43   148
    #2 b      5.01   144
    #3 c      5.35   156
    #4 d      5.19   152
    #5 e      5.65    72
    #6 f      5.31    36
    #7 g      5.24    42
    
    #[[2]]
    # A tibble: 7 x 3
    #  group2 score     n
    #  <chr>  <dbl> <int>
    #1 a       5.43   148
    #2 b       5.01   144
    #3 c       5.35   156
    #4 d       5.19   152
    #5 e       5.65    72
    #6 f       5.31    36
    #7 g       5.24    42
    

    【讨论】:

    • 非常感谢您的回答!这个解决方案是正确的。我只是想知道为什么对 NSE 和 dplyr 以及我在网上找到的所有解决方法有如此大的影响,包括关于使用 dplyr 编程的 tidyverse 小插图(dplyr.tidyverse.org/articles/programming.html),当有像 group_by_at 这样简单的解决方案时。
    • @Hellwalker 使用tidyverse,每天都在构建多个函数。所以,当文章被创建时,可能只有几种处理方法
    • 在玩弄了您的解决方案后,我意识到这有助于通过 map() 进行调用,但现在直接调用该函数会产生错误。一个理想的解决方案可以直接和通过 map/lapply 两种方式工作。目前看来,NSE 的问题只能在一个层面上解决:直接调用函数或创建只能在迭代中使用的函数。
    • @Hellwalker 对不起,我不明白你是怎么打电话的。 sum_tbl1(test_tbl, vars[1]) 对我来说很好,或者 sum_tbl1(test_tbl1, "group") 如果你有兴趣取消引用,那么你函数中的 enquo 就会发挥作用
    • 基于您更新的答案,我想出了一种方法,在函数调用中不需要取消引用。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多