Voici un exemple de trame de données:
d <- data.frame(
x = runif(90),
grp = gl(3, 30)
)
Je veux le sous-ensemble de d
contenant les lignes avec les 5 premières valeurs de x
pour chaque valeur de grp
.
En utilisant base-R, mon approche ressemblerait à ceci:
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.
Échange head
contre top_n
renvoie la totalité de d
.
d %>%
arrange_(~ desc(x)) %>%
group_by_(~ grp) %>%
top_n(n = 5)
Comment puis-je obtenir le sous-ensemble correct?
De ?top_n
, à propos de l'argument wt
:
La variable à utiliser pour ordonner [...] par défaut, la dernière variable dans le tableau ".
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_n
tentative "retourne la totalité de d". Ainsi, si vous souhaitez classer par "x" dans votre ensemble de données, vous devez spécifier wt = x
.
set.seed(123)
d <- data.frame(
x = runif(90),
grp = gl(3, 30))
d %>%
group_by(grp) %>%
top_n(n = 5, wt = x)
# x grp
# 1 0.9404673 1
# 2 0.9568333 1
# 3 0.8998250 1
# 4 0.9545036 1
# 5 0.9942698 1
# 6 0.9630242 2
# 7 0.9022990 2
# 8 0.8578277 2
# 9 0.7989248 2
# 10 0.8950454 2
# 11 0.8146400 3
# 12 0.8123895 3
# 13 0.9849570 3
# 14 0.8930511 3
# 15 0.8864691 3
Assez facile avec data.table
aussi...
library(data.table)
setorder(setDT(d), -x)[, head(.SD, 5), keyby = grp]
Ou
setorder(setDT(d), grp, -x)[, head(.SD, 5), by = grp]
Ou (Doit être plus rapide pour le Big Data Set car éviter d'appeler .SD
pour chaque groupe)
setorder(setDT(d), grp, -x)[, indx := seq_len(.N), by = grp][indx <= 5]
Edit: Voici comment dplyr
se compare à 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’un peu plus rapide data.table
Solution:
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
Vous devez envelopper head
dans un appel à do
. Dans le code suivant, .
représente le groupe actuel (voir la description de ...
dans la page d'aide do
).
d %>%
arrange_(~ desc(x)) %>%
group_by_(~ grp) %>%
do(head(., n = 5))
Comme mentionné par akrun, slice
est une alternative.
d %>%
arrange_(~ desc(x)) %>%
group_by_(~ grp) %>%
slice(1:5)
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 slice
est probablement la plus rapide, mais vous pouvez également utiliser filter
qui sera probablement plus rapide que d’utiliser do(head(., 5))
:
d %>%
arrange(desc(x)) %>%
group_by(grp) %>%
filter(row_number() <= 5L)
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
top_n (n = 1) renverra toujours plusieurs lignes pour chaque groupe si la variable classement n'est 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)