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/fread
ordem 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::mutate
nã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_join
também foi testado e é o mais lento com ~ 9.000 ms, usa mais memória do que os {data.table} update_by_reference
e 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 + update
e update
são ~ 11 e ~ 6,5 vezes mais rápidos que normal join
, respectivamente
- na primeira junção, o desempenho de
setkey + update
é semelhante ao update
overhead de setkey
compensar amplamente seus próprios ganhos de desempenho
- na segunda e subsequente junções, conforme
setkey
não é necessário, setkey + update
é mais rápido que update
~ 1,8 vezes (ou mais rápido que normal join
~ 11 vezes)
Exemplos
Para junções eficientes em desempenho e memória, use update
ou 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 setjoin
acima - 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 on
pode 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.
setkey
. Consulte Chaves e subconjunto rápido baseado em pesquisa binária
- Índices secundários e indexação automática
- Usando .SD para análise de dados
- funções de séries temporais: pense
frollapply
. funções de rolamento, agregados de rolamento, janela deslizante, média móvel
- junção contínua , junção não equi , (algumas) junção "cruzada"
- {data.table} construiu a base em velocidade e eficiência de memória; no futuro, pode se estender para incluir muitas funções (como a implementação de funções de séries temporais mencionadas acima)
- Em geral, as operações mais complexas sobre o data.table
i
, j
ou by
operações (você pode usar quase qualquer expressão em lá), eu acho que os mais difíceis as traduções, especialmente quando se combinam com atualização por referência , setkey
e outra data.table nativa funções comofrollapply
- Outro ponto está relacionado ao uso da base R ou arrumado. Eu uso ambos data.table + tidyverse (exceto dplyr / readr / tidyr). Para operações grandes, geralmente faço benchmark, por exemplo,
stringr::str_*
funções família versus base R e acho que a base R é mais rápida até certo ponto e as utilizo. O ponto é: não se prenda apenas ao arrumação ou à tabela de dados ou ..., explore outras opções para fazer o trabalho.
Muitos desses aspectos estão inter-relacionados com os pontos mencionados acima
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 ☺☺
dplyr
que você não pode fazer bemdata.table
? Caso contrário, mudardata.table
para será melhor quedtplyr
.