Selecione a primeira linha por grupo

87

De um dataframe como este

test <- data.frame('id'= rep(1:5,2), 'string'= LETTERS[1:10])
test <- test[order(test$id), ]
rownames(test) <- 1:10

> test
    id string
 1   1      A
 2   1      F
 3   2      B
 4   2      G
 5   3      C
 6   3      H
 7   4      D
 8   4      I
 9   5      E
 10  5      J

Quero criar um novo com a primeira linha de cada par id / string. Se sqldf aceitasse o código R dentro dele, a consulta poderia ter a seguinte aparência:

res <- sqldf("select id, min(rownames(test)), string 
              from test 
              group by id, string")

> res
    id string
 1   1      A
 3   2      B
 5   3      C
 7   4      D
 9   5      E

Existe uma solução sem a criação de uma nova coluna como

test$row <- rownames(test)

e executando a mesma consulta sqldf com min (linha)?

dmvianna
fonte
1
@Matthew, minha pergunta é mais antiga.
dmvianna
2
Sua pergunta é de 1 ano e a outra pergunta é de 4 anos, não? Existem tantas duplicatas desta pergunta
Mateus,
@Matthew Desculpe, devo ter interpretado mal as datas.
dmvianna

Respostas:

120

Você pode usar duplicatedpara fazer isso muito rapidamente.

test[!duplicated(test$id),]

Benchmarks, para os fanáticos por velocidade:

ju <- function() test[!duplicated(test$id),]
gs1 <- function() do.call(rbind, lapply(split(test, test$id), head, 1))
gs2 <- function() do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
jply <- function() ddply(test,.(id),function(x) head(x,1))
jdt <- function() {
  testd <- as.data.table(test)
  setkey(testd,id)
  # Initial solution (slow)
  # testd[,lapply(.SD,function(x) head(x,1)),by = key(testd)]
  # Faster options :
  testd[!duplicated(id)]               # (1)
  # testd[, .SD[1L], by=key(testd)]    # (2)
  # testd[J(unique(id)),mult="first"]  # (3)
  # testd[ testd[,.I[1L],by=id] ]      # (4) needs v1.8.3. Allows 2nd, 3rd etc
}

library(plyr)
library(data.table)
library(rbenchmark)

# sample data
set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]

benchmark(ju(), gs1(), gs2(), jply(), jdt(),
    replications=5, order="relative")[,1:6]
#     test replications elapsed relative user.self sys.self
# 1   ju()            5    0.03    1.000      0.03     0.00
# 5  jdt()            5    0.03    1.000      0.03     0.00
# 3  gs2()            5    3.49  116.333      2.87     0.58
# 2  gs1()            5    3.58  119.333      3.00     0.58
# 4 jply()            5    3.69  123.000      3.11     0.51

Vamos tentar de novo, mas apenas com os contendores da primeira bateria e com mais dados e mais replicações.

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
benchmark(ju(), jdt(), order="relative")[,1:6]
#    test replications elapsed relative user.self sys.self
# 1  ju()          100    5.48    1.000      4.44     1.00
# 2 jdt()          100    6.92    1.263      5.70     1.15
Joshua Ulrich
fonte
O vencedor: system.time (dat3 [! Duplicated (dat3 $ id),]) sistema do usuário decorrido 0,07 0,00 0,07
dmvianna
2
@dmvianna: Não o tenho instalado e não me apetece incomodar. :)
Joshua Ulrich
Temos certeza de que meu código data.table é o mais eficiente possível? Não estou confiante na minha capacidade de obter o melhor desempenho dessa ferramenta.
joran
2
Além disso, eu acho que se você for avaliar o data.table, a codificação deve incluir a ordem por id nas chamadas de base.
mnel de
1
@JoshuaUlrich Mais uma pergunta: por que a primeira frase é necessária, isto é, supor que os dados já estão classificados. !duplicated(x)encontra o primeiro de cada grupo, mesmo se não estiver classificado, iiuc.
Matt Dowle
38

Eu sou a favor da abordagem dplyr.

group_by(id) seguido por qualquer um

  • filter(row_number()==1) ou
  • slice(1) ou
  • slice_head(1) # (dplyr => 1.0)
  • top_n(n = -1)
    • top_n()usa internamente a função de classificação. Negativo seleciona na parte inferior da classificação.

Em alguns casos, pode ser necessário organizar os ids após group_by.

library(dplyr)

# using filter(), top_n() or slice()

m1 <-
test %>% 
  group_by(id) %>% 
  filter(row_number()==1)

m2 <-
test %>% 
  group_by(id) %>% 
  slice(1)

m3 <-
test %>% 
  group_by(id) %>% 
  top_n(n = -1)

Todos os três métodos retornam o mesmo resultado

# A tibble: 5 x 2
# Groups:   id [5]
     id string
  <int> <fct> 
1     1 A     
2     2 B     
3     3 C     
4     4 D     
5     5 E
Kresten
fonte
2
Vale a pena comentar slicetambém. slice(x)é um atalho para filter(row_number() %in% x).
Gregor Thomas de
Muito elegante. Você sabe por que tenho que converter meu data.tablepara um data.framepara que isso funcione?
James Hirschorn
@JamesHirschorn Não sou um especialista em todas as diferenças. Mas data.tableherda do data.frameentão, em muitos casos, você pode usar os comandos dplyr em um data.table. O exemplo acima, por exemplo, também funciona se testfor a data.table. Consulte, por exemplo, stackoverflow.com/questions/13618488/… para uma explicação mais detalhada
Kresten
Esta é uma maneira tidyverse de fazer isso e como você vê o data.frame é na verdade um tibble aqui. Eu pessoalmente aconselho você a trabalhar sempre com tibbles também porque ggplot2 é construído de maneira semelhante.
Garini
17

A respeito

DT <- data.table(test)
setkey(DT, id)

DT[J(unique(id)), mult = "first"]

Editar

Há também um método único para o data.tablesqual retornará a primeira linha por chave

jdtu <- function() unique(DT)

Eu acho que, se você está pedindo testfora do benchmark, então você pode remover a conversão setkeye data.tabledo benchmark também (como o setkey basicamente classifica por id, o mesmo que order).

set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]
DT <- data.table(DT, key = 'id')
ju <- function() test[!duplicated(test$id),]

jdt <- function() DT[J(unique(id)),mult = 'first']


 library(rbenchmark)
benchmark(ju(), jdt(), replications = 5)
##    test replications elapsed relative user.self sys.self 
## 2 jdt()            5    0.01        1      0.02        0        
## 1  ju()            5    0.05        5      0.05        0         

e com mais dados

** Edite com método único **

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
DT <- data.table(test, key = 'id')
       test replications elapsed relative user.self sys.self 
2  jdt()            5    0.09     2.25      0.09     0.00    
3 jdtu()            5    0.04     1.00      0.05     0.00      
1   ju()            5    0.22     5.50      0.19     0.03        

O método exclusivo é mais rápido aqui.

mnel
fonte
4
Você nem mesmo precisa definir a chave. unique(DT,by="id")trabalha diretamente
Mateus
Para sua informação, a partir da data.tableversão> = 1.9.8, o byargumento padrão para uniqueé by = seq_along(x)(todas as colunas), em vez do padrão anteriorby = key(x)
IceCreamToucan
12

Uma ddplyopção simples :

ddply(test,.(id),function(x) head(x,1))

Se a velocidade for um problema, uma abordagem semelhante pode ser adotada com data.table:

testd <- data.table(test)
setkey(testd,id)
testd[,.SD[1],by = key(testd)]

ou isso pode ser consideravelmente mais rápido:

testd[testd[, .I[1], by = key(testd]$V1]
Joran
fonte
Surpreendentemente, sqldf faz isso mais rápido: 1,77 0,13 1,92 vs 10,53 0,00 10,79 com data.table
dmvianna
3
@dmvianna Eu não contaria necessariamente data.table. Não sou um especialista com essa ferramenta, então meu código data.table pode não ser a maneira mais eficiente de fazer isso.
joran
Eu votei a favor prematuramente. Quando o executei em um data.table grande, ele ficou ridiculamente lento e não funcionou: o número de linhas foi o mesmo depois.
James Hirschorn
@JamesHirachorn Escrevi isso há muito tempo, o pacote mudou muito e quase não uso data.table. Se você encontrar a maneira certa de fazer isso com esse pacote, fique à vontade para sugerir uma edição para torná-lo melhor.
joran
8

agora, para dplyradicionar um contador distinto.

df %>%
    group_by(aa, bb) %>%
    summarise(first=head(value,1), count=n_distinct(value))

Você cria grupos, eles resumem dentro de grupos.

Se os dados forem numéricos, você pode usar:
first(value)[também há last(value)] no lugar dehead(value, 1)

consulte: http://cran.rstudio.com/web/packages/dplyr/vignettes/introduction.html

Cheio:

> df
Source: local data frame [16 x 3]

   aa bb value
1   1  1   GUT
2   1  1   PER
3   1  2   SUT
4   1  2   GUT
5   1  3   SUT
6   1  3   GUT
7   1  3   PER
8   2  1   221
9   2  1   224
10  2  1   239
11  2  2   217
12  2  2   221
13  2  2   224
14  3  1   GUT
15  3  1   HUL
16  3  1   GUT

> library(dplyr)
> df %>%
>   group_by(aa, bb) %>%
>   summarise(first=head(value,1), count=n_distinct(value))

Source: local data frame [6 x 4]
Groups: aa

  aa bb first count
1  1  1   GUT     2
2  1  2   SUT     2
3  1  3   SUT     3
4  2  1   221     3
5  2  2   217     3
6  3  1   GUT     2
Paulo
fonte
Esta resposta é bastante desatualizada - existem maneiras melhores de fazer isso dplyrsem exigir a escrita de uma declaração para cada coluna a ser incluída (veja a resposta do atomman abaixo, por exemplo) . Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use primeiro (valor) `vs head(value)(ou apenas value[1])
Gregor Thomas
7

(1) O SQLite tem uma rowidpseudocoluna embutida, então isso funciona:

sqldf("select min(rowid) rowid, id, string 
               from test 
               group by id")

dando:

  rowid id string
1     1  1      A
2     3  2      B
3     5  3      C
4     7  4      D
5     9  5      E

(2) Também sqldftem um row.names=argumento:

sqldf("select min(cast(row_names as real)) row_names, id, string 
              from test 
              group by id", row.names = TRUE)

dando:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

(3) Uma terceira alternativa que mistura os elementos das duas anteriores pode ser ainda melhor:

sqldf("select min(rowid) row_names, id, string 
               from test 
               group by id", row.names = TRUE)

dando:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

Observe que todos os três contam com uma extensão SQLite para SQL em que o uso de minou maxé garantido para resultar na escolha de outras colunas na mesma linha. (Em outros bancos de dados baseados em SQL, isso pode não ser garantido.)

G. Grothendieck
fonte
Obrigado! Isso é muito melhor do que a resposta aceita IMO porque é generalizável para pegar o primeiro / último elemento em uma etapa de agregação usando várias funções de agregação (ou seja, pegar o primeiro desta variável, somar essa variável, etc).
Bridgeburners
4

Uma opção de base R é o split()- lapply()- do.call()idiom:

> do.call(rbind, lapply(split(test, test$id), head, 1))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

Uma opção mais direta é lapply()a [função:

> do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

O espaço de vírgula 1, )no final da lapply()chamada é essencial, pois isso equivale a chamar [1, ]para selecionar a primeira linha e todas as colunas.

Gavin Simpson
fonte
Isso foi muito lento, Gavin: sistema do usuário decorrido 91,84 6,02 101,10
dmvianna
Qualquer coisa envolvendo frames de dados será. Sua utilidade tem um preço. Daí data.table, por exemplo.
Gavin Simpson
em minha defesa, e R's, você não mencionou nada sobre eficiência na pergunta. Freqüentemente, a facilidade de uso é um recurso. Testemunhe a popularidade do ply, que também é "lento", pelo menos até a próxima versão que tenha suporte data.table.
Gavin Simpson
1
Concordo. Eu não queria te insultar. Eu fiz encontrar, porém, que @ método de Joshua-Ulrich era tanto fácil e rápido. : 7)
dmvianna
Não há necessidade de se desculpar e não considerei isso um insulto. Estava apenas apontando que era oferecido sem qualquer pretensão de eficiência. Lembre-se de que estas perguntas e respostas do Stack Overflow não são apenas para o seu benefício, mas também para outros usuários que encontrarem sua dúvida por terem um problema semelhante.
Gavin Simpson