R语言如何高效按唯一ID合并DataFrame重复行
问题场景
我有一个数据框,所有参与者的唯一subject ID都恰好重复出现两次。数据存在如下特征:部分列在其中一条记录的取值为NA,另一条记录对应位置是有效值,但这个规律不是绝对的,方案需要兼容不满足该规律的场景。
示例数据如下:
Name <- c("Jon", "Jon", "Maria", "Maria", "Tina", "Tina", "dan", 'dan', 'wen', 'wen') a <- c(1, 1, 2, 2, 3, 4, 4, 4, 5, 6) b <- c(NA, 1, NA, 2, NA, 3, NA, 4, NA, 5) c <- c(1, NA, 2, NA, 3, NA, 4, NA, 5, NA) df <- data.frame(Name, a, b, c)
我原本的实现思路是遍历所有唯一ID(示例中为Name字段),把两类记录拆成两个独立数据框,代码如下:
# 初始化用于存储结果的列表 firstdf <- c() seconddf <- c() # 按唯一ID遍历原数据框,拆分两条记录到不同列表 for (i in unique(df$Name)) { innerlist1 <- c() innerlist2 <- c() for (x in c(1:length(df[df['Name'] == i]))) { if (x%%2 == 1) { # 取每个ID的第一条记录 innerlist1 <- c(innerlist1, df[df['Name'] == i][x]) } else if (x%%2 == 0) { # 取每个ID的第二条记录 innerlist2 <- c(innerlist2, df[df['Name'] == i][x]) } } firstdf <- c(firstdf, list(innerlist1)) seconddf <- c(seconddf, list(innerlist2)) } # 将列表转为数据框 firstdf <- do.call(rbind.data.frame, firstdf) names(firstdf) <- names(df) seconddf <- do.call(rbind.data.frame, seconddf) names(seconddf) <- names(df)
后续计划用merge函数、设置by="Name"参数合并两个数据框。但原始数据体量较大,该方法效率低、写法冗余,求更优的改进方案。
最优解决方案
完全不需要写嵌套循环拆分再合并,根据你的实际需求选对应实现即可,所有方案都是向量化操作,运行效率比嵌套循环高几个数量级,同时兼容两条记录都有值、都为NA的非规律场景。
场景1:按ID合并两条记录,优先取非NA值
这也是你描述的最常见需求:同一个ID的两条记录,某列如果一个是NA一个是有效值,就保留有效值;如果两个都是有效值,默认保留第一条的取值(可以自行调整为保留第二条);如果两个都是NA就返回NA。
dplyr实现(可读性最好,适合中小数据集)
library(dplyr) df %>% group_by(Name) %>% # 要保留第二条有效值就把first替换成last summarise(across(everything(), ~ifelse(all(is.na(.)), NA, first(na.omit(.)))), .groups = "drop")
data.table实现(性能最高,适合百万级以上大数据)
library(data.table) setDT(df) # 要保留第二条有效值就把first替换成last df[, lapply(.SD, function(x) if(all(is.na(x))) NA else first(na.omit(x))), by = Name]
场景2:需要把两条记录拆成宽表,保留所有取值
如果你确实需要和原思路一致,把同ID的两条记录拆成不同列(比如第一条的字段加_1后缀,第二条加_2后缀),也不需要写循环拆分再merge,一步即可完成:
dplyr+tidyr实现
library(dplyr) library(tidyr) df %>% group_by(Name) %>% mutate(seq = row_number()) %>% pivot_wider(id_cols = Name, names_from = seq, values_from = c(a, b, c), names_sep = "_")
data.table实现
library(data.table) setDT(df) df[, seq := seq_len(.N), by = Name] dcast(df, Name ~ seq, value.var = c("a", "b", "c"), sep = "_")
如果你坚持要沿用「拆分两个df再merge」的思路,也不要写嵌套循环,直接分组取对应位置的记录即可,代码简洁且性能远高于原实现:
library(dplyr) firstdf <- df %>% group_by(Name) %>% slice(1) %>% ungroup() seconddf <- df %>% group_by(Name) %>% slice(2) %>% ungroup() merged_df <- merge(firstdf, seconddf, by = "Name", suffixes = c("_1", "_2"))
原代码性能差的核心原因
- R中
for循环逐行逐ID遍历本身性能差,你还在循环里反复执行df[df['Name'] == i]的子集筛选,相当于每次循环都要遍历全表匹配ID,数据量越大速度衰减越明显 - 循环里动态增长
firstdf、seconddf对象,会触发R频繁复制整块内存,带来极大的额外开销 - 拆分后再
merge属于冗余操作,分组计算可以一步完成所有逻辑,省去不必要的内存占用
内容的提问来源于stack exchange,提问作者JED HK
相关产品推荐
相关产品推荐

