R语言:为listcolumn添加列的高效方法与mutate性能疑问
咱们先解决第一个问题:如何在已有的listcolumn内部添加新列,而不是用mutate生成全新的listcolumn。其实核心思路是用purrr::map配合mutate,直接修改原有的listcolumn——让每个子数据框(listcolumn里的元素)都新增列,而不是额外生成一列list。
举个实际例子,用你提到的repurrrsive包的gh_users数据:
library(repurrrsive) library(tidyverse) # 先把gh_users转成带listcolumn的tibble users_tbl <- tibble(user = gh_users) # 直接在原listcolumn(user)里添加新列 users_tbl <- users_tbl %>% mutate(user = map(user, ~ .x %>% mutate(full_name = str_c(name, login, sep = " - "))))
这里的逻辑是:用map遍历user这个listcolumn里的每一个子数据框,对每个子框用mutate添加full_name列,最后把修改后的子框重新赋值给原listcolumn,这样就实现了“在listcolumn内创建新列”的需求,而不是生成新的listcolumn。
再来说第二个问题:双重mutate为什么慢,以及mutate的性能开销。
其实mutate本身的开销并不大,慢的根源是你做了两次不必要的遍历和map操作。比如如果你先mutate生成一个临时listcolumn,再用第二次mutate去关联处理,相当于把整个tibble和listcolumn遍历了两遍,每一遍都要执行map操作,自然会比一次性完成所有操作慢很多。
举个反例(就是你说的慢的双重mutate写法):
# 效率较低的双重mutate写法 slow_tbl <- users_tbl %>% mutate(temp_name = map(user, ~ .x$name)) %>% # 第一次遍历生成临时列 mutate(user = map2(user, temp_name, ~ .x %>% mutate(full_name = str_c(.y, login)))) # 第二次遍历处理
优化方法:合并操作到单次遍历
把所有需要对listcolumn子数据框做的操作,都放到同一个map调用里,这样只需要遍历一次listcolumn,性能会提升很多:
# 优化后的单次mutate写法 fast_tbl <- users_tbl %>% mutate(user = map(user, ~ .x %>% mutate(full_name = str_c(name, login, sep = " - "), name_length = str_length(name)))) # 可以一次性加多个列
如果想直观看到性能差异,可以用microbenchmark测试:
library(microbenchmark) bench_result <- microbenchmark( double_mutate = { users_tbl %>% mutate(temp_name = map(user, ~ .x$name)) %>% mutate(user = map2(user, temp_name, ~ .x %>% mutate(full_name = str_c(.y, login)))) }, single_mutate = { users_tbl %>% mutate(user = map(user, ~ .x %>% mutate(full_name = str_c(name, login, sep = " - ")))) }, times = 100 # 重复运行100次取平均 ) print(bench_result)
运行后你会发现,single_mutate的速度比double_mutate快不少——本质就是减少了一次完整的遍历和map操作。
额外的性能小技巧
- 如果只是从子数据框提取单个向量(比如提取所有name),用
map_chr/map_int这类类型明确的函数,比map返回list更高效; - 当处理超大数据量时,可以考虑用
data.table来处理listcolumn,它的内部实现通常比tidyverse的list操作更快; - 避免在map里做重复计算,比如某个值需要多次使用,先在外部计算好再传入。
内容的提问来源于stack exchange,提问作者Misha

