Obteniendo los mejores valores por grupo

93

Aquí hay un marco de datos de muestra:

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

Quiero que el subconjunto dcontenga las filas con los 5 valores superiores de xpara cada valor de grp.

Usando base-R, mi enfoque sería algo como:

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

Usando dplyr, esperaba que esto funcionara:

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

pero solo devuelve las 5 filas superiores generales.

Cambiando headpor top_ndevoluciones todo d.

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

¿Cómo obtengo el subconjunto correcto?

Richie algodón
fuente

Respuestas:

126

Desde dplyr 1.0.0 , " slice_min()y slice_max()seleccione las filas con los valores mínimos o máximos de una variable, sustituyendo a los confusos 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.0uso top_n:

De ?top_n, sobre el wtargumento:

La variable a usar para ordenar [...] por defecto es la última variable de la tabla ".

La última variable en su conjunto de datos es "grp", que no es la variable que desea clasificar, y es por eso que su top_nintento "devuelve la totalidad de d". Por lo tanto, si desea clasificar por "x" en su conjunto de datos, debe especificar wt = x.

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

Datos:

set.seed(123)
d <- data.frame(
  x = runif(90),
  grp = gl(3, 30))
Henrik
fuente
7
¿Hay alguna forma de ignorar los lazos?
Matías Guzmán Naranjo
@ MatíasGuzmánNaranjo, stackoverflow.com/questions/21308436/…
nanselm2
41

Bastante fácil con data.tabledemasiado ...

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

O

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

O (debería ser más rápido para el conjunto de big data porque se evita llamar .SDa cada grupo)

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

Editar: así es como se dplyrcompara con data.table(si alguien está interesado)

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

Añadiendo una data.tablesolución ligeramente más rápida :

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
)

salida de tiempo:

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
David Arenburg
fuente
Añadiendo otro data.tablemétodo que debería ser un poco más rápido:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon12
@ chinsoon12 sea mi invitado. No tengo tiempo para comparar estas soluciones nuevamente.
David Arenburg
Añadiendo otro data.tablemétodo más fácil:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu
@TaoHu es muy parecido a las dos primeras soluciones. No creo :que ganehead
David Arenburg
@DavidArenburg Sí, estoy de acuerdo contigo, creo que la mayor diferencia es setordermás rápida queorder
Tao Hu
34

Necesitas completar headuna llamada a do. En el siguiente código, .representa el grupo actual (ver descripción de... en la dopágina de ayuda).

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

Como lo menciona akrun, slice es una alternativa.

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

Aunque no pregunté esto, para completar, una posible data.tableversión es (gracias a @Arun por la corrección):

setDT(d)[order(-x), head(.SD, 5), by = grp]
Richie algodón
fuente
1
@akrun Gracias. No sabía nada de esa función.
Richie Cotton
@DavidArenburg Gracias. Eso es lo que se obtiene al publicar una respuesta a toda prisa. He eliminado las tonterías.
Richie Cotton
2
Richie, FWIW, solo necesitas una pequeña adición:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun
Esta respuesta está un poco desactualizada, pero la segunda parte es la forma idomática si suelta ~y usa arrangey en group_bylugar de arrange_ygroup_by_
Moody_Mudskipper
15

Mi enfoque en la base R sería:

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

Y usando dplyr, el enfoque con slicees probablemente más rápido, pero también podría usar, filterque probablemente será más rápido que usar do(head(., 5)):

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

punto de referencia 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
talat
fuente
@akrun filterrequiere una función adicional, mientras que su sliceversión no ...
David Arenburg
1
Sabes por qué no agregaste data.tableaquí;)
David Arenburg
5
Lo sé y puedo decirte: porque la pregunta pedía específicamente una solución dplyr.
talat
1
Solo estaba bromeando ... No es como si nunca hubieras hecho lo mismo (solo en la dirección opuesta).
David Arenburg
@DavidArenburg, no estaba diciendo que sea "ilegal" ni nada por el estilo proporcionar una respuesta data.table .. Por supuesto que puede hacer eso y proporcionar cualquier punto de referencia que desee :) Por cierto, la pregunta a la que enlazó es un buen ejemplo donde la sintaxis de dplyr es mucho más conveniente (¡lo sé, subjetivo!) que data.table.
talat
1

top_n (n = 1) todavía devolverá varias filas para cada grupo si la variable de orden no es única dentro de cada grupo. Para seleccionar precisamente una ocurrencia para cada grupo, agregue una variable única a cada fila:

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)
Jan Vydra
fuente
0

Una data.tablesolución más para resaltar su sintaxis concisa:

setDT(d)
d[order(-x), .SD[1:5], grp]
sindri_baldur
fuente