Wybierz pierwszy i ostatni wiersz z pogrupowanych danych

137

Pytanie

Używając dplyr, jak wybrać górne i dolne obserwacje / wiersze zgrupowanych danych w jednej instrukcji?

Dane i przykład

Biorąc pod uwagę ramkę danych

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

Mogę uzyskać górne i dolne obserwacje z każdej grupy, używając slice, ale używając dwóch oddzielnych instrukcji:

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

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

Czy mogę połączyć te dwa zestawienia statystyk w jedno, które wybiera zarówno górne, jak i dolne obserwacje?

tospig
źródło

Odpowiedzi:

232

Prawdopodobnie jest szybszy sposób:

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  filter(row_number()==1 | row_number()==n())
jeremycg
źródło
66
rownumber() %in% c(1, n())wyeliminowałoby potrzebę dwukrotnego uruchamiania skanowania wektorów
MichaelChirico,
13
@MichaelChirico Podejrzewam, że pominąłeś _? tj.filter(row_number() %in% c(1, n()))
Eric Fail
107

Dla kompletności: możesz podać slicewektor indeksów:

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

co daje

  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
Szczery
źródło
może być nawet szybszy niż filter- nie testowałem tego, ale zobacz tutaj
Tjebo,
1
@Tjebo W przeciwieństwie do filtra, plaster może zwrócić ten sam wiersz wiele razy, np. mtcars[1, ] %>% slice(c(1, n()))W tym sensie wybór między nimi zależy od tego, co chcesz zwrócić. Spodziewałbym się, że czasy będą bliskie, chyba że nsą bardzo duże (gdzie może być preferowany wycinek), ale też nie testowałem.
Frank,
15

Nie dplyr, ale jest to znacznie bardziej bezpośrednie użycie 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

Bardziej szczegółowe wyjaśnienie:

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

Aby zapoznać się z podstawami, zajrzyj na wiki Pierwsze krokidata.table

MichaelChirico
źródło
1
Lub df[ df[order(stopSequence), .I[c(1,.N)], keyby=id]$V1 ]. Widząc idpojawiają się dwa razy to dziwne dla mnie.
Frank
Możesz ustawić klucze w setDTrozmowie. Więc ordertelefon nie ma potrzeby tutaj.
Artem Klevtsov
1
@ArtemKlevtsov - możesz jednak nie zawsze chcieć ustawić klucze.
SymbolixAU
2
Lub df[order(stopSequence), .SD[c(1L,.N)], by = id]. Zobacz tutaj
JWilliman
@JWilliman, który niekoniecznie będzie dokładnie taki sam, ponieważ nie zostanie ponownie zamówiony id. Myślę, że df[order(stopSequence), .SD[c(1L, .N)], keyby = id]powinno wystarczyć (z tą różnicą, Minor to rozwiązanie powyższego, że wynik będzie keyed
MichaelChirico
8

Coś jak:

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

Z domożesz wykonać dowolną liczbę operacji na grupie, ale odpowiedź @ jeremycg jest o wiele bardziej odpowiednia tylko do tego zadania.

hrbrmstr
źródło
1
Nie rozważałem napisania funkcji - z pewnością dobry sposób na zrobienie czegoś bardziej złożonego.
tospig
1
Wydaje się to zbyt skomplikowane w porównaniu do zwykłego używania slice, na przykładdf %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))
Frank
4
Nie zgadzam się (i wskazałem na jeremycg jako lepszą odpowiedź w poście), ale doprzykład tutaj może pomóc innym, gdy slicenie zadziała (tj. Bardziej złożone operacje na grupie). Jako odpowiedź powinieneś zamieścić swój komentarz (jest najlepszy).
hrbrmstr
6

Znam określone pytanie dplyr. Ale ponieważ inni już opublikowali rozwiązania korzystające z innych pakietów, zdecydowałem się wypróbować również inne pakiety:

Pakiet podstawowy:

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

Tabela danych:

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

W jednym zapytaniu:

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

Wynik:

  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
źródło
3

za pomocą which.mini 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

reper

Jest również znacznie szybszy niż obecnie akceptowana odpowiedź, ponieważ znajdujemy minimalną i maksymalną wartość według grup, zamiast sortować całą kolumnę 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
źródło
2

Używając 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
źródło
1

Inne podejście z lapply i oświadczeniem dplyr. Możemy zastosować dowolną liczbę dowolnych funkcji podsumowujących do tego samego stwierdzenia:

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

Możesz na przykład zainteresować się wierszami z wartością max stopSequence i zrobić:

lapply(c(first, last, max("stopSequence")), 
       function(x) df %>% group_by(id) %>% summarize_all(funs(x))) %>%
bind_rows()
Sahir Moosvi
źródło
0

Inną alternatywą bazową R byłoby pierwsze orderby idi stopSequence, splitna podstawie idi dla każdego idwybieramy tylko pierwszy i ostatni indeks i podzestawiamy ramkę danych przy użyciu tych indeksów.

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

Lub podobne użycie by

df[unlist(with(df, by(order(id, stopSequence), id, function(x) 
                   c(x[1], x[length(x)])))), ]
Ronak Shah
źródło