data.table vs dplyr: um pode fazer algo bem, o outro não pode ou faz mal?

760

Visão geral

Estou relativamente familiarizado data.table, não muito dplyr. Eu li algumas dplyrvinhetas e exemplos que apareceram no SO, e até agora minhas conclusões são as seguintes:

  1. data.tablee dplyrsão comparáveis ​​em velocidade, exceto quando existem muitos grupos (ou seja,> 10-100K) e em outras circunstâncias (consulte os benchmarks abaixo)
  2. dplyr tem sintaxe mais acessível
  3. dplyr abstrai (ou irá) possíveis interações com o banco de dados
  4. Existem algumas pequenas diferenças de funcionalidade (consulte "Exemplos / uso" abaixo)

Na minha opinião, 2. não tem muito peso porque estou bastante familiarizado com isso data.table, apesar de entender que, para os novos usuários de ambos, será um grande fator. Eu gostaria de evitar uma discussão sobre o que é mais intuitivo, pois isso é irrelevante para minha pergunta específica feita da perspectiva de alguém já familiarizado data.table. Também gostaria de evitar uma discussão sobre como "mais intuitivo" leva a análises mais rápidas (certamente verdade, mas, novamente, não é o que mais me interessa aqui).

Questão

O que eu quero saber é:

  1. Existem tarefas analíticas muito mais fáceis de codificar com um ou outro pacote para pessoas familiarizadas com os pacotes (ou seja, alguma combinação de pressionamentos de tecla exigida versus nível de esoterismo exigido, em que menos de cada uma é uma coisa boa).
  2. Existem tarefas analíticas executadas substancialmente (ou seja, mais de 2x) com mais eficiência em um pacote versus outro.

Uma recente pergunta do SO me fez pensar um pouco mais sobre isso, porque até aquele momento eu não pensava dplyrem oferecer muito além do que já posso fazer data.table. Aqui está a dplyrsolução (dados no final de Q):

dat %.%
  group_by(name, job) %.%
  filter(job != "Boss" | year == min(year)) %.%
  mutate(cumu_job2 = cumsum(job2))

O que foi muito melhor do que minha tentativa de hackear uma data.tablesolução. Dito isto, boas data.tablesoluções também são muito boas (obrigado Jean-Robert, Arun, e observe que eu favoreci a declaração única sobre a solução estritamente mais ideal):

setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

A sintaxe para o último pode parecer muito esotérica, mas na verdade é bastante direta se você está acostumado data.table(ou seja, não usa alguns dos truques mais esotéricos).

Idealmente, o que eu gostaria de ver são alguns bons exemplos, se a maneira dplyrou data.tablefor substancialmente mais concisa ou tiver um desempenho substancialmente melhor.

Exemplos

Uso
  • dplyrnão permite operações agrupadas que retornam número arbitrário de linhas ( da pergunta de eddi , observe: isso parece que será implementado no dplyr 0.5 , também, @beginneR mostra uma possível dosolução alternativa usando a resposta à pergunta de @ eddi).
  • data.tablesuporta junções rolantes (obrigado @dholstius), bem como junções de sobreposição
  • data.tableotimiza internamente expressões do formulário DT[col == value]ou DT[col %in% values]para velocidade por meio de indexação automática que usa pesquisa binária enquanto usa a mesma sintaxe R básica. Veja aqui para mais alguns detalhes e uma pequena referência.
  • dplyroferece versões de avaliação padrão de funções (por exemplo regroup, summarize_each_) que podem simplificar o uso programático de dplyr(note que o uso programático de data.tableé definitivamente possível, requer apenas uma reflexão cuidadosa, substituição / citação, etc., pelo menos que eu saiba)
Benchmarks

Dados

Este é o primeiro exemplo que mostrei na seção de perguntas.

dat <- structure(list(id = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L), name = c("Jane", "Jane", "Jane", "Jane", 
"Jane", "Jane", "Jane", "Jane", "Bob", "Bob", "Bob", "Bob", "Bob", 
"Bob", "Bob", "Bob"), year = c(1980L, 1981L, 1982L, 1983L, 1984L, 
1985L, 1986L, 1987L, 1985L, 1986L, 1987L, 1988L, 1989L, 1990L, 
1991L, 1992L), job = c("Manager", "Manager", "Manager", "Manager", 
"Manager", "Manager", "Boss", "Boss", "Manager", "Manager", "Manager", 
"Boss", "Boss", "Boss", "Boss", "Boss"), job2 = c(1L, 1L, 1L, 
1L, 1L, 1L, 0L, 0L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L)), .Names = c("id", 
"name", "year", "job", "job2"), class = "data.frame", row.names = c(NA, 
-16L))
BrodieG
fonte
9
A solução que é semelhante ao ler a dplyrum é:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)]
eddi
7
Para # 1 tanto dplyre data.tableequipes estão trabalhando em benchmarks, portanto, uma resposta vai estar lá em algum ponto. # 2 (sintaxe) imO é estritamente falso, mas isso claramente se aventura no território das opiniões, por isso estou votando para fechar também.
eddi
13
bem, mais uma vez a OMI, o conjunto de problemas que são mais limpa expressa em (d)plyrtem medida 0
eddi
28
@BrodieG A única coisa que realmente me incomoda sobre ambos dplyre plyrcom relação à sintaxe, e é basicamente a principal razão pela qual eu não gosto da sintaxe deles, é que eu tenho que aprender muitas funções extras (leia mais de 1) (com nomes que ainda não faz sentido para mim), lembre-se do que eles fazem, que argumentos eles tomam etc. Isso sempre foi uma grande desvantagem para mim da filosofia da plyr.
Eddi
43
@eddi [explícita] a única coisa que realmente me incomoda com a sintaxe data.table é que eu tenho que aprender como muitos argumentos de função interagem e o que significam atalhos enigmáticos (por exemplo .SD). [sério] Eu acho que essas são diferenças legítimas de design que irão agradar a pessoas diferentes
hadley

Respostas:

532

Precisamos de cobertura, pelo menos estes aspectos para fornecer uma abrangente resposta / comparação (em nenhuma ordem particular de importância): Speed, Memory usage, Syntaxe Features.

Minha intenção é cobrir cada um deles o mais claramente possível da perspectiva data.table.

Nota: a menos que seja mencionado explicitamente o contrário, referindo-se ao dplyr, nos referimos à interface data.frame do dplyr cujos internos estão em C ++ usando Rcpp.


A sintaxe data.table é consistente em sua forma - DT[i, j, by]. Para manter i, je byjuntos é por design. Ao manter as operações relacionadas juntas, ele permite otimizar facilmente as operações de velocidade e, mais importante, o uso da memória , além de fornecer alguns recursos poderosos , mantendo a consistência na sintaxe.

1. Speed

Muito poucas benchmarks (embora na maior parte em operações de agrupamento) foram adicionados à questão já mostrando data.table fica mais rápido do que dplyr como o número de grupos e / ou linhas para grupo de aumento, incluindo benchmarks por Matt no agrupamento de 10 milhões para 2 bilhões de linhas (100 GB de RAM) em 100 a 10 milhões de grupos e colunas de agrupamento variáveis, que também são comparadas pandas. Consulte também benchmarks atualizados , que incluem Sparke pydatatabletambém.

Nos benchmarks, seria ótimo cobrir também esses aspectos restantes:

  • Operações de agrupamento envolvendo um subconjunto de linhas - ou seja, DT[x > val, sum(y), by = z]operações de tipo.

  • Compare outras operações, como atualização e junções .

  • Também avalie a pegada de memória para cada operação, além do tempo de execução.

2. Uso de memória

  1. As operações que envolvem filter()ou slice()no dplyr podem ser ineficientes na memória (nos quadros de dados e tabelas de dados). Veja este post .

    Observe que o comentário de Hadley fala sobre velocidade (esse dplyr é muito rápido para ele), enquanto a principal preocupação aqui é a memória .

  2. No momento, a interface data.table permite modificar / atualizar colunas por referência (observe que não precisamos atribuir novamente o resultado a uma variável).

    # sub-assign by reference, updates 'y' in-place
    DT[x >= 1L, y := NA]

    Mas o dplyr nunca será atualizado por referência. O equivalente dplyr seria (observe que o resultado precisa ser redesignado):

    # copies the entire 'y' column
    ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))

    Uma preocupação para isso é a transparência referencial . Atualizar um objeto data.table por referência, especialmente dentro de uma função, nem sempre é desejável. Mas esse é um recurso incrivelmente útil: veja este e este post para casos interessantes. E nós queremos mantê-lo.

    Portanto, estamos trabalhando para exportar a shallow()função no data.table que fornecerá ao usuário as duas possibilidades . Por exemplo, se é desejável não modificar a tabela de dados de entrada em uma função, é possível:

    foo <- function(DT) {
        DT = shallow(DT)          ## shallow copy DT
        DT[, newcol := 1L]        ## does not affect the original DT 
        DT[x > 2L, newcol := 2L]  ## no need to copy (internally), as this column exists only in shallow copied DT
        DT[x > 2L, x := 3L]       ## have to copy (like base R / dplyr does always); otherwise original DT will 
                                  ## also get modified.
    }

    Ao não usar shallow(), a funcionalidade antiga é mantida:

    bar <- function(DT) {
        DT[, newcol := 1L]        ## old behaviour, original DT gets updated by reference
        DT[x > 2L, x := 3L]       ## old behaviour, update column x in original DT.
    }

    Ao criar uma cópia superficial usando shallow(), entendemos que você não deseja modificar o objeto original. Nós cuidamos de tudo internamente para garantir que, ao mesmo tempo, assegure-se de copiar as colunas que você modifica apenas quando for absolutamente necessário . Quando implementado, isso deve resolver completamente a questão da transparência referencial , fornecendo ao usuário as duas possibilidades.

    Além disso, uma vez shallow()exportada, a interface data.table do dplyr deve evitar quase todas as cópias. Portanto, aqueles que preferem a sintaxe do dplyr podem usá-lo com data.tables.

    Mas ainda faltam muitos recursos que o data.table fornece, incluindo a (sub) atribuição por referência.

  3. Agregue ao ingressar:

    Suponha que você tenha duas tabelas de dados da seguinte maneira:

    DT1 = data.table(x=c(1,1,1,1,2,2,2,2), y=c("a", "a", "b", "b"), z=1:8, key=c("x", "y"))
    #    x y z
    # 1: 1 a 1
    # 2: 1 a 2
    # 3: 1 b 3
    # 4: 1 b 4
    # 5: 2 a 5
    # 6: 2 a 6
    # 7: 2 b 7
    # 8: 2 b 8
    DT2 = data.table(x=1:2, y=c("a", "b"), mul=4:3, key=c("x", "y"))
    #    x y mul
    # 1: 1 a   4
    # 2: 2 b   3

    E você gostaria de obter sum(z) * mulpara cada linha DT2ao ingressar em colunas x,y. Nós podemos:

    • 1) agregar DT1para obter sum(z), 2) realizar uma junção e 3) multiplicar (ou)

      # data.table way
      DT1[, .(z = sum(z)), keyby = .(x,y)][DT2][, z := z*mul][]
      
      # dplyr equivalent
      DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
          right_join(DF2) %>% mutate(z = z * mul)
    • 2) faça tudo de uma só vez (usando o by = .EACHIrecurso):

      DT1[DT2, list(z=sum(z) * mul), by = .EACHI]

    Qual a vantagem?

    • Não precisamos alocar memória para o resultado intermediário.

    • Não precisamos agrupar / hash duas vezes (um para agregação e outro para associação).

    • E, mais importante, a operação que queríamos executar é clara olhando jem (2).

    Confira este post para uma explicação detalhada de by = .EACHI. Nenhum resultado intermediário é materializado, e a junção + agregada é realizada de uma só vez.

    Dê uma olhada nisso , isso e isso postagens para cenários de uso real.

    Em dplyrvocê teria que ingressar e agregar ou agregar primeiro e depois ingressar , nenhum dos quais são tão eficientes, em termos de memória (que por sua vez se traduz em velocidade).

  4. Atualização e junções:

    Considere o código data.table mostrado abaixo:

    DT1[DT2, col := i.mul]

    adiciona / atualiza DT1a coluna colcom mulde DT2nas linhas em que DT2a coluna principal corresponde DT1. Eu não acho que exista um equivalente exato dessa operação em dplyr, ou seja, sem evitar uma *_joinoperação, que teria que copiar todo oDT1 apenas para adicionar uma nova coluna a ela, o que é desnecessário.

    Verifique esta postagem para um cenário de uso real.

Para resumir, é importante perceber que todo tipo de otimização é importante. Como diria Grace Hopper , cuide dos nanossegundos !

3. Sintaxe

Vamos agora olhar para a sintaxe . Hadley comentou aqui :

As tabelas de dados são extremamente rápidas, mas acho que a concisão delas dificulta o aprendizado e o código que usa é mais difícil de ler depois que você a escreve ...

Acho essa observação inútil porque é muito subjetiva. O que talvez possamos tentar é contrastar a consistência na sintaxe . Compararemos a sintaxe data.table e dplyr lado a lado.

Trabalharemos com os dados fictícios mostrados abaixo:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. Operações básicas de agregação / atualização.

    # case (a)
    DT[, sum(y), by = z]                       ## data.table syntax
    DF %>% group_by(z) %>% summarise(sum(y)) ## dplyr syntax
    DT[, y := cumsum(y), by = z]
    ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
    
    # case (b)
    DT[x > 2, sum(y), by = z]
    DF %>% filter(x>2) %>% group_by(z) %>% summarise(sum(y))
    DT[x > 2, y := cumsum(y), by = z]
    ans <- DF %>% group_by(z) %>% mutate(y = replace(y, which(x > 2), cumsum(y)))
    
    # case (c)
    DT[, if(any(x > 5L)) y[1L]-y[2L] else y[2L], by = z]
    DF %>% group_by(z) %>% summarise(if (any(x > 5L)) y[1L] - y[2L] else y[2L])
    DT[, if(any(x > 5L)) y[1L] - y[2L], by = z]
    DF %>% group_by(z) %>% filter(any(x > 5L)) %>% summarise(y[1L] - y[2L])
    • A sintaxe data.table é compacta e o dplyr é bastante detalhado. As coisas são mais ou menos equivalentes no caso (a).

    • No caso (b), tivemos que usar filter()em dplyr enquanto resumindo . Mas durante a atualização , tivemos que mudar a lógica para dentro mutate(). No data.table, no entanto, expressamos as duas operações com a mesma lógica - operamos em linhas onde x > 2, mas no primeiro caso, obtém sum(y), enquanto no segundo caso atualizamos essas linhas ycom sua soma cumulativa.

      É isso que queremos dizer quando dizemos que o DT[i, j, by]formulário é consistente .

    • Da mesma forma, no caso (c), quando temos if-elsecondição, somos capazes de expressar a lógica "como está" tanto em data.table quanto em dplyr. No entanto, se quisermos retornar apenas as linhas em que a ifcondição satisfaz e pular de outra forma, não podemos usar summarise()diretamente (AFAICT). Temos que filter()primeiro e depois resumir, porque summarise()sempre espera um valor único .

      Embora retorne o mesmo resultado, usar filter() aqui torna a operação real menos óbvia.

      Pode muito bem ser possível usar também filter()no primeiro caso (não me parece óbvio), mas meu argumento é que não devemos.

  2. Agregação / atualização em várias colunas

    # case (a)
    DT[, lapply(.SD, sum), by = z]                     ## data.table syntax
    DF %>% group_by(z) %>% summarise_each(funs(sum)) ## dplyr syntax
    DT[, (cols) := lapply(.SD, sum), by = z]
    ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
    
    # case (b)
    DT[, c(lapply(.SD, sum), lapply(.SD, mean)), by = z]
    DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
    
    # case (c)
    DT[, c(.N, lapply(.SD, sum)), by = z]     
    DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
    • No caso (a), os códigos são mais ou menos equivalentes. data.table usa a função base familiar lapply(), ao passo que dplyrapresenta, *_each()juntamente com várias funções ,funs() .

    • O data.table's :=requer que os nomes das colunas sejam fornecidos, enquanto o dplyr os gera automaticamente.

    • No caso (b), a sintaxe do dplyr é relativamente direta. Melhorar agregações / atualizações em várias funções está na lista de data.table.

    • No caso (c), o dplyr retornaria n()tantas vezes quantas colunas, em vez de apenas uma vez. No data.table, tudo o que precisamos fazer é retornar uma lista j. Cada elemento da lista se tornará uma coluna no resultado. Assim, podemos usar, mais uma vez, a função básica familiar c()para concatenar .Na uma listque retorna a list.

    Nota: Mais uma vez, no data.table, tudo o que precisamos fazer é retornar uma lista j. Cada elemento da lista se tornará uma coluna no resultado. Você pode usar c(), as.list(), lapply(),list() funções etc ... de base para alcançar este objetivo, sem ter que aprender quaisquer novas funções.

    Você precisará aprender apenas as variáveis ​​especiais - .Ne .SDpelo menos. O equivalente em dplyr são n()e.

  3. Junções

    O dplyr fornece funções separadas para cada tipo de junção, em que data.table permite junções usando a mesma sintaxe DT[i, j, by](e com razão). Ele também fornece uma merge.data.table()função equivalente como alternativa.

    setkey(DT1, x, y)
    
    # 1. normal join
    DT1[DT2]            ## data.table syntax
    left_join(DT2, DT1) ## dplyr syntax
    
    # 2. select columns while join    
    DT1[DT2, .(z, i.mul)]
    left_join(select(DT2, x, y, mul), select(DT1, x, y, z))
    
    # 3. aggregate while join
    DT1[DT2, .(sum(z) * i.mul), by = .EACHI]
    DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
        inner_join(DF2) %>% mutate(z = z*mul) %>% select(-mul)
    
    # 4. update while join
    DT1[DT2, z := cumsum(z) * i.mul, by = .EACHI]
    ??
    
    # 5. rolling join
    DT1[DT2, roll = -Inf]
    ??
    
    # 6. other arguments to control output
    DT1[DT2, mult = "first"]
    ??
    • Alguns podem encontrar uma função separada para cada junção muito melhor (esquerda, direita, interna, anti, semi etc), enquanto outros podem gostar de data.table's DT[i, j, by], ou merge()que é semelhante à base R.

    • No entanto, o dplyr join faz exatamente isso. Nada mais. Nada menos.

    • O data.tables pode selecionar colunas durante a junção (2) e, no dplyr, você precisará select()primeiro nos data.frames antes de ingressar, como mostrado acima. Caso contrário, você usaria a junção com colunas desnecessárias apenas para removê-las posteriormente e isso é ineficiente.

    • O data.tables pode ser agregado ao ingressar (3) e também atualizar ao ingressar (4), usando o by = .EACHIrecurso Por que materializar todo o resultado da junção para adicionar / atualizar apenas algumas colunas?

    • data.table é capaz de rolar junções (5) - rolar para frente, LOCF , rolar para trás, NOCB , mais próximo .

    • data.table também possui mult =argumento que seleciona primeiro , último ou todos os resultados (6).

    • data.table tem allow.cartesian = TRUEargumento para se proteger de junções acidentais inválidas.

Mais uma vez, a sintaxe é consistente com DT[i, j, by]argumentos adicionais que permitem controlar ainda mais a saída.

  1. do()...

    O resumo do dplyr é projetado especialmente para funções que retornam um único valor. Se sua função retornar valores múltiplos / desiguais, você precisará recorrer do(). Você precisa saber de antemão sobre todas as suas funções retornam valor.

    DT[, list(x[1], y[1]), by = z]                 ## data.table syntax
    DF %>% group_by(z) %>% summarise(x[1], y[1]) ## dplyr syntax
    DT[, list(x[1:2], y[1]), by = z]
    DF %>% group_by(z) %>% do(data.frame(.$x[1:2], .$y[1]))
    
    DT[, quantile(x, 0.25), by = z]
    DF %>% group_by(z) %>% summarise(quantile(x, 0.25))
    DT[, quantile(x, c(0.25, 0.75)), by = z]
    DF %>% group_by(z) %>% do(data.frame(quantile(.$x, c(0.25, 0.75))))
    
    DT[, as.list(summary(x)), by = z]
    DF %>% group_by(z) %>% do(data.frame(as.list(summary(.$x))))
    • .SDO equivalente de .

    • No data.table, você pode lançar praticamente qualquer coisa j - a única coisa a lembrar é retornar uma lista para que cada elemento da lista seja convertido em uma coluna.

    • No dplyr, não pode fazer isso. É necessário recorrer, do()dependendo da certeza de que sua função retornaria sempre um único valor. E é bem lento.

Mais uma vez, a sintaxe do data.table é consistente com DT[i, j, by]. Podemos apenas continuar expressando expressões jsem precisar nos preocupar com essas coisas.

Dê uma olhada nesta questão SO e esta . Gostaria de saber se seria possível expressar a resposta como direta usando a sintaxe do dplyr ...

Para resumir, destaquei especialmente várias instâncias em que a sintaxe do dplyr é ineficiente, limitada ou falha em tornar as operações simples. Isso ocorre principalmente porque o data.table recebe um pouco de folga sobre a sintaxe "mais difícil de ler / aprender" (como a colada / vinculada acima). A maioria das postagens que cobrem o dplyr fala sobre as operações mais diretas. E isso é ótimo. Mas é importante perceber também sua sintaxe e limitações de recursos, e ainda estou para ver um post sobre isso.

O data.table também tem suas peculiaridades (algumas das quais eu indiquei que estamos tentando corrigir). Também estamos tentando melhorar as junções do data.table, como destaquei aqui .

Mas também se deve considerar o número de recursos que faltam ao dplyr em comparação com o data.table.

4. Recursos

Eu apontei a maioria dos recursos aqui e também neste post. Além do que, além do mais:

  • O leitor de arquivos fread -fast está disponível há muito tempo.

  • fwrite - um gravador de arquivos rápido em paralelo está agora disponível. Veja este post para obter uma explicação detalhada sobre a implementação e # 1664 para acompanhar os desenvolvimentos futuros.

  • Indexação automática - outro recurso útil para otimizar a sintaxe básica de R, internamente.

  • Agrupamento ad-hoc : dplyrclassifica automaticamente os resultados agrupando variáveis ​​durante summarise(), o que nem sempre é desejável.

  • Inúmeras vantagens nas junções data.table (para eficiência de velocidade / memória e sintaxe) mencionadas acima.

  • <=, <, >, >=Junções não equi : permite junções usando outros operadores, além de todas as outras vantagens das junções data.table.

  • As junções de intervalo sobrepostas foram implementadas recentemente em data.table. Confira esta postagem para uma visão geral com referências.

  • setorder() função no data.table que permite uma reordenação realmente rápida do data.tables por referência.

  • O dplyr fornece interface para bancos de dados usando a mesma sintaxe, que data.table não possui no momento.

  • data.tablefornece equivalentes mais rápidos de operações de conjunto (escrito por Jan Gorecki) - fsetdiff, fintersect, funione fsetequalcom adicional allargumento (como em SQL).

  • O data.table é carregado de forma limpa, sem avisos de mascaramento e possui um mecanismo descrito aqui para [.data.framecompatibilidade quando passado para qualquer pacote R. dplyr muda funções de base filter, lage [que pode causar problemas; por exemplo, aqui e aqui .


Finalmente:

  • Nos bancos de dados - não há razão para que o data.table não possa fornecer uma interface semelhante, mas isso não é uma prioridade agora. Pode ser difícil se os usuários gostarem muito desse recurso ... não tenho certeza.

  • No paralelismo - Tudo é difícil, até que alguém vá em frente e faça. É claro que será necessário esforço (sendo seguro para threads).

    • Atualmente, está sendo feito progresso (na versão v1.9.7) para paralelizar peças conhecidas que consomem tempo para obter ganhos de desempenho incrementais OpenMP.
Arun
fonte
9
@ bluefeet: Eu não acho que você tenha prestado um bom serviço ao resto de nós, movendo essa discussão para o bate-papo. Fiquei com a impressão de que Arun era um dos desenvolvedores e isso pode ter resultado em informações úteis.
IRTFM
2
Quando fui conversar usando o seu link, parecia que todo o material após o comentário, que começava com "Você deve usar um filtro", desapareceu. Estou perdendo algo sobre o mecanismo de bate-papo SO?
IRTFM
6
Eu acho que em todo lugar onde você está usando atribuição por referência ( :=), dplyrequivalente devem ser também utilizando <-como em DF <- DF %>% mutate...vez de apenasDF %>% mutate...
David Arenburg
4
Em relação à sintaxe. Acredito que dplyrpode ser mais fácil para usuários que costumavam fazer plyrsintaxe, mas data.tablepode ser mais fácil para usuários que costumavam consultar linguagens como a sintaxe SQLe a álgebra relacional por trás dela, que trata da transformação de dados tabulares. @ Arun, você deve observar que os operadores de conjunto são muito fáceis de executar, envolvendo a data.tablefunção e, é claro, traz uma aceleração significativa.
Jangorecki
9
Eu li este post tantas vezes, e isso me ajudou muito a entender o data.table e a usá-lo melhor. Eu, na maioria dos casos, prefiro data.table em vez de dplyr ou pandas ou PL / pgSQL. No entanto, eu não conseguia parar de pensar em como expressá-lo. A sintaxe não é fácil, clara ou detalhada. De fato, mesmo depois de usar muito o data.table, muitas vezes ainda luto para compreender meu próprio código, escrevi literalmente há uma semana. Este é um exemplo de vida de um idioma somente gravação. pt.wikipedia.org/wiki/Write-only_language Então, esperemos que um dia possamos usar o dplyr no data.table.
Ufos
385

Aqui está minha tentativa de uma resposta abrangente da perspectiva dplyr, seguindo o amplo esboço da resposta de Arun (mas um pouco reorganizada com base em diferentes prioridades).

Sintaxe

Há alguma subjetividade na sintaxe, mas eu mantenho minha afirmação de que a concisão de data.table torna mais difícil de aprender e mais difícil de ler. Isso ocorre em parte porque o dplyr está resolvendo um problema muito mais fácil!

Uma coisa realmente importante que o dplyr faz por você é que ele restringe suas opções. Afirmo que a maioria dos problemas de tabela única pode ser resolvida com apenas cinco verbos principais, filtrar, selecionar, alterar, organizar e resumir, juntamente com um advérbio "por grupo". Essa restrição é uma grande ajuda quando você está aprendendo a manipulação de dados, porque ajuda a ordenar sua opinião sobre o problema. No dplyr, cada um desses verbos é mapeado para uma única função. Cada função executa um trabalho e é fácil de entender isoladamente.

Você cria complexidade canalizando essas operações simples %>%. Aqui está um exemplo de uma das postagens às quais Arun vinculou :

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

Mesmo se você nunca viu o dplyr antes (ou mesmo o R!), Ainda pode entender o que está acontecendo, porque as funções são todos verbos em inglês. A desvantagem dos verbos em inglês é que eles exigem mais digitação do que [, mas acho que isso pode ser amplamente mitigado por um preenchimento automático melhor.

Aqui está o código data.table equivalente:

diamondsDT <- data.table(diamonds)
diamondsDT[
  cut != "Fair", 
  .(AvgPrice = mean(price),
    MedianPrice = as.numeric(median(price)),
    Count = .N
  ), 
  by = cut
][ 
  order(-Count) 
]

É mais difícil seguir esse código, a menos que você já esteja familiarizado com o data.table. (Eu também não conseguia descobrir como recuar o repetido [ de uma maneira que pareça boa para os meus olhos). Pessoalmente, quando olho para o código que escrevi há 6 meses, é como olhar para um código escrito por um estranho, então passei a preferir um código direto, se bem detalhado.

Dois outros fatores menores que eu acho que diminuem ligeiramente a legibilidade:

  • Como quase todas as operações da tabela de dados usam, [você precisa de um contexto adicional para descobrir o que está acontecendo. Por exemplo, x[y] juntar duas tabelas de dados ou extrair colunas de um quadro de dados? Esse é apenas um pequeno problema, porque em códigos bem escritos os nomes das variáveis ​​devem sugerir o que está acontecendo.

  • Eu gosto que group_by()é uma operação separada no dplyr. Isso muda fundamentalmente a computação, então eu acho que deveria ser óbvio ao analisar o código, e é mais fácil identificar do group_by()que o byargumento [.data.table.

Também gosto que o cano não se limite apenas a um pacote. Você pode começar organizando seus dados com o tidyr e finalizando com um gráfico no ggvis . E você não está limitado aos pacotes que eu escrevo - qualquer um pode escrever uma função que faz parte integrante de um tubo de manipulação de dados. Na verdade, prefiro o código data.table reescrito com %>%:

diamonds %>% 
  data.table() %>% 
  .[cut != "Fair", 
    .(AvgPrice = mean(price),
      MedianPrice = as.numeric(median(price)),
      Count = .N
    ), 
    by = cut
  ] %>% 
  .[order(-Count)]

E a ideia de canalizar %>%não se limita apenas a quadros de dados e é facilmente generalizada para outros contextos: gráficos interativos da Web , raspagem da Web , dicas , contratos de tempo de execução , ...)

Memória e desempenho

Eu os juntei porque, para mim, eles não são tão importantes. A maioria dos usuários de R trabalha com menos de 1 milhão de linhas de dados, e o dplyr é suficientemente rápido para esse tamanho de dados que você não conhece o tempo de processamento. Otimizamos o dplyr para expressividade em dados médios; fique à vontade para usar data.table para obter velocidade bruta em dados maiores.

A flexibilidade do dplyr também significa que você pode ajustar facilmente as características de desempenho usando a mesma sintaxe. Se o desempenho do dplyr com o back-end do quadro de dados não for bom o suficiente para você, você poderá usar o back-end data.table (embora com um conjunto de funcionalidades um tanto restrito). Se os dados com os quais você está trabalhando não cabem na memória, é possível usar um back-end do banco de dados.

Tudo isso dito, o desempenho do dplyr melhorará a longo prazo. Definitivamente, implementaremos algumas das grandes idéias de data.table, como ordenação por raiz e uso do mesmo índice para junções e filtros. Também estamos trabalhando na paralelização para que possamos tirar proveito de vários núcleos.

Recursos

Algumas coisas que planejamos trabalhar em 2015:

  • o readrpacote, para facilitar a retirada de arquivos do disco e da memória, análoga a fread().

  • Junções mais flexíveis, incluindo suporte para junções não equi.

  • Agrupamento mais flexível, como amostras de bootstrap, rollups e muito mais

Também estou investindo tempo na melhoria dos conectores de banco de dados de R , na capacidade de conversar com APIs da Web e em facilitar a raspagem de páginas html .

Hadley
fonte
27
Apenas uma observação, concordo com muitos de seus argumentos (embora eu prefira a data.tablesintaxe), mas você pode usá-lo facilmente %>%para canalizar data.tableoperações se não gostar de [estilo. %>%não é específico dplyr, mas vem de um pacote separado (do qual você também é co-autor), então não tenho certeza de que entendi o que você está tentando dizer na maioria dos parágrafos da sintaxe .
David Arenburg
11
@DavidArenburg good point. Eu re-escrito sintaxe para espero fazer mais claro o que meus pontos principais são, e destaque que você pode usar %>%com data.table
Hadley
5
Obrigado Hadley, esta é uma perspectiva útil. Recuar normalmente eu faço DT[\n\texpression\n][\texpression\n]( gist ) que realmente funciona muito bem. Estou mantendo a resposta de Arun como a resposta, pois ele responde mais diretamente às minhas perguntas específicas, que não são muito sobre a acessibilidade da sintaxe, mas acho que é uma boa resposta para as pessoas que tentam ter uma idéia geral das diferenças / semelhanças entre dplyre data.table.
BrodieG
33
Por que trabalhar com rapidez quando já existe fread()? Não seria melhor gastar tempo melhorando o fread () ou trabalhando em outras coisas (subdesenvolvidas)?
EDi
10
A API do data.tablebaseia-se em um abuso maciço da []notação. Essa é sua maior força e sua maior fraqueza.
Paul
65

Em resposta direta ao título da pergunta ...

dplyr definitivamente faz coisas que data.tablenão podem.

O seu ponto # 3

abstratos de dplyr (ou vontade) interações potenciais do DB

é uma resposta direta à sua própria pergunta, mas não é elevada a um nível alto o suficiente. dplyré realmente um front-end extensível para vários mecanismos de armazenamento de dados, e data.tabletambém uma extensão para um único.

Veja dplyrcomo uma interface independente de back-end, com todos os destinos usando a mesma gramática, onde você pode estender os destinos e manipuladores à vontade. data.tableé, do dplyrponto de vista, um desses alvos.

Você (nunca) verá (espero) um dia que data.tabletente traduzir suas consultas para criar instruções SQL que operam com armazenamentos de dados em disco ou em rede.

dplyrpossivelmente pode fazer coisas data.tableque não farão ou não.

Com base no design de trabalhar na memória, data.tablepode ser muito mais difícil se estender para o processamento paralelo de consultas do que dplyr.


Em resposta às perguntas do corpo ...

Uso

Existem tarefas analíticas muito mais fáceis de codificar com um ou outro pacote para pessoas familiarizadas com os pacotes (ou seja, alguma combinação de pressionamentos de tecla exigida versus nível de esoterismo exigido, onde menos de cada uma é uma coisa boa).

Isso pode parecer um truque, mas a resposta real é não. As pessoas familiarizadas com as ferramentas parecem usar a mais familiar ou a mais adequada para o trabalho em questão. Com isso dito, às vezes você deseja apresentar uma legibilidade específica, às vezes um nível de desempenho, e quando você precisa de um nível alto o suficiente de ambos, você pode precisar apenas de outra ferramenta para acompanhar o que você já tem para tornar abstrações mais claras .

atuação

Existem tarefas analíticas executadas substancialmente (ou seja, mais de 2x) com mais eficiência em um pacote versus outro.

Novamente não. data.tabledestaca-se por ser eficiente em tudo o que faz, onde dplyrfica o ônus de ser limitado em alguns aspectos ao armazenamento de dados subjacente e aos manipuladores registrados.

Isso significa que, quando você se deparar com um problema de desempenho, data.tablepode ter certeza de que ele está na sua função de consulta e se é realmente um gargalo data.table, você ganhou a alegria de arquivar um relatório. Isso também é verdade quando dplyrestá sendo usado data.tablecomo back-end; você pode ver algumas despesas gerais, dplyrmas é provável que seja sua consulta.

Quando dplyrhá problemas de desempenho com back-ends, é possível contorná-los registrando uma função para avaliação híbrida ou (no caso de bancos de dados) manipulando a consulta gerada antes da execução.

Veja também a resposta aceita para quando o plyr é melhor que o data.table?

Thell
fonte
3
Não é possível dplyr quebrar uma data.table com tbl_dt? Por que não apenas obter o melhor dos dois mundos?
Aaa90210
22
Você esquece de mencionar a afirmação inversa "data.table definitivamente faz coisas que o dplyr não pode", o que também é verdadeiro.
Jangorecki
25
A resposta de Arun explica isso bem. O mais importante (em termos de desempenho) seria o medo, atualização por referência, junções rolantes, junções sobrepostas. Eu acredito que não há nenhum pacote (não apenas o dplyr) que possa competir com esses recursos. Um bom exemplo pode ser o último slide desta apresentação.
Jangorecki
15
Totalmente, data.table é por que eu ainda uso R. Caso contrário, eu usaria pandas. É ainda melhor / mais rápido que os pandas.
marbel
8
Eu gosto do data.table devido à sua simplicidade e semelhança com a estrutura da sintaxe SQL. Meu trabalho envolve fazer análises e gráficos ad hoc de dados muito intensos todos os dias para modelagem estatística, e eu realmente preciso de uma ferramenta simples o suficiente para fazer coisas complicadas. Agora, posso reduzir meu kit de ferramentas para apenas data.table para dados e estrutura para gráfico no meu trabalho diário. Dê um exemplo: eu posso até fazer operações como esta: $ DT [group == 1, y_hat: = prever (ajuste1, dados = .SD),] $, que é realmente legal e considero uma grande vantagem do SQL em ambiente R clássico.
Xappppp