dplyr em data.table, estou realmente usando data.table?

89

Se eu usar a sintaxe dplyr no topo de uma tabela de dados , obtenho todos os benefícios de velocidade da tabela de dados enquanto ainda uso a sintaxe de dplyr? Em outras palavras, eu uso indevidamente a tabela de dados se a consultar com a sintaxe dplyr? Ou preciso usar sintaxe de tabela de dados pura para aproveitar todo o seu poder.

Agradecemos antecipadamente por qualquer conselho. Exemplo de código:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count))

Resultados:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

Aqui está a equivalência de dados que eu vim. Não tenho certeza se está em conformidade com as boas práticas da DT. Mas eu me pergunto se o código é realmente mais eficiente do que a sintaxe dplyr nos bastidores:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]
Polimerase
fonte
7
Por que você não usaria a sintaxe da tabela de dados? É elegante e eficiente também. A pergunta não tem resposta, pois é muito ampla. Sim, existem dplyrmétodos para tabelas de dados, mas a tabela de dados também tem seus próprios métodos comparáveis
Rich Scriven
7
Posso usar sintaxe de tabela de dados ou curso. Mas de alguma forma, acho a sintaxe dplyr mais elegante. Independentemente da minha preferência pela sintaxe. O que realmente quero saber é: preciso usar sintaxe de tabela de dados pura para obter 100% dos benefícios do poder de tabela de dados.
Polimerase
3
Para um benchmark recente onde dplyré usado em data.frames e correspondentes data.tables, veja aqui (e referências nele).
Henrik
2
@Polymerase - Eu acho que a resposta a essa pergunta é definitivamente "Sim"
Rich Scriven
1
@Henrik: Percebi mais tarde que havia interpretado mal aquela página porque eles exibiam apenas o código para a construção do dataframe, mas não o código que usaram para a construção do data.table. Quando percebi, excluí meu comentário (esperando que você não o tivesse visto).
IRTFM

Respostas:

75

Não há uma resposta direta / simples porque as filosofias de ambos os pacotes diferem em certos aspectos. Portanto, alguns compromissos são inevitáveis. Aqui estão algumas das preocupações que você pode precisar abordar / considerar.

Operações envolvendo i(== filter()e slice()em dplyr)

Suponha DTcom, digamos, 10 colunas. Considere estas expressões data.table:

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1) fornece o número de linhas em DTque coluna a > 1. (2) retorna mean(b)agrupado por c,dpara a mesma expressão em i(1).

dplyrExpressões comumente usadas seriam:

DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

Claramente, os códigos data.table são mais curtos. Além disso, eles também são mais eficientes em termos de memória 1 . Por quê? Porque em (3) e (4), filter()retorna linhas para todas as 10 colunas primeiro, quando em (3) precisamos apenas do número de linhas, e em (4) precisamos apenas de colunas b, c, dpara as operações sucessivas. Para superar isso, temos que select()colunas a priori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

É essencial destacar uma grande diferença filosófica entre os dois pacotes:

  • Em data.table, gostamos de manter essas operações relacionadas juntas, e isso permite olhar para j-expression(da mesma chamada de função) e perceber que não há necessidade de quaisquer colunas em (1). A expressão em ié calculada e .Né apenas a soma daquele vetor lógico que fornece o número de linhas; todo o subconjunto nunca é realizado. Em (2), apenas as colunas b,c,dsão materializadas no subconjunto, as demais colunas são ignoradas.

  • Mas em dplyr, a filosofia é ter uma função que faz exatamente uma coisa bem . Não há (pelo menos atualmente) nenhuma maneira de dizer se a operação posterior filter()precisa de todas as colunas que filtramos. Você precisará pensar no futuro se quiser realizar essas tarefas com eficiência. Pessoalmente, acho isso contra-intutivo neste caso.

Observe que em (5) e (6), ainda subconjuntos de colunas aque não exigimos. Mas não tenho certeza de como evitar isso. Se a filter()função tivesse um argumento para selecionar as colunas a serem retornadas, poderíamos evitar esse problema, mas a função não fará apenas uma tarefa (que também é uma escolha de design do dplyr).

Subatribuir por referência

dplyr nunca será atualizado por referência. Esta é outra grande diferença (filosófica) entre os dois pacotes.

Por exemplo, em data.table, você pode fazer:

DT[a %in% some_vals, a := NA]

que atualiza a coluna a por referência apenas nas linhas que satisfazem a condição. No momento, o dplyr deep copia todo o data.table internamente para adicionar uma nova coluna. @BrodieG já mencionou isso em sua resposta.

Mas a cópia profunda pode ser substituída por uma cópia superficial quando FR # 617 for implementado. Também relevante: dplyr: FR # 614 . Observe que, ainda assim, a coluna que você modificar sempre será copiada (portanto, um pouco mais lenta / menos eficiente em termos de memória). Não haverá como atualizar as colunas por referência.

Outras funcionalidades

  • Em data.table, você pode agregar durante a junção, e isso é mais simples de entender e é eficiente em termos de memória, pois o resultado da junção intermediária nunca é materializado. Verifique esta postagem para um exemplo. Você não pode (no momento?) Fazer isso usando a sintaxe data.table / data.frame do dplyr.

  • O recurso rolling joins de data.table também não é compatível com a sintaxe do dplyr.

  • Recentemente, implementamos junções de sobreposição em data.table para juntar em intervalos de intervalo ( aqui está um exemplo ), que é uma função separada foverlaps()no momento e, portanto, pode ser usada com os operadores de pipe (magrittr / pipeR? - nunca tentei sozinho).

    Mas, no final das contas, nosso objetivo é integrá-lo ao [.data.tablepara que possamos colher os outros recursos como agrupamento, agregação durante a união etc., que terão as mesmas limitações descritas acima.

  • Desde 1.9.4, data.table implementa indexação automática usando chaves secundárias para subconjuntos baseados em busca binária rápida na sintaxe R regular. Ex: DT[x == 1]e DT[x %in% some_vals]criará automaticamente um índice na primeira execução, que será usado em subconjuntos sucessivos da mesma coluna para subconjunto rápido usando pesquisa binária. Esse recurso continuará a evoluir. Verifique esta essência para uma breve visão geral desse recurso.

    Da maneira como filter()é implementado para data.tables, não tira proveito deste recurso.

  • Um recurso do dplyr é que ele também fornece interface para bancos de dados usando a mesma sintaxe, o que data.table não faz no momento.

Portanto, você terá que pesar esses (e provavelmente outros pontos) e decidir com base se essas compensações são aceitáveis ​​para você.

HTH


(1) Observe que ter memória eficiente impacta diretamente a velocidade (especialmente à medida que os dados ficam maiores), já que o gargalo na maioria dos casos é mover os dados da memória principal para o cache (e usar os dados no cache tanto quanto possível - reduzir perdas de cache - de modo a reduzir o acesso à memória principal). Não vou entrar em detalhes aqui.

Uma corrida
fonte
4
Absolutamente brilhante. Obrigado por isso
David Arenburg
6
Essa é uma boa resposta, mas seria possível (se não provável) para dplyr implementar um filter()plus eficiente summarise()usando a mesma abordagem que dplyr usa para SQL - isto é, construir uma expressão e então executar apenas uma vez sob demanda. É improvável que isso seja implementado em um futuro próximo porque dplyr é rápido o suficiente para mim e implementar um planejador / otimizador de consulta é relativamente difícil.
hadley
24

Apenas tente.

library(rbenchmark)
library(dplyr)
library(data.table)

benchmark(
dplyr = diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair", 
                        list(AvgPrice = mean(price),
                             MedianPrice = as.numeric(median(price)),
                             Count = .N), by = cut][order(-Count)])[1:4]

Sobre este problema, parece que data.table é 2,4x mais rápido que dplyr usando data.table:

        test replications elapsed relative
2 data.table          100    2.39    1.000
1      dplyr          100    5.77    2.414

Revisado com base no comentário da Polymerase.

G. Grothendieck
fonte
2
Usando o microbenchmarkpacote, descobri que executar o dplyrcódigo do OP na versão original (quadro de dados) diamondslevou um tempo médio de 0,012 segundos, enquanto levou um tempo médio de 0,024 segundos após a conversão diamondspara uma tabela de dados. A execução do data.tablecódigo de G. Grothendieck levou 0,013 segundos. Pelo menos no meu sistema, parece dplyre data.tabletem quase o mesmo desempenho. Mas por que dplyrseria mais lento quando o quadro de dados é convertido pela primeira vez em uma tabela de dados?
eipi10
Caro G. Grothendieck, isso é maravilhoso. Obrigado por me mostrar este utilitário de referência. BTW você esqueceu [ordem (-Contagem)] na versão da tabela de dados para fazer a equivalência do arranjo de dplyr (desc (Contagem)). Depois de adicionar isso, a tabela de dados é ainda mais rápida em cerca de x1,8 (em vez de 2,9).
Polimerase
@ eipi10 você pode executar novamente sua bancada com a versão da tabela de dados aqui (classificação adicionada por desc Count na última etapa): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median (preço)), Contagem = .N), por = corte] [pedido (-Contagem)]
Polimerase
Ainda 0,013 segundos. A operação de pedido quase não leva tempo porque está apenas reordenando a mesa final, que tem apenas quatro linhas.
eipi10
1
Há alguma sobrecarga fixa para a conversão da sintaxe dplyr para a sintaxe da tabela de dados, então pode valer a pena tentar vários tamanhos de problema. Também posso não ter implementado o código de tabela de dados mais eficiente em dplyr; patches são sempre bem
hadley
21

Para responder às suas perguntas:

  • Sim voce esta usando data.table
  • Mas não tão eficientemente como faria com data.tablesintaxe pura

Em muitos casos, isso será um meio-termo aceitável para aqueles que desejam a dplyrsintaxe, embora possivelmente seja mais lento do que dplyrcom quadros de dados simples.

Um grande fator parece ser que dplyrcopiará o data.tablepor padrão ao agrupar. Considere (usando o microbenchmark):

Unit: microseconds
                                                               expr       min         lq    median
                                diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                          diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                               diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

A filtragem tem velocidade comparável, mas o agrupamento não. Eu acredito que o culpado é esta linha em dplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}

onde o copypadrão é TRUE(e não pode ser facilmente alterado para FALSE que eu posso ver). Isso provavelmente não é responsável por 100% da diferença, mas a sobrecarga geral sozinha em algo do tamanho diamondsmais provável não é a diferença total.

A questão é que, para ter uma gramática consistente, dplyrfaça o agrupamento em duas etapas. Primeiro, ele define as chaves em uma cópia da tabela de dados original que correspondem aos grupos e, somente depois, agrupa. data.tableapenas aloca memória para o maior grupo de resultados, que neste caso é apenas uma linha, de modo que faz uma grande diferença em quanta memória precisa ser alocada.

Para sua informação, se alguém se importa, eu encontrei isso usando treeprof(install_github("brodieg/treeprof") ), um visualizador de árvore experimental (e ainda muito alfa) para a Rprofsaída:

insira a descrição da imagem aqui

Observe que o acima só funciona atualmente em macs AFAIK. Além disso, infelizmente, Rprofregistra chamadas do tipopackagename::funname como anônimas, portanto, podem ser quaisquer e todas as datatable::chamadas internas grouped_dtque são responsáveis, mas, a partir de um teste rápido, parecia que datatable::copyé a maior.

Dito isso, você pode ver rapidamente como não há tanta sobrecarga em torno da [.data.tablechamada, mas também há uma ramificação completamente separada para o agrupamento.


EDITAR : para confirmar a cópia:

> tracemem(diamondsDT)
[1] "<0x000000002747e348>"    
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
Source: local data table [5 x 2]

        cut AvgPrice
1      Fair 4358.758
2      Good 3928.864
3 Very Good 3981.760
4   Premium 4584.258
5     Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
         cut       V1
1:     Ideal 3457.542
2:   Premium 4584.258
3:      Good 3928.864
4: Very Good 3981.760
5:      Fair 4358.758
> untracemem(diamondsDT)
BrodieG
fonte
Isso é incrível, obrigado. Isso significa que dplyr :: group_by () dobrará o requisito de memória (em comparação com a sintaxe de tabela de dados pura) por causa da etapa de cópia de dados interna? Isso significa que se o tamanho do meu objeto de tabela de dados é 1 GB e eu uso a sintaxe em cadeia dplyr semelhante à do post original. Vou precisar de pelo menos 2 GB de memória livre para obter os resultados?
Polimerase
2
Eu sinto que consertei isso na versão dev?
hadley
@hadley, estava trabalhando a partir da versão CRAN. Olhando para dev, parece que você resolveu parcialmente o problema, mas a cópia real permanece (não testei, apenas olhando para as linhas c (20, 30:32) em R / grouped-dt.r. Provavelmente é mais rápido agora, mas Aposto que o passo lento é a cópia.
BrodieG
3
Também estou esperando por uma função de cópia superficial em data.table; até então acho melhor estar seguro do que rápido.
hadley
2

Você pode usar dtplyr agora, que faz parte do tidyverse . Ele permite que você use instruções no estilo dplyr como de costume, mas utiliza avaliação preguiçosa e converte suas instruções em código data.table nos bastidores. A sobrecarga na tradução é mínima, mas você obtém todos, se não, a maioria dos benefícios de data.table. Mais detalhes no repositório git oficial aqui e na página tidyverse .

Leite Preto
fonte