Obtenir les principales valeurs par groupe

92

Voici un exemple de cadre de données:

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

Je veux que le sous-ensemble dcontienne les lignes avec les 5 premières valeurs de xpour chaque valeur de grp.

En utilisant base-R, mon approche serait quelque chose comme:

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

En utilisant dplyr, je m'attendais à ce que cela fonctionne:

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

mais il ne renvoie que les 5 premières lignes globales.

L'échange headpour top_nrenvoie la totalité de d.

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

Comment obtenir le sous-ensemble correct?

Coton Richie
la source

Réponses:

125

Depuis dplyr 1.0.0 , " slice_min()et slice_max()sélectionnez les lignes avec les valeurs minimales ou maximales d'une variable, en prenant le relais de la confusion 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

Pré- dplyr 1.0.0utilisation top_n:

De ?top_n, à propos de l' wtargument:

La variable à utiliser pour classer [...] par défaut la dernière variable dans le tbl ".

La dernière variable de votre ensemble de données est "grp", qui n'est pas la variable que vous souhaitez classer, et c'est pourquoi votre top_ntentative "renvoie la totalité de d". Ainsi, si vous souhaitez classer par «x» dans votre ensemble de données, vous devez spécifier wt = x.

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

Les données:

set.seed(123)
d <- data.frame(
  x = runif(90),
  grp = gl(3, 30))
Henrik
la source
7
est-il possible d'ignorer les liens?
Matías Guzmán Naranjo
@ MatíasGuzmánNaranjo, stackoverflow.com/questions/21308436/…
nanselm2
40

Assez facile avec data.tableaussi ...

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

Ou

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

Ou (devrait être plus rapide pour l'ensemble de données volumineuses car éviter d'appeler .SDpour chaque groupe)

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

Edit: voici comment se dplyrcompare à data.table(si quelqu'un est intéressé)

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

Ajout d'une data.tablesolution légèrement plus rapide :

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
)

sortie de synchronisation:

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
la source
Ajout d'une autre data.tableméthode qui devrait être légèrement plus rapide:dt <- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) <= 5L], grp]$V1]
chinsoon12
@ chinsoon12 soit mon invité. Je n'ai pas le temps de comparer à nouveau ces solutions.
David Arenburg
Ajout d'une autre data.tableméthode plus facile:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
Tao Hu
@TaoHu, c'est à peu près comme les deux premières solutions. Je ne pense pas qu'il :battrahead
David Arenburg
@DavidArenburg Ouais , Je suis d'accord avec vous, je pense que la plus grande différence est setorderplus rapide queorder
Tao Hu
33

Vous devez terminer headun appel à do. Dans le code suivant, .représente le groupe actuel (voir la description de ...dans la dopage d'aide).

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

Comme mentionné par akrun, sliceest une alternative.

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

Bien que je ne l'ai pas demandé, pour être complet, une data.tableversion possible est (merci à @Arun pour le correctif):

setDT(d)[order(-x), head(.SD, 5), by = grp]
Coton Richie
la source
1
@akrun Merci. Je ne connaissais pas cette fonction.
Richie Cotton
@DavidArenburg Merci. C'est ce qui vient de poster une réponse à la hâte. J'ai supprimé le non-sens.
Richie Cotton
2
Richie, FWIW vous avez juste besoin d'un petit ajout:setDT(d)[order(-x), head(.SD, 5L), by=grp]
Arun
Cette réponse est un peu dépassée, mais la deuxième partie est la manière idomatique si vous supprimez ~et utilisez arrangeet group_byau lieu de arrange_etgroup_by_
Moody_Mudskipper
15

Mon approche en base R serait:

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

Et en utilisant dplyr, l'approche avec sliceest probablement la plus rapide, mais vous pouvez également utiliser filterce qui sera probablement plus rapide que d'utiliser do(head(., 5)):

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

benchmark 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
la source
@akrun filternécessite une fonction supplémentaire, alors que votre sliceversion ne le fait pas ...
David Arenburg
1
Vous savez pourquoi vous n'avez pas ajouté data.tableici;)
David Arenburg
5
Je le sais et je peux vous le dire: parce que la question demandait spécifiquement une solution dplyr.
talat le
1
Je plaisantais ... Ce n'est pas comme si vous n'aviez jamais fait la même chose (juste dans la direction opposée).
David Arenburg le
@DavidArenburg, je ne disais pas que c'est "illégal" ou quoi que ce soit de similaire pour fournir une réponse data.table .. Bien sûr, vous pouvez le faire et fournir n'importe quel benchmark que vous aimez :) Btw, la question à laquelle vous avez lié est un bel exemple où la syntaxe de dplyr est bien plus pratique (je sais, subjective!) que data.table.
talat le
1

top_n (n = 1) retourne toujours plusieurs lignes pour chaque groupe , si la commande variable ne soit pas unique dans chaque groupe. Afin de sélectionner précisément une occurrence pour chaque groupe, ajoutez une variable unique à chaque ligne:

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
la source
0

Une autre data.tablesolution pour mettre en évidence sa syntaxe concise:

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