Seleziona la prima riga per gruppo


87

Da un dataframe come questo

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

Voglio crearne uno nuovo con la prima riga di ogni coppia id / stringa. Se sqldf accettasse il codice R al suo interno, la query potrebbe essere simile a questa:

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

C'è una soluzione a corto di creare una nuova colonna come

test$row <- rownames(test)

ed eseguire la stessa query sqldf con min (riga)?



1
@ Matteo, la mia domanda è più vecchia.
dmvianna

2
La tua domanda è di 1 anno e l'altra domanda è di 4 anni, no? Ci sono così tanti duplicati di questa domanda
Matteo

@ Matteo Scusa, devo aver letto male le date.
dmvianna

Risposte:


120

Puoi usare duplicatedper farlo molto rapidamente.

test[!duplicated(test$id),]

Benchmark, per i fanatici della velocità:

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

Riproviamo, ma solo con i contendenti della prima manche e con più dati e più repliche.

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

Il vincitore: system.time (dat3 [! Duplicated (dat3 $ id),]) utente sistema trascorso 0,07 0,00 0,07
dmvianna

2
@dmvianna: non l'ho installato e non me la sentivo di preoccuparmene. :)
Joshua Ulrich

Siamo sicuri che il mio codice data.table sia il più efficiente possibile? Non sono sicuro della mia capacità di ottenere le migliori prestazioni da quello strumento.
joran

2
Inoltre, ritengo che, se hai intenzione di eseguire il benchmark di data.table, la codifica dovresti includere l'ordinamento per id all'interno delle chiamate di base.
Mnel

1
@JoshuaUlrich Ancora una domanda: perché è necessaria la prima frase, ovvero l'ipotesi che i dati siano già ordinati. !duplicated(x)trova il primo di ogni gruppo anche se non è ordinato, iiuc.
Matt Dowle

38

Prediligo l'approccio dplyr.

group_by(id) seguito da entrambi

  • filter(row_number()==1) o
  • slice(1) o
  • slice_head(1) # (dplyr => 1.0)
  • top_n(n = -1)
    • top_n()utilizza internamente la funzione di rango. Negativo seleziona dal fondo della classifica.

In alcuni casi può essere necessario disporre gli ID dopo 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)

Tutti e tre i metodi restituiscono lo stesso risultato

# 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

2
Vale anche la pena fare un grido slice. slice(x)è una scorciatoia per filter(row_number() %in% x).
Gregor Thomas,

Molto elegante. Sai perché devo convertire il mio in data.tablea data.frameaffinché funzioni?
James Hirschorn

@JamesHirschorn Non sono un esperto di tutte le differenze. Ma data.tableeredita da data.framequindi in molti casi puoi usare i comandi dplyr su un file data.table. L'esempio sopra, ad esempio, funziona anche se testè un file data.table. Vedi ad esempio stackoverflow.com/questions/13618488/… per una spiegazione più approfondita
Kresten

Questo è un modo ordinato e inverso per farlo e, come vedi, data.frame è in realtà una tabella qui. Personalmente ti consiglio di lavorare sempre con i tibbles anche perché ggplot2 è costruito in modo simile.
Garini

17

Che dire

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

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

modificare

C'è anche un metodo univoco per il data.tablesquale restituirà la prima riga per chiave

jdtu <- function() unique(DT)

Penso che, se stai ordinando testal di fuori del benchmark, puoi rimuovere anche la conversione setkeye data.tabledal benchmark (poiché il setkey sostanzialmente ordina per ID, lo stesso di 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         

e con più dati

** Modifica con metodo unico **

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        

Il metodo unico è più veloce qui.


4
Non devi nemmeno impostare la chiave. unique(DT,by="id")funziona direttamente
Matteo

Cordiali saluti a partire dalla data.tableversione> = 1.9.8, l' byargomento predefinito per uniqueè by = seq_along(x)(tutte le colonne), invece del precedente predefinitoby = key(x)
IceCreamToucan

12

Una semplice ddplyopzione:

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

Se la velocità è un problema, un approccio simile potrebbe essere adottato con data.table:

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

o questo potrebbe essere notevolmente più veloce:

testd[testd[, .I[1], by = key(testd]$V1]

Sorprendentemente, sqldf lo fa più velocemente: 1.77 0.13 1.92 vs 10.53 0.00 10.79 con data.table
dmvianna

3
@dmvianna Non vorrei necessariamente contare data.table. Non sono un esperto con quello strumento, quindi il mio codice data.table potrebbe non essere il modo più efficiente per farlo.
joran

Ho votato in alto prematuramente. Quando l'ho eseguito su un grande data.table, era incredibilmente lento e non ha funzionato: il numero di righe era lo stesso dopo.
James Hirschorn

@JamesHirachorn L'ho scritto molto tempo fa, il pacchetto è cambiato molto e uso a malapena data.table. Se trovi il modo giusto per farlo con quel pacchetto, sentiti libero di suggerire una modifica per migliorarlo.
joran

8

ora, per l' dplyraggiunta di un contatore distinto.

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

Crei gruppi, li riassumi all'interno di gruppi.

Se i dati sono numerici, puoi usare:
first(value)[c'è anche last(value)] al posto dihead(value, 1)

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

Pieno:

> 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

Questa risposta è piuttosto datata: ci sono modi migliori per farlo dplyrche non richiedono la scrittura di una dichiarazione per ogni singola colonna da includere (vedi la risposta di atomman sotto, ad esempio) . Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use first (value) `vs head(value)(o just value[1])
Gregor Thomas

7

(1) SQLite ha una rowidpseudo-colonna incorporata , quindi funziona:

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) Anche sqldfse stesso ha un row.names=argomento:

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 terza alternativa che mescola gli elementi dei due precedenti potrebbe essere ancora migliore:

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

Si noti che tutti e tre questi si basano su un'estensione SQLite a SQL in cui l'uso di mino maxè garantito per far sì che le altre colonne vengano scelte dalla stessa riga. (In altri database basati su SQL che potrebbero non essere garantiti.)


Grazie! Questo è molto meglio della risposta accettata IMO perché è generalizzabile per prendere il primo / ultimo elemento in un passaggio aggregato utilizzando più funzioni aggregate (cioè prendere la prima di questa variabile, sommare quella variabile, ecc.).
Bridgeburners

4

Un'opzione di base R è l' idioma split()- lapply()- do.call():

> 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

Un'opzione più diretta è lapply()la [funzione:

> 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

La virgola 1, )alla fine della lapply()chiamata è essenziale poiché equivale a chiamare [1, ]per selezionare la prima riga e tutte le colonne.


Questo è stato molto lento, Gavin: il sistema utente è scaduto 91,84 6,02 101,10
dmvianna

Tutto ciò che riguarda i frame di dati lo sarà. La loro utilità ha un prezzo. Quindi data.table, per esempio.
Gavin Simpson

a mia difesa, e di R., non hai menzionato nulla sull'efficienza nella domanda. Spesso la facilità d'uso è una caratteristica. Testimone la popolarità di ply, che è anch'esso "lento", almeno fino alla prossima versione che supporta data.table.
Gavin Simpson

1
Sono d'accordo. Non volevo insultarti. Ho trovato, però, che il metodo di @ Joshua-Ulrich era sia facile e veloce. : 7)
dmvianna

Non c'è bisogno di scusarsi e non l'ho preso come un insulto. Stavo solo sottolineando che è stato offerto senza alcuna pretesa di efficienza. Ricorda che questa domanda e risposta su Stack Overflow non è solo a tuo vantaggio, ma anche a quello di altri utenti che incontrano la tua domanda perché hanno un problema simile.
Gavin Simpson
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.