Schnelle Wege in R, um die erste Zeile eines Datenrahmens zu erhalten, der nach einem Bezeichner gruppiert ist [closed]


14

Manchmal muss ich nur die erste Zeile eines Datensatzes abrufen, der nach einem Bezeichner gruppiert ist, z. B. beim Abrufen von Alter und Geschlecht, wenn mehrere Beobachtungen pro Person vorliegen. Was ist ein schneller (oder der schnellste) Weg, dies in R zu tun? Ich habe aggregate () unten verwendet und vermute, dass es bessere Möglichkeiten gibt. Bevor ich diese Frage postete, suchte ich ein bisschen bei Google, fand und probierte ddply und war überrascht, dass es extrem langsam war und mir Speicherfehler in meinem Datensatz (400.000 Zeilen x 16 Spalten, 7.000 eindeutige IDs) und in der aggregate () -Version bescherte war einigermaßen schnell.

(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: Siehe Chases Antwort und Matt Parkers Kommentar zu dem, was ich für den elegantesten Ansatz halte. Die schnellste Lösung, die das data.tablePaket verwendet, finden Sie in der Antwort von @Matthew Dowle .


Vielen Dank für alle Ihre Antworten. Die data.table-Lösung von @Steve war mit einem Faktor von ~ 5 in meinem Datensatz schneller als die aggregate () -Lösung von @Gavin (die wiederum schneller als mein aggregate () -Code war) und mit einem Faktor von ~ 7,5 über die by () Lösung von @Matt. Ich habe die Umgestaltungsidee nicht zeitlich festgelegt, weil ich sie nicht schnell zum Laufen bringen konnte. Ich vermute, dass die Lösung, die @Chase gegeben hat, die schnellste sein wird und genau das war, wonach ich gesucht habe, aber als ich anfing, diesen Kommentar zu schreiben, funktionierte der Code nicht (ich sehe, dass er jetzt behoben ist!).
gesperrt

Eigentlich war @Chase um einen Faktor von ~ 9 schneller als data.table, also habe ich meine akzeptierte Antwort geändert. Nochmals vielen Dank an alle - eine Reihe neuer Tools gelernt.
gesperrt

Entschuldigung, ich habe meinen Code repariert. Die einzige Einschränkung oder der einzige Trick besteht darin, einen Wert zu verketten, der nicht zu Ihren IDs in gehört, diff()damit Sie die erste ID in auswählen können dx.
Chase

Antworten:


10

Ist Ihre ID-Spalte wirklich ein Faktor? Wenn es tatsächlich numerisch ist, können Sie die diffFunktion zu Ihrem Vorteil nutzen. Sie könnten es auch zwingen, mit zu numerisch 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
Klug! Sie könnten auch dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]für nicht numerische Daten tun - ich erhalte 0,03 für Zeichen, 0,05 für Faktoren. PS: )In Ihrer ersten system.time()Funktion gibt es ein Extra , nach der zweiten Null.
Matt Parker

@Matt - guter Anruf und schöner Fang. Ich scheine heute keinen Code kopieren / einfügen zu können, der einen Flip wert ist.
Chase

Ich arbeite am London Cycle Hire-Programm und musste einen Weg finden, um die erste und letzte Instanz von Fahrradverleihern zu finden. Bei 1 Million Benutzern, 10 Millionen Fahrten pro Jahr und Daten von mehreren Jahren machte meine "for" -Schleife 1 Benutzer pro Sekunde. Ich habe die "By" -Lösung ausprobiert und sie konnte nach einer Stunde nicht abgeschlossen werden. Zuerst konnte ich mir nicht vorstellen, was "Matt Parkers Alternative zu Chases Lösung" bedeutete, aber schließlich fiel der Cent ab und es ist in Sekunden erledigt. Der Punkt, dass die Verbesserung bei größeren Datenmengen größer wird, wird durch meine Erfahrung belegt.
George Simpson

@ GeorgeSimpson - froh zu sehen, dass immer noch darauf verwiesen wird! Die data.tableLösung unten sollte sich als die schnellste herausstellen, also würde ich das überprüfen, wenn ich Sie wäre (es sollte wahrscheinlich die akzeptierte Antwort hier sein).
Chase

17

Nach der Antwort von Steve gibt es in data.table einen viel schnelleren Weg:

> # 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 

Wenn Sie lediglich die erste Zeile jeder Gruppe benötigen, können Sie sich viel schneller direkt an diese Zeile anschließen. Warum sollte das SD-Objekt jedes Mal erstellt werden, um nur die erste Zeile zu verwenden?

Vergleichen Sie die 0.064 von data.table mit "Matt Parkers Alternative zu Chases Lösung" (die bisher die schnellste zu sein schien):

> 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 

Also ~ 5 mal schneller, aber es ist ein winziger Tisch mit weniger als 1 Million Zeilen. Mit zunehmender Größe nimmt auch der Unterschied zu.


Wow, ich habe nie wirklich geschätzt, wie "schlau" die [.data.tableFunktion werden kann ... Ich glaube, ich habe nicht gemerkt, dass Sie kein .SDObjekt erstellt haben, wenn Sie es nicht wirklich brauchten. Schön!
Steve Lianoglou

Ja, das geht ja schnell! Auch wenn Sie dxt <- data.table(dx, key='ID')system.time () aufrufen, ist dies schneller als die @ Matt-Lösung.
gesperrt

Ich vermute, dass dies jetzt veraltet ist, da die neueren data.table-Versionen SD[1L]vollständig optimiert wurden und die Antwort von @SteveLianoglou für 5e7-Zeilen doppelt so schnell wäre.
David Arenburg

@DavidArenburg Ab Version 1.9.8, November 2016, ja. Fühlen Sie sich frei, diese Antwort direkt zu bearbeiten, oder vielleicht muss es sich bei diesem Q um ein Community-Wiki oder ähnliches handeln.
Matt Dowle

10

Sie brauchen nicht mehrere merge()Schritte, nur aggregate()beide Variablen von Interesse:

> 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

Vergleichszeitpunkte:

1) Matts Lösung:

> 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) Zachs Reshape2-Lösung:

> 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) Steve's data.table Lösung:

> 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) Chases schnelle Lösung unter Verwendung von Zahlen und nicht von Faktoren 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

und 5) Matt Parkers Alternative zu Chases Lösung für Charakter oder Faktor ID, die etwas schneller ist als Chases numerische Lösung 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

Oh, richtig, danke! Ich habe diese Syntax für aggregate vergessen.
gesperrt

Wenn Sie Chases Lösung hinzufügen 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
möchten

@lockedoff - fertig, danke, aber ich habe das nicht zufällig ausgewählt ID s so dass das Ergebnis mit anderen Lösungen vergleichbar war.
Setzen Sie Monica - G. Simpson am

Und Zeit @ Matt Parker Version in den Kommentaren zu @ Chase Antwort
Reinstate Monica - G. Simpson

2
Danke, dass du das Timing gemacht hast, Gavin - das ist wirklich hilfreich für Fragen wie diese.
Matt Parker

9

Sie können versuchen, das Paket data.table zu verwenden .

Für Ihren speziellen Fall ist der Vorteil, dass es (wahnsinnig) schnell ist. Als ich zum ersten Mal darauf aufmerksam wurde, arbeitete ich an data.frame-Objekten mit Hunderttausenden von Zeilen. "Normal" aggregateoder ddplyMethoden wurden ~ 1-2 Minuten in Anspruch genommen (dies war, bevor Hadley das idata.frameMojo einführte ddply). Verwendendata.table war die Operation buchstäblich in wenigen Sekunden erledigt.

Der Nachteil ist, dass es so schnell ist, weil es Ihre data.table (es ist wie ein data.frame) nach "Schlüsselspalten" sortiert und eine intelligente Suchstrategie verwendet, um Teilmengen Ihrer Daten zu finden. Dies führt zu einer Neuordnung Ihrer Daten, bevor Sie Statistiken darüber sammeln.

Vorausgesetzt, Sie möchten nur die erste Zeile jeder Gruppe - möglicherweise wird die Neuordnung die erste Zeile durcheinander bringen, weshalb dies in Ihrer Situation möglicherweise nicht angemessen ist.

Wie auch immer, Sie müssen hier beurteilen, ob dies data.tableangemessen ist oder nicht , aber so würden Sie es mit den Daten verwenden, die Sie präsentiert haben:

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

Update: Matthew Dowle (der Hauptentwickler des data.table-Pakets) bietet eine bessere / intelligentere / (extrem) effizientere Möglichkeit, data.table zur Lösung dieses Problems zu verwenden .


4

Versuchen Sie es mit reshape2

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

3

Du könntest es versuchen

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)

Ich habe jedoch keine Ahnung, ob dies schneller sein plyrwird.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.