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


14

有时,我只需要按标识符将数据集的第一行获取,例如当每个人有多个观察值时检索年龄和性别时。在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的答案。


感谢您的所有答复。@Steve的data.table解决方案在我的数据集上是最快的,比@Gavin的aggregate()解决方案快了约5倍(这反过来比我的aggregate()代码要快),并且是〜7.5倍在@Matt的by()解决方案上。我没有时间调整想法,因为我无法使其迅速运作。我猜想@Chase提供的解决方案将是最快的,这实际上是我在寻找的解决方案,但是当我开始编写此注释时,该代码无法正常工作(我看到它已经固定了!)。
锁定2011年

实际上,@Chase的速度比data.table快〜9倍,因此我更改了我接受的答案。再次感谢大家-学习了一堆新工具。
锁定2011年

抱歉,我修改了我的代码。一个警告或窍门是将一个不是您ID中的ID之一的值连接起来,diff()以便您可以在中提取第一个ID dx
大通

Answers:


10

您的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, ]

1
聪明!您也可以dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]对非数字数据进行处理-我得到0.03的字符,0.05的因数。PS:第二个零之后),您的第一个system.time()功能有一个额外功能。
马特·帕克

@Matt-不错的电话,很好的接球。我今天似乎无法复制/粘贴值得进行翻转的代码。
大通

我正在研究伦敦自行车租赁计划,需要找到一种方法来查找自行车租赁用户的第一个和最后一个实例。拥有100万用户,每年1000万次旅行和数年的数据,我的“ for”循环每秒完成1个用户。我尝试了“按”解决方案,但一个小时后未能完成。起初我无法理解“马特·帕克(Matt Parker)替代大通银行解决方案的方式”在做什么,但最终一分钱掉了下来,它在几秒钟内就执行了。因此,我的经验证明,随着数据集的增加,改进的意义将越来越大。
乔治·辛普森

@GeorgeSimpson-很高兴看到这个仍然被引用!下面的data.table解决方案应该被证明是最快的,因此我将检查我是否是您(这里应该是公认的答案)。
Chase

17

跟着Steve的回复,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倍,但这是一个很小的表,不足一百万行。随着大小的增加,差异也随之增加。


哇,我从来没有真正欣赏过该[.data.table功能如何获得“智能” ……我想我没有意识到.SD如果您真的不需要对象,就不会创建对象。好一个!
Steve Lianoglou 2011年

是的,那的确快!即使包含dxt <- data.table(dx, key='ID')在对system.time()的调用中,它也比@Matt的解决方案要快。
2011年

我猜这与更新的data.table版本已经过时了SD[1L],实际上@SteveLianoglou的答案对于5e7行将是两倍。
大卫·阿伦堡

@DavidArenburg从2016年11月v1.9.8起,是的。可以直接直接编辑此答案,或者此Q需要是社区Wiki之类的内容。
马特·道尔

10

您不需要多个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

哦,对了,谢谢!忘记了用于聚合的语法。
锁定2011年

如果您想添加Chase的解决方案,这是我得到的:dx$ID <- sample(as.numeric(dx$ID)) #assuming IDs arent presorted system.time(replicate(1000, { dy <- dx[order(dx$ID),] dy[ diff(c(0,dy$ID)) != 0, ] })) user system elapsed 0.58 0.00 0.58
锁定2011年

@lockedoff-完成,谢谢,但是我没有对IDs进行随机采样,因此结果与其他解决方案相当。
恢复莫妮卡-G.辛普森

@Chase的答案的评论中还有@Matt Parker的版本
恢复Monica-G. Simpson

2
加文感谢您安排时间,这对于此类问题确实很有帮助。
马特·帕克

9

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

对于您的特殊情况,好处是(非常快)。第一次向我介绍它时,我正在处理具有成千上万行的data.frame对象。大约1-2分钟采用“正常” aggregateddply方法(这是在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来解决此问题,这是这里的答案之一...肯定要检查一下。


4

尝试重塑2

library(reshape2)
dx <- melt(dx,id=c('ID','FEM'))
dcast(dx,ID+FEM~variable,fun.aggregate=mean)

3

你可以试试

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)

我不知道这是否会比快plyr

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.