Obteniendo los valores principales por grupo

Resuelto Richie Cotton asked hace 54 años • 6 respuestas

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 principales 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 primeras filas generales.

Intercambio headpor top_ndevoluciones la totalidad de d.

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

¿Cómo obtengo el subconjunto correcto?

Richie Cotton avatar Jan 01 '70 08:01 Richie Cotton
Aceptado

Desde dplyr 1.0.0 , " slice_min()y slice_max()seleccione las filas con los valores mínimo o máximo de una variable, reemplazando el confuso 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 utilizar para ordenar [...] por defecto es la última variable del tbl".

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 avatar Jan 04 '2015 13:01 Henrik

Bastante fácil con data.tabletambién...

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 conjuntos de big data porque evita llamar .SDa cada grupo)

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

Editar: así es como dplyrse compara 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

Agregar 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 sincronización:

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 avatar Jan 04 '2015 13:01 David Arenburg

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

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

Como menciona akrun, slicees una alternativa.

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

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

setDT(d)[order(-x), head(.SD, 5), by = grp]
Richie Cotton avatar Jan 04 '2015 13:01 Richie Cotton

Mi enfoque en 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 sliceprobablemente sea más rápido, pero también podrías usar filterwhich 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 avatar Jan 04 '2015 13:01 talat

top_n(n = 1) seguirá devolviendo 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 avatar Sep 09 '2017 19:09 Jan Vydra