You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 23:30:38