Formas rápidas en R para obtener la primera fila de un marco de datos agrupada por un identificador [cerrado]

14

A veces necesito obtener solo la primera fila de un conjunto de datos agrupados por un identificador, como al recuperar la edad y el género cuando hay múltiples observaciones por individuo. ¿Cuál es una forma rápida (o la más rápida) de hacer esto en R? Usé agregado () a continuación y sospecho que hay mejores formas. Antes de publicar esta pregunta, busqué un poco en Google, encontré e intenté ddply, y me sorprendió que fuera extremadamente lento y me dio errores de memoria en mi conjunto de datos (400,000 filas x 16 cols, 7,000 ID únicos), mientras que la versión agregada () fue razonablemente rápido.

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

ACTUALIZACIÓN: Vea la respuesta de Chase y el comentario de Matt Parker para lo que considero el enfoque más elegante. Consulte la respuesta de @Matthew Dowle para obtener la solución más rápida que utiliza el data.tablepaquete.

bloqueado
fuente
Gracias por todas sus respuestas. La solución data.table de @Steve fue la más rápida por un factor de ~ 5 en mi conjunto de datos sobre la solución agregada () de @Gavin (que a su vez fue más rápida que mi código agregado) y un factor de ~ 7.5 sobre la solución by () de @Matt. No cronometré la idea de remodelación porque no pude hacer que funcionara rápidamente. Supongo que la solución que dio @Chase será la más rápida y en realidad era lo que estaba buscando, pero cuando comencé a escribir este comentario, el código no funcionaba (¡veo que ahora está solucionado!).
cerrado el
En realidad, @Chase fue más rápido por un factor de ~ 9 sobre data.table, así que cambié mi respuesta aceptada. Gracias de nuevo a todos: aprendimos un montón de nuevas herramientas.
cerrado el
lo siento, arreglé mi código. La única advertencia o truco aquí es concatenar un valor que no sea uno de sus ID diff()para que pueda recoger la primera ID dx.
Chase

Respuestas:

10

¿Es su columna de ID realmente un factor? Si de hecho es numérico, creo que puede usar la difffunción para su ventaja. También podría obligarlo a numérico con 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, ]
Persecución
fuente
1
¡Inteligente! También podría hacerlo dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]para datos no numéricos: obtengo 0.03 por carácter, 0.05 por factores. PD: hay un extra )en tu primera system.time()función, después del segundo cero.
Matt Parker
@ Matt - buena llamada y buena captura. Parece que hoy no puedo copiar / pegar código que valga la pena.
Chase
Estoy trabajando en el esquema de London Cycle Hire, y necesitaba encontrar la manera de encontrar la primera y la última instancia de usuarios de bicicletas. Con 1 millón de usuarios, 10 millones de viajes por año y datos de varios años, mi ciclo "for" estaba haciendo 1 usuario por segundo. Probé la solución "by" y no se pudo completar después de una hora. Al principio no pude entender qué estaba haciendo "la alternativa de Matt Parker a la solución de Chase", pero finalmente el centavo cayó y se ejecuta en segundos. Por lo tanto, mi experiencia demuestra que la mejora cada vez es mayor con conjuntos de datos más grandes.
George Simpson
@GeorgeSimpson: ¡me alegra ver que todavía se hace referencia a esto! La data.tablesolución a continuación debería ser la más rápida, por lo que lo comprobaría si fuera usted (probablemente debería ser la respuesta aceptada aquí).
Chase
17

Siguiendo la respuesta de Steve, hay una forma mucho más rápida en 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 simplemente necesita la primera fila de cada grupo, es mucho más rápido unirse a esa fila directamente. ¿Por qué crear el objeto .SD cada vez, solo para usar la primera fila?

Compare el 0.064 de data.table con la "alternativa de Matt Parker a la solución de Chase" (que parecía ser la más rápida hasta ahora):

> 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 

Entonces ~ 5 veces más rápido, pero es una tabla pequeña con menos de 1 millón de filas. A medida que aumenta el tamaño, también lo hace la diferencia.

Matt Dowle
fuente
Wow, nunca aprecié realmente cuán "inteligente" [.data.tablepuede ser la función ... Creo que no me di cuenta de que no creaste un .SDobjeto si realmente no lo necesitas. ¡Buena esa!
Steve Lianoglou
Sí, eso es realmente rápido! Incluso si incluye dxt <- data.table(dx, key='ID')en la llamada a system.time (), es más rápido que la solución de @ Matt.
cerrado el
Supongo que esto está desactualizado ahora, ya que con las nuevas versiones de data.table SD[1L]estaba completamente optimizado y, en realidad, la respuesta de @SteveLianoglou sería el doble de rápida para 5e7 filas.
David Arenburg
@DavidArenburg Desde v1.9.8 Nov 2016, sí. Siéntase libre de editar esta respuesta directamente, o tal vez esta Q debe ser una wiki comunitaria o algo así.
Matt Dowle
10

No necesitas multiples merge() pasos, solo aggregate()ambas variables de interés:

> 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

Tiempos de comparación:

1) la solución 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 solución 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) Solución 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) La solución rápida de Chase usando numérico, no factor 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

y 5) la alternativa de Matt Parker a la solución de Chase, por carácter o factor ID, que es ligeramente más rápida que la numérica 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
Restablece a Mónica - G. Simpson
fuente
Oh cierto, gracias! Olvidé esa sintaxis para el agregado.
cerrado el
Si desea agregar la solución de Chase, esto es lo que obtuve: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
cerrado el
@lockedoff: hecho, gracias, pero no tomé muestras de forma aleatoria IDpara que el resultado fuera comparable a otras soluciones.
Restablece a Monica - G. Simpson el
Y cronometra la versión de @Matt Parker en los comentarios a la respuesta de @ Chase
Restablece a Monica - G. Simpson el
2
Gracias por hacer los horarios, Gavin, eso es realmente útil para preguntas como estas.
Matt Parker
9

Puede intentar usar el paquete data.table .

Para su caso particular, lo bueno es que es (increíblemente) rápido. La primera vez que me presentaron, estaba trabajando en objetos data.frame con cientos de miles de filas. "Normal" aggregateo los ddplymétodos se tomaron ~ 1-2 minutos para completar (esto fue antes de que Hadley introdujera el idata.framemojo ddply). Utilizandodata.table , la operación se realizó literalmente en cuestión de segundos.

La desventaja es que es muy rápido porque recurrirá a su data.table (es como un data.frame) por "columnas clave" y usará una estrategia de búsqueda inteligente para encontrar subconjuntos de sus datos. Esto dará como resultado un reordenamiento de sus datos antes de recopilar estadísticas sobre ellos.

Dado que solo querrá la primera fila de cada grupo, tal vez el reordenamiento estropeará qué fila es la primera, por lo que podría no ser apropiado en su situación.

De todos modos, tendrás que juzgar si data.tablees apropiado o no aquí, pero así es como lo usarías con los datos que has presentado:

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

Actualización: Matthew Dowle (el desarrollador principal del paquete data.table) ha proporcionado una forma mejor / más inteligente / (extremadamente) más eficiente de usar data.table para resolver este problema como una de las respuestas aquí ... definitivamente échale un vistazo .

Steve Lianoglou
fuente
4

Prueba reshape2

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

Tu podrías intentar

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)

Sin plyrembargo, no tengo idea de si esto será más rápido .

Matt Parker
fuente