R中获取按标识符分组的数据框第一行的快速方法

机器算法验证 r 数据集 聚合 plyr
2022-02-15 15:39:16

有时我只需要获取按标识符分组的数据集的第一行,例如当每个人有多个观察值时检索年龄和性别。在 R 中执行此操作的快速(或最快)方法是什么?我在下面使用了 aggregate() 并怀疑有更好的方法。在发布这个问题之前,我在 google 上搜索了一下,找到并尝试了 ddply,但令我惊讶的是它非常慢并且在我的数据集(400,000 行 x 16 列,7,000 个唯一 ID)上出现内存错误,而 aggregate() 版本相当快。

(dx <- data.frame(ID = factor(c(1,1,2,2,3,3)), AGE = c(30,30,40,40,35,35), FEM = factor(c(1,1,0,0,1,1))))
# ID AGE FEM
#  1  30   1
#  1  30   1
#  2  40   0
#  2  40   0
#  3  35   1
#  3  35   1
ag <- data.frame(ID=levels(dx$ID))
ag <- merge(ag, aggregate(AGE ~ ID, data=dx, function(x) x[1]), "ID")
ag <- merge(ag, aggregate(FEM ~ ID, data=dx, function(x) x[1]), "ID")
ag
# ID AGE FEM
#  1  30   1
#  2  40   0
#  3  35   1
#same result:
library(plyr)
ddply(.data = dx, .var = c("ID"), .fun = function(x) x[1,])

更新:关于我认为最优雅的方法,请参阅 Chase 的回答和 Matt Parker 的评论。有关使用该data.table软件包的最快解决方案,请参阅@Matthew Dowle 的答案。

4个回答

按照史蒂夫的回复,在 data.table 中有一个更快的方法:

> # Preamble
> dx <- data.frame(
+     ID = sort(sample(1:7000, 400000, TRUE))
+     , AGE = sample(18:65, 400000, TRUE)
+     , FEM = sample(0:1, 400000, TRUE)
+ )
> dxt <- data.table(dx, key='ID')

> # fast self join
> system.time(ans2<-dxt[J(unique(ID)),mult="first"])
 user  system elapsed 
0.048   0.016   0.064

> # slower using .SD
> system.time(ans1<-dxt[, .SD[1], by=ID])
  user  system elapsed 
14.209   0.012  14.281 

> mapply(identical,ans1,ans2)  # ans1 is keyed but ans2 isn't, otherwise identical
  ID  AGE  FEM 
TRUE TRUE TRUE 

如果您只需要每个组的第一行,则直接加入该行会快得多。为什么每次都创建 .SD 对象,只使用它的第一行?

将 data.table 的 0.064 与“Matt Parker 替代 Chase 的解决方案”(这似乎是迄今为止最快的)进行比较:

> system.time(ans3<-dxt[c(TRUE, dxt$ID[-1] != dxt$ID[-length(dxt$ID)]), ])
 user  system elapsed 
0.284   0.028   0.310 
> identical(ans1,ans3)
[1] TRUE 

所以大约快 5 倍,但它是一个不到 100 万行的小表。随着尺寸的增加,差异也会增加。

您的 ID 列真的是一个因素吗?如果它实际上是数字,我认为您可以使用该diff功能来发挥自己的优势。你也可以用as.numeric().

dx <- data.frame(
    ID = sort(sample(1:7000, 400000, TRUE))
    , AGE = sample(18:65, 400000, TRUE)
    , FEM = sample(0:1, 400000, TRUE)
)

dx[ diff(c(0,dx$ID)) != 0, ]

您不需要多个merge()步骤,只需要aggregate()两个感兴趣的变量:

> aggregate(dx[, -1], by = list(ID = dx$ID), head, 1)
  ID AGE FEM
1  1  30   1
2  2  40   0
3  3  35   1

> system.time(replicate(1000, aggregate(dx[, -1], by = list(ID = dx$ID), 
+                                       head, 1)))
   user  system elapsed 
  2.531   0.007   2.547 
> system.time(replicate(1000, {ag <- data.frame(ID=levels(dx$ID))
+ ag <- merge(ag, aggregate(AGE ~ ID, data=dx, function(x) x[1]), "ID")
+ ag <- merge(ag, aggregate(FEM ~ ID, data=dx, function(x) x[1]), "ID")
+ }))
   user  system elapsed 
  9.264   0.009   9.301

比较时间:

1)马特的解决方案:

> system.time(replicate(1000, {
+ agg <- by(dx, dx$ID, FUN = function(x) x[1, ])
+ # Which returns a list that you can then convert into a data.frame thusly:
+ do.call(rbind, agg)
+ }))
   user  system elapsed 
  3.759   0.007   3.785

2) Zach 的 reshape2 解决方案:

> system.time(replicate(1000, {
+ dx <- melt(dx,id=c('ID','FEM'))
+ dcast(dx,ID+FEM~variable,fun.aggregate=mean)
+ }))
   user  system elapsed 
 12.804   0.032  13.019

3)史蒂夫的data.table解决方案:

> system.time(replicate(1000, {
+ dxt <- data.table(dx, key='ID')
+ dxt[, .SD[1,], by=ID]
+ }))
   user  system elapsed 
  5.484   0.020   5.608 
> dxt <- data.table(dx, key='ID') ## one time step
> system.time(replicate(1000, {
+ dxt[, .SD[1,], by=ID] ## try this one line on own
+ }))
   user  system elapsed 
  3.743   0.006   3.784

4) Chase 使用数字而非因子的快速解决方案ID

> dx2 <- within(dx, ID <- as.numeric(ID))
> system.time(replicate(1000, {
+ dy <- dx[order(dx$ID),]
+ dy[ diff(c(0,dy$ID)) != 0, ]
+ }))
   user  system elapsed 
  0.663   0.000   0.663

和 5) Matt Parker 替代 Chase 的解决方案,对于 character 或 factor ID,它比 Chase 的数字略快ID

> system.time(replicate(1000, {
+ dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)]), ]
+ }))
   user  system elapsed 
  0.513   0.000   0.516

您可以尝试使用data.table包。

对于您的特定情况,好处是它(非常)快。我第一次被介绍到它时,我正在处理具有数十万行的 data.frame 对象。“正常”aggregateddply方法需要 1-2 分钟才能完成(这是在 Hadley 将idata.framemojo 引入之前ddply)。使用data.table,操作实际上是在几秒钟内完成的。

缺点是它太快了,因为它会通过“关键列”来使用您的 data.table(它就像一个 data.frame)并使用智能搜索策略来查找数据的子集。这将导致在您收集统计数据之前对您的数据进行重新排序。

鉴于您只需要每组的第一行 - 也许重新排序会弄乱哪一行是第一行,这就是为什么它可能不适合您的情况。

无论如何,您必须判断data.table这里是否合适,但这就是您将如何将它与您提供的数据一起使用:

install.packages('data.table') ## if yo udon't have it already
library(data.table)
dxt <- data.table(dx, key='ID')
dxt[, .SD[1,], by=ID]
     ID AGE FEM
[1,]  1  30   1
[2,]  2  40   0
[3,]  3  35   1

更新: Matthew Dowle(data.table 包的主要开发人员)提供了一种更好/更智能/(非常)更有效的方法来使用 data.table 来解决这个问题,作为这里的答案之一......一定要检查一下.