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?


Odpowiedzi:


232

Prawdopodobnie jest szybszy sposób:

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  filter(row_number()==1 | row_number()==n())

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

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


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.


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

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

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

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

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)])))), ]
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.