Ottenere i valori migliori per gruppo


92

Ecco un frame di dati di esempio:

d <- data.frame(
  x   = runif(90),
  grp = gl(3, 30)
) 

Voglio che il sottoinsieme dcontenga le righe con i primi 5 valori di xper ogni valore di grp.

Usando base-R, il mio approccio sarebbe qualcosa del tipo:

ordered <- d[order(d$x, decreasing = TRUE), ]    
splits <- split(ordered, ordered$grp)
heads <- lapply(splits, head)
do.call(rbind, heads)
##              x grp
## 1.19 0.8879631   1
## 1.4  0.8844818   1
## 1.12 0.8596197   1
## 1.26 0.8481809   1
## 1.18 0.8461516   1
## 1.29 0.8317092   1
## 2.31 0.9751049   2
## 2.34 0.9269764   2
## 2.57 0.8964114   2
## 2.58 0.8896466   2
## 2.45 0.8888834   2
## 2.35 0.8706823   2
## 3.74 0.9884852   3
## 3.73 0.9837653   3
## 3.83 0.9375398   3
## 3.64 0.9229036   3
## 3.69 0.8021373   3
## 3.86 0.7418946   3

Utilizzando dplyr, mi aspettavo che funzionasse:

d %>%
  arrange_(~ desc(x)) %>%
  group_by_(~ grp) %>%
  head(n = 5)

ma restituisce solo le prime 5 righe complessive.

Lo scambio headper top_nrestituisce l'intero d.

d %>%
  arrange_(~ desc(x)) %>%
  group_by_(~ grp) %>%
  top_n(n = 5)

Come ottengo il sottoinsieme corretto?

Risposte:


125

Da dplyr 1.0.0 , " slice_min()e slice_max()seleziona le righe con i valori minimo o massimo di una variabile, sostituendo la confusione top_n()."

d %>% group_by(grp) %>% slice_max(order_by = x, n = 5)
# # A tibble: 15 x 2
# # Groups:   grp [3]
#     x grp  
# <dbl> <fct>
#  1 0.994 1    
#  2 0.957 1    
#  3 0.955 1    
#  4 0.940 1    
#  5 0.900 1    
#  6 0.963 2    
#  7 0.902 2    
#  8 0.895 2    
#  9 0.858 2    
# 10 0.799 2    
# 11 0.985 3    
# 12 0.893 3    
# 13 0.886 3    
# 14 0.815 3    
# 15 0.812 3

Pre- dplyr 1.0.0utilizzo top_n:

Da ?top_n, wtsull'argomento:

La variabile da utilizzare per l'ordinamento [...] ha come valore predefinito l'ultima variabile in tbl ".

L'ultima variabile nel tuo set di dati è "grp", che non è la variabile che desideri classificare, ed è per questo che il tuo top_ntentativo "restituisce l'intera d". Pertanto, se desideri classificare per "x" nel tuo set di dati, devi specificare wt = x.

d %>%
  group_by(grp) %>%
  top_n(n = 5, wt = x)

Dati:

set.seed(123)
d <- data.frame(
  x = runif(90),
  grp = gl(3, 30))

7
c'è comunque da ignorare i legami?
Matías Guzmán Naranjo


40

Abbastanza facile data.tableanche con ...

library(data.table)
setorder(setDT(d), -x)[, head(.SD, 5), keyby = grp]

O

setorder(setDT(d), grp, -x)[, head(.SD, 5), by = grp]

Oppure (dovrebbe essere più veloce per big data set perché evita di chiamare .SDper ogni gruppo)

setorder(setDT(d), grp, -x)[, indx := seq_len(.N), by = grp][indx <= 5]

Modifica: ecco come si dplyrconfronta con data.table(se qualcuno è interessato)

set.seed(123)
d <- data.frame(
  x   = runif(1e6),
  grp = sample(1e4, 1e6, TRUE))

library(dplyr)
library(microbenchmark)
library(data.table)
dd <- copy(d)

microbenchmark(
  top_n = {d %>%
             group_by(grp) %>%
             top_n(n = 5, wt = x)},
  dohead = {d %>%
              arrange_(~ desc(x)) %>%
              group_by_(~ grp) %>%
              do(head(., n = 5))},
  slice = {d %>%
             arrange_(~ desc(x)) %>%
             group_by_(~ grp) %>%
             slice(1:5)},
  filter = {d %>% 
              arrange(desc(x)) %>%
              group_by(grp) %>%
              filter(row_number() <= 5L)},
  data.table1 = setorder(setDT(dd), -x)[, head(.SD, 5L), keyby = grp],
  data.table2 = setorder(setDT(dd), grp, -x)[, head(.SD, 5L), grp],
  data.table3 = setorder(setDT(dd), grp, -x)[, indx := seq_len(.N), grp][indx <= 5L],
  times = 10,
  unit = "relative"
)


#        expr        min         lq      mean     median        uq       max neval
#       top_n  24.246401  24.492972 16.300391  24.441351 11.749050  7.644748    10
#      dohead 122.891381 120.329722 77.763843 115.621635 54.996588 34.114738    10
#       slice  27.365711  26.839443 17.714303  26.433924 12.628934  7.899619    10
#      filter  27.755171  27.225461 17.936295  26.363739 12.935709  7.969806    10
# data.table1  13.753046  16.631143 10.775278  16.330942  8.359951  5.077140    10
# data.table2  12.047111  11.944557  7.862302  11.653385  5.509432  3.642733    10
# data.table3   1.000000   1.000000  1.000000   1.000000  1.000000  1.000000    10

Aggiunta di una data.tablesoluzione leggermente più veloce :

set.seed(123L)
d <- data.frame(
    x   = runif(1e8),
    grp = sample(1e4, 1e8, TRUE))
setDT(d)
setorder(d, grp, -x)
dd <- copy(d)

library(microbenchmark)
microbenchmark(
    data.table3 = d[, indx := seq_len(.N), grp][indx <= 5L],
    data.table4 = dd[dd[, .I[seq_len(.N) <= 5L], grp]$V1],
    times = 10L
)

uscita di temporizzazione:

Unit: milliseconds
        expr      min       lq     mean   median        uq      max neval
 data.table3 826.2148 865.6334 950.1380 902.1689 1006.1237 1260.129    10
 data.table4 729.3229 783.7000 859.2084 823.1635  966.8239 1014.397    10

Aggiungendo un altro data.tablemetodo che dovrebbe essere leggermente più veloce:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon12

@ chinsoon12 sii mio ospite. Non ho tempo per confrontare nuovamente queste soluzioni.
David Arenburg

Aggiunta di un altro data.tablemetodo più semplice:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu

@ TaoHu è più o meno come le prime due soluzioni. Non credo :che batteràhead
David Arenburg

@DavidArenburg Sì , Sono d'accordo con te, penso che la differenza setordermaggiore sia più veloce diorder
Tao Hu

33

Devi concludere headuna chiamata a do. Nel codice seguente, .rappresenta il gruppo corrente (vedere la descrizione di ...nella dopagina della guida).

d %>%
  arrange_(~ desc(x)) %>%
  group_by_(~ grp) %>%
  do(head(., n = 5))

Come accennato da akrun, sliceè un'alternativa.

d %>%
  arrange_(~ desc(x)) %>%
  group_by_(~ grp) %>%
  slice(1:5)

Anche se non l'ho chiesto, per completezza, una possibile data.tableversione è (grazie a @Arun per la correzione):

setDT(d)[order(-x), head(.SD, 5), by = grp]

1
@akrun Thanks. Non sapevo di quella funzione.
Richie Cotton

@DavidArenburg Thanks. Questo è ciò che si ottiene postando una risposta in fretta. Ho rimosso le sciocchezze.
Richie Cotton

2
Richie, FWIW ti serve solo una piccola aggiunta:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun

Questa risposta è un po 'obsoleta, ma la seconda parte è il modo idomatico se lasci cadere ~e usi arrangee group_byinvece di arrange_egroup_by_
Moody_Mudskipper

15

Il mio approccio in base R sarebbe:

ordered <- d[order(d$x, decreasing = TRUE), ]
ordered[ave(d$x, d$grp, FUN = seq_along) <= 5L,]

E usando dplyr, l'approccio con sliceè probabilmente il più veloce, ma potresti anche usare quello filterche sarà probabilmente più veloce dell'uso do(head(., 5)):

d %>% 
  arrange(desc(x)) %>%
  group_by(grp) %>%
  filter(row_number() <= 5L)

benchmark dplyr

set.seed(123)
d <- data.frame(
  x   = runif(1e6),
  grp = sample(1e4, 1e6, TRUE))

library(microbenchmark)

microbenchmark(
  top_n = {d %>%
             group_by(grp) %>%
             top_n(n = 5, wt = x)},
  dohead = {d %>%
              arrange_(~ desc(x)) %>%
              group_by_(~ grp) %>%
              do(head(., n = 5))},
  slice = {d %>%
             arrange_(~ desc(x)) %>%
             group_by_(~ grp) %>%
             slice(1:5)},
  filter = {d %>% 
              arrange(desc(x)) %>%
              group_by(grp) %>%
              filter(row_number() <= 5L)},
  times = 10,
  unit = "relative"
)

Unit: relative
   expr       min        lq    median        uq       max neval
  top_n  1.042735  1.075366  1.082113  1.085072  1.000846    10
 dohead 18.663825 19.342854 19.511495 19.840377 17.433518    10
  slice  1.000000  1.000000  1.000000  1.000000  1.000000    10
 filter  1.048556  1.044113  1.042184  1.180474  1.053378    10

@akrun filterrichiede una funzione aggiuntiva, mentre la tua sliceversione no ...
David Arenburg

1
Sai perché non hai aggiunto data.tablequi;)
David Arenburg

5
Lo so e posso dirtelo: perché la domanda chiedeva specificamente una soluzione dplyr.
talat

1
Stavo solo scherzando ... Non è che tu non abbia mai fatto lo stesso (solo nella direzione opposta).
David Arenburg

@DavidArenburg, non stavo dicendo che è "illegale" o qualcosa di simile fornire una risposta data.table .. Ovviamente puoi farlo e fornire qualsiasi benchmark che ti piace :) A proposito, la domanda a cui ti sei collegato è un bell'esempio dove la sintassi di dplyr è molto più conveniente (lo so, soggettiva!) Di data.table.
talat

1

top_n (n = 1) restituirà ancora più righe per ogni gruppo se la variabile di ordinamento non è univoca all'interno di ogni gruppo. Per selezionare esattamente un'occorrenza per ogni gruppo, aggiungi una variabile univoca a ciascuna riga:

set.seed(123)
d <- data.frame(
  x   = runif(90),
  grp = gl(3, 30))

d %>%
  mutate(rn = row_number()) %>% 
  group_by(grp) %>%
  top_n(n = 1, wt = rn)

0

Un'altra data.tablesoluzione per evidenziare la sua sintassi concisa:

setDT(d)
d[order(-x), .SD[1:5], grp]
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.