Maneiras rápidas no R de obter a primeira linha de um quadro de dados agrupada por um identificador [fechado]

14

Às vezes, preciso obter apenas a primeira linha de um conjunto de dados agrupada por um identificador, como ao recuperar idade e sexo, quando há várias observações por indivíduo. Qual é a maneira mais rápida (ou mais rápida) de fazer isso no R? Usei o agregate () abaixo e suspeito que existem maneiras melhores. Antes de postar essa pergunta, pesquisei um pouco no google, encontrei e tentei o ddply, e fiquei surpreso que era extremamente lento e me deu erros de memória no meu conjunto de dados (400.000 linhas x 16 cols, 7.000 IDs exclusivos), enquanto a versão agregada () foi razoavelmente rápido.

(dx <- data.frame(ID = factor(c(1,1,2,2,3,3)), AGE = c(30,30,40,40,35,35), FEM = factor(c(1,1,0,0,1,1))))
# ID AGE FEM
#  1  30   1
#  1  30   1
#  2  40   0
#  2  40   0
#  3  35   1
#  3  35   1
ag <- data.frame(ID=levels(dx$ID))
ag <- merge(ag, aggregate(AGE ~ ID, data=dx, function(x) x[1]), "ID")
ag <- merge(ag, aggregate(FEM ~ ID, data=dx, function(x) x[1]), "ID")
ag
# ID AGE FEM
#  1  30   1
#  2  40   0
#  3  35   1
#same result:
library(plyr)
ddply(.data = dx, .var = c("ID"), .fun = function(x) x[1,])

ATUALIZAÇÃO: Veja a resposta de Chase e o comentário de Matt Parker sobre o que considero a abordagem mais elegante. Veja a resposta de @Matthew Dowle para a solução mais rápida que usa o data.tablepacote.

bloqueado
fonte
Obrigado por todas as suas respostas. A solução data.table do @Steve foi a mais rápida por um fator de ~ 5 no meu conjunto de dados sobre a solução agregada () do @Gavin (que por sua vez foi mais rápida que o meu código agregado ()) e um fator de ~ 7,5 na solução by () do @Matt. Não cronometrei a ideia de reformular, porque não conseguia fazê-la funcionar rapidamente. Acho que a solução que o @Chase deu será a mais rápida e era realmente o que eu estava procurando, mas quando comecei a escrever este comentário, o código não estava funcionando (vejo que está corrigido agora!).
Bloqueado4
Na verdade, o @Chase foi mais rápido por um fator de ~ 9 sobre o data.table, então mudei minha resposta aceita. Mais uma vez obrigado a todos - aprendemos várias novas ferramentas.
lockedoff
desculpe, eu corrigi meu código. A única ressalva ou truque aqui é concatenar um valor que não seja um dos seus IDs diff()para que você possa escolher o primeiro ID dx.
Chase

Respostas:

10

A sua coluna de identificação é realmente um fator? Se é de fato numérico, acho que você pode usar a difffunção a seu favor. Você também pode forçá-lo a numérico com as.numeric().

dx <- data.frame(
    ID = sort(sample(1:7000, 400000, TRUE))
    , AGE = sample(18:65, 400000, TRUE)
    , FEM = sample(0:1, 400000, TRUE)
)

dx[ diff(c(0,dx$ID)) != 0, ]
correr atrás
fonte
1
Esperto! Você também pode dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]usar dados não numéricos - recebo 0,03 por caractere e 0,05 por fatores. PS: há um extra )na sua primeira system.time()função, após o segundo zero.
Matt Parker
@ Matt - boa chamada e boa captura. Hoje não pareço capaz de copiar / colar código que vale a pena dar uma olhada.
Chase
Estou trabalhando no esquema do London Cycle Hire e precisava encontrar uma maneira de encontrar a primeira e a última instância de usuários que contratam bicicletas. Com 1 milhão de usuários, 10 milhões de viagens por ano e dados de vários anos, meu loop "for" fazia 1 usuário por segundo. Tentei a solução "por" e ela não foi concluída após uma hora. No começo, eu não conseguia entender o que "a alternativa de Matt Parker à solução de Chase" estava fazendo, mas finalmente o centavo caiu, e ele é executado em segundos. Portanto, o argumento sobre a melhoria cada vez maior com conjuntos de dados maiores é comprovado pela minha experiência.
21415 George Simpson #
@GeorgeSimpson - fico feliz em ver que isso ainda está sendo referenciado! A data.tablesolução abaixo deve ser a mais rápida, então eu verificaria se fosse você (provavelmente deve ser a resposta aceita aqui).
Perseguição
17

Após a resposta de Steve, existe uma maneira muito mais rápida de data.table:

> # Preamble
> dx <- data.frame(
+     ID = sort(sample(1:7000, 400000, TRUE))
+     , AGE = sample(18:65, 400000, TRUE)
+     , FEM = sample(0:1, 400000, TRUE)
+ )
> dxt <- data.table(dx, key='ID')

> # fast self join
> system.time(ans2<-dxt[J(unique(ID)),mult="first"])
 user  system elapsed 
0.048   0.016   0.064

> # slower using .SD
> system.time(ans1<-dxt[, .SD[1], by=ID])
  user  system elapsed 
14.209   0.012  14.281 

> mapply(identical,ans1,ans2)  # ans1 is keyed but ans2 isn't, otherwise identical
  ID  AGE  FEM 
TRUE TRUE TRUE 

Se você apenas precisa da primeira linha de cada grupo, é muito mais rápido ingressar nessa linha diretamente. Por que criar o objeto .SD de cada vez, apenas para usar a primeira linha dele?

Compare o 0,064 de data.table com "A alternativa de Matt Parker à solução de Chase" (que parecia ser a mais rápida até agora):

> system.time(ans3<-dxt[c(TRUE, dxt$ID[-1] != dxt$ID[-length(dxt$ID)]), ])
 user  system elapsed 
0.284   0.028   0.310 
> identical(ans1,ans3)
[1] TRUE 

Tão ~ 5 vezes mais rápido, mas é uma pequena mesa com menos de 1 milhão de linhas. À medida que o tamanho aumenta, o mesmo acontece com a diferença.

Matt Dowle
fonte
Uau, nunca gostei muito de quão "inteligente" a [.data.tablefunção pode ficar ... Acho que não percebi que você não criou um .SDobjeto se realmente não precisava dele. Agradável!
Steve Lianoglou 8/03/11
Sim, isso é realmente rápido! Mesmo se você incluir dxt <- data.table(dx, key='ID')na chamada para system.time (), é mais rápido que a solução de @ Matt.
Bloqueado # 8/11
Eu acho que isso está desatualizado agora, como nas versões data.table mais recentes, SD[1L]foi totalmente otimizado e, na verdade, a resposta @SteveLianoglou seria duas vezes mais rápida para 5e7 linhas.
David Arenburg
@DavidArenburg A partir da v1.9.8 de novembro de 2016, sim. Sinta-se à vontade para editar essa resposta diretamente, ou talvez esse Q precise ser um wiki da comunidade ou algo assim.
Matt Dowle
10

Você não precisa de várias merge()etapas, apenas as aggregate()duas variáveis ​​de interesse:

> aggregate(dx[, -1], by = list(ID = dx$ID), head, 1)
  ID AGE FEM
1  1  30   1
2  2  40   0
3  3  35   1

> system.time(replicate(1000, aggregate(dx[, -1], by = list(ID = dx$ID), 
+                                       head, 1)))
   user  system elapsed 
  2.531   0.007   2.547 
> system.time(replicate(1000, {ag <- data.frame(ID=levels(dx$ID))
+ ag <- merge(ag, aggregate(AGE ~ ID, data=dx, function(x) x[1]), "ID")
+ ag <- merge(ag, aggregate(FEM ~ ID, data=dx, function(x) x[1]), "ID")
+ }))
   user  system elapsed 
  9.264   0.009   9.301

Tempos de comparação:

1) Solução de Matt:

> system.time(replicate(1000, {
+ agg <- by(dx, dx$ID, FUN = function(x) x[1, ])
+ # Which returns a list that you can then convert into a data.frame thusly:
+ do.call(rbind, agg)
+ }))
   user  system elapsed 
  3.759   0.007   3.785

2) Solução de remodelação2 de Zach:

> system.time(replicate(1000, {
+ dx <- melt(dx,id=c('ID','FEM'))
+ dcast(dx,ID+FEM~variable,fun.aggregate=mean)
+ }))
   user  system elapsed 
 12.804   0.032  13.019

3) solução data.table de Steve:

> system.time(replicate(1000, {
+ dxt <- data.table(dx, key='ID')
+ dxt[, .SD[1,], by=ID]
+ }))
   user  system elapsed 
  5.484   0.020   5.608 
> dxt <- data.table(dx, key='ID') ## one time step
> system.time(replicate(1000, {
+ dxt[, .SD[1,], by=ID] ## try this one line on own
+ }))
   user  system elapsed 
  3.743   0.006   3.784

4) A solução rápida do Chase usando numérico, não fator ID:

> dx2 <- within(dx, ID <- as.numeric(ID))
> system.time(replicate(1000, {
+ dy <- dx[order(dx$ID),]
+ dy[ diff(c(0,dy$ID)) != 0, ]
+ }))
   user  system elapsed 
  0.663   0.000   0.663

e 5) A alternativa de Matt Parker à solução de Chase, por caráter ou fator ID, que é um pouco mais rápida que a numérica de Chase ID:

> system.time(replicate(1000, {
+ dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)]), ]
+ }))
   user  system elapsed 
  0.513   0.000   0.516
Restabelecer Monica - G. Simpson
fonte
Ah, certo, obrigado! Esqueceu-se dessa sintaxe para agregação.
lockedoff
Se você gostaria de adicionar a solução da Chase, aqui está o que eu tenho:dx$ID <- sample(as.numeric(dx$ID)) #assuming IDs arent presorted system.time(replicate(1000, { dy <- dx[order(dx$ID),] dy[ diff(c(0,dy$ID)) != 0, ] })) user system elapsed 0.58 0.00 0.58
lockedoff
@lockedoff - pronto, obrigado, mas não amostramos aleatoriamente IDos resultados para que o resultado fosse comparável a outras soluções.
Reintegrar Monica - G. Simpson
E a versão de tempo @ Matt Parker nos comentários de @ resposta de Chase
Reintegrar Monica - G. Simpson
2
Obrigado por fazer os horários, Gavin - isso é realmente útil para perguntas como estas.
Matt Parker
9

Você pode tentar usar o pacote data.table .

Para o seu caso em particular, a vantagem é que é (insanamente) rápido. A primeira vez que fui apresentado a ele, estava trabalhando em objetos data.frame com centenas de milhares de linhas. "Normal" aggregateou ddplymétodos foram tomados ~ 1-2 minutos para serem concluídos (isso foi antes de Hadley introduzir o idata.framemojo ddply). Usando data.table, a operação foi literalmente realizada em questão de segundos.

A desvantagem é que é tão rápido, porque ele recorre à sua tabela de dados (é como um arquivo de dados) por "colunas-chave" e usa uma estratégia de pesquisa inteligente para encontrar subconjuntos de dados. Isso resultará em uma reordenação dos seus dados antes de você coletar estatísticas sobre eles.

Dado que você deseja apenas a primeira linha de cada grupo - talvez a reordenação atrapalhe qual linha é a primeira, e é por isso que pode não ser adequada à sua situação.

De qualquer forma, você terá que julgar se data.tableé apropriado ou não aqui, mas é assim que você o usaria com os dados que você apresentou:

install.packages('data.table') ## if yo udon't have it already
library(data.table)
dxt <- data.table(dx, key='ID')
dxt[, .SD[1,], by=ID]
     ID AGE FEM
[1,]  1  30   1
[2,]  2  40   0
[3,]  3  35   1

Atualização: Matthew Dowle (o principal desenvolvedor do pacote data.table) forneceu uma maneira melhor / mais inteligente / (extremamente) mais eficiente de usar o data.table para resolver esse problema como uma das respostas aqui ... definitivamente verifique isso .

Steve Lianoglou
fonte
4

Tente remodelar2

library(reshape2)
dx <- melt(dx,id=c('ID','FEM'))
dcast(dx,ID+FEM~variable,fun.aggregate=mean)
Zach
fonte
3

Você poderia tentar

agg <- by(dx, dx$ID, FUN = function(x) x[1, ])
# Which returns a list that you can then convert into a data.frame thusly:
do.call(rbind, agg)

Eu não tenho idéia se isso será mais rápido do que plyr, no entanto.

Matt Parker
fonte