Sélectionnez la première et la dernière ligne parmi les données groupées

137

Question

En utilisant dplyr , comment sélectionner les observations / lignes du haut et du bas de données groupées dans une instruction?

Données et exemple

Étant donné une trame de données

df <- data.frame(id=c(1,1,1,2,2,2,3,3,3), 
                 stopId=c("a","b","c","a","b","c","a","b","c"), 
                 stopSequence=c(1,2,3,3,1,4,3,1,2))

Je peux obtenir les observations du haut et du bas de chaque groupe en utilisant slice, mais en utilisant deux déclarations distinctes:

firstStop <- df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  slice(1) %>%
  ungroup

lastStop <- df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  slice(n()) %>%
  ungroup

Puis-je combiner ces deux statmenets en un seul qui sélectionne les observations du haut et du bas?

tospig
la source

Réponses:

232

Il existe probablement un moyen plus rapide:

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  filter(row_number()==1 | row_number()==n())
Jeremycg
la source
66
rownumber() %in% c(1, n())éviterait la nécessité d'exécuter deux fois l'analyse vectorielle
MichaelChirico
13
@MichaelChirico Je soupçonne que vous avez omis un _? iefilter(row_number() %in% c(1, n()))
Eric Fail
107

Juste pour être complet: vous pouvez passer sliceun vecteur d'indices:

df %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))

qui donne

  id stopId stopSequence
1  1      a            1
2  1      c            3
3  2      b            1
4  2      c            4
5  3      b            1
6  3      a            3
Franc
la source
pourrait même être plus rapide que filter- je n'ai pas testé cela, mais voir ici
Tjebo
1
@Tjebo Contrairement au filtre, slice peut renvoyer la même ligne plusieurs fois, par exemple mtcars[1, ] %>% slice(c(1, n())), dans ce sens, le choix entre eux dépend de ce que vous voulez retourner. Je m'attendrais à ce que les horaires soient proches à moins que ce ne nsoit très grand (où la tranche pourrait être favorisée), mais je n'ai pas testé non plus.
Frank
15

Non dplyr, mais c'est beaucoup plus direct en utilisant data.table:

library(data.table)
setDT(df)
df[ df[order(id, stopSequence), .I[c(1L,.N)], by=id]$V1 ]
#    id stopId stopSequence
# 1:  1      a            1
# 2:  1      c            3
# 3:  2      b            1
# 4:  2      c            4
# 5:  3      b            1
# 6:  3      a            3

Explication plus détaillée:

# 1) get row numbers of first/last observations from each group
#    * basically, we sort the table by id/stopSequence, then,
#      grouping by id, name the row numbers of the first/last
#      observations for each id; since this operation produces
#      a data.table
#    * .I is data.table shorthand for the row number
#    * here, to be maximally explicit, I've named the variable V1
#      as row_num to give other readers of my code a clearer
#      understanding of what operation is producing what variable
first_last = df[order(id, stopSequence), .(row_num = .I[c(1L,.N)]), by=id]
idx = first_last$row_num

# 2) extract rows by number
df[idx]

Assurez-vous de consulter le wiki de mise en route pour connaître les data.tablebases

MichaelChirico
la source
1
Ou df[ df[order(stopSequence), .I[c(1,.N)], keyby=id]$V1 ]. Voir idapparaître deux fois est bizarre pour moi.
Frank
Vous pouvez définir des touches dans l' setDTappel. Donc un orderappel pas besoin ici.
Artem Klevtsov
1
@ArtemKlevtsov - vous ne voudrez peut-être pas toujours définir les clés, cependant.
SymbolixAU
2
Ou df[order(stopSequence), .SD[c(1L,.N)], by = id]. Voir ici
JWilliman
@JWilliman ce ne sera pas nécessairement exactement la même chose, car il ne sera pas réorganisé id. Je pense que df[order(stopSequence), .SD[c(1L, .N)], keyby = id]ça devrait faire l'affaire (avec la petite différence avec la solution ci-dessus, le résultat sera keyed
MichaelChirico
8

Quelque chose comme:

library(dplyr)

df <- data.frame(id=c(1,1,1,2,2,2,3,3,3),
                 stopId=c("a","b","c","a","b","c","a","b","c"),
                 stopSequence=c(1,2,3,3,1,4,3,1,2))

first_last <- function(x) {
  bind_rows(slice(x, 1), slice(x, n()))
}

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  do(first_last(.)) %>%
  ungroup

## Source: local data frame [6 x 3]
## 
##   id stopId stopSequence
## 1  1      a            1
## 2  1      c            3
## 3  2      b            1
## 4  2      c            4
## 5  3      b            1
## 6  3      a            3

Avec dovous pouvez effectuer à peu près n'importe quel nombre d'opérations sur le groupe, mais la réponse de @ jeremycg est bien plus appropriée pour cette tâche.

hrbrmstr
la source
1
Je n'avais pas envisagé d'écrire une fonction - certainement une bonne façon de faire quelque chose de plus complexe.
tospig
1
Cela semble trop compliqué par rapport à simplement utiliser slice, commedf %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))
Frank
4
Pas en désaccord (et j'ai indiqué que jeremycg était une meilleure réponse dans le post) mais avoir un doexemple ici pourrait aider les autres quand slicecela ne fonctionnera pas (c'est-à-dire des opérations plus complexes sur un groupe). Et, vous devriez poster votre commentaire comme réponse (c'est la meilleure).
hrbrmstr
6

Je connais la question spécifiée dplyr . Mais, comme d'autres ont déjà publié des solutions utilisant d'autres packages, j'ai décidé d'essayer également d'autres packages:

Paquet de base:

df <- df[with(df, order(id, stopSequence, stopId)), ]
merge(df[!duplicated(df$id), ], 
      df[!duplicated(df$id, fromLast = TRUE), ], 
      all = TRUE)

data.table:

df <-  setDT(df)
df[order(id, stopSequence)][, .SD[c(1,.N)], by=id]

sqldf:

library(sqldf)
min <- sqldf("SELECT id, stopId, min(stopSequence) AS StopSequence
      FROM df GROUP BY id 
      ORDER BY id, StopSequence, stopId")
max <- sqldf("SELECT id, stopId, max(stopSequence) AS StopSequence
      FROM df GROUP BY id 
      ORDER BY id, StopSequence, stopId")
sqldf("SELECT * FROM min
      UNION
      SELECT * FROM max")

En une seule requête:

sqldf("SELECT * 
        FROM (SELECT id, stopId, min(stopSequence) AS StopSequence
              FROM df GROUP BY id 
              ORDER BY id, StopSequence, stopId)
        UNION
        SELECT *
        FROM (SELECT id, stopId, max(stopSequence) AS StopSequence
              FROM df GROUP BY id 
              ORDER BY id, StopSequence, stopId)")

Production:

  id stopId StopSequence
1  1      a            1
2  1      c            3
3  2      b            1
4  2      c            4
5  3      a            3
6  3      b            1
mpalanco
la source
3

en utilisant which.minet which.max:

library(dplyr, warn.conflicts = F)
df %>% 
  group_by(id) %>% 
  slice(c(which.min(stopSequence), which.max(stopSequence)))

#> # A tibble: 6 x 3
#> # Groups:   id [3]
#>      id stopId stopSequence
#>   <dbl> <fct>         <dbl>
#> 1     1 a                 1
#> 2     1 c                 3
#> 3     2 b                 1
#> 4     2 c                 4
#> 5     3 b                 1
#> 6     3 a                 3

référence

C'est également beaucoup plus rapide que la réponse acceptée actuelle car nous trouvons les valeurs min et max par groupe, au lieu de trier toute la colonne stopSequence.

# create a 100k times longer data frame
df2 <- bind_rows(replicate(1e5, df, F)) 
bench::mark(
  mm =df2 %>% 
    group_by(id) %>% 
    slice(c(which.min(stopSequence), which.max(stopSequence))),
  jeremy = df2 %>%
    group_by(id) %>%
    arrange(stopSequence) %>%
    filter(row_number()==1 | row_number()==n()))
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 mm           22.6ms     27ms     34.9     14.2MB     21.3
#> 2 jeremy      254.3ms    273ms      3.66    58.4MB     11.0
Moody_Mudskipper
la source
2

Utilisation data.table:

# convert to data.table
setDT(df) 
# order, group, filter
df[order(stopSequence)][, .SD[c(1, .N)], by = id]

   id stopId stopSequence
1:  1      a            1
2:  1      c            3
3:  2      b            1
4:  2      c            4
5:  3      b            1
6:  3      a            3
sindri_baldur
la source
1

Une autre approche avec lapply et une déclaration de dplyr. Nous pouvons appliquer un nombre arbitraire de toutes les fonctions récapitulatives à la même instruction:

lapply(c(first, last), 
       function(x) df %>% group_by(id) %>% summarize_all(funs(x))) %>% 
bind_rows()

Vous pouvez par exemple vous intéresser également aux lignes avec la valeur max stopSequence et faire:

lapply(c(first, last, max("stopSequence")), 
       function(x) df %>% group_by(id) %>% summarize_all(funs(x))) %>%
bind_rows()
Sahir Moosvi
la source
0

Une alternative de base R différente serait de commencer orderpar idet stopSequence, spliten fonction de idet pour chaque, idnous sélectionnons uniquement le premier et le dernier index et sous-ensemble la trame de données en utilisant ces indices.

df[sapply(with(df, split(order(id, stopSequence), id)), function(x) 
                   c(x[1], x[length(x)])), ]


#  id stopId stopSequence
#1  1      a            1
#3  1      c            3
#5  2      b            1
#6  2      c            4
#8  3      b            1
#7  3      a            3

Ou utilisation similaire by

df[unlist(with(df, by(order(id, stopSequence), id, function(x) 
                   c(x[1], x[length(x)])))), ]
Ronak Shah
la source