Получение лучших значений по группам
Вот пример кадра данных:
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)
Как получить правильное подмножество?
Ответы
Ответ 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)