Uzyskiwanie najwyższych wartości według grup

93

Oto przykładowa ramka danych:

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

Chcę, aby podzbiór dzawierał wiersze z 5 górnymi wartościami xdla 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 headna top_nzwroty w całości d.

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

Jak uzyskać właściwy podzbiór?

Richie Cotton
źródło

Odpowiedzi:

126

Od dplyr 1.0.0 " slice_min()i slice_max()wybierz wiersze z minimalnymi lub maksymalnymi wartościami zmiennej, przejmując od niejasnego 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

Przed dplyr 1.0.0użyciem top_n:

Od ?top_n, o wtkłótni:

Zmienna używana do uporządkowania [...] jest domyślną ostatnią zmienną w tabeli ".

Ostatnią zmienną w twoim zestawie danych jest „grp”, która nie jest zmienną, którą chcesz uszeregować, i dlatego twoja top_npróba „zwraca całą wartość d”. Tak więc, jeśli chcesz uszeregować według „x” w zestawie danych, musisz określić wt = x.

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

Dane:

set.seed(123)
d <- data.frame(
  x = runif(90),
  grp = gl(3, 30))
Henrik
źródło
7
czy w ogóle można ignorować więzi?
Matías Guzmán Naranjo
@ MatíasGuzmánNaranjo, stackoverflow.com/questions/21308436/ ...
nanselm2
41

Całkiem łatwe ze data.tablezbyt ...

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ń .SDdla każdej grupy)

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

Edycja: Oto jak dplyrwypada w porównaniu z data.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.tablerozwią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
David Arenburg
źródło
Dodanie kolejnej data.tablemetody, która powinna być nieco szybsza:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon12
@ chinsoon12 bądź moim gościem. Nie mam czasu na ponowne testowanie tych rozwiązań.
David Arenburg,
Dodanie innej data.tablemetody jest łatwiejsze:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu
@TaoHu to prawie tak, jak pierwsze dwa rozwiązania. Myślę, że nie :pobijehead
David Arenburg
@DavidArenburg Tak , Zgadzam się z tobą, myślę, że największa różnica jest setorderszybsza niżorder
Tao Hu
34

Musisz nawiązać headpołączenie z do. W poniższym kodzie .reprezentuje bieżącą grupę (zobacz opis ...na dostronie pomocy).

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

Jak wspomniał akrun, slicejest alternatywą.

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

Chociaż nie pytałem o to, dla kompletności, możliwa data.tablewersja to (dzięki @Arun za poprawkę):

setDT(d)[order(-x), head(.SD, 5), by = grp]
Richie Cotton
źródło
1
@akrun Thanks. Nie wiedziałem o tej funkcji.
Richie Cotton
@DavidArenburg Thanks. To właśnie wynika z szybkiego zamieszczania odpowiedzi. Usunąłem bzdury.
Richie Cotton
2
Richie, FWIW potrzebujesz tylko małego dodatku:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun
Ta odpowiedź jest nieco przestarzały, ale druga część jest idomatic sposób, jeśli upuść ~oraz wykorzystanie arrangei group_byzamiast arrange_igroup_by_
Moody_Mudskipper
15

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 slicejest prawdopodobnie najszybsze, ale możesz również użyć tego, filterktóre prawdopodobnie będzie szybsze niż użycie 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
talat
źródło
@akrun filterwymaga dodatkowej funkcji, podczas gdy twoja slicewersja nie ...
David Arenburg
1
Wiesz, dlaczego nie dodałeś data.tabletutaj;)
David Arenburg
5
Wiem o tym i mogę ci powiedzieć: ponieważ pytanie dotyczyło konkretnie rozwiązania dplyr.
talat
1
Tylko żartowałem ... To nie jest tak, że nigdy nie zrobiłeś tego samego (tylko po przeciwnej stronie).
David Arenburg
@DavidArenburg, nie mówiłem, że to „nielegalne” lub coś podobnego, aby podać odpowiedź data.table .. Oczywiście możesz to zrobić i podać dowolny test porównawczy, który Ci się podoba :) Przy okazji, pytanie, do którego umieściłeś link, jest dobrym przykładem gdzie składnia dplyr jest o wiele wygodniejsza (wiem, subiektywna!) niż data.table.
talat
1

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)
Jan Vydra
źródło
0

Jeszcze jedno data.tablerozwiązanie, aby podkreślić zwięzłą składnię:

setDT(d)
d[order(-x), .SD[1:5], grp]
sindri_baldur
źródło