Seleccione la primera fila por grupo

85

De un marco de datos como este

test <- data.frame('id'= rep(1:5,2), 'string'= LETTERS[1:10])
test <- test[order(test$id), ]
rownames(test) <- 1:10

> test
    id string
 1   1      A
 2   1      F
 3   2      B
 4   2      G
 5   3      C
 6   3      H
 7   4      D
 8   4      I
 9   5      E
 10  5      J

Quiero crear uno nuevo con la primera fila de cada par de id / string. Si sqldf aceptó el código R dentro de él, la consulta podría verse así:

res <- sqldf("select id, min(rownames(test)), string 
              from test 
              group by id, string")

> res
    id string
 1   1      A
 3   2      B
 5   3      C
 7   4      D
 9   5      E

¿Existe una solución que no sea la de crear una nueva columna como

test$row <- rownames(test)

y ejecutando la misma consulta sqldf con min (fila)?

dmvianna
fuente
1
@Matthew, mi pregunta es más antigua.
dmvianna
2
Su pregunta tiene 1 año y la otra pregunta tiene 4 años, ¿no? Hay tantos duplicados de esta pregunta
Mateo
@Matthew Lo siento, debo haber leído mal las fechas.
dmvianna

Respuestas:

119

Puede utilizar duplicatedpara hacer esto muy rápidamente.

test[!duplicated(test$id),]

Puntos de referencia, para los fanáticos de la velocidad:

ju <- function() test[!duplicated(test$id),]
gs1 <- function() do.call(rbind, lapply(split(test, test$id), head, 1))
gs2 <- function() do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
jply <- function() ddply(test,.(id),function(x) head(x,1))
jdt <- function() {
  testd <- as.data.table(test)
  setkey(testd,id)
  # Initial solution (slow)
  # testd[,lapply(.SD,function(x) head(x,1)),by = key(testd)]
  # Faster options :
  testd[!duplicated(id)]               # (1)
  # testd[, .SD[1L], by=key(testd)]    # (2)
  # testd[J(unique(id)),mult="first"]  # (3)
  # testd[ testd[,.I[1L],by=id] ]      # (4) needs v1.8.3. Allows 2nd, 3rd etc
}

library(plyr)
library(data.table)
library(rbenchmark)

# sample data
set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]

benchmark(ju(), gs1(), gs2(), jply(), jdt(),
    replications=5, order="relative")[,1:6]
#     test replications elapsed relative user.self sys.self
# 1   ju()            5    0.03    1.000      0.03     0.00
# 5  jdt()            5    0.03    1.000      0.03     0.00
# 3  gs2()            5    3.49  116.333      2.87     0.58
# 2  gs1()            5    3.58  119.333      3.00     0.58
# 4 jply()            5    3.69  123.000      3.11     0.51

Intentémoslo de nuevo, pero solo con los contendientes de la primera manga y con más datos y más réplicas.

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
benchmark(ju(), jdt(), order="relative")[,1:6]
#    test replications elapsed relative user.self sys.self
# 1  ju()          100    5.48    1.000      4.44     1.00
# 2 jdt()          100    6.92    1.263      5.70     1.15
Joshua Ulrich
fuente
El ganador: system.time (dat3 [! Duplicated (dat3 $ id),]) usuario del sistema transcurrido 0.07 0.00 0.07
dmvianna
2
@dmvianna: No lo tengo instalado y no tenía ganas de molestarme con él. :)
Joshua Ulrich
¿Estamos seguros de que mi código data.table es lo más eficiente posible? No confío en mi capacidad para obtener el mejor rendimiento de esa herramienta.
joran
2
Además, creo que si va a comparar el data.table, la clave debe incluir el orden por id dentro de las llamadas base.
mnel
1
@JoshuaUlrich Una pregunta más: ¿por qué se necesita la primera oración, es decir, suponer que los datos ya están ordenados? !duplicated(x)encuentra el primero de cada grupo incluso si no está ordenado, iiuc.
Matt Dowle
36

Yo prefiero el enfoque dplyr.

group_by(id) seguido de cualquiera

  • filter(row_number()==1) o
  • slice(1) o
  • slice_head(1) # (dplyr => 1.0)
  • top_n(n = -1)
    • top_n()utiliza internamente la función de rango. Negativo selecciona desde la parte inferior del rango.

En algunos casos, puede ser necesario organizar los identificadores después de group_by.

library(dplyr)

# using filter(), top_n() or slice()

m1 <-
test %>% 
  group_by(id) %>% 
  filter(row_number()==1)

m2 <-
test %>% 
  group_by(id) %>% 
  slice(1)

m3 <-
test %>% 
  group_by(id) %>% 
  top_n(n = -1)

Los tres métodos devuelven el mismo resultado

# A tibble: 5 x 2
# Groups:   id [5]
     id string
  <int> <fct> 
1     1 A     
2     2 B     
3     3 C     
4     4 D     
5     5 E
Kresten
fuente
2
sliceTambién vale la pena mencionarlo . slice(x)es un atajo para filter(row_number() %in% x).
Gregor Thomas
Muy elegante. ¿Sabes por qué tengo que convertir mi data.tablea data.framepara que esto funcione?
James Hirschorn
@JamesHirschorn No soy un experto en todas las diferencias. Pero data.tablehereda del, por data.framelo que en muchos casos puede usar comandos dplyr en un data.table. El ejemplo anterior, por ejemplo, también funciona si testes a data.table. Consulte, por ejemplo, stackoverflow.com/questions/13618488/… para una explicación más profunda
Kresten
Esta es una forma tidyverse de hacerlo y, como puede ver, el data.frame es en realidad un tibble aquí. Personalmente le aconsejo que trabaje siempre con tibbles también porque ggplot2 está construido de manera similar.
Garini
17

Qué pasa

DT <- data.table(test)
setkey(DT, id)

DT[J(unique(id)), mult = "first"]

Editar

También hay un método único para el data.tablesque devolverá la primera fila por clave

jdtu <- function() unique(DT)

Creo que si realiza un pedido testfuera del punto de referencia, también puede eliminar la conversión setkeyy data.tabledel punto de referencia (ya que la clave de configuración básicamente se ordena por id, lo mismo que order).

set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]
DT <- data.table(DT, key = 'id')
ju <- function() test[!duplicated(test$id),]

jdt <- function() DT[J(unique(id)),mult = 'first']


 library(rbenchmark)
benchmark(ju(), jdt(), replications = 5)
##    test replications elapsed relative user.self sys.self 
## 2 jdt()            5    0.01        1      0.02        0        
## 1  ju()            5    0.05        5      0.05        0         

y con mas datos

** Edite con un método único **

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
DT <- data.table(test, key = 'id')
       test replications elapsed relative user.self sys.self 
2  jdt()            5    0.09     2.25      0.09     0.00    
3 jdtu()            5    0.04     1.00      0.05     0.00      
1   ju()            5    0.22     5.50      0.19     0.03        

El método único es el más rápido aquí.

mnel
fuente
4
Ni siquiera tiene que configurar la clave. unique(DT,by="id")funciona directamente
Mateo
Para su información a partir de la data.tableversión> = 1.9.8, el byargumento predeterminado para uniquees by = seq_along(x)(todas las columnas), en lugar del predeterminado anteriorby = key(x)
IceCreamToucan
12

Una ddplyopción sencilla :

ddply(test,.(id),function(x) head(x,1))

Si la velocidad es un problema, se podría tomar un enfoque similar con data.table:

testd <- data.table(test)
setkey(testd,id)
testd[,.SD[1],by = key(testd)]

o esto podría ser considerablemente más rápido:

testd[testd[, .I[1], by = key(testd]$V1]
joran
fuente
Sorprendentemente, sqldf lo hace más rápido: 1.77 0.13 1.92 vs 10.53 0.00 10.79 con data.table
dmvianna
3
@dmvianna No necesariamente descartaría data.table. No soy un experto con esa herramienta, por lo que mi código data.table puede no ser la forma más eficiente de hacerlo.
joran
Elegí esto prematuramente. Cuando lo ejecuté en una tabla de datos grande, fue ridículamente lento y no funcionó: el número de filas fue el mismo después.
James Hirschorn
@JamesHirachorn Escribí esto hace mucho tiempo, el paquete ha cambiado mucho y casi no uso data.table. Si encuentra la manera correcta de hacer esto con ese paquete, no dude en sugerir una edición para mejorarlo.
joran
8

ahora, para dplyragregar un contador distinto.

df %>%
    group_by(aa, bb) %>%
    summarise(first=head(value,1), count=n_distinct(value))

Creas grupos, los resumen dentro de grupos.

Si los datos son numéricos, puede utilizar:
first(value)[también hay last(value)] en lugar dehead(value, 1)

ver: http://cran.rstudio.com/web/packages/dplyr/vignettes/introduction.html

Lleno:

> df
Source: local data frame [16 x 3]

   aa bb value
1   1  1   GUT
2   1  1   PER
3   1  2   SUT
4   1  2   GUT
5   1  3   SUT
6   1  3   GUT
7   1  3   PER
8   2  1   221
9   2  1   224
10  2  1   239
11  2  2   217
12  2  2   221
13  2  2   224
14  3  1   GUT
15  3  1   HUL
16  3  1   GUT

> library(dplyr)
> df %>%
>   group_by(aa, bb) %>%
>   summarise(first=head(value,1), count=n_distinct(value))

Source: local data frame [6 x 4]
Groups: aa

  aa bb first count
1  1  1   GUT     2
2  1  2   SUT     2
3  1  3   SUT     3
4  2  1   221     3
5  2  2   217     3
6  3  1   GUT     2
Paul
fuente
Esta respuesta está bastante anticuada: hay mejores maneras de hacer esto dplyrque no requieren escribir una declaración para que se incluya cada columna (consulte la respuesta de atomman a continuación, por ejemplo) . Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use primero (valor) `vs head(value)(o simplemente value[1])
Gregor Thomas
7

(1) SQLite tiene una rowidpseudocolumna incorporada, por lo que esto funciona:

sqldf("select min(rowid) rowid, id, string 
               from test 
               group by id")

dando:

  rowid id string
1     1  1      A
2     3  2      B
3     5  3      C
4     7  4      D
5     9  5      E

(2) También en sqldfsí mismo tiene un row.names=argumento:

sqldf("select min(cast(row_names as real)) row_names, id, string 
              from test 
              group by id", row.names = TRUE)

dando:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

(3) Una tercera alternativa que combine los elementos de los dos anteriores podría ser incluso mejor:

sqldf("select min(rowid) row_names, id, string 
               from test 
               group by id", row.names = TRUE)

dando:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

Tenga en cuenta que los tres se basan en una extensión de SQLite para SQL donde se garantiza que el uso de mino maxdará como resultado que las otras columnas se elijan de la misma fila. (En otras bases de datos basadas en SQL, esto puede no estar garantizado).

G. Grothendieck
fuente
¡Gracias! Esto es mucho mejor que la respuesta aceptada en mi opinión porque es generalizable para tomar el primer / último elemento en un paso agregado utilizando múltiples funciones agregadas (es decir, tomar la primera de esta variable, sumar esa variable, etc.).
Bridgeburners
4

Una opción de base R es el split()- lapply()- do.call()modismo:

> do.call(rbind, lapply(split(test, test$id), head, 1))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

Una opción más directa es lapply()la [función:

> do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

El espacio de coma 1, )al final de la lapply()llamada es esencial, ya que equivale a llamar [1, ]para seleccionar la primera fila y todas las columnas.

Gavin Simpson
fuente
Esto fue muy lento, Gavin: el sistema de usuario pasó 91.84 6.02 101.10
dmvianna
Todo lo que involucre marcos de datos lo será. Su utilidad tiene un precio. De ahí data.table, por ejemplo.
Gavin Simpson
en mi defensa y en la de R, no mencionaste nada sobre la eficiencia en la pregunta. A menudo, la facilidad de uso es una característica. Sea testigo de la popularidad de ply, que también es "lenta", al menos hasta la próxima versión que tenga soporte para data.table.
Gavin Simpson
1
Estoy de acuerdo. No quise insultarte. He encontrado, sin embargo, que el método de Joshua @-Ulrich era tanto rápido y fácil. : 7)
dmvianna
No hay necesidad de disculparme y no lo tomé como un insulto. Solo estaba señalando que se ofreció sin ningún reclamo de eficiencia. Recuerde que estas preguntas y respuestas de Stack Overflow no son solo para su beneficio, sino también para otros usuarios que se encuentran con su pregunta, ya que tienen un problema similar.
Gavin Simpson