Oto przykładowa ramka danych:
d <- data.frame(
x = runif(90),
grp = gl(3, 30)
)
Chcę, aby podzbiór d
zawierał wiersze z 5 górnymi wartościami x
dla każdej wartości grp
.
Używając base-R, moje podejście wyglądałoby mniej więcej tak:
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
Używając dplyr
, spodziewałem się, że to zadziała:
d %>%
arrange_(~ desc(x)) %>%
group_by_(~ grp) %>%
head(n = 5)
ale zwraca tylko 5 pierwszych wierszy.
Zamiana head
na top_n
zwroty w całości d
.
d %>%
arrange_(~ desc(x)) %>%
group_by_(~ grp) %>%
top_n(n = 5)
Jak uzyskać właściwy podzbiór?
źródło
Całkiem łatwe ze
data.table
zbyt ...library(data.table) setorder(setDT(d), -x)[, head(.SD, 5), keyby = grp]
Lub
setorder(setDT(d), grp, -x)[, head(.SD, 5), by = grp]
Lub (Powinno być szybsze w przypadku zestawu dużych zbiorów danych, ponieważ unikam wywołań
.SD
dla każdej grupy)setorder(setDT(d), grp, -x)[, indx := seq_len(.N), by = grp][indx <= 5]
Edycja: Oto jak
dplyr
wypada w porównaniu zdata.table
(jeśli ktoś jest zainteresowany)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
Dodanie marginalnie szybszego
data.table
rozwiązania: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 )
wyjście czasowe:
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
źródło
data.table
metody, która powinna być nieco szybsza:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
data.table
metody jest łatwiejsze:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
:
pobijehead
setorder
szybsza niżorder
Musisz nawiązać
head
połączenie zdo
. W poniższym kodzie.
reprezentuje bieżącą grupę (zobacz opis...
nado
stronie pomocy).d %>% arrange_(~ desc(x)) %>% group_by_(~ grp) %>% do(head(., n = 5))
Jak wspomniał akrun,
slice
jest alternatywą.d %>% arrange_(~ desc(x)) %>% group_by_(~ grp) %>% slice(1:5)
Chociaż nie pytałem o to, dla kompletności, możliwa
data.table
wersja to (dzięki @Arun za poprawkę):setDT(d)[order(-x), head(.SD, 5), by = grp]
źródło
setDT(d)[order(-x), head(.SD, 5L), by=grp]
~
oraz wykorzystaniearrange
igroup_by
zamiastarrange_
igroup_by_
Moje podejście w bazie R byłoby następujące:
ordered <- d[order(d$x, decreasing = TRUE), ] ordered[ave(d$x, d$grp, FUN = seq_along) <= 5L,]
A używając dplyr, podejście z
slice
jest prawdopodobnie najszybsze, ale możesz również użyć tego,filter
które prawdopodobnie będzie szybsze niż użyciedo(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
źródło
filter
wymaga dodatkowej funkcji, podczas gdy twojaslice
wersja nie ...data.table
tutaj;)top_n (n = 1) nadal zwróci wiele wierszy dla każdej grupy, jeśli zmienna porządkowania nie jest unikalna w każdej grupie. Aby wybrać dokładnie jedno wystąpienie dla każdej grupy, dodaj unikalną zmienną do każdego wiersza:
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)
źródło
Jeszcze jedno
data.table
rozwiązanie, aby podkreślić zwięzłą składnię:setDT(d) d[order(-x), .SD[1:5], grp]
źródło