Подтвердить что ты не робот

Получение лучших значений по группам

Вот пример кадра данных:

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

Я хочу, чтобы подмножество d содержало строки с верхними 5 значениями x для каждого значения grp.

Используя base-R, мой подход будет примерно таким:

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

Используя dplyr, я ожидал, что это сработает:

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

но он возвращает только верхние 5 строк.

Перестановка head для top_n возвращает все d.

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

Как получить правильное подмножество?

4b9b3361

Ответ 1

От ?top_n, "Переменная, используемая для упорядочения [...] по умолчанию для последней переменной в tbl". Последняя переменная в вашем наборе данных - это "grp", которая не является переменной, которую вы хотите ранжировать, и именно поэтому ваша попытка top_n "возвращает весь d". Таким образом, если вы хотите ранжировать по "x" в своем наборе данных, вам нужно указать wt = x.

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

d %>%
  group_by(grp) %>%
  top_n(n = 5, wt = x)
#            x grp
# 1  0.9404673   1
# 2  0.9568333   1
# 3  0.8998250   1
# 4  0.9545036   1
# 5  0.9942698   1
# 6  0.9630242   2
# 7  0.9022990   2
# 8  0.8578277   2
# 9  0.7989248   2
# 10 0.8950454   2
# 11 0.8146400   3
# 12 0.8123895   3
# 13 0.9849570   3
# 14 0.8930511   3
# 15 0.8864691   3

Ответ 2

Довольно легко с помощью data.table тоже...

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

или

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

Или (должен быть быстрее для большого набора данных, потому что избегать вызова .SD для каждой группы)

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

Изменить: Здесь dplyr сравнивается с data.table (если кому интересно)

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

Ответ 3

Вам нужно заключить head в вызов do. В следующем коде . представляет текущую группу (см. Описание ... на странице справки do).

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

Как упоминалось akrun, slice является альтернативой.

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

Ответ 4

Мой подход в базе R:

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

И используя dplyr, подход с slice, вероятно, самый быстрый, но вы также можете использовать filter, который скорее всего будет быстрее, чем с помощью do(head(., 5)):

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

dplyr benchmark

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

Ответ 5

top_n (n = 1) все равно будет возвращать несколько строк для каждой группы, если переменная заказа не является уникальной в каждой группе. Чтобы точно выбрать одно место для каждой группы, добавьте уникальную переменную в каждую строку:

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)