Des moyens rapides dans R pour obtenir la première ligne d'une trame de données regroupées par un identifiant [fermé]

14

Parfois, je n'ai besoin d'obtenir que la première ligne d'un ensemble de données regroupées par identifiant, comme lors de la récupération de l'âge et du sexe lorsqu'il y a plusieurs observations par individu. Quel est le moyen le plus rapide (ou le plus rapide) de le faire dans R? J'ai utilisé l'agrégat () ci-dessous et je pense qu'il existe de meilleures façons. Avant de poster cette question, j'ai cherché un peu sur Google, trouvé et essayé ddply, et j'ai été surpris que cela soit extrêmement lent et m'a donné des erreurs de mémoire sur mon jeu de données (400 000 lignes x 16 colonnes, 7 000 ID uniques), tandis que la version agrégée () était assez rapide.

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

MISE À JOUR: Voir la réponse de Chase et le commentaire de Matt Parker pour ce que je considère comme l'approche la plus élégante. Voir la réponse de @Matthew Dowle pour la solution la plus rapide qui utilise le data.tablepackage.

verrouillé
la source
Merci pour toutes vos réponses. La solution data.table de @Steve était la plus rapide par un facteur de ~ 5 sur mon ensemble de données sur la solution d'agrégat () de @Gavin (qui à son tour était plus rapide que mon code d'agrégat ()), et un facteur de ~ 7,5 sur la solution by () de @Matt. Je n'ai pas chronométré l'idée de remodeler parce que je ne pouvais pas la faire fonctionner rapidement. J'imagine que la solution que @Chase a donnée sera la plus rapide et c'était en fait ce que je cherchais, mais quand j'ai commencé à écrire ce commentaire, le code ne fonctionnait pas (je vois qu'il est corrigé maintenant!).
verrouillé
En fait, @Chase était plus rapide d'un facteur ~ 9 par rapport à data.table, j'ai donc changé ma réponse acceptée. Merci encore à tout le monde - appris un tas de nouveaux outils.
verrouillé
désolé, j'ai corrigé mon code. La seule mise en garde ou astuce ici consiste à concaténer une valeur qui n'est pas l'un de vos identifiants dans le diff()afin que vous puissiez récupérer le premier identifiant dx.
Chase

Réponses:

10

Votre colonne d'identification est-elle vraiment un facteur? Si elle est en fait numérique, je pense que vous pouvez utiliser la difffonction à votre avantage. Vous pouvez également le contraindre au numérique avec 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, ]
Chasse
la source
1
Intelligent! Vous pouvez également le faire dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]pour les données non numériques - j'obtiens 0,03 pour le caractère, 0,05 pour les facteurs. PS: il y a un extra )dans votre première system.time()fonction, après le deuxième zéro.
Matt Parker
@Matt - bon appel et belle prise. Je ne semble pas être en mesure de copier / coller du code qui mérite un retournement aujourd'hui.
Chase
Je travaille sur le programme London Cycle Hire et j'avais besoin de trouver un moyen de trouver les premiers et derniers cas de location de vélos par les utilisateurs. Avec 1 million d'utilisateurs, 10 millions de trajets par an et plusieurs années de données, ma boucle "for" faisait 1 utilisateur par seconde. J'ai essayé la solution "par", et elle n'a pas réussi à se terminer après une heure. Au début, je ne pouvais pas comprendre ce que faisait "l'alternative de Matt Parker à la solution de Chase", mais finalement le sou a chuté et il s'exécute en quelques secondes. Donc, le point sur l'amélioration de plus en plus grande avec des ensembles de données plus importants est prouvé par mon expérience.
George Simpson
@GeorgeSimpson - heureux de voir que cela est toujours référencé! La data.tablesolution ci-dessous devrait s'avérer la plus rapide, donc je vérifierais si j'étais vous (ce devrait probablement être la réponse acceptée ici).
Chase
17

Suite à la réponse de Steve, il y a un moyen beaucoup plus rapide dans 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 

Si vous avez simplement besoin de la première ligne de chaque groupe, il est beaucoup plus rapide de rejoindre directement cette ligne. Pourquoi créer l'objet .SD à chaque fois, uniquement pour en utiliser la première ligne?

Comparez les 0,064 de data.table à "l'alternative de Matt Parker à la solution de Chase" (qui semblait être la plus rapide jusqu'à présent):

> 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 

Donc ~ 5 fois plus rapide, mais c'est une petite table à moins de 1 million de lignes. À mesure que la taille augmente, la différence augmente également.

Matt Dowle
la source
Wow, je n'ai jamais vraiment apprécié à quel point la [.data.tablefonction peut être "intelligente" ... Je suppose que je ne savais pas que vous n'aviez pas créé d' .SDobjet si vous n'en aviez pas vraiment besoin. Joli!
Steve Lianoglou
Oui, c'est vraiment rapide! Même si vous incluez dxt <- data.table(dx, key='ID')l'appel à system.time (), c'est plus rapide que la solution de @ Matt.
verrouillé
Je suppose que cela est obsolète maintenant, car avec les versions plus récentes de data.table a SD[1L]été entièrement optimisé et en fait, la réponse @SteveLianoglou serait deux fois plus rapide pour les lignes 5e7.
David Arenburg
@DavidArenburg Depuis la v1.9.8 nov 2016, oui. N'hésitez pas à modifier cette réponse directement, ou peut-être que ce Q doit être un wiki communautaire ou quelque chose.
Matt Dowle
10

Vous n'avez pas besoin de plusieurs merge()étapes, seulement les aggregate()deux variables d'intérêt:

> 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

Horaires de comparaison:

1) La solution de 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 solution Reshape2 de 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) La solution data.table de 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) Solution rapide de Chase utilisant des chiffres, et non des facteurs 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

et 5) l'alternative de Matt Parker à la solution de Chase, pour le caractère ou le facteur ID, qui est légèrement plus rapide que la solution numérique de 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
Réintégrer Monica - G. Simpson
la source
Oh, c'est vrai, merci! Oublié cette syntaxe pour l'agrégat.
verrouillé
Si vous souhaitez ajouter la solution de Chase, voici ce que j'ai obtenu: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
verrouillé
@lockedoff - fait, merci, mais je n'ai pas échantillonné au hasard les IDs donc le résultat était comparable à d'autres solutions.
Rétablir Monica - G. Simpson
Et chronométrez la version de @Matt Parker dans les commentaires de la réponse de @ Chase
Réinstallez Monica - G. Simpson
2
Merci d'avoir fait les horaires, Gavin - c'est vraiment utile pour des questions comme celles-ci.
Matt Parker
9

Vous pouvez essayer d'utiliser le package data.table .

Pour votre cas particulier, l'avantage est que c'est (incroyablement) rapide. La première fois que je l'ai découvert, je travaillais sur des objets data.frame avec des centaines de milliers de lignes. "Normal" aggregateou des ddplyméthodes ont été prises ~ 1-2 minutes pour terminer (c'était avant que Hadley n'introduise le idata.framemojo ddply). À l'aide data.table, l'opération a été littéralement effectuée en quelques secondes.

L'inconvénient est que c'est tellement rapide car il va utiliser votre data.table (c'est comme un data.frame) par "colonnes clés" et utiliser une stratégie de recherche intelligente pour trouver des sous-ensembles de vos données. Cela entraînera une réorganisation de vos données avant de collecter des statistiques dessus.

Étant donné que vous voudrez simplement la première ligne de chaque groupe - peut-être que la réorganisation va gâcher la première ligne, c'est pourquoi cela pourrait ne pas être approprié dans votre situation.

Quoi qu'il en soit, vous devrez juger si cela data.tableest approprié ou non , mais voici comment vous l'utiliseriez avec les données que vous avez présentées:

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

Mise à jour: Matthew Dowle (le principal développeur du package data.table) a fourni un moyen meilleur / plus intelligent / (extrêmement) plus efficace d'utiliser data.table pour résoudre ce problème comme l'une des réponses ici ... certainement vérifier cela .

Steve Lianoglou
la source
4

Essayez remodeler2

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

Tu pourrais essayer

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)

Je ne sais pas si cela sera plus rapide que plyrcela.

Matt Parker
la source