Por que rbindlist é "melhor" que rbind?

135

Estou examinando a documentação data.tablee também notei em algumas das conversas aqui no SO que rbindlistdeveriam ser melhores do que rbind.

Gostaria de saber por que é rbindlistmelhor do que rbinde em quais cenários rbindlistrealmente supera rbind?

Existe alguma vantagem em termos de utilização de memória?

Chinmay Patil
fonte

Respostas:

155

rbindlisté uma versão otimizada do.call(rbind, list(...)), conhecida por ser lenta ao usarrbind.data.frame


Onde ele realmente se destaca

Algumas perguntas que mostram onde rbindlistestão os brilhos

Mesclagem vetorizada rápida da lista de data.frames por linha

Problemas ao converter uma lista longa de data.frames (~ 1 milhão) em data.frame único usando do.call e ldply

Eles têm referências que mostram quão rápido pode ser.


rbind.data.frame é lento, por um motivo

rbind.data.framefaz muitas verificações e corresponde ao nome. (ou seja, rbind.data.frame levará em consideração o fato de que as colunas podem estar em ordens diferentes e serem correspondidas por nome), rbindlistnão faz esse tipo de verificação e se unirá por posição

por exemplo

do.call(rbind, list(data.frame(a = 1:2, b = 2:3), data.frame(b = 1:2, a = 2:3)))
##    a b
## 1  1 2
## 2  2 3
## 3  2 1
## 4  3 2

rbindlist(list(data.frame(a = 1:5, b = 2:6), data.frame(b = 1:5, a = 2:6)))
##     a b
##  1: 1 2
##  2: 2 3
##  3: 1 2
##  4: 2 3

Algumas outras limitações do rbindlist

É usado para lutam para lidar com factors, devido a um bug que, desde então, foram corrigidos:

rbindlist dois data.tables em que um possui fator e outro possui tipo de caractere para uma coluna ( Bug # 2650 )

Tem problemas com nomes de colunas duplicados

consulte Mensagem de aviso: no rbindlist (allargs): NAs introduzidas por coerção: possível bug no data.table? ( Bug # 2384 )


Os nomes de rbind.data.frame podem ser frustrantes

rbindlistpode lidar com lists data.framese data.tables, e irá retornar um data.table sem rownames

você pode entrar em uma confusão de rownames usando do.call(rbind, list(...)) see

Como evitar renomear linhas ao usar rbind dentro de do.call?


Eficiência de memória

Em termos de memória rbindlisté implementada C, assim como é eficiente em termos de memória , ela usa setattrpara definir atributos por referência

rbind.data.frameé implementado R, ele faz muitas atribuições e usa attr<-( class<-e rownames<-todos os quais criarão (internamente) cópias do data.frame criado).

mnel
fonte
1
Para sua informação attr<-, class<-e (acho) rownames<-todas as modificações são feitas no local.
Hadley
5
@hadley Você tem certeza? Tente DF = data.frame(a=1:3); .Internal(inspect(DF)); tracemem(DF); attr(DF,"test") <- "hello"; .Internal(inspect(DF)).
precisa
4
rbind.data.framepossui uma lógica especial de "seqüestro" - quando seu primeiro argumento é a data.table, ele chama .rbind.data.table, o que faz uma pequena verificação e depois chama rbindlistinternamente. Portanto, se você já possui data.tableobjetos para vincular, provavelmente há pouca diferença de desempenho entre rbinde rbindlist.
Ken Williams
6
Em geral, esse post talvez precise ser editado, agora rbindlistcapaz de corresponder por nomes ( use.names=TRUE) e também preencher colunas ausentes ( fill=TRUE). Eu atualizei este , este e este post. Você se importa de editar este ou está tudo bem se eu fizer? De qualquer maneira é bom para mim.
Arun
1
dplyr::rbind_listtambém é bem parecido
hadley 06/06
48

Por v1.9.2, rbindlistevoluiu bastante, implementando muitos recursos, incluindo:

  • Escolhendo a SEXPTYPEcoluna mais alta durante a encadernação - implementada no v1.9.2fechamento da FR # 2456 e Bug # 4981 .
  • Manipulando factorcolunas corretamente - implementado pela primeira vez no v1.8.10fechamento do Bug # 2650 e estendido aos fatores ordenados de ligação com cuidado v1.9.2também, fechando o FR # 4856 e o Bug # 5019 .

Além disso, em v1.9.2, rbind.data.tabletambém ganhou um fillargumento, que permite vincular preenchendo colunas ausentes, implementadas em R.

Agora v1.9.3, há ainda mais melhorias nesses recursos existentes:

  • rbindlistganha um argumento use.names, que por padrão é FALSEpara compatibilidade com versões anteriores.
  • rbindlisttambém ganha um argumento fill, que por padrão também é FALSEpara compatibilidade com versões anteriores.
  • Esses recursos são todos implementados em C e escritos com cuidado para não comprometer a velocidade ao adicionar funcionalidades.
  • Como rbindlistagora pode corresponder por nomes e preencher as colunas ausentes, rbind.data.tablebasta ligar rbindlistagora. A única diferença é que, use.names=TRUEpor padrão rbind.data.table, para compatibilidade com versões anteriores.

rbind.data.framediminui bastante devido principalmente às cópias (que o @mnel também aponta) que poderiam ser evitadas (passando para C). Eu acho que essa não é a única razão. A implementação para verificar / combinar nomes de colunas emrbind.data.frame também pode ficar mais lenta quando houver muitas colunas por data.frame e existirem muitos data.frames a serem vinculados (como mostrado na referência abaixo).

No entanto, essa rbindlistfalta (ed) de certos recursos (como verificar níveis de fator ou nomes correspondentes) tem um peso muito pequeno (ou nenhum) para que seja mais rápido do que rbind.data.frame. É porque eles foram cuidadosamente implementados em C, otimizados para velocidade e memória.

Aqui está uma referência que destaca a ligação eficiente, enquanto faz a correspondência por nomes de colunas, usando também rbindlist o use.namesrecurso de v1.9.3. O conjunto de dados consiste em 10000 quadros de dados, cada um com tamanho 10 * 500.

NB: esta referência foi atualizada para incluir uma comparação com dplyrasbind_rows

library(data.table) # 1.11.5, 2018-06-02 00:09:06 UTC
library(dplyr) # 0.7.5.9000, 2018-06-12 01:41:40 UTC
set.seed(1L)
names = paste0("V", 1:500)
cols = 500L
foo <- function() {
    data = as.data.frame(setDT(lapply(1:cols, function(x) sample(10))))
    setnames(data, sample(names))
}
n = 10e3L
ll = vector("list", n)
for (i in 1:n) {
    .Call("Csetlistelt", ll, i, foo())
}

system.time(ans1 <- rbindlist(ll))
#  user  system elapsed 
# 1.226   0.070   1.296 

system.time(ans2 <- rbindlist(ll, use.names=TRUE))
#  user  system elapsed 
# 2.635   0.129   2.772 

system.time(ans3 <- do.call("rbind", ll))
#   user  system elapsed 
# 36.932   1.628  38.594 

system.time(ans4 <- bind_rows(ll))
#   user  system elapsed 
# 48.754   0.384  49.224 

identical(ans2, setDT(ans3)) 
# [1] TRUE
identical(ans2, setDT(ans4))
# [1] TRUE

A ligação de colunas como tal, sem verificar os nomes, levou apenas 1,3, enquanto que a verificação dos nomes das colunas e a ligação, levaram apenas 1,5 segundos a mais. Comparado à solução base, isso é 14x mais rápido e 18x mais rápido que dplyra versão.

Uma corrida
fonte