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



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

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

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.


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]

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

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


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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.