Abrufen der Top-Werte nach Gruppe


91

Hier ist ein Beispieldatenrahmen:

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

Ich möchte die Teilmenge dder Zeilen mit den Top 5 Werten von xfür jeden Wert von grp.

Mit base-R wäre mein Ansatz ungefähr so:

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

Mit dplyrerwartete ich, dass dies funktioniert:

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

Es werden jedoch nur die gesamten oberen 5 Zeilen zurückgegeben.

Tauschen headgegen top_ngibt das ganze zurück d.

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

Wie bekomme ich die richtige Teilmenge?

Antworten:


123

Ab dplyr 1.0.0 " slice_min()und slice_max()wählen Sie die Zeilen mit den Minimal- oder Maximalwerten einer Variablen aus, die die Verwirrung übernehmen 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

Pre- dplyr 1.0.0mit top_n:

Aus ?top_n, über das wtArgument:

Die Variable, die zum Bestellen von [...] verwendet werden soll, ist standardmäßig die letzte Variable im tbl ".

Die letzte Variable in Ihrem Datensatz ist "grp". Dies ist nicht die Variable, die Sie bewerten möchten, und aus diesem Grund gibt Ihr top_nVersuch "das gesamte d zurück". Wenn Sie also in Ihrem Datensatz nach "x" rangieren möchten, müssen Sie angeben wt = x.

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

Daten:

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

7
Gibt es überhaupt die Möglichkeit, Krawatten zu ignorieren?
Matías Guzmán Naranjo


40

Ziemlich einfach mit data.table...

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

Oder

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

Oder (Sollte für große Datenmengen schneller sein, da nicht .SDfür jede Gruppe angerufen werden muss)

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

Bearbeiten: Hier ist wie im dplyrVergleich zu data.table(wenn jemand interessiert ist)

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

Hinzufügen einer geringfügig schnelleren data.tableLösung:

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
)

Timing-Ausgabe:

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

Hinzufügen einer weiteren data.tableMethode, die etwas schneller sein sollte:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon12

@ chinsoon12 sei mein Gast. Ich habe keine Zeit, diese Lösungen erneut zu bewerten.
David Arenburg

Eine weitere data.tableMethode einfacher hinzufügen:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu

@TaoHu es ist ziemlich ähnlich wie die ersten beiden Lösungen. Ich glaube nicht, :wird schlagenhead
David Arenburg

@ DavidArenburg Ja , Ich stimme Ihnen zu, ich denke, der größte Unterschied ist setorderschneller alsorder
Tao Hu

33

Sie müssen headeinen Anruf bei einschließen do. Stellt im folgenden Code .die aktuelle Gruppe dar (siehe Beschreibung ...auf der doHilfeseite).

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

Wie von Akrun erwähnt, sliceist eine Alternative.

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

Obwohl ich dies der Vollständigkeit data.tablehalber nicht gefragt habe, ist eine mögliche Version (danke an @Arun für das Update):

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

1
@akrun Danke. Ich wusste nichts über diese Funktion.
Richie Cotton

@ DavidArenburg Danke. Das ist es, was kommt, wenn man schnell eine Antwort veröffentlicht. Ich habe den Unsinn entfernt.
Richie Cotton

2
Richie, FWIW du brauchst nur eine kleine Ergänzung:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun

Diese Antwort ist etwas veraltet, aber der zweite Teil ist der idomatische Weg, wenn Sie das fallen lassen ~und verwenden arrangeund group_byanstelle von arrange_undgroup_by_
Moody_Mudskipper

15

Mein Ansatz in Basis R wäre:

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

Und mit dplyr ist der Ansatz mit slicewahrscheinlich am schnellsten, aber Sie können auch verwenden, filterwas wahrscheinlich schneller ist als mit 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

@akrun filtererfordert eine zusätzliche Funktion, während Ihre sliceVersion nicht ...
David Arenburg

1
Sie wissen, warum Sie hier nicht hinzugefügt data.tablehaben;)
David Arenburg

5
Ich weiß es und ich kann Ihnen sagen: weil die Frage speziell nach einer dplyr-Lösung gefragt hat.
Talat

1
Ich habe nur Spaß gemacht ... Es ist nicht so, dass du nie dasselbe getan hast (nur in der entgegengesetzten Richtung).
David Arenburg

@DavidArenburg, ich habe nicht gesagt, dass es "illegal" oder ähnliches ist, eine data.table-Antwort bereitzustellen. Natürlich können Sie das tun und jeden Benchmark angeben, den Sie mögen :) Übrigens ist die Frage, mit der Sie verlinkt haben, ein schönes Beispiel wo dplyr Syntax ist viel bequemer (ich weiß, subjektiv!) als data.table.
Talat

1

top_n (n = 1) gibt weiterhin mehrere Zeilen für jede Gruppe zurück, wenn die Ordnungsvariable nicht in jeder Gruppe eindeutig ist. Fügen Sie jeder Zeile eine eindeutige Variable hinzu, um genau ein Vorkommen für jede Gruppe auszuwählen:

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

Eine weitere data.tableLösung, um die prägnante Syntax hervorzuheben:

setDT(d)
d[order(-x), .SD[1:5], grp]
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.