如何用vctrs::vec_chop替代filter(row_number() == 1)优化R代码?
我正在优化R代码,尝试用vctrs包的vec_chop()替代以下分组取每组首行的逻辑,但没能成功实现:
df2 <- df1 %>% group_by(clnt_label, term1, term2) %>% filter(row_number() == 1)
测试数据:
df1 <- tibble(clnt_label = rep(LETTERS[1:3], each = 10), term1 = rev(rep(LETTERS[1:3], times = 10)), term2 = rep(LETTERS[1:3], each = 5, times = 2))
我在全量数据集上测试了多种实现方案,发现distinct()是目前最快的方式,测试结果如下:
各方案性能测试
- 原分组filter方案:
microbenchmark::microbenchmark( all_pairs4_old <- all_pairs3 %>% group_by(clnt_label, term1, term2) %>% filter(row_number() == 1), times = 1, unit = "millisecond")
单位:毫秒
min lq mean median neval
31938.37 31938.37 31938.37 31938.37 1
- slice_head方案:
microbenchmark::microbenchmark( all_pairs4_head <- all_pairs3 %>% group_by(clnt_label, term1, term2) %>% slice_head(n = 1), times = 1, unit = "millisecond")
单位:毫秒
min lq mean median neval
214474.4 214474.4 214474.4 214474.4 1
- slice(1)方案:
microbenchmark::microbenchmark( all_pairs4_slice <- all_pairs3 %>% group_by(clnt_label, term1, term2) %>% slice(1), times = 1, unit = "millisecond")
单位:毫秒
min lq mean median neval
144225.7 144225.7 144225.7 144225.7 1
- distinct方案:
microbenchmark::microbenchmark( all_pairs4_distinct <- all_pairs3 %>% distinct(clnt_label, term1, term2, .keep_all = TRUE), times = 1, unit = "millisecond")
单位:毫秒
min lq mean median neval
242.9775 242.9775 242.9775 242.9775 1
实现建议
保留distinct方案:你的测试已经证明
distinct(clnt_label, term1, term2, .keep_all = TRUE)是当前最快的实现,它内部基于哈希表去重,比dplyr分组操作的效率高很多,尤其在大数据集上优势明显。如果业务逻辑允许(若严格要求保留每组首行,需确保数据集在分组键上的顺序与原逻辑一致),这是最优选择。用vctrs实现的思路:如果一定要基于vctrs实现,可以结合
vec_group_loc生成分组索引,再提取每组首行:
# 生成分组位置索引 groups <- vctrs::vec_group_loc(df1, c("clnt_label", "term1", "term2")) # 提取每个组的首行位置 first_rows <- purrr::map_int(groups$loc, ~ .x[[1]]) # 获取结果 df2 <- df1[first_rows, ]
该方案性能接近distinct,vec_group_loc是vctrs的高效分组函数,避免了dplyr分组的额外开销。
- 备选优化方案:如果数据集已按分组键排序,
data.table::unique(df1, by = c("clnt_label", "term1", "term2"))也是极快的选择,可作为额外测试项。
内容的提问来源于stack exchange,提问作者PhDavey

