基于tidyeval的R自定义函数如何提取分组参数并实现多层级分组小计
实现方案
我们可以借助rlang::eval_select捕获用户传入的分组变量对应的列名,再基于分组数量动态生成多层小计结果,最后合并排序输出即可。
问题1解答:提取grps参数的第一个列名
你可以用rlang::eval_select直接解析用户传入的tidy选择语法,拿到所有分组变量的有序字符串向量,直接取第一位即可使用:
# 放在函数内部即可捕获分组列名 grp_names <- names(rlang::eval_select(rlang::expr({{ grps }}), mtcars)) # 第一个分组列名 first_grp <- grp_names[1]
后续操作该列时,既可以用.data[[first_grp]]的方式引用,也可以在mutate中用:=赋值。
问题2解答:支持3个及以上分组变量的通用实现
通用逻辑如下:
- 先计算所有分组维度的明细统计结果,同时把所有分组列转成字符型,避免后续插入
total时出现类型冲突 - 如果开启小计,按「去掉最后1个分组→去掉最后2个分组→…→只剩第1个分组」的规则逐层计算小计,每一层的小计结果都将被去掉的分组列赋值为
total - 合并明细和所有小计结果,按分组列排序,保证
total行固定出现在对应分组的末尾
完整可运行代码如下:
library(dplyr) library(rlang) test_function <- function(grps, subtotalrows = TRUE){ # 捕获所有分组列名,顺序和用户传入一致 grp_names <- names(eval_select(expr({{ grps }}), mtcars)) n_grp <- length(grp_names) # 计算明细统计结果,分组列转字符避免类型冲突 grouped_result <- mtcars %>% group_by(across({{ grps }})) %>% summarise(Count = n(), .groups = "drop") %>% mutate(across(all_of(grp_names), as.character)) if(!subtotalrows || n_grp == 1){ return(grouped_result) } # 动态生成各层小计结果 subtotal_list <- lapply(1:(n_grp-1), function(i){ # 当前层要保留的前N个分组 keep_grps <- grp_names[1:(n_grp - i)] # 当前层要设为total的分组 total_grps <- setdiff(grp_names, keep_grps) mtcars %>% group_by(across(all_of(keep_grps))) %>% summarise(Count = n(), .groups = "drop") %>% mutate(across(all_of(keep_grps), as.character)) %>% # 被去掉的分组统一赋值为total bind_cols(!!!setNames(rep("total", length(total_grps)), total_grps)) %>% select(all_of(grp_names), Count) }) # 合并明细+所有小计,按分组列排序 bind_rows(grouped_result, !!!subtotal_list) %>% arrange(!!!syms(grp_names)) }
运行测试
双分组测试(和预期结果一致)
test_function(grps = c(cyl, gear), subtotalrows = TRUE)
输出:
# A tibble: 11 × 3 cyl gear Count <chr> <chr> <int> 1 4 3 1 2 4 4 8 3 4 5 2 4 4 total 11 5 6 3 2 6 6 4 4 7 6 5 1 8 6 total 7 9 8 3 12 10 8 5 2 11 8 total 14
三分组测试(自动生成两层小计)
test_function(grps = c(cyl, gear, carb), subtotalrows = TRUE)
输出会包含三类结果:cyl+gear+carb的明细行、cyl+gear维度的小计(carb为total)、cyl维度的小计(gear和carb为total)。
内容的提问来源于stack exchange,提问作者mafiale
相关产品推荐
相关产品推荐

