O que não posso fazer com o dtplyr que consigo no data.table


9

Devo investir meu esforço de aprendizagem para os dados barafustante em R, especificamente entre dplyr, dtplyre data.table?

  • Eu uso dplyrprincipalmente, mas quando os dados forem grandes demais para isso, usarei data.table, o que é uma ocorrência rara. Então agora que a dtplyrv1.0 saiu como uma interface data.table, parece que eu nunca mais preciso me preocupar em usar a data.tableinterface novamente.

  • Então, quais são os recursos ou aspectos mais úteis data.tableque não podem ser usados dtplyrno momento e que provavelmente nunca serão feitos dtplyr?

  • Em seu rosto, dplyrcom os benefícios de data.tablefaz parecer que dtplyrvai ultrapassar dplyr. Haverá algum motivo para usar dplyruma vez que dtplyresteja totalmente maduro?

Nota: Não estou perguntando sobre dplyrvs data.table(como em data.table vs dplyr: um pode fazer algo bem que o outro não pode ou faz mal? ), Mas, como um é preferido sobre o outro para um problema específico, por que não ' Não dtplyrseja a ferramenta a ser usada.


11
Existe algo que você pode fazer bem em dplyrque você não pode fazer bem data.table? Caso contrário, mudar data.tablepara será melhor que dtplyr.
26419 sindri_baldur

2
No dtplyrleia - me, 'Algumas data.tableexpressões não têm dplyrequivalente direto . Por exemplo, não há como expressar junções cruzadas ou rotativas dplyr. e 'Para corresponder à dplyrsemântica, mutate() não é modificado no local por padrão. Isso significa que a maioria das expressões envolvidas mutate()deve fazer uma cópia que não seria necessária se você estivesse usando data.tablediretamente. ' Existe uma maneira de contornar essa segunda parte, mas considerando a frequência com que mutateé usada, essa é uma desvantagem bastante grande para os meus olhos.
ClancyStats

Respostas:


15

Vou tentar dar meus melhores guias, mas não é fácil, porque é preciso estar familiarizado com todos os dados (data.table), {dplyr}, {dtplyr} e também com a base R. Eu uso o {data.table} e muitos pacotes do {tidy-world} (exceto o {dplyr}). Ame os dois, embora prefira sintaxe de data.table a dplyr. Espero que todos os pacotes do mundo organizado usem {dtplyr} ou {data.table} como back-end sempre que necessário.

Como em qualquer outra tradução (pense em dplyr-to-sparkly / SQL), há coisas que podem ou não podem ser traduzidas, pelo menos por enquanto. Quero dizer, talvez um dia {dtplyr} possa torná-lo 100% traduzido, quem sabe. A lista abaixo não é exaustiva nem 100% correta, pois tentarei responder da melhor maneira possível com base no meu conhecimento sobre tópicos / pacotes / questões / etc.

É importante ressaltar que, para as respostas que não são totalmente precisas, espero que ele ofereça alguns guias sobre quais aspectos do {data.table} você deve prestar atenção e compare-o ao {dtplyr} e descubra as respostas por si mesmo. Não tome essas respostas como garantidas.

E espero que este post possa ser usado como um dos recursos para todos os usuários / criadores de {dplyr}, {data.table} ou {dtplyr} para discussões e colaborações e para tornar o #RStats ainda melhor.

{data.table} não é usado apenas para operações rápidas e com eficiência de memória. Muitas pessoas, inclusive eu, preferem a sintaxe elegante de {data.table}. Ele também inclui outras operações rápidas, como funções de séries temporais, como família de rolamento (ou seja frollapply), escrita em C. Ele pode ser usado com qualquer função, incluindo o tidyverse. Eu uso muito {data.table} + {purrr}!

Complexidade das operações

Isso pode ser facilmente traduzido

library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)

# dplyr 
diamonds %>%
  filter(cut != "Fair") %>% 
  group_by(cut) %>% 
  summarize(
    avg_price    = mean(price),
    median_price = as.numeric(median(price)),
    count        = n()
  ) %>%
  arrange(desc(count))

# data.table
data [
  ][cut != 'Fair', by = cut, .(
      avg_price    = mean(price),
      median_price = as.numeric(median(price)),
      count        = .N
    )
  ][order( - count)]

{data.table} é muito rápido e economiza memória porque (quase?) tudo é construído desde o início, a partir de C, com os principais conceitos de atualização por referência , chave (pense em SQL) e sua otimização implacável em todos os lugares do pacote (ou seja fifelse, fread/freadordem de classificação radix adotada pela base R), ao mesmo tempo em que a sintaxe é concisa e consistente, é por isso que acho elegante.

Da Introdução à tabela de dados, as principais operações de manipulação de dados, como subconjunto, grupo, atualização, associação, etc., são mantidas juntas por

  • sintaxe concisa e consistente ...

  • executando análise fluidamente sem a carga cognitiva de ter que mapear cada operação ...

  • otimizando automaticamente as operações internamente e com muita eficiência, conhecendo com precisão os dados necessários para cada operação, resultando em códigos muito rápidos e com eficiência de memória

O último ponto, como exemplo,

# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
        .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
  • Primeiro, subconjuntamos em i para encontrar índices de linhas correspondentes em que o aeroporto de origem é igual a "JFK" e o mês é igual a 6L. Ainda não subconjuntamos toda a tabela de dados correspondente a essas linhas.

  • Agora, olhamos para j e descobrimos que ele usa apenas duas colunas. E o que precisamos fazer é calcular a média (). Portanto, subconjuntamos apenas as colunas correspondentes às linhas correspondentes e calculamos sua média ().

Como os três principais componentes da consulta (i, j e by) estão juntos dentro [...] , o data.table pode ver todos os três e otimizar completamente a consulta antes da avaliação, não cada um separadamente . Portanto, somos capazes de evitar todo o subconjunto (ou seja, subconjuntos das colunas além de arr_delay e dep_delay), para velocidade e eficiência de memória.

Dado que, para colher os benefícios de {data.table}, a tradução de {dtplr} deve estar correta nesse aspecto. Quanto mais complexas as operações, mais difíceis as traduções. Para operações simples como acima, certamente pode ser facilmente traduzido. Para os complexos, ou aqueles não suportados pelo {dtplyr}, você deve descobrir a si mesmo como mencionado acima, é preciso comparar a sintaxe e o benchmark traduzidos e ser pacotes relacionados familiares.

Para operações complexas ou não suportadas, posso fornecer alguns exemplos abaixo. Mais uma vez, estou apenas tentando o meu melhor. Seja gentil comigo.

Atualização por referência

Não vou entrar na introdução / detalhes, mas aqui estão alguns links

Recurso principal: Semântica de Referência

Mais detalhes: Entendendo exatamente quando uma data.table é uma referência a (contra uma cópia de) outra data.table

Atualizar por referência , na minha opinião, o recurso mais importante do {data.table} e é isso que o torna tão rápido e eficiente em termos de memória. dplyr::mutatenão suporta por padrão. Como não estou familiarizado com o {dtplyr}, não tenho certeza de quanto e quais operações podem ou não ser suportadas pelo {dtplyr}. Como mencionado acima, também depende da complexidade das operações, que por sua vez afetam as traduções.

Existem duas maneiras de usar a atualização por referência em {data.table}

  • operador de atribuição de {data.table} :=

  • set-family: set, setnames, setcolorder, setkey, setDT, fsetdiff, e muitos mais

:=é mais comumente usado em comparação com set. Para conjuntos de dados complexos e grandes, a atualização por referência é a chave para obter velocidade máxima e eficiência de memória. A maneira mais fácil de pensar (não 100% precisa, pois os detalhes são muito mais complicados do que isso, pois envolve cópia impressa / superficial e muitos outros fatores), digamos que você esteja lidando com grandes conjuntos de dados de 10 GB, com 10 colunas e 1 GB cada . Para manipular uma coluna, você precisa lidar apenas com 1 GB.

O ponto principal é que, com a atualização por referência , você só precisa lidar com os dados necessários. É por isso que ao usar {data.table}, especialmente ao lidar com grandes conjuntos de dados, usamos atualização por referência o tempo todo, sempre que possível. Por exemplo, manipulando grandes conjuntos de dados de modelagem

# Manipulating list columns

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)

# data.table
dt [,
    by = Species, .(data   = .( .SD )) ][,  # `.(` shorthand for `list`
    model   := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
    summary := map(model, summary) ][,
    plot    := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                           geom_point())]

# dplyr
df %>% 
  group_by(Species) %>% 
  nest() %>% 
  mutate(
    model   = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
    summary = map(model, summary),
    plot    = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                          geom_point())
  )

A operação de aninhamento list(.SD)pode não ser suportada por {dtlyr}, como os usuários usam em ordem tidyr::nest? Portanto, não tenho certeza se as operações subseqüentes podem ser traduzidas da maneira que o {data.table} é mais rápido e menos memória.

NOTA: o resultado do data.table está em "milissegundo", dplyr em "minuto"

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))

bench::mark(
  check = FALSE,

  dt[, by = Species, .(data = list(.SD))],
  df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
#   expression                                   min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#   <bch:expr>                              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms   2.49      705.8MB     1.24     2     1
# 2 df %>% group_by(Species) %>% nest()        6.85m    6.85m   0.00243     1.4GB     2.28     1   937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# #   gc <list>

Existem muitos casos de uso de atualização por referência e até os usuários do {data.table} não usam a versão avançada o tempo todo, pois exigem mais códigos. Se o {dtplyr} suporta esses itens prontos, você precisa descobrir a si mesmo.

Múltipla atualização por referência para as mesmas funções

Recurso principal: atribuir elegantemente várias colunas em data.table com lapply ()

Isso envolve o mais comumente usado :=ou set.

dt <- data.table( matrix(runif(10000), nrow = 100) )

# A few variants

for (col in paste0('V', 20:100))
  set(dt, j = col, value = sqrt(get(col)))

for (col in paste0('V', 20:100))
  dt[, (col) := sqrt(get(col))]

# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])

De acordo com o criador de {data.table} Matt Dowle

(Observe que pode ser mais comum fazer loop em um grande número de linhas do que em um grande número de colunas.)

Join + setkey + atualização por referência

Eu precisava de junção rápida com dados relativamente grandes e padrões de junção semelhantes recentemente, então uso o poder da atualização por referência , em vez de junções normais. Como eles exigem mais códigos, envolvo-os em um pacote privado com avaliação não padrão para reutilização e legibilidade, onde eu chamo setjoin.

Eu fiz algumas referências aqui: data.table join + update-by-reference + setkey

Sumário

# For brevity, only the codes for join-operation are shown here. Please refer to the link for details

# Normal_join
x <- y[x, on = 'a']

# update_by_reference
x_2[y_2, on = 'a', c := c]

# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]

NOTA: dplyr::left_jointambém foi testado e é o mais lento com ~ 9.000 ms, usa mais memória do que os {data.table} update_by_referencee setkey_n_update, mas usa menos memória que o normal_join de {data.table}. Ele consumiu cerca de 2,0 GB de memória. Não o incluí, pois quero me concentrar apenas em {data.table}.

Principais conclusões

  • setkey + updatee updatesão ~ 11 e ~ 6,5 vezes mais rápidos que normal join, respectivamente
  • na primeira junção, o desempenho de setkey + updateé semelhante ao updateoverhead de setkeycompensar amplamente seus próprios ganhos de desempenho
  • na segunda e subsequente junções, conforme setkeynão é necessário, setkey + updateé mais rápido que update~ 1,8 vezes (ou mais rápido que normal join~ 11 vezes)

Imagem

Exemplos

Para junções eficientes em desempenho e memória, use updateou setkey + update, onde o último for mais rápido, ao custo de mais códigos.

Vamos ver alguns pseudo- códigos, por questões de brevidade. As lógicas são as mesmas.

Para uma ou algumas colunas

a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)

# `update`
a[b, on = .(x), y := y]
a[b, on = .(x),  `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x),  `:=` (y = y, z = z, ...) ]

Para muitas colunas

cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]

Wrapper para junções rápidas e eficientes na memória ... muitas delas ... com padrão de junção semelhante, envolva-as como setjoinacima - com update - com ou semsetkey

setjoin(a, b, on = ...)  # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop   = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
  setjoin(...) %>%
  setjoin(...)

Com setkey, o argumento onpode ser omitido. Também pode ser incluído para facilitar a leitura, especialmente para colaborar com outras pessoas.

Operação em linha grande

  • como mencionado acima, use set
  • preencha previamente sua tabela, use técnicas de atualização por referência
  • subconjunto usando a chave (ou seja setkey)

Recurso relacionado: Inclua uma linha por referência no final de um objeto data.table

Resumo da atualização por referência

Esses são apenas alguns casos de uso de atualização por referência . Existem muitos mais.

Como você pode ver, para o uso avançado de lidar com dados grandes, existem muitos casos de uso e técnicas usando atualização por referência para conjuntos de dados grandes. Não é tão fácil de usar no {data.table} e, se o {dtplyr} suporta, você pode descobrir por si mesmo.

Eu me concentro na atualização por referência neste post, pois acho que é o recurso mais poderoso do {data.table} para operações rápidas e com eficiência de memória. Dito isto, existem muitos outros aspectos que também o tornam tão eficiente e acho que não são suportados nativamente pelo {dtplyr}.

Outros aspectos-chave

O que é / não é suportado, também depende da complexidade das operações e se envolve o recurso nativo do data.table, como atualização por referência ou setkey. E se o código traduzido é o mais eficiente (aquele que os usuários do data.table escrevem) também é outro fator (ou seja, o código é traduzido, mas é a versão eficiente?). Muitas coisas estão interconectadas.

Muitos desses aspectos estão inter-relacionados com os pontos mencionados acima

  • complexidade das operações

  • atualização por referência

Você pode descobrir se o {dtplyr} suporta essas operações, especialmente quando elas são combinadas.

Outro truque útil ao lidar com um conjunto de dados pequeno ou grande, durante a sessão interativa, {data.table} realmente cumpre sua promessa de reduzir a programação e calcular o tempo tremendamente.

Chave de configuração para variável usada repetidamente para velocidade e 'nomes de domínio sobrealimentados' (subconjunto sem especificar o nome da variável).

dt <- data.table(iris)
setkey(dt, Species) 

dt['setosa',    do_something(...), ...]
dt['virginica', do_another(...),   ...]
dt['setosa',    more(...),         ...]

# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders. 
# It's simply elegant
dt['setosa', do_something(...), Species, ...]

Se suas operações envolvem apenas operações simples, como no primeiro exemplo, {dtplyr} pode fazer o trabalho. Para os complexos / não suportados, você pode usar este guia para comparar os traduzidos de {dtplyr} com a forma como os usuários experientes de data.table codificariam de maneira rápida e eficiente de memória com a sintaxe elegante do data.table. A tradução não significa que é a maneira mais eficiente, pois pode haver diferentes técnicas para lidar com diferentes casos de grandes dados. Para um conjunto de dados ainda maior, você pode combinar {data.table} com {disk.frame} , {fst} e {drake} e outros pacotes incríveis para obter o melhor dele. Há também uma {big.data.table}, mas ela está inativa no momento.

Espero que ajude a todos. Tenha um bom dia ☺☺


2

Junções não equi e junções rolantes vêm à mente. Parece não haver planos para incluir funções equivalentes no dplyr, portanto não há nada para o dtplyr traduzir.

Também há a remodelagem (dcast e derretimento otimizados equivalentes às mesmas funções no reshape2) que também não está no dplyr.

Todas as funções * _if e * _at atualmente não podem ser traduzidas com o dtplyr também, mas estão em andamento.


0

Atualizar uma coluna ao ingressar Alguns truques .SD Muitas funções f E Deus sabe o que mais, porque #rdatatable é mais do que apenas uma biblioteca simples e não pode ser resumido com poucas funções

É um ecossistema inteiro por si só

Eu nunca precisei do dplyr desde o dia em que comecei a R. Porque o data.table é tão bom

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.