使用 dplyr 和 NSE 动态构造具有不同参数的函数调用
Dynamically construct function calls with varying arguments using dplyr and NSE
我希望能够使用 dplyr 通过不同的分组 variables/arguments 动态构建函数调用。函数调用的数量可能非常大,这意味着 programming with dplyr 小插图中的示例不实用。理想情况下,我希望能够预先构建一个对象(例如列表),该对象存储要在每个函数调用中传递的 arguments/variables 。下面是一个示例数据集,我们希望在其中应用一些基于不断变化的分组变量的汇总函数。
set.seed(1)
df <- data.frame(values = sample(x = 1:10, size = 10),
grouping_var1 = sample(x = letters[1:2], size = 10, replace = TRUE),
grouping_var2 = sample(x = letters[24:26], size = 10, replace = TRUE),
grouping_var3 = sample(x = LETTERS[1:2], size = 10, replace = TRUE))
> df
values grouping_var1 grouping_var2 grouping_var3
1 9 a x B
2 4 a z B
3 7 a x A
4 1 a x B
5 2 a x A
6 5 b x A
7 3 b y B
8 10 b x A
9 6 b x B
10 8 a y B
根据 programming with dplyr 小插图,我们可以提出这样的解决方案:
f <- function(df, ...){
group_var <- enquos(...)
df %>%
group_by(!!! group_var) %>%
summarise_at(.vars = "values", .funs = sum) %>%
print(n = 10)
}
> f(df, grouping_var1)
# A tibble: 2 x 2
grouping_var1 values
<fct> <int>
1 a 31
2 b 24
> f(df, grouping_var1, grouping_var2)
# A tibble: 5 x 3
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values
<fct> <fct> <int>
1 a x 19
2 a y 8
3 a z 4
4 b x 21
5 b y 3
如果我想构造大量的调用,上面的例子是不切实际和不灵活的。另一个限制是,我可能希望包含的其他信息不能轻易地一起传递或与分组变量一起传递。
假设我有一个列表,其中包含要在每个函数调用中传递的分组变量。还假设对于这些列表元素中的每一个,都有一个单独的字段,其中 "id" 描述了所执行的分组。请参阅下面的示例:
list(group_vars = list(c("grouping_var1"),
c("grouping_var1", "grouping_var2"),
c("grouping_var1", "grouping_var3")),
group_ids = list("var_1",
c("var_1_2"),
c("var_1_3")))
如何将这些 arguments/variables 和 ID 的列表动态传递给函数调用,并使用 dplyr 成功评估它们?假设我想在结果数据框中创建一个列,除了汇总数据外,它还包含 group_ids。例如,如果我的 group_vars
是 c("grouping_var1", "grouping_var2")
而 group_ids
是 "var_1_2"
对于特定的函数调用,我希望输出:
# A tibble: 5 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values group_ids
<fct> <fct> <int> <chr>
1 a x 19 var_1_2
2 a y 8 var_1_2
3 a z 4 var_1_2
4 b x 21 var_1_2
5 b y 3 var_1_2
我希望看到一个解决方案来实现此 而无需 使用现在已弃用的 group_by_
接受字符串的函数。
在结束语中,我觉得使用 NSE 在函数中使用 dplyr 编程有这样的进入障碍,这让人感到相当沮丧。每当我遇到一些应该很简单的事情时,我都需要花费数小时才能找到解决方案。
传递要用作分组变量的字符串列表时,一种解决方案是使用 rlang::syms()
.
f <- function(df, group_var){
# Allows us to pass dplyr variables as strings in a list
my_group_vars <- syms(group_var$group_var)
df %>%
group_by(!!! my_group_vars) %>%
summarise_at(.vars = "values", .funs = sum) %>%
mutate(group_ids = group_var$group_ids)
}
params_list <- list(
list(group_var = c("grouping_var1"), group_ids = "var_1"),
list(group_var = c("grouping_var1", "grouping_var2"), group_ids = "var_1_2"),
list(group_var = c("grouping_var1", "grouping_var3"), group_ids = "var_1_3")
)
lapply(params_list, f, df = df)
[[1]]
# A tibble: 2 x 3
grouping_var1 values group_ids
<fct> <int> <chr>
1 a 31 var_1
2 b 24 var_1
[[2]]
# A tibble: 5 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values group_ids
<fct> <fct> <int> <chr>
1 a x 19 var_1_2
2 a y 8 var_1_2
3 a z 4 var_1_2
4 b x 21 var_1_2
5 b y 3 var_1_2
[[3]]
# A tibble: 4 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var3 values group_ids
<fct> <fct> <int> <chr>
1 a A 9 var_1_3
2 a B 22 var_1_3
3 b A 15 var_1_3
4 b B 9 var_1_3
我不确定这里的 "standard" tidyverse 方法是什么,因为当我尝试为我的典型代码编写广义的 tidyverse 函数时,我从来不知道我是否 "doing it right"工作流程,但这是另一种方法。*
首先,我们可以生成分组列组合的列表,而不是对它们进行硬编码。在这种情况下,该列表包括 1、2 或 3 个分组列的所有可能组合,但可以根据需要进行缩减。
library(tidyverse)
# Generate a list of combinations of grouping variables.
groups.list = map(1:3, ~combn(names(df)[map_lgl(df, ~!is.numeric(.))], .x, simplify=FALSE)) %>%
flatten
下面是一个使用group_by_at
的汇总函数,它可以接受字符串,所以不需要非标准的评估。此外,我们从 group_vars
本身获取 group.ids
值,因此我们不需要单独的参数或参数(尽管这可能需要调整,具体取决于您对名称的期望)分组列)。
# Summarise for each combination of groups
# Generate group.ids from group_vars itself
f2 <- function(data, group_vars) {
data %>%
group_by_at(group_vars) %>%
summarise(values=sum(values)) %>%
mutate(group.ids=paste0("var_", paste(str_extract(group_vars, "[0-9]"), collapse="_")))
}
现在我们可以 运行 运行 group.list
的每个元素上的函数
map(groups.list, ~f2(df, .x))
[[1]]
# A tibble: 2 x 3
grouping_var1 values group.ids
<fct> <int> <chr>
1 a 31 var_1
2 b 24 var_1
[[2]]
# A tibble: 3 x 3
grouping_var2 values group.ids
<fct> <int> <chr>
1 x 40 var_2
2 y 11 var_2
3 z 4 var_2
[[3]]
# A tibble: 2 x 3
grouping_var3 values group.ids
<fct> <int> <chr>
1 A 24 var_3
2 B 31 var_3
[[4]]
# A tibble: 5 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values group.ids
<fct> <fct> <int> <chr>
1 a x 19 var_1_2
2 a y 8 var_1_2
3 a z 4 var_1_2
4 b x 21 var_1_2
5 b y 3 var_1_2
[[5]]
# A tibble: 4 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var3 values group.ids
<fct> <fct> <int> <chr>
1 a A 9 var_1_3
2 a B 22 var_1_3
3 b A 15 var_1_3
4 b B 9 var_1_3
[[6]]
# A tibble: 4 x 4
# Groups: grouping_var2 [3]
grouping_var2 grouping_var3 values group.ids
<fct> <fct> <int> <chr>
1 x A 24 var_2_3
2 x B 16 var_2_3
3 y B 11 var_2_3
4 z B 4 var_2_3
[[7]]
# A tibble: 7 x 5
# Groups: grouping_var1, grouping_var2 [5]
grouping_var1 grouping_var2 grouping_var3 values group.ids
<fct> <fct> <fct> <int> <chr>
1 a x A 9 var_1_2_3
2 a x B 10 var_1_2_3
3 a y B 8 var_1_2_3
4 a z B 4 var_1_2_3
5 b x A 15 var_1_2_3
6 b x B 6 var_1_2_3
7 b y B 3 var_1_2_3
或者,如果您想合并所有结果,您可以这样做:
map(groups.list, ~f2(df, .x)) %>%
bind_rows() %>%
mutate_if(is.factor, fct_explicit_na, na_level="All") %>%
select(group.ids, matches("grouping"), values)
group.ids grouping_var1 grouping_var2 grouping_var3 values
<chr> <fct> <fct> <fct> <int>
1 var_1 a All All 31
2 var_1 b All All 24
3 var_2 All x All 40
4 var_2 All y All 11
5 var_2 All z All 4
6 var_3 All All A 24
7 var_3 All All B 31
8 var_1_2 a x All 19
9 var_1_2 a y All 8
10 var_1_2 a z All 4
11 var_1_2 b x All 21
12 var_1_2 b y All 3
13 var_1_3 a All A 9
14 var_1_3 a All B 22
15 var_1_3 b All A 15
16 var_1_3 b All B 9
17 var_2_3 All x A 24
18 var_2_3 All x B 16
19 var_2_3 All y B 11
20 var_2_3 All z B 4
21 var_1_2_3 a x A 9
22 var_1_2_3 a x B 10
23 var_1_2_3 a y B 8
24 var_1_2_3 a z B 4
25 var_1_2_3 b x A 15
26 var_1_2_3 b x B 6
27 var_1_2_3 b y B 3
- 这个问题被交叉发布到 RStudio Community,我也在那里添加了这个答案。
我希望能够使用 dplyr 通过不同的分组 variables/arguments 动态构建函数调用。函数调用的数量可能非常大,这意味着 programming with dplyr 小插图中的示例不实用。理想情况下,我希望能够预先构建一个对象(例如列表),该对象存储要在每个函数调用中传递的 arguments/variables 。下面是一个示例数据集,我们希望在其中应用一些基于不断变化的分组变量的汇总函数。
set.seed(1)
df <- data.frame(values = sample(x = 1:10, size = 10),
grouping_var1 = sample(x = letters[1:2], size = 10, replace = TRUE),
grouping_var2 = sample(x = letters[24:26], size = 10, replace = TRUE),
grouping_var3 = sample(x = LETTERS[1:2], size = 10, replace = TRUE))
> df
values grouping_var1 grouping_var2 grouping_var3
1 9 a x B
2 4 a z B
3 7 a x A
4 1 a x B
5 2 a x A
6 5 b x A
7 3 b y B
8 10 b x A
9 6 b x B
10 8 a y B
根据 programming with dplyr 小插图,我们可以提出这样的解决方案:
f <- function(df, ...){
group_var <- enquos(...)
df %>%
group_by(!!! group_var) %>%
summarise_at(.vars = "values", .funs = sum) %>%
print(n = 10)
}
> f(df, grouping_var1)
# A tibble: 2 x 2
grouping_var1 values
<fct> <int>
1 a 31
2 b 24
> f(df, grouping_var1, grouping_var2)
# A tibble: 5 x 3
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values
<fct> <fct> <int>
1 a x 19
2 a y 8
3 a z 4
4 b x 21
5 b y 3
如果我想构造大量的调用,上面的例子是不切实际和不灵活的。另一个限制是,我可能希望包含的其他信息不能轻易地一起传递或与分组变量一起传递。
假设我有一个列表,其中包含要在每个函数调用中传递的分组变量。还假设对于这些列表元素中的每一个,都有一个单独的字段,其中 "id" 描述了所执行的分组。请参阅下面的示例:
list(group_vars = list(c("grouping_var1"),
c("grouping_var1", "grouping_var2"),
c("grouping_var1", "grouping_var3")),
group_ids = list("var_1",
c("var_1_2"),
c("var_1_3")))
如何将这些 arguments/variables 和 ID 的列表动态传递给函数调用,并使用 dplyr 成功评估它们?假设我想在结果数据框中创建一个列,除了汇总数据外,它还包含 group_ids。例如,如果我的 group_vars
是 c("grouping_var1", "grouping_var2")
而 group_ids
是 "var_1_2"
对于特定的函数调用,我希望输出:
# A tibble: 5 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values group_ids
<fct> <fct> <int> <chr>
1 a x 19 var_1_2
2 a y 8 var_1_2
3 a z 4 var_1_2
4 b x 21 var_1_2
5 b y 3 var_1_2
我希望看到一个解决方案来实现此 而无需 使用现在已弃用的 group_by_
接受字符串的函数。
在结束语中,我觉得使用 NSE 在函数中使用 dplyr 编程有这样的进入障碍,这让人感到相当沮丧。每当我遇到一些应该很简单的事情时,我都需要花费数小时才能找到解决方案。
传递要用作分组变量的字符串列表时,一种解决方案是使用 rlang::syms()
.
f <- function(df, group_var){
# Allows us to pass dplyr variables as strings in a list
my_group_vars <- syms(group_var$group_var)
df %>%
group_by(!!! my_group_vars) %>%
summarise_at(.vars = "values", .funs = sum) %>%
mutate(group_ids = group_var$group_ids)
}
params_list <- list(
list(group_var = c("grouping_var1"), group_ids = "var_1"),
list(group_var = c("grouping_var1", "grouping_var2"), group_ids = "var_1_2"),
list(group_var = c("grouping_var1", "grouping_var3"), group_ids = "var_1_3")
)
lapply(params_list, f, df = df)
[[1]]
# A tibble: 2 x 3
grouping_var1 values group_ids
<fct> <int> <chr>
1 a 31 var_1
2 b 24 var_1
[[2]]
# A tibble: 5 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var2 values group_ids
<fct> <fct> <int> <chr>
1 a x 19 var_1_2
2 a y 8 var_1_2
3 a z 4 var_1_2
4 b x 21 var_1_2
5 b y 3 var_1_2
[[3]]
# A tibble: 4 x 4
# Groups: grouping_var1 [2]
grouping_var1 grouping_var3 values group_ids
<fct> <fct> <int> <chr>
1 a A 9 var_1_3
2 a B 22 var_1_3
3 b A 15 var_1_3
4 b B 9 var_1_3
我不确定这里的 "standard" tidyverse 方法是什么,因为当我尝试为我的典型代码编写广义的 tidyverse 函数时,我从来不知道我是否 "doing it right"工作流程,但这是另一种方法。*
首先,我们可以生成分组列组合的列表,而不是对它们进行硬编码。在这种情况下,该列表包括 1、2 或 3 个分组列的所有可能组合,但可以根据需要进行缩减。
library(tidyverse)
# Generate a list of combinations of grouping variables.
groups.list = map(1:3, ~combn(names(df)[map_lgl(df, ~!is.numeric(.))], .x, simplify=FALSE)) %>%
flatten
下面是一个使用group_by_at
的汇总函数,它可以接受字符串,所以不需要非标准的评估。此外,我们从 group_vars
本身获取 group.ids
值,因此我们不需要单独的参数或参数(尽管这可能需要调整,具体取决于您对名称的期望)分组列)。
# Summarise for each combination of groups
# Generate group.ids from group_vars itself
f2 <- function(data, group_vars) {
data %>%
group_by_at(group_vars) %>%
summarise(values=sum(values)) %>%
mutate(group.ids=paste0("var_", paste(str_extract(group_vars, "[0-9]"), collapse="_")))
}
现在我们可以 运行 运行 group.list
map(groups.list, ~f2(df, .x))
[[1]] # A tibble: 2 x 3 grouping_var1 values group.ids <fct> <int> <chr> 1 a 31 var_1 2 b 24 var_1 [[2]] # A tibble: 3 x 3 grouping_var2 values group.ids <fct> <int> <chr> 1 x 40 var_2 2 y 11 var_2 3 z 4 var_2 [[3]] # A tibble: 2 x 3 grouping_var3 values group.ids <fct> <int> <chr> 1 A 24 var_3 2 B 31 var_3 [[4]] # A tibble: 5 x 4 # Groups: grouping_var1 [2] grouping_var1 grouping_var2 values group.ids <fct> <fct> <int> <chr> 1 a x 19 var_1_2 2 a y 8 var_1_2 3 a z 4 var_1_2 4 b x 21 var_1_2 5 b y 3 var_1_2 [[5]] # A tibble: 4 x 4 # Groups: grouping_var1 [2] grouping_var1 grouping_var3 values group.ids <fct> <fct> <int> <chr> 1 a A 9 var_1_3 2 a B 22 var_1_3 3 b A 15 var_1_3 4 b B 9 var_1_3 [[6]] # A tibble: 4 x 4 # Groups: grouping_var2 [3] grouping_var2 grouping_var3 values group.ids <fct> <fct> <int> <chr> 1 x A 24 var_2_3 2 x B 16 var_2_3 3 y B 11 var_2_3 4 z B 4 var_2_3 [[7]] # A tibble: 7 x 5 # Groups: grouping_var1, grouping_var2 [5] grouping_var1 grouping_var2 grouping_var3 values group.ids <fct> <fct> <fct> <int> <chr> 1 a x A 9 var_1_2_3 2 a x B 10 var_1_2_3 3 a y B 8 var_1_2_3 4 a z B 4 var_1_2_3 5 b x A 15 var_1_2_3 6 b x B 6 var_1_2_3 7 b y B 3 var_1_2_3
或者,如果您想合并所有结果,您可以这样做:
map(groups.list, ~f2(df, .x)) %>%
bind_rows() %>%
mutate_if(is.factor, fct_explicit_na, na_level="All") %>%
select(group.ids, matches("grouping"), values)
group.ids grouping_var1 grouping_var2 grouping_var3 values <chr> <fct> <fct> <fct> <int> 1 var_1 a All All 31 2 var_1 b All All 24 3 var_2 All x All 40 4 var_2 All y All 11 5 var_2 All z All 4 6 var_3 All All A 24 7 var_3 All All B 31 8 var_1_2 a x All 19 9 var_1_2 a y All 8 10 var_1_2 a z All 4 11 var_1_2 b x All 21 12 var_1_2 b y All 3 13 var_1_3 a All A 9 14 var_1_3 a All B 22 15 var_1_3 b All A 15 16 var_1_3 b All B 9 17 var_2_3 All x A 24 18 var_2_3 All x B 16 19 var_2_3 All y B 11 20 var_2_3 All z B 4 21 var_1_2_3 a x A 9 22 var_1_2_3 a x B 10 23 var_1_2_3 a y B 8 24 var_1_2_3 a z B 4 25 var_1_2_3 b x A 15 26 var_1_2_3 b x B 6 27 var_1_2_3 b y B 3
- 这个问题被交叉发布到 RStudio Community,我也在那里添加了这个答案。