วิธีที่รวดเร็วใน R เพื่อให้แถวแรกของเฟรมข้อมูลจัดกลุ่มตามตัวระบุ [ปิด]


14

บางครั้งฉันต้องรับเฉพาะแถวแรกของชุดข้อมูลที่จัดกลุ่มตามตัวระบุเช่นเดียวกับเมื่อดึงข้อมูลอายุและเพศเมื่อมีการสังเกตหลายครั้งต่อบุคคล อะไรคือวิธีที่รวดเร็ว (หรือเร็วที่สุด) ในการทำเช่นนี้ใน R? ฉันใช้การรวม () ด้านล่างและสงสัยว่ามีวิธีที่ดีกว่า ก่อนโพสต์คำถามนี้ฉันค้นหาบิตบน google พบและลองใช้ ddply และรู้สึกประหลาดใจที่มันช้ามากและทำให้ฉันมีข้อผิดพลาดของหน่วยความจำในชุดข้อมูลของฉัน (400,000 แถว x 16 cols, 7,000 ID ที่ไม่ซ้ำ) ในขณะที่รุ่นรวม เร็วพอสมควร

(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,])

UPDATE:ดูคำตอบของ Chase และความคิดเห็นของ Matt Parker's สำหรับสิ่งที่ฉันคิดว่าเป็นวิธีการที่หรูหราที่สุด ดูคำตอบของ @Matthew Dowle สำหรับวิธีแก้ปัญหาที่เร็วที่สุดซึ่งใช้data.tableแพ็คเกจ


ขอบคุณสำหรับคำตอบทั้งหมดของคุณ โซลูชัน data.table ของ @Steve นั้นเร็วที่สุดโดยปัจจัยที่ ~ 5 ในข้อมูลของฉันที่ตั้งไว้เหนือการรวม () วิธีการแก้ปัญหาของ @Gavin (ซึ่งในทางกลับกันก็เร็วกว่ารหัสรวมของฉัน) และปัจจัย ~ 7.5 แก้ปัญหาโดย () ของ @Matt ฉันไม่ได้ใช้เวลาคิดสร้างสรรค์เพราะฉันไม่สามารถทำงานได้อย่างรวดเร็ว ฉันคาดเดาทางออกที่ @Chase ให้จะเป็นวิธีที่เร็วที่สุดและมันก็เป็นสิ่งที่ฉันกำลังมองหา แต่เมื่อฉันเริ่มเขียนความคิดเห็นนี้รหัสไม่ทำงาน (ฉันเห็นว่ามันได้รับการแก้ไขแล้ว!)
ล็อค

ที่จริง @Chase เร็วกว่าด้วยอัตรา ~ 9 บน data.table ดังนั้นฉันจึงเปลี่ยนคำตอบที่ฉันยอมรับ ขอขอบคุณทุกคนอีกครั้ง - เรียนรู้เครื่องมือใหม่มากมาย
ล็อค

ขอโทษฉันแก้ไขรหัสของฉัน หนึ่งข้อแม้หรือเคล็ดลับที่นี่คือการเชื่อมโยงค่าที่ไม่ใช่หนึ่งใน ID ของคุณdiff()เพื่อให้คุณสามารถรับ ID แรกdxได้
Chase

คำตอบ:


11

คอลัมน์ 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 Parker

@ Matt - โทรดีและจับได้ดี ฉันดูเหมือนจะไม่สามารถคัดลอก / วางรหัสได้อย่างคุ้มค่าในวันนี้
Chase

ฉันกำลังทำงานกับโครงการ London Cycle Hire และจำเป็นต้องหาวิธีในการค้นหาอินสแตนซ์แรกและครั้งสุดท้ายของผู้ใช้จักรยานที่ได้รับการว่าจ้าง ด้วยผู้ใช้ 1 ล้านคนการเดินทาง 10 ล้านครั้งต่อปีและข้อมูลหลายปีทำให้วง "ต่อ" ของฉันมีผู้ใช้ 1 คนต่อวินาที ฉันลองใช้วิธี "ตาม" และไม่สามารถดำเนินการให้เสร็จภายในหนึ่งชั่วโมง ตอนแรกฉันไม่สามารถเข้าใจได้ว่า "ทางเลือกของ Matt Parker กับทางออกของ Chase" กำลังทำอะไร แต่ในที่สุดเงินก็ลดลงและดำเนินการในไม่กี่วินาที ดังนั้นจุดเกี่ยวกับการปรับปรุงให้ดียิ่งขึ้นด้วยชุดข้อมูลขนาดใหญ่จึงได้รับการพิสูจน์จากประสบการณ์ของฉัน
George Simpson

@GeorgeSimpson - ดีใจที่เห็นว่ายังคงมีการอ้างอิง! data.tableวิธีการแก้ปัญหาด้านล่างนี้ควรเป็นวิธีที่เร็วที่สุดดังนั้นฉันจะตรวจสอบว่าฉันเป็นคุณหรือไม่ (ควรเป็นคำตอบที่ยอมรับได้ที่นี่)
เชส

17

การติดตามการตอบกลับของสตีฟมีวิธีที่เร็วกว่ามากใน 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 ทุกครั้งที่ใช้แถวแรกของวัตถุเท่านั้น

เปรียบเทียบ 0.064 ของ data.table กับ "ทางเลือกของ 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 เท่า แต่มันเป็นโต๊ะเล็ก ๆ ที่ต่ำกว่า 1 ล้านแถว เมื่อขนาดเพิ่มขึ้นความแตกต่างก็เช่นกัน


ว้าวฉันไม่เคยพอใจเลยว่า[.data.tableฟังก์ชั่น"ฉลาด" จะได้รับอย่างไรฉันคิดว่าฉันไม่ได้ตระหนักว่าคุณไม่ได้สร้าง.SDวัตถุถ้าคุณไม่ต้องการมัน ทำได้ดีนี่!
Steve Lianoglou

ใช่เร็วจริง ๆ ! แม้ว่าคุณจะรวมdxt <- data.table(dx, key='ID')ไว้ในการเรียกไปยัง system.time () มันเร็วกว่าโซลูชันของ @ Matt
ล็อคใน

ฉันเดาว่าสิ่งนี้ล้าสมัยแล้วเช่นเดียวกับ data.table รุ่นใหม่ที่SD[1L]ได้รับการปรับปรุงอย่างเต็มที่และจริง ๆ @SteveLianoglou คำตอบจะเร็วเป็นสองเท่าสำหรับ 5e7 แถว
David Arenburg

@DavidArenburg ตั้งแต่วันที่ v1.9.8 พ.ย. 2559 ใช่ อย่าลังเลที่จะแก้ไขคำตอบนี้โดยตรงหรืออาจจะถามว่านี่เป็นวิกิชุมชนหรืออะไรบางอย่าง
Matt Dowle

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) โซลูชันของ Matt:

> 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) โซลูชัน reshape2 ของ Zach:

> 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 ของ Steve:

> 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 สำหรับตัวละครหรือปัจจัย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

โอ้ใช่แล้วขอบคุณ! ลืมเกี่ยวกับไวยากรณ์นั้นสำหรับการรวม
lockedoff

หากคุณต้องการที่จะเพิ่มวิธีการแก้ปัญหาของเชสนี่คือสิ่งที่ผมได้รับ: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
lockedoff

@lockedoff - เรียบร้อยแล้วขอบคุณ แต่ฉันไม่ได้สุ่มตัวอย่างIDs ดังนั้นผลลัพธ์จึงเปรียบได้กับโซลูชันอื่น ๆ
Reinstate Monica - G. Simpson

และเวลาในเวอร์ชัน @Matt Parker ในความคิดเห็นต่อคำตอบของ @ Chase
Reinstate Monica - G. Simpson

2
ขอขอบคุณที่ทำเวลากาวิน - มันมีประโยชน์สำหรับคำถามเช่นนี้
Matt Parker

10

คุณสามารถลองใช้data.tableแพ็คเกจได้

สำหรับกรณีเฉพาะของคุณข้อดีคือมันเร็ว (บ้า) ครั้งแรกที่ฉันได้รับการแนะนำให้รู้จักกับฉันฉันกำลังทำงานกับวัตถุ data.frame ที่มีหลายแสนแถว "ปกติ" aggregateหรือddplyวิธีการใช้เวลาประมาณ 1-2 นาทีเพื่อให้เสร็จสมบูรณ์ (นี่คือก่อนที่ Hadley จะนำidata.frameโมโจเข้ามา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 package) ให้วิธีที่ดีกว่า / ฉลาดกว่า / / (อย่างมาก) ที่มีประสิทธิภาพมากขึ้นในการใช้ data.table เพื่อแก้ปัญหานี้เป็นหนึ่งในคำตอบที่นี่ ... .



โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.