Выберите первую и последнюю строку из сгруппированных данных

137

Вопрос

Используя 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))

Я могу получить верхние и нижние наблюдения от каждой группы, используя slice, но используя две отдельные оценки:

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

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

Могу ли я объединить эти два statmenet в один, который выбирает и верхние и нижние наблюдения?

tospig
источник

Ответы:

232

Вероятно, есть более быстрый способ:

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  filter(row_number()==1 | row_number()==n())
jeremycg
источник
66
rownumber() %in% c(1, n())избавит от необходимости запускать векторное сканирование дважды
MichaelChirico
13
@MichaelChirico Я подозреваю, что вы пропустили _? то естьfilter(row_number() %in% c(1, n()))
Эрик
107

Просто для полноты: вы можете передать sliceвектор индексов:

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

который дает

  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
Фрэнк
источник
может быть даже быстрее, чем filter- не проверял это, но смотрите здесь
Tjebo
1
@Tjebo В отличие от фильтра, слайс может возвращать одну и ту же строку несколько раз, например, mtcars[1, ] %>% slice(c(1, n()))в этом смысле выбор между ними зависит от того, что вы хотите вернуть. Я ожидаю, что время будет близким, если только nоно не будет очень большим (где срез может быть предпочтительным), но также не проверил.
Фрэнк
15

Нет dplyr, но это гораздо более прямое использование 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

Более подробное объяснение:

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

Обязательно ознакомьтесь с вики « Приступая к работе» , чтобы узнать data.tableосновы

MichaelChirico
источник
1
Или df[ df[order(stopSequence), .I[c(1,.N)], keyby=id]$V1 ]. Видеть idдважды появляется странно для меня.
Фрэнк
Вы можете установить ключи в setDTвызове. Так что orderзвонить здесь не нужно.
Артем Клевцов
1
@ArtemKlevtsov - вы не всегда хотите устанавливать ключи, хотя.
SymbolixAU
2
Или df[order(stopSequence), .SD[c(1L,.N)], by = id]. Смотрите здесь
JWilliman
@JWilliman, который не обязательно будет точно таким же, поскольку он не будет переупорядочен id. Я думаю , что df[order(stopSequence), .SD[c(1L, .N)], keyby = id]следует сделать трюк (с разницей в незначительной к решению выше , что результат будет keyэд
MichaelChirico
8

Что-то вроде:

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

С ним doвы можете выполнять любое количество операций над группой, но ответ @ jeremycg более подходит для этой задачи.

hrbrmstr
источник
1
Не подумал написать функцию - конечно, хороший способ сделать что-то более сложное.
Tospig
1
Это кажется слишком сложным по сравнению с просто использованием slice, какdf %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))
Фрэнк
4
Не спорю (и я указал на jeremycg как лучший ответ в посте), но doприведенный здесь пример может помочь другим, когда sliceне будет работать (т.е. более сложные операции в группе). И вы должны опубликовать свой комментарий в качестве ответа (это лучший).
hrbrmstr
6

Я знаю заданный вопрос dplyr. Но, поскольку другие уже опубликовали решения с использованием других пакетов, я решил попробовать и другие пакеты:

Базовый пакет:

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

Таблица данных:

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

В одном запросе:

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

Вывод:

  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
источник
3

используя which.minи 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

эталонный тест

Это также намного быстрее, чем текущий принятый ответ, потому что мы находим минимальное и максимальное значение по группам, а не сортируем весь столбец 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
источник
2

Использование 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
источник
1

Другой подход с lapply и заявлением dplyr. Мы можем применить произвольное число любых итоговых функций к одному и тому же утверждению:

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

Например, вы можете быть заинтересованы в строках со значением max stopSequence и сделать:

lapply(c(first, last, max("stopSequence")), 
       function(x) df %>% group_by(id) %>% summarize_all(funs(x))) %>%
bind_rows()
Сахир Моосви
источник
0

Другая база R Альтернатива будет первым orderна idи stopSequence, splitих на основе idи для каждых idмы выбираем только первый и последний индекс и подмножество в dataframe с помощью этих индексов.

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

Или аналогично, используя by

df[unlist(with(df, by(order(id, stopSequence), id, function(x) 
                   c(x[1], x[length(x)])))), ]
Ронак Шах
источник