R:循环自定义 dplyr 函数
R: Looping over custom dplyr function
我想构建一个自定义 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 可以被寻址到使用迭代,则变量只能称为 "strings"。
基于 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)
}
决赛update/solution
在他的更新版本中,akrun 提供了一个解决方案,其中包含调用变量 x 的四种方式:
- 作为普通(非字符串)变量名:
sum_tbl(test_tbl, group)
- 作为字符串名称:
sum_tbl(test_tbl, "group")
- 作为索引向量:
sum_tbl(test_tbl, !!vars[1])
- 并作为
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)
}
我们可以只使用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))
或者另一种选择是转换为 sym
bol,然后在 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 中提到的条件下使用它 post
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
我想构建一个自定义 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 可以被寻址到使用迭代,则变量只能称为 "strings"。
基于 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)
}
决赛update/solution
在他的更新版本中,akrun 提供了一个解决方案,其中包含调用变量 x 的四种方式:
- 作为普通(非字符串)变量名:
sum_tbl(test_tbl, group)
- 作为字符串名称:
sum_tbl(test_tbl, "group")
- 作为索引向量:
sum_tbl(test_tbl, !!vars[1])
- 并作为
purr::map()
中的向量:map(vars, ~ sum_tbl(test_tbl, !!.x))
在 (3) 和 (4) 中需要使用 !!
.
如果我只为自己使用该功能,这不会有问题,但一旦其他团队成员使用该功能,我就需要解释,记录该功能。
为了避免这种情况,我现在扩展了 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)
}
我们可以只使用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))
或者另一种选择是转换为 sym
bol,然后在 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 中提到的条件下使用它 post
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