Metodi rapidi in R per raggruppare la prima riga di un frame di dati in base a un identificatore [chiuso]


14

A volte ho bisogno di ottenere solo la prima riga di un set di dati raggruppati per un identificatore, come quando si recuperano età e sesso quando ci sono più osservazioni per individuo. Qual è un modo veloce (o il più veloce) per farlo in R? Ho usato aggregate () sotto e sospetto che ci siano modi migliori. Prima di pubblicare questa domanda ho cercato un po 'su Google, ho trovato e provato ddply e sono rimasto sorpreso dal fatto che fosse estremamente lento e mi ha dato errori di memoria nel mio set di dati (400.000 righe x 16 cols, 7.000 ID univoci), mentre la versione aggregate () era ragionevolmente veloce.

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

AGGIORNAMENTO: vedi la risposta di Chase e il commento di Matt Parker per quello che considero l'approccio più elegante. Vedi la risposta di @Matthew Dowle per la soluzione più veloce che utilizza il data.tablepacchetto.


Grazie per tutte le tue risposte La soluzione data.table di @Steve è stata la più veloce di un fattore di ~ 5 sul mio set di dati rispetto alla soluzione aggregate () di @Gavin (che a sua volta era più veloce del mio codice aggregate ()) e un fattore di ~ 7.5 sulla soluzione by () di @Matt. Non ho cronometrato l'idea di rimodellare perché non riuscivo a farlo funzionare rapidamente. Immagino che la soluzione fornita da @Chase sarà la più veloce ed era effettivamente quello che stavo cercando, ma quando ho iniziato a scrivere questo commento, il codice non funzionava (vedo che è stato risolto ora!).
chiuso il

In realtà @Chase era più veloce di un fattore di ~ 9 su data.table, quindi ho cambiato la mia risposta accettata. Grazie ancora a tutti - ho imparato un sacco di nuovi strumenti.
chiuso il

scusa, ho corretto il mio codice. L'unico avvertimento o trucco qui è concatenare un valore che non è uno dei tuoi ID in diff()modo da poter raccogliere il primo ID in dx.
Chase

Risposte:


10

La tua colonna ID è davvero un fattore? Se in effetti è numerico, penso che tu possa usare la difffunzione a tuo vantaggio. Si potrebbe anche forzare a numerico 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
Intelligente! Potresti anche fare dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]per dati non numerici: ottengo 0,03 per carattere, 0,05 per fattori. PS: c'è un extra )nella tua prima system.time()funzione, dopo il secondo zero.
Matt Parker

@Matt - buona chiamata e bella cattura. Oggi non riesco a copiare / incollare il codice che vale la pena capovolgere.
Chase

Sto lavorando allo schema del London Cycle Hire e dovevo trovare un modo per trovare la prima e l'ultima istanza di noleggio bici da parte degli utenti. Con 1 milione di utenti, 10 milioni di viaggi all'anno e dati di diversi anni, il mio ciclo "for" stava facendo 1 utente al secondo. Ho provato la soluzione "by" e non è stato possibile completarla dopo un'ora. All'inizio non riuscivo a capire cosa stesse facendo "l'alternativa di Matt Parker alla soluzione di Chase", ma alla fine il centesimo è caduto e si esegue in pochi secondi. Quindi il punto sul miglioramento che sta diventando maggiore con set di dati più grandi è dimostrato dalla mia esperienza.
George Simpson,

@GeorgeSimpson - felice di vedere che questo è ancora referenziato! La data.tablesoluzione in basso dovrebbe dimostrarsi la più veloce, quindi controllerei se fossi in te (probabilmente dovrebbe essere la risposta accettata qui).
Insegui il

17

In seguito alla risposta di Steve, c'è un modo molto più veloce in 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 

Se hai semplicemente bisogno della prima riga di ciascun gruppo, è molto più veloce unirti direttamente a quella riga. Perché creare l'oggetto .SD ogni volta, solo per usarne la prima riga?

Confronta lo 0.064 di data.table con "l'alternativa di Matt Parker alla soluzione di Chase" (che finora sembrava essere la più veloce):

> 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 

Quindi ~ 5 volte più veloce, ma è una piccola tabella con meno di 1 milione di righe. All'aumentare delle dimensioni, aumenta anche la differenza.


Caspita, non ho mai davvero apprezzato quanto "intelligente" [.data.tablepossa avere la funzione ... Immagino di non aver realizzato che non hai creato un .SDoggetto se non ne avessi davvero bisogno. Ben fatto!
Steve Lianoglou

Sì, è davvero veloce! Anche se includi dxt <- data.table(dx, key='ID')nella chiamata a system.time (), è più veloce della soluzione di @ Matt.
chiuso l'

Immagino che questo obsoleto ora come con le nuove versioni data.table sia SD[1L]stato completamente ottimizzato e in realtà la risposta di @SteveLianoglou sarebbe due volte più veloce per 5e7 righe.
David Arenburg,

@DavidArenburg A partire dalla v1.9.8 nov 2016, sì. Sentiti libero di modificare direttamente questa risposta, o forse questa Q deve essere wiki della community o qualcosa del genere.
Matt Dowle,

10

Non hai bisogno di più merge()passaggi, solo aggregate()entrambe le variabili di 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

Tempi di confronto:

1) Soluzione di 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) La soluzione reshape2 di 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) Soluzione data.table di 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) La soluzione rapida di Chase usando numeri, non fattori 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

e 5) l'alternativa di Matt Parker alla soluzione di Chase, per carattere o fattore ID, che è leggermente più veloce di quella numerica di 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

Oh, giusto, grazie! Dimenticato quella sintassi per aggregato.
chiuso il

Se desideri aggiungere la soluzione di Chase, ecco cosa ho ottenuto: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
bloccato il

@lockedoff: fatto, grazie, ma non ho provato a caso IDi risultati, quindi il risultato era paragonabile ad altre soluzioni.
Ripristina Monica - G. Simpson il

E ora la versione di @Matt Parker nei commenti alla risposta di @ Chase
Ripristina Monica - G. Simpson

2
Grazie per il tempo, Gavin - è davvero utile per domande come queste.
Matt Parker

9

Puoi provare a usare il pacchetto data.table .

Per il tuo caso particolare, il lato positivo è che è (follemente) veloce. La prima volta che mi è stato presentato, stavo lavorando su oggetti data.frame con centinaia di migliaia di righe. "Normale" aggregateo ddplymetodi sono stati presi ~ 1-2 minuti per completare (questo era prima che Hadley introducesse il idata.framemojo ddply). Utilizzando data.table, l'operazione è stata letteralmente eseguita in pochi secondi.

Il rovescio della medaglia è che è così veloce perché farà ricorso al tuo data.table (è proprio come un data.frame) da "colonne chiave" e usa una strategia di ricerca intelligente per trovare sottoinsiemi dei tuoi dati. Ciò comporterà un riordino dei dati prima di raccogliere statistiche su di essi.

Dato che vorrai solo la prima riga di ogni gruppo - forse il riordino incasinerà quale riga è la prima, motivo per cui potrebbe non essere appropriato nella tua situazione.

Ad ogni modo, dovrai giudicare se data.tableè appropriato qui, ma è così che lo useresti con i dati che hai presentato:

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

Aggiornamento: Matthew Dowle (il principale sviluppatore del pacchetto data.table) ha fornito un modo migliore / più intelligente / (estremamente) più efficiente di utilizzare data.table per risolvere questo problema come una delle risposte qui ... sicuramente verificalo .


4

Prova reshape2

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

3

Potresti provare

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)

Non ho idea se questo sarà più veloce di plyr, però.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.