Je vais essayer de vous donner mes meilleurs guides mais ce n'est pas facile car il faut être familier avec tous les {data.table}, {dplyr}, {dtplyr} et aussi {base R}. J'utilise {data.table} et de nombreux packages {tidy-world} (sauf {dplyr}). J'adore les deux, bien que je préfère la syntaxe de data.table à celle de dplyr. J'espère que tous les packages tidy-world utiliseront {dtplyr} ou {data.table} comme backend chaque fois que cela sera nécessaire.
Comme pour toute autre traduction (pensez dplyr-to-sparkly / SQL), il y a des choses qui peuvent ou ne peuvent pas être traduites, du moins pour l'instant. Je veux dire, peut-être qu'un jour {dtplyr} pourra le traduire à 100%, qui sait. La liste ci-dessous n'est pas exhaustive ni 100% correcte car je ferai de mon mieux pour répondre en fonction de mes connaissances sur des sujets / packages / problèmes / etc.
Surtout, pour ces réponses qui ne sont pas entièrement exactes, j'espère que cela vous donne quelques guides sur les aspects de {data.table} auxquels vous devez prêter attention et, comparez-le à {dtplyr} et découvrez les réponses par vous-même. Ne prenez pas ces réponses pour acquises.
Et j'espère que ce message peut être utilisé comme l'une des ressources pour tous les utilisateurs / créateurs {dplyr}, {data.table} ou {dtplyr} pour des discussions et des collaborations et améliorer encore #RStats.
{data.table} n'est pas seulement utilisé pour des opérations rapides et efficaces en mémoire. Il y a beaucoup de gens, y compris moi-même, qui préfèrent la syntaxe élégante de {data.table}. Il comprend également d'autres opérations rapides comme les fonctions de séries chronologiques comme la famille de roulement (c'est-à-dire frollapply
) écrites en C. Il peut être utilisé avec toutes les fonctions, y compris tidyverse. J'utilise beaucoup {data.table} + {purrr}!
Complexité des opérations
Cela peut être facilement traduit
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} est très rapide et efficace en mémoire car (presque?) tout est construit à partir de zéro à partir de C avec les concepts clés de mise à jour par référence , clé (pensez SQL), et leur optimisation implacable partout dans le package (c'est-à fifelse
- dire , l' fread/fread
ordre de tri radix adopté par la base R), tout en s'assurant que la syntaxe est concise et cohérente, c'est pourquoi je pense que c'est élégant.
De l' introduction à data.table , les principales opérations de manipulation de données telles que le sous-ensemble, le groupe, la mise à jour, la jointure, etc. sont conservées ensemble pour
syntaxe concise et cohérente ...
effectuer une analyse fluide sans la charge cognitive d'avoir à cartographier chaque opération ...
optimisation automatique des opérations en interne et très efficacement, en connaissant précisément les données requises pour chaque opération, conduisant à un code très rapide et efficace en mémoire
Le dernier point, à titre d'exemple,
# 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))]
Nous avons d'abord sous-ensemble dans i pour trouver des indices de lignes correspondants où l'aéroport d'origine est égal à "JFK" et le mois est égal à 6L. Nous ne sous-ensemble pas encore l'ensemble de data.table correspondant à ces lignes.
Maintenant, nous regardons j et constatons qu'il n'utilise que deux colonnes. Et ce que nous devons faire, c'est calculer leur moyenne (). Par conséquent, nous sous-ensemble uniquement les colonnes correspondant aux lignes correspondantes, et calculons leur moyenne ().
Étant donné que les trois principaux composants de la requête (i, j et by) sont ensemble à l'intérieur [...] , data.table peut voir les trois et optimiser la requête complètement avant l'évaluation, pas chacun séparément . Nous sommes donc en mesure d'éviter l'ensemble du sous-ensemble (c'est-à-dire, le sous-ensemble des colonnes en plus d'arr_delay et dep_delay), à la fois pour la vitesse et l'efficacité de la mémoire.
Étant donné que, pour profiter des avantages de {data.table}, la traduction de {dtplr} doit être correcte à cet égard. Plus les opérations sont complexes, plus les traductions sont dures. Pour les opérations simples comme ci-dessus, il peut certainement être facilement traduit. Pour les complexes, ou ceux qui ne sont pas pris en charge par {dtplyr}, vous devez vous renseigner comme mentionné ci-dessus, il faut comparer la syntaxe traduite et le benchmark et être des packages familiers.
Pour les opérations complexes ou les opérations non prises en charge, je pourrais être en mesure de fournir quelques exemples ci-dessous. Encore une fois, je fais de mon mieux. Soyez doux avec moi.
Mise à jour par référence
Je n'entrerai pas dans l'intro / détails mais voici quelques liens
Ressource principale: Sémantique de référence
Plus de détails: Comprendre exactement quand un data.table est une référence à (vs une copie de) un autre data.table
Mise à jour par référence , à mon avis, la caractéristique la plus importante de {data.table} et c'est ce qui la rend si rapide et efficace en mémoire. dplyr::mutate
ne le prend pas en charge par défaut. Comme je ne connais pas {dtplyr}, je ne sais pas combien et quelles opérations peuvent ou ne peuvent pas être prises en charge par {dtplyr}. Comme mentionné ci-dessus, cela dépend également de la complexité des opérations, qui à leur tour affectent les traductions.
Il existe deux façons d'utiliser la mise à jour par référence dans {data.table}
opérateur d'affectation de {data.table} :=
set
-family: set
, setnames
, setcolorder
, setkey
, setDT
, fsetdiff
, et beaucoup d' autres
:=
est plus couramment utilisé par rapport à set
. Pour les ensembles de données complexes et volumineux, la mise à jour par référence est la clé pour obtenir une vitesse maximale et une efficacité de la mémoire. La façon de penser facile (pas précise à 100%, car les détails sont beaucoup plus compliqués que cela car cela implique une copie dure / peu profonde et de nombreux autres facteurs), disons que vous avez affaire à un grand ensemble de données de 10 Go, avec 10 colonnes et 1 Go chacune . Pour manipuler une colonne, vous devez traiter uniquement 1 Go.
Le point clé est, avec la mise à jour par référence , il vous suffit de traiter les données requises. C'est pourquoi lorsque vous utilisez {data.table}, en particulier pour les grands ensembles de données, nous utilisons la mise à jour par référence tout le temps possible. Par exemple, manipuler un grand ensemble de données de modélisation
# 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())
)
L'opération d'imbrication list(.SD)
peut ne pas être prise en charge par {dtlyr} comme les utilisateurs de tidyverse l'utilisent tidyr::nest
? Donc, je ne sais pas si les opérations suivantes peuvent être traduites comme la manière de {data.table} est plus rapide et moins de mémoire.
REMARQUE: le résultat de data.table est en "milliseconde", dplyr en "minute"
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>
Il existe de nombreux cas d'utilisation de la mise à jour par référence et même les utilisateurs de {data.table} n'en utiliseront pas la version avancée tout le temps car ils nécessitent plus de codes. Que {dtplyr} prenne en charge ces éléments prêts à l'emploi, vous devez vous renseigner.
Mise à jour multiple par référence pour les mêmes fonctions
Ressource principale: Affectation élégante de plusieurs colonnes dans data.table avec lapply ()
Cela implique soit le plus couramment utilisé :=
soit 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(.))])
Selon le créateur de {data.table} Matt Dowle
(Notez qu'il peut être plus courant de boucler un ensemble sur un grand nombre de lignes qu'un grand nombre de colonnes.)
Join + setkey + update-by-reference
J'ai eu besoin d'une jointure rapide avec des données relativement volumineuses et des modèles de jointure similaires récemment, donc j'utilise la puissance de la mise à jour par référence , au lieu des jointures normales. Comme ils nécessitent plus de codes, je les enveloppe dans un package privé avec une évaluation non standard pour la réutilisabilité et la lisibilité où je l'appelle setjoin
.
J'ai fait un benchmark ici: data.table join + update-by-reference + setkey
Sommaire
# 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 ]
REMARQUE: dplyr::left_join
a également été testé et c'est le plus lent avec environ 9 000 ms, utilisez plus de mémoire que {data.table} update_by_reference
et setkey_n_update
, mais utilisez moins de mémoire que normal_join de {data.table}. Il a consommé environ ~ 2,0 Go de mémoire. Je ne l'ai pas inclus car je veux me concentrer uniquement sur {data.table}.
Principales conclusions
setkey + update
et update
sont ~ 11 et ~ 6,5 fois plus rapides que normal join
, respectivement
- à la première jointure, la performance de
setkey + update
est similaire à update
celle des frais généraux setkey
qui compense largement ses propres gains de performance
- lors de la deuxième jointure et des jointures suivantes, comme cela
setkey
n'est pas nécessaire, setkey + update
est plus rapide que update
~ 1,8 fois (ou plus rapide que normal join
~ 11 fois)
Exemples
Pour des jointures performantes et efficaces en mémoire, utilisez soit update
ou setkey + update
, lorsque ce dernier est plus rapide au prix de plus de codes.
Voyons quelques pseudo codes, par souci de concision. Les logiques sont les mêmes.
Pour une ou quelques colonnes
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, ...) ]
Pour de nombreuses colonnes
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 pour des jointures rapides et efficaces en mémoire ... beaucoup d'entre elles ... avec un modèle de jointure similaire, enveloppez-les comme setjoin
ci-dessus - avec update
- avec ou sanssetkey
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(...)
Avec setkey
, l'argument on
peut être omis. Il peut également être inclus pour plus de lisibilité, en particulier pour collaborer avec d'autres.
Fonctionnement sur grande rangée
- comme mentionné ci-dessus, utilisez
set
- pré-remplir votre table, utilisez la mise à jour par référence techniques
- sous-ensemble utilisant la clé (ie
setkey
)
Ressource associée: Ajouter une ligne par référence à la fin d'un objet data.table
Résumé de la mise à jour par référence
Ce ne sont que quelques cas d'utilisation de la mise à jour par référence . Il y en a bien d'autres.
Comme vous pouvez le voir, pour une utilisation avancée du traitement des données volumineuses, il existe de nombreux cas d'utilisation et techniques utilisant la mise à jour par référence pour un grand ensemble de données. Ce n'est pas si facile à utiliser dans {data.table} et si {dtplyr} le prend en charge, vous pouvez le découvrir vous-même.
Je me concentre sur la mise à jour par référence dans ce post car je pense que c'est la fonctionnalité la plus puissante de {data.table} pour des opérations rapides et efficaces en mémoire. Cela dit, il y a beaucoup, beaucoup d'autres aspects qui le rendent aussi efficace et je pense qu'ils ne sont pas supportés nativement par {dtplyr}.
Autres aspects clés
Ce qui est / n'est pas pris en charge, cela dépend également de la complexité des opérations et si cela implique la fonctionnalité native de data.table comme la mise à jour par référence ou setkey
. Et si le code traduit est le plus efficace (celui que les utilisateurs de data.table écriraient) est également un autre facteur (c'est-à-dire que le code est traduit, mais est-ce la version efficace?). Beaucoup de choses sont interconnectées.
setkey
. Voir Clés et sous-ensemble basé sur la recherche binaire rapide
- Indices secondaires et indexation automatique
- Utilisation de .SD pour l'analyse des données
- fonctions chronologiques: pensez
frollapply
. fonctions de roulement, agrégats de roulement, fenêtre coulissante, moyenne mobile
- roulement rejoindre , non équi rejoignent , (certains) "croix" join
- {data.table} a jeté les bases de la vitesse et de l'efficacité de la mémoire, à l'avenir, il peut s'étendre pour inclure de nombreuses fonctions (comme la façon dont elles implémentent les fonctions de séries chronologiques mentionnées ci-dessus)
- en général, les opérations plus complexes sur de data.table
i
, j
ou des by
opérations (vous pouvez utiliser presque toutes les expressions là - dedans), je pense que les plus dures traductions, surtout quand il se combiner avec la mise à jour par référence , setkey
et d' autres data.table native des fonctions commefrollapply
- Un autre point est lié à l'utilisation de la base R ou de l'inverse. J'utilise à la fois data.table + tidyverse (sauf dplyr / readr / tidyr). Pour les grandes opérations, je compare souvent, par exemple, les fonctions
stringr::str_*
famille vs base R et je trouve que la base R est plus rapide dans une certaine mesure et les utilise. Le point est, ne vous limitez pas à seulement tidyverse ou data.table ou ..., explorez d'autres options pour faire le travail.
Beaucoup de ces aspects sont liés aux points mentionnés ci-dessus
Vous pouvez savoir si {dtplyr} prend en charge ces opérations, en particulier lorsqu'elles sont combinées.
Autre astuce utile lors de la manipulation de petits ou de grands ensembles de données, lors d'une session interactive, {data.table} tient vraiment sa promesse de réduire considérablement la programmation et le temps de calcul .
Clé de réglage pour la variable utilisée de manière répétitive à la fois pour la vitesse et les «noms de domaine suralimentés» (sous-ensemble sans spécifier le nom de la variable).
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, ...]
Si vos opérations impliquent uniquement des opérations simples comme dans le premier exemple, {dtplyr} peut faire le travail. Pour les fichiers complexes / non pris en charge, vous pouvez utiliser ce guide pour comparer les fichiers traduits de {dtplyr} avec la façon dont les utilisateurs expérimentés de data.table coderaient de manière rapide et efficace en mémoire avec la syntaxe élégante de data.table. La traduction ne signifie pas que c'est le moyen le plus efficace car il peut y avoir différentes techniques pour traiter différents cas de données volumineuses. Pour un ensemble de données encore plus volumineux, vous pouvez combiner {data.table} avec {disk.frame} , {fst} et {drake} et d'autres packages impressionnants pour en tirer le meilleur . Il existe également un {big.data.table} mais il est actuellement inactif.
J'espère que cela aide tout le monde. Passez une bonne journée ☺☺
dplyr
lequel vous ne pouvez pas bien fairedata.table
? Sinon, passer àdata.table
va être meilleur quedtplyr
.