Nhận các giá trị hàng đầu theo nhóm


92

Đây là khung dữ liệu mẫu:

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

Tôi muốn tập hợp con dchứa các hàng có 5 giá trị hàng đầu xcho mỗi giá trị của grp.

Sử dụng base-R, cách tiếp cận của tôi sẽ như sau:

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

Sử dụng dplyr, tôi mong đợi điều này sẽ hoạt động:

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

nhưng nó chỉ trả về tổng thể 5 hàng trên cùng.

Hoán đổi headđể top_ntrả lại toàn bộ d.

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

Làm cách nào để lấy đúng tập hợp con?

Câu trả lời:


125

Từ dplyr 1.0.0 , " slice_min()slice_max()chọn các hàng có giá trị tối thiểu hoặc tối đa của một biến, tiếp nhận từ khó hiểu 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

Trước khi dplyr 1.0.0sử dụng top_n:

Từ ?top_n, về wtđối số:

Biến được sử dụng để sắp xếp [...] mặc định là biến cuối cùng trong tbl ".

Biến cuối cùng trong tập dữ liệu của bạn là "grp", không phải là biến mà bạn muốn xếp hạng và đó là lý do tại sao top_nnỗ lực của bạn "trả về toàn bộ của d". Do đó, nếu bạn muốn xếp hạng theo "x" trong tập dữ liệu của mình, bạn cần chỉ định wt = x.

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

Dữ liệu:

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

7
là có anyway của việc bỏ qua các mối quan hệ?
Matías Guzmán Naranjo


40

Khá dễ dàng với data.tablequá ...

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

Hoặc là

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

Hoặc (Sẽ nhanh hơn cho tập dữ liệu lớn vì tránh gọi .SDcho từng nhóm)

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

Chỉnh sửa: Đây là cách dplyrso sánh với data.table(nếu ai đó quan tâm)

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

Thêm một data.tablegiải pháp nhanh hơn một chút :

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
)

đầu ra thời gian:

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

Thêm một data.tablephương pháp khác sẽ nhanh hơn một chút:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon

@ chinsoon12 làm khách của tôi. Tôi không có thời gian để đánh giá lại các giải pháp này.
David Arenburg

Thêm một data.tablephương pháp khác dễ dàng hơn:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu

@TaoHu, nó khá giống với hai giải pháp đầu tiên. Tôi không nghĩ :sẽ đánh bạihead
David Arenburg

@DavidArenburg Vâng , Tôi đồng ý với bạn, tôi nghĩ sự khác biệt nhất là setordernhanh hơnorder
Tao Hu

33

Bạn cần kết thúc headcuộc gọi tới do. Trong đoạn mã sau, .đại diện cho nhóm hiện tại (xem mô tả ...trong dotrang trợ giúp).

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

Như đã đề cập bởi akrun, slicelà một sự thay thế.

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

Mặc dù tôi không hỏi điều này, nhưng để hoàn thiện, một data.tablephiên bản khả thi là (cảm ơn @Arun đã sửa chữa):

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

1
@akrun Cảm ơn. Tôi không biết về chức năng đó.
Richie Cotton

@DavidArenburg Cảm ơn. Đó là những gì xảy ra khi đăng một câu trả lời vội vàng. Tôi đã loại bỏ những điều vô nghĩa.
Richie Cotton

2
Richie, FWIW bạn chỉ cần một bổ sung nhỏ:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun

Câu trả lời này được một chút lỗi thời nhưng phần thứ hai là cách idomatic nếu bạn thả ~và sử dụng arrangegroup_bythay vì arrange_group_by_
Moody_Mudskipper

15

Cách tiếp cận của tôi trong cơ sở R sẽ là:

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

Và sử dụng dplyr, cách tiếp cận với slicecó thể là nhanh nhất, nhưng bạn cũng có thể sử dụng cách filternày có thể sẽ nhanh hơn so với sử dụng do(head(., 5)):

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

điểm chuẩn 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

@akrun filterđòi hỏi một chức năng bổ sung, trong khi bạn slicephiên bản không ...
David Arenburg

1
Bạn biết tại sao bạn không thêm data.tableở đây;)
David Arenburg

5
Tôi biết điều đó và tôi có thể nói với bạn: bởi vì câu hỏi được hỏi cụ thể cho một giải pháp dplyr.
talat

1
Tôi chỉ đùa thôi ... Nó không giống như bạn không bao giờ làm như vậy (chỉ là ở phía đối diện).
David Arenburg

@DavidArenburg, tôi không nói việc cung cấp câu trả lời data.table là "bất hợp pháp" hay bất cứ điều gì tương tự .. Tất nhiên bạn có thể làm điều đó và cung cấp bất kỳ điểm chuẩn nào bạn thích :) Btw, câu hỏi mà bạn đã liên kết là một ví dụ hay trong đó cú pháp dplyr thuận tiện hơn (tôi biết, chủ quan!) so với data.table.
talat

1

top_n (n = 1) sẽ vẫn trả về nhiều hàng cho mỗi nhóm nếu biến thứ tự không phải là duy nhất trong mỗi nhóm. Để chọn chính xác một lần xuất hiện cho mỗi nhóm, hãy thêm một biến duy nhất vào mỗi hàng:

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)

0

Một data.tablegiải pháp khác để làm nổi bật cú pháp ngắn gọn của nó:

setDT(d)
d[order(-x), .SD[1:5], grp]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.