Wählen Sie die erste und letzte Zeile aus den gruppierten Daten aus

137

Frage

Verwendung dplyr, wie wähle ich die obere und untere Beobachtungen / Reihen gruppierten Daten in einer Aussage?

Daten & Beispiel

Gegeben ein Datenrahmen

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))

Ich kann die oberen und unteren Beobachtungen von jeder Gruppe erhalten slice, indem ich zwei separate Aussagen verwende:

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

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

Kann ich diese beiden Statmenets zu einem kombinieren, das sowohl obere als auch untere Beobachtungen auswählt ?

tospig
quelle

Antworten:

232

Es gibt wahrscheinlich einen schnelleren Weg:

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  filter(row_number()==1 | row_number()==n())
jeremycg
quelle
66
rownumber() %in% c(1, n())würde die Notwendigkeit vermeiden, Vektorscan zweimal
auszuführen
13
@ MichaelChirico Ich vermute du hast ein weggelassen _? dhfilter(row_number() %in% c(1, n()))
Eric Fail
106

Der Vollständigkeit halber: Sie können sliceeinen Indexvektor übergeben:

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

was gibt

  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
Frank
quelle
könnte sogar schneller sein als filter - habe dies nicht getestet, aber siehe hier
Tjebo
1
@Tjebo Im Gegensatz zum Filter kann Slice dieselbe Zeile mehrmals zurückgeben. mtcars[1, ] %>% slice(c(1, n()))In diesem Sinne hängt die Auswahl zwischen ihnen davon ab, was zurückgegeben werden soll. Ich würde erwarten, dass die Timings nahe beieinander liegen, es nsei denn, sie sind sehr groß (wo Slice bevorzugt wird), aber ich habe sie auch nicht getestet.
Frank
15

Nicht dplyr, aber es ist viel direkter mit 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

Detailliertere Erklärung:

# 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]

Achten Sie darauf , die auschecken Erste Schritte die Wiki für das Erhalten data.tableGrundlagen abgedeckt

MichaelChirico
quelle
1
Oder df[ df[order(stopSequence), .I[c(1,.N)], keyby=id]$V1 ]. idZweimal erscheinen zu sehen , ist komisch für mich.
Frank
Sie können im setDTAnruf Schlüssel festlegen . Also ein orderAnruf hier nicht nötig.
Artem Klevtsov
1
@ArtemKlevtsov - Möglicherweise möchten Sie die Schlüssel jedoch nicht immer festlegen.
SymbolixAU
2
Oder df[order(stopSequence), .SD[c(1L,.N)], by = id]. Siehe hier
JWilliman
@JWilliman das wird nicht unbedingt genau das gleiche sein, da es nicht nachbestellt wird id. Ich denke , df[order(stopSequence), .SD[c(1L, .N)], keyby = id]soll über dem Ergebnis den Trick (mit dem kleinen Unterschied zu der Lösung tun wird keyed
MichaelChirico
8

Etwas wie:

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

Mit können doSie so ziemlich eine beliebige Anzahl von Operationen an der Gruppe ausführen, aber die Antwort von @ jeremycg ist viel besser für genau diese Aufgabe geeignet.

hrbrmstr
quelle
1
Ich hatte nicht daran gedacht, eine Funktion zu schreiben - sicherlich eine gute Möglichkeit, etwas Komplexeres zu tun.
tospig
1
Dies scheint zu kompliziert im Vergleich zu nur verwenden slice, wiedf %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))
Frank
4
Nicht anderer Meinung zu sein (und ich habe auf Jeremyyg's als bessere Antwort in der Post hingewiesen ), aber ein doBeispiel hier zu haben, könnte anderen helfen, wenn slicees nicht funktioniert (dh komplexere Operationen in einer Gruppe). Und du solltest deinen Kommentar als Antwort posten (es ist der beste).
hrbrmstr
6

Ich kenne die angegebene Frage dplyr . Da andere bereits Lösungen mit anderen Paketen veröffentlicht haben, habe ich mich entschlossen, auch andere Pakete zu verwenden:

Basispaket:

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

Datentabelle:

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")

In einer Abfrage:

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)")

Ausgabe:

  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
quelle
3

mit which.minund 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

Benchmark

Es ist auch viel schneller als die aktuell akzeptierte Antwort, da wir den Min- und Max-Wert nach Gruppe finden, anstatt die gesamte stopSequence-Spalte zu sortieren.

# 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
quelle
2

Verwenden von 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
quelle
1

Ein anderer Ansatz mit lapply und einer dplyr-Anweisung. Wir können eine beliebige Anzahl beliebiger Zusammenfassungsfunktionen auf dieselbe Anweisung anwenden:

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

Sie könnten beispielsweise auch an Zeilen mit dem maximalen Wert stopSequence interessiert sein und Folgendes tun:

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

Eine andere Basis-R-Alternative wäre, zuerst ordernach idund stopSequence, splitbasierend auf idund für jeden id, wählen wir nur den ersten und letzten Index aus und unterteilen den Datenrahmen unter Verwendung dieser Indizes.

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

Oder ähnlich mit by

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