【问题标题】:Find duplicate IDs across columns and combine rows together in R在 R 中跨列查找重复 ID 并将行组合在一起
【发布时间】:2018-06-15 14:03:45
【问题描述】:

我正在处理识别鱼标签 ID 的历史数据集。多年来,一些鱼被多次标记,因此具有与同一个人相关的多个标签 ID(即,一个标签被一个新标签替换)。我想为每条鱼识别所有可能的标签 ID。每条鱼没有唯一的标识符;我只能识别标签 ID 之间的关联。我设法清理了一些数据集并在一列中识别出唯一的标签 ID,并将任何相关的其他标签 ID 添加到同一行的其他列中。但是,列之间存在重复。这是我的数据集的示例:

ID1<-c(101,102,103,105,106,107,108,110,111,112,113,114)
ID2<-c(NA,101,290,309,105,108,NA,220,NA,113,112,112)
ID3<-c(NA,NA,400,106,NA,NA,NA,111,NA,NA,NA,NA)
data<-data.frame(ID1,ID2,ID3)
    ID1   ID2  ID3 
1   101    NA   NA
2   102   101   NA     
3   103   290  400    
4   105   309  106     
5   106   105   NA    
6   107   108   NA  
7   108    NA   NA
8   110   220  111  
9   111    NA   NA
10  112   113   NA
11  113   112   NA
12  114   112   NA

正如您在此示例中看到的,鱼 1 的标签 ID 为 101 和 102 - 现在,如果您只查看 ID1 列,它们看起来就像两条独立的鱼,但我们知道它是同一条鱼,因为它也具有与标签 102 关联的标签 ID 101。

我生成的数据框应该如下所示:

    ID1   ID2  ID3 
1   101   102   NA
2   103   290  400        
3   105   309  106         
4   107   108   NA  
5   110   220  111  
6   112   113  114

虽然 ID1(第一列)内没有重复的标签 ID,但在 ID1 和 ID2 以及 ID1 和 ID3 之间确实存在重复(ID2 和 ID3 之间不存在重复的标签 ID,NA 除外)。 ID2 中有一些重复项,因为它们与 ID1 中的另一个 ID 相关联(请参阅上面示例中的第 10:12 行,其中标签 ID 112 在 ID1 中显示一次,在 ID2 中显示两次)。

例如,我已经使用 %in% 命令确定了哪些 ID 在列之间重复

data$ID1[data$ID1 %in% data$ID2]
data$ID1[data$ID1 %in% data$ID3]

我已将其构建到 ifelse 语句中:

ifelse(data$ID1 %in% data$ID2| data$ID1 %in% data$ID3, "Match", "Nomatch")

但这只会告诉我哪些 ID 是重复的,我不知道如何将信息实际组合到一行中。

我还尝试将这些数据分成两个不同的数据框,以便我可以使用连接命令,但我丢失了相关信息。

我在想我可能需要使用 aggregate() 或 combine() 并将其包装到我的 ifelse 语句中?或者也许有办法在 dplyr 中做到这一点?任何帮助将不胜感激!

【问题讨论】:

  • 很难,以后得考虑考虑。
  • 当我尝试将数据转换回“现实”场景时,这没有多大意义。第 10 行和第 11 行的数据是如何产生的?请不要使用as.data.frame(cbind(...))。只需使用data,frame,您将避免与混合矩阵和数据框类相关的错误。
  • 这是一个庞大的历史数据集,我最好的猜测是输入数据的人没有考虑到标签 ID 的输入顺序很重要。但我确实在文件中有这样的实例。
  • 我不清楚您是如何决定构建数据框的。您说过:“ID1 和 ID2 以及 ID1 和 ID3 之间确实存在重复项(ID2 和 ID3 之间不存在重复项,NA 除外)”。根据您的数据示例,同一条鱼最多有三个 ID。整个数据集都是真的吗?不管是不是真的,我对你的问题有一个不优雅的解决方案。

标签: r duplicates dplyr aggregate


【解决方案1】:

我建议您为您的问题提供一个不雅的解决方案,该解决方案仅适合您的玩具数据和您所说的条件:“ID1 和 ID2 以及 ID1 和 ID3 之间确实存在重复项(ID2 和 ID3 之间不存在重复项,NA 除外) )”。 实际上,根据您的数据,似乎每条鱼最多有三个重复的 ID。出于这个原因,这是我的解决方案:

library(tidyverse)

您的数据:

ID1<-c(101,102,103,105,106,107,108,110,111,112,113,114)
ID2<-c(NA,101,290,309,105,108,NA,220,NA,113,112,112)
ID3<-c(NA,NA,400,106,NA,NA,NA,111,NA,NA,NA,NA)
Your data frame:
data <- data.frame(cbind(ID1,ID2,ID3))

根据您的声明,我创建了两个数据框,确认 ID 之间仅在 ID1 和 ID2 之间或 ID1 和 ID3 之间存在链接:

data1 <- data.frame(cbind(ID1,ID2)) %>% 
    rename(A=ID1,B=ID2)
data2 <- data.frame(cbind(ID1,ID3)) %>% 
    rename(A=ID1,B=ID3)

我绑定了两个数据框,并将 ID 的值逐行反转,从最小值到最大值。因此,我可以只选择不同的 ID 对,并且可以通过一些数据整理操作创建重复 ID 的三元组:

bind_rows(data1,data2) %>% 
    filter(complete.cases(.)) %>% 
    mutate(ID1=pmin(A,B),
           ID2=pmax(A,B)) %>% 
    select(ID1,ID2) %>% 
    arrange(ID1,ID2) %>% 
    group_by(ID1) %>% 
    mutate(ID3=max(ID2)) %>% 
    distinct(.) %>% 
    filter(ID2==first(ID2))

结果(没有 NA):

# A tibble: 6 x 3
# Groups: ID1 [6]
    ID1   ID2   ID3
  <dbl> <dbl> <dbl>
1   101   102   102
2   103   290   400
3   105   106   309
4   107   108   108
5   110   111   220
6   112   113   114

【讨论】:

  • 谢谢;这适用于测试数据。但是,事后看来,我应该在示例数据中添加字符,因为标签 ID 是字符串,通常包含字母和数字。我在创建示例数据时没有考虑到这一点——我只是想确保我捕获了 ID 放置的示例,并且错误地没有考虑 ID 结构。对于如何编辑 mutate() 函数,您是否有建议,以便它使用字符而不是整数进行变异?
  • 另外,您能否澄清一下 filter(ID2==first(ID2)) 正在做什么?看起来代码将每个唯一 ID 的第一个条目保留在 ID2 中;但是,如果您将代码运行到此行之前(即,以 distinct(.) 结尾),则 ID2 中没有重复项,但 ID1 中有重复项。我误解了吗?
  • 关于 ID 中是否存在字符变量而不是数字变量,我没有一个简单的解决方案。我向您建议的代码运行良好,因为它使用跨行和列的数值属性(函数如pminpmax 等)。所以我必须研究这个问题以找到字符的替代解决方案。
  • 理论上,您可以创建具有所有 ID 的唯一字符向量,并将其转换为因子。然后,您可以将 ID 值(数字或字符)替换为已创建因子的对应级别(数值)到您的数据框中。我不知道它是否可行,以及我是否很好地解释了我的想法。
  • 关于您的第二个问题:filter(ID2==first(ID2)) 我建议您使用示例逐步运行代码,直到每个管道命令(不选择%&gt;%)。通过比较渐进式输出,您可以很容易地理解代码在做什么。我认为这也有助于理解我为什么使用distinct 函数。
【解决方案2】:

我对这个问题采取了不同的方法,我认为这与从您提供的数据生成 ID 值集有关。我不假设三个 ID 列在解决方案中是不同的或以其他方式存在的。该解决方案还没有假设应用于实际数据的列数 - 如果任何结果集中有超过三个条目,则带有结果的数据框将增加其列数以适应这些相应的。

我已经使用 tidyverse 函数和集合循环处理的组合复制了请求的结果。 @Scipione 的回答很好地证明了单独使用 tidyverse 函数来完成所有这些工作,但正如他所提到的,该解决方案与示例中的数据一致;我想尽可能使用基于集合的方法进行概括。

正如@Scipione 的回答一样,我从您的数据开始,然后使用 set union 识别重复的 ID。这些在下面的解决方案中没有进一步使用,但它们本身可能很有趣:

library(dplyr)
library(tidyr)

ID1<-c(101,102,103,105,106,107,108,110,111,112,113,114)
ID2<-c(NA,101,290,309,105,108,NA,220,NA,113,112,112)
ID3<-c(NA,NA,400,106,NA,NA,NA,111,NA,NA,NA,NA)
data<-data.frame(ID1,ID2,ID3)

data.duplicates = union(intersect(ID1, ID2), intersect(ID1, ID3))

> data.duplicates
[1] 101 105 108 112 113 106 111

因此,在示例数据中,有 7 个 ID 在 ID1 和 ID2 列或 ID1 和 ID3 列中出现多次。

为了正确开始生成 ID 行,我转置数据,然后将结果转换为长格式,然后在 ID 上自连接表:

data.t = 
  data.frame(t(data)) %>%
  mutate(Cols = rownames(.)) %>%
  gather(key = row, value = ID, starts_with("X"))

data.t.joined = 
  filter(data.t, !is.na(ID)) %>% 
  inner_join(data.t, by = "ID")

 > head(data.t.joined, 6)
   Cols.x row.x  ID Cols.y row.y
1     ID1    X1 101    ID1    X1
2     ID1    X1 101    ID2    X2
3     ID1    X2 102    ID1    X2
4     ID2    X2 101    ID1    X1
5     ID2    X2 101    ID2    X2
6     ID1    X3 103    ID1    X3

这会生成从不同行组合的 ID,然后可以进一步处理这些 ID 以生成原始行的唯一 ID 列表(数据框中的 row.x):

data.t.combined =
  data.t.joined %>%
  mutate(row.x = as.integer(gsub("X", "", row.x))) %>%
  select(row.x, ID) %>%
  group_by(row.x) %>%
  summarise(IDs = list(sort(unique(ID))))

此时,我们有许多包含 ID 集的列表,但这些列表包括单个 ID 和其他子集,这些子集随后将合并在一起以生成联合的最终 ID 集。中间 IDs 列的摘录如下所示:

> head(data.t.combined$IDs)
[[1]]
[1] 101

[[2]]
[1] 101 102

[[3]]
[1] 103 290 400

[[4]]
[1] 105 106 309

[[5]]
[1] 105 106

[[6]]
[1] 107 108

现在是蛮力方法。下面列出的函数合并子集并删除重复集。如果数据中的行数非常大,这是一种低效的方法,因为它涉及 n * (n-1) 次比较,实际上是 n^2,如果实际行数达到几十个,这将非常耗时数以千计。

mergesubsets <- function(thedata){
  thedata$NewIDs = thedata$IDs
  rows = nrow(thedata)
  for (i in 1:rows){
    entry = unlist(thedata$NewIDs[i])
    for (j in 1:rows){
      if (i != j){
        otherentry = unlist(thedata$NewIDs[j])
        if(max(entry %in% otherentry)==1) {
          thedata$NewIDs[i] = list(sort(union(entry, otherentry)))
        }
      }
    }
  }
  thedata[!duplicated(thedata$NewIDs),]
}

data.t.merged = 
  mergesubsets(data.t.combined)

上面生成了一系列列表,因此最后一步是将这些列表转换为矩阵,然后转换为数据框以进行输出。再次蛮力但这次相当快(我确信有更简单的方法来转换不同长度的列表,但我无法在可用的时间内找到它们):

listtodataframe <- function(thedata){
  rows = nrow(thedata)
  cols = max(sapply(thedata$NewIDs, length))
  result = matrix(nrow = rows, ncol = cols)
  for (i in 1:rows){
    entry = unlist(thedata$NewIDs[i])
    for (j in 1:length(entry)){
      result[i, j] = entry[j]
    }
  }
  data.frame(result)
}

result = listtodataframe(data.t.merged)

最终结果反映了您帖子中关于您预期结果的内容,尽管在我的行中,行按 ID 升序排列:

> result
   X1  X2  X3
1 101 102  NA
2 103 290 400
3 105 106 309
4 107 108  NA
5 110 111 220
6 112 113 114

作为比较,这是您按预期发布的内容:

    ID1   ID2  ID3 
1   101   102   NA
2   103   290  400        
3   105   309  106         
4   107   108   NA  
5   110   220  111  
6   112   113  114

我应该重申,循环处理对于大量行来说是低效的。无论如何,就像在@Scipione 的解决方案中一样,它从测试数据中出现以生成您期望的结果,并且它应该适用于更大的真实世界数据集,尽管它可能不是最省时的。

【讨论】:

  • 感谢您的建议。对于样本数据,这确实有效;但是它不适用于我的整个数据集。我最终在第一列中有重复的 ID。这可能是因为我的实际数据集中的标签 ID 是字符串,因为它们包含字母和数字。事后看来,我应该在我的示例数据中添加字符。您是正确的,因为这对于大型数据集来说很慢;我有超过 11,000 行,运行大约需要 5.5 小时。如果您有改进处理时间的建议以及您认为 ID 的结构存在问题,请告诉我
【解决方案3】:

我从我最初发布的例程中发现了一些遗漏,这些遗漏导致了您发现的重复。我在下面的修改代码中更正了这些:

data.duplicates = union(intersect(data$ID1, data$ID2), intersect(data$ID1, data$ID3))

data.t = 
  data.frame(t(data)) %>%
  mutate(Cols = rownames(.)) %>%
  gather(key = row, value = ID, starts_with("X"))

data.t.joined = 
  filter(data.t, !is.na(ID)) %>% 
  inner_join(data.t, by = "ID")

data.t.combined =
  data.t.joined %>%
  mutate(row.x = as.integer(gsub("X", "", row.x))) %>%
  select(row.x, ID) %>%
  group_by(row.x) %>%
  summarise(IDs = list(sort(unique(ID))))

mergesubsets <- function(thedata){
  rows = nrow(thedata)
  for (i in 1:rows){
    entry = unlist(thedata$IDs[i])
    for (j in 1:rows){
      if (i!=j){
        otherentry = unlist(thedata$IDs[j])
        if(max(entry %in% otherentry)==1) {
          entry = sort(union(entry, otherentry))
          thedata$IDs[i] = list(entry)
        }
      }
    }
  }
  thedata[!duplicated(thedata$IDs),]
}

listtodataframe <- function(thedata){
  rows = nrow(thedata)
  cols = max(sapply(thedata$IDs, length))
  result = matrix(nrow = rows, ncol = cols)
  for (i in 1:rows){
    entry = unlist(thedata$IDs[i])
    for (j in 1:length(entry)){
      result[i, j] = entry[j]
    }
  }
  data.frame(result)
}


data.t.merged = data.t.combined

prevrows = 0
rows = nrow(data.t.merged)
starttime = proc.time()[3]
while(rows != prevrows) {
  prevrows = rows
  data.t.merged = 
    mergesubsets(data.t.merged)
  rows = nrow(data.t.merged)
}
endtime = proc.time()[3]
timetorun = endtime - starttime 
timetorun


result = listtodataframe(data.t.merged)
result
write.csv(result, "result.csv", row.names = FALSE)

我已经使用包含 500 行数字数据的不同数据集测试了修改后的代码,该数据集太大而无法在此处发布。它现在可以正确识别所有唯一的值集,无论这些值有多少。当我使用一组随机数据进行测试时,在某些情况下,我最终会得到一个包含所有唯一值的单行。

很抱歉,到目前为止,我还不能加快这个版本的速度,正如你提到的,在 11,000 行上运行时非常耗时。我很欣赏您的真实数据是非数字的,这也比数字数据的处理效率低。

我的测试已经进行了 500 行,大约需要 500 行。 25 秒完成。

500 行全数字测试数据的输出示例如下所示。

> result
        X1     X2    X3    X4    X5    X6    X7    X8    X9   X10   X11   X12   X13
1      100    101   102   103   104   105   200   300    NA    NA    NA    NA    NA
2      110    113   210   321    NA    NA    NA    NA    NA    NA    NA    NA    NA
3      111    211   311    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
4      112    312   412    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
5      500    600   601   602   603   604   605   610   613   700   710   800   821
6      611    711   811    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
7      612    812   912    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
8     1000   1100  1101  1102  1103  1104  1105  1110  1113  1200  1210  1300  1321
9     1111   1211  1311    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
10    1112   1312  1412    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
11    1500   1600  1601  1602  1603  1604  1605  1610  1613  1700  1710  1800  1821

【讨论】:

  • 另外,能否请您解释一下starttime = proc.time()[3],具体来说[3]索引是什么?
  • 我需要知道发生错误的行号 - 我重组了一些函数,因此它与您确定的已被替换的行无关。如果我可以获得您的数据集的副本,我可以更轻松地识别出问题所在 - 我确实使用 5000 行数字数据进行了测试,但您的数据集并不像您之前提到的那么简单。 re proc time 上的限定符,返回三个时间分量,经过的时间是第三个元素。
  • 感谢@Stewart Ross 的快速回复——在看到您的回复之前,我删除了我之前的评论,因为我认为我发现了我的错误。它目前正在整个数据集上运行。交叉我的手指它有效!感谢您的帮助。
  • 我认为这行得通!运行我的 11,852 行数据集大约需要 7 个小时。有 1 条鱼有 4 个可能的 ID,而我之前没有意识到这一点,所以由于您的代码的通用性,我能够识别出这条鱼。非常感谢!
  • 很高兴能帮上忙。
猜你喜欢
  • 2021-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-20
  • 2019-05-13
  • 2015-11-22
  • 1970-01-01
相关资源
最近更新 更多