Mendapatkan nilai tertinggi menurut kelompok

93

Berikut ini contoh kerangka data:

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

Saya ingin subset dberisi baris dengan 5 nilai teratas xuntuk setiap nilai grp.

Menggunakan base-R, pendekatan saya akan menjadi seperti:

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

Dengan menggunakan dplyr, saya berharap ini berhasil:

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

tetapi hanya mengembalikan 5 baris teratas secara keseluruhan.

Menukar headuntuk top_nmengembalikan keseluruhan d.

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

Bagaimana cara mendapatkan subset yang benar?

Richie Cotton
sumber

Jawaban:

126

Dari dplyr 1.0.0 , " slice_min()dan slice_max()pilih baris dengan nilai minimum atau maksimum variabel, mengambil alih dari 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

Pra- dplyr 1.0.0penggunaan top_n:

Dari ?top_n, tentang wtdalil:

Variabel yang digunakan untuk mengurutkan [...] defaultnya adalah variabel terakhir di tbl ".

Variabel terakhir dalam kumpulan data Anda adalah "grp", yang bukan variabel yang ingin Anda rangking, dan itulah sebabnya top_nupaya Anda "mengembalikan seluruh d". Jadi, jika Anda ingin memberi peringkat dengan "x" dalam kumpulan data Anda, Anda perlu menentukan wt = x.

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

Data:

set.seed(123)
d <- data.frame(
  x = runif(90),
  grp = gl(3, 30))
Henrik
sumber
7
apakah ada yang mengabaikan ikatan?
Matías Guzmán Naranjo
@ MatíasGuzmánNaranjo, stackoverflow.com/questions/21308436/…
nanselm2
41

Cukup mudah dengan data.tablejuga ...

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

Atau

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

Atau (Harus lebih cepat untuk kumpulan data besar karena menghindari panggilan .SDuntuk setiap grup)

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

Edit: Berikut dplyrperbandingannya data.table(jika ada yang tertarik)

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

Menambahkan solusi yang sedikit lebih cepat data.table:

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
)

keluaran waktu:

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
sumber
Menambahkan data.tablemetode lain yang seharusnya sedikit lebih cepat:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon12
@ chinsoon12 jadilah tamu saya. Saya tidak punya waktu untuk membandingkan solusi ini lagi.
David Arenburg
Menambahkan data.tablemetode lain lebih mudah:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu
@TaoHu hampir sama dengan dua solusi pertama. Saya tidak berpikir :akan mengalahkanhead
David Arenburg
@DavidArenburg Ya, saya setuju dengan Anda, saya pikir perbedaan terbesar setorderlebih cepat daripadaorder
Tao Hu
34

Anda perlu menyelesaikan headpanggilan ke do. Dalam kode berikut, .mewakili grup saat ini (lihat deskripsi ...di dohalaman bantuan).

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

Seperti yang disebutkan akrun, slicemerupakan alternatif.

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

Meskipun saya tidak menanyakan ini, untuk kelengkapan, data.tableversi yang mungkin adalah (terima kasih kepada @Arun untuk perbaikannya):

setDT(d)[order(-x), head(.SD, 5), by = grp]
Richie Cotton
sumber
1
@akrun Terima kasih. Saya tidak tahu tentang fungsi itu.
Richie Cotton
@DvidArenburg Terima kasih. Itulah hasil dari memposting jawaban dengan terburu-buru. Saya telah menghapus omong kosong itu.
Richie Cotton
2
Richie, FWIW Anda hanya perlu sedikit tambahan:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun
Jawaban ini agak ketinggalan jaman tetapi bagian kedua adalah cara idomatic jika Anda menjatuhkan ~dan menggunakan arrangedan group_bybukannya arrange_dangroup_by_
Moody_Mudskipper
15

Pendekatan saya di basis R adalah:

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

Dan menggunakan dplyr, pendekatan dengan slicemungkin tercepat, tetapi Anda juga bisa menggunakan filteryang kemungkinan akan lebih cepat daripada menggunakan do(head(., 5)):

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

patokan 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
sumber
@akrun filtermembutuhkan fungsi tambahan, sementara sliceversi Anda tidak ...
David Arenburg
1
Anda tahu mengapa Anda tidak menambahkan di data.tablesini;)
David Arenburg
5
Saya mengetahuinya dan saya dapat memberi tahu Anda: karena pertanyaannya secara khusus menanyakan solusi dplyr.
talat
1
Saya hanya bercanda ... Ini tidak seperti Anda tidak pernah melakukan hal yang sama (hanya pada arah yang berlawanan).
David Arenburg
@DavidArenburg, saya tidak mengatakan itu "ilegal" atau semacamnya untuk memberikan jawaban data.table .. Tentu saja Anda dapat melakukannya dan memberikan patokan apa pun yang Anda suka :) Btw, pertanyaan yang Anda tautkan adalah contoh yang bagus di mana sintaks dplyr jauh lebih nyaman (saya tahu, subjektif!) daripada data.table.
talat
1

top_n (n = 1) masih akan mengembalikan beberapa baris untuk setiap grup jika variabel pemesanan tidak unik dalam setiap grup. Untuk memilih dengan tepat satu kemunculan untuk setiap grup, tambahkan variabel unik ke setiap baris:

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
sumber
0

Satu lagi data.tablesolusi untuk menyoroti sintaksisnya yang ringkas:

setDT(d)
d[order(-x), .SD[1:5], grp]
sindri_baldur
sumber