Selecione a primeira e a última linha dos dados agrupados

137

Questão

Usando dplyr, como faço para selecionar as observações / linhas superior e inferior dos dados agrupados em uma instrução?

Dados e exemplo

Dado um quadro de dados

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

Posso obter as observações superior e inferior de cada grupo usando slice, mas usando duas declarações separadas:

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

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

Posso combinar esses dois conjuntos de estatísticas em um que selecione as observações superior e inferior?

tospig
fonte

Respostas:

232

Provavelmente existe uma maneira mais rápida:

df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  filter(row_number()==1 | row_number()==n())
jeremycg
fonte
66
rownumber() %in% c(1, n())evitaria a necessidade de executar vector varredura duas vezes
MichaelChirico
13
@MichaelChirico Eu suspeito que você omitiu um _? iefilter(row_number() %in% c(1, n()))
Eric Falha
107

Apenas para completar: você pode passar sliceum vetor de índices:

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

que dá

  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
fonte
pode até ser mais rápido do que filter- não testei isso, mas veja aqui
Tjebo
1
@Tjebo Ao contrário do filtro, a fatia pode retornar a mesma linha várias vezes, por exemplo, mtcars[1, ] %>% slice(c(1, n()))nesse sentido, a escolha entre elas depende do que você deseja devolver. Eu esperaria que os horários fossem próximos, a menos que nseja muito grande (onde a fatia pode ser favorecida), mas também não foram testados.
31718 Frank
15

Não dplyr, mas é muito mais direto usando 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

Explicação mais detalhada:

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

Certifique-se de verificar o wiki Introdução para obter o data.tablebásico coberto

MichaelChirico
fonte
1
Or df[ df[order(stopSequence), .I[c(1,.N)], keyby=id]$V1 ]. Ver idaparecer duas vezes é estranho para mim.
Frank
Você pode definir as teclas na setDTchamada. Portanto, uma orderligação não é necessária aqui.
Artem Klevtsov 01/02
1
@ArtemKlevtsov - talvez nem sempre você queira definir as chaves.
precisa saber é o seguinte
2
Or df[order(stopSequence), .SD[c(1L,.N)], by = id]. Veja aqui
JWilliman
@JWilliman que não será necessariamente exatamente o mesmo, pois não será reordenado id. Eu acho que df[order(stopSequence), .SD[c(1L, .N)], keyby = id]deve fazer o truque (com a pequena diferença para a solução acima que o resultado será keyed
MichaelChirico
8

Algo como:

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

Com dovocê, você pode executar qualquer número de operações no grupo, mas a resposta do @ jeremycg é muito mais apropriada para essa tarefa.

hrbrmstr
fonte
1
Não considerara escrever uma função - certamente uma boa maneira de fazer algo mais complexo.
tospig 21/07/2015
1
Isso parece complicado demais em comparação com apenas o uso slice, comodf %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))
Frank
4
Não discordo (e apontei para jeremycg como uma resposta melhor no post), mas ter um doexemplo aqui pode ajudar outras pessoas quando slicenão funcionar (ou seja, operações mais complexas em um grupo). E você deve postar seu comentário como resposta (é o melhor).
Hrbrmstr 21/07/2015
6

Eu sei a pergunta especificada dplyr. Mas, como outros já postaram soluções usando outros pacotes, decidi usar outros pacotes também:

Pacote base:

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

Tabela de dados:

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

Em uma consulta:

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

Resultado:

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

usando which.mine 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

referência

Também é muito mais rápido que a resposta atualmente aceita, porque encontramos o valor mínimo e máximo por grupo, em vez de classificar toda a coluna 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
fonte
2

Usando 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
fonte
1

Outra abordagem com lapply e uma declaração dplyr. Podemos aplicar um número arbitrário de quaisquer funções de resumo à mesma declaração:

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

Por exemplo, você pode estar interessado em linhas com o valor máximo de stopSequence e fazer:

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

Uma alternativa base de R diferente seria a primeira orderpor ide stopSequence, split-los com base em ide para cada idselecionamos apenas o primeiro eo último índice e subconjunto da trama de dados usando esses índices.

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 similar usando by

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