Que ne puis-je pas faire avec dtplyr que je peux dans data.table

9

Dois-je investir mon effort d'apprentissage pour la lutte des données dans R, en particulier entre dplyr, dtplyret data.table?

  • J'utilise dplyrprincipalement, mais lorsque les données sont trop volumineuses pour cela, je vais les utiliser data.table, ce qui est rare. Alors maintenant que la dtplyrv1.0 est sortie en tant qu'interface data.table, il semble que je n'ai plus jamais à me soucier de data.tableréutiliser l' interface.

  • Quelles sont donc les fonctionnalités ou les aspects les plus utiles data.tablequi ne peuvent pas être utilisés dtplyrpour le moment, et qui ne seront probablement jamais réalisés avec dtplyr?

  • Sur son visage, dplyravec les avantages de data.tablesonne comme un dtplyrdépassement dplyr. Y aura-t-il une raison d'utiliser dplyrune fois qu'il dtplyrest complètement arrivé à maturité?

Remarque: je ne pose pas de question sur dplyrvsdata.table (comme dans data.table vs dplyr: l'un peut-il faire quelque chose de bien l'autre ne peut pas ou fait mal? ), Mais étant donné que l'un est préféré à l'autre pour un problème particulier, pourquoi ne le ferait-il pas? t dtplyrêtre l'outil à utiliser.

dule arnaux
la source
1
Y a-t-il quelque chose que vous pouvez bien faire dans dplyrlequel vous ne pouvez pas bien faire data.table? Sinon, passer à data.tableva être meilleur que dtplyr.
sindri_baldur
2
D'après le dtplyrreadme, «Certaines data.tableexpressions n'ont pas d' dplyréquivalent direct . Par exemple, il n'y a aucun moyen d'exprimer des jointures croisées ou continues dplyr. et 'Pour faire correspondre la dplyrsémantique, mutate() ne modifie pas en place par défaut. Cela signifie que la plupart des expressions impliquant mutate()doivent faire une copie qui ne serait pas nécessaire si vous l'utilisiez data.tabledirectement. ' Il y a un peu un moyen de contourner cette deuxième partie, mais compte tenu de la fréquence d' mutateutilisation, c'est un gros inconvénient à mes yeux.
ClancyStats

Réponses:

15

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/freadordre 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::mutatene 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_joina également été testé et c'est le plus lent avec environ 9 000 ms, utilisez plus de mémoire que {data.table} update_by_referenceet 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 + updateet updatesont ~ 11 et ~ 6,5 fois plus rapides que normal join, respectivement
  • à la première jointure, la performance de setkey + updateest similaire à updatecelle des frais généraux setkeyqui compense largement ses propres gains de performance
  • lors de la deuxième jointure et des jointures suivantes, comme cela setkeyn'est pas nécessaire, setkey + updateest plus rapide que update~ 1,8 fois (ou plus rapide que normal join~ 11 fois)

Image

Exemples

Pour des jointures performantes et efficaces en mémoire, utilisez soit updateou 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 setjoinci-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 onpeut ê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.

Beaucoup de ces aspects sont liés aux points mentionnés ci-dessus

  • complexité des opérations

  • mise à jour par référence

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 ☺☺

K22
la source
2

Les jointures non équi et les jointures tournantes viennent à l'esprit. Il ne semble pas être prévu d'inclure des fonctions équivalentes dans dplyr, il n'y a donc rien à traduire pour dtplyr.

Il y a aussi un remodelage (optimisation de diffusion et fusion optimisées équivalant aux mêmes fonctions dans reshape2) qui n'est pas également dans dplyr.

Actuellement, toutes les fonctions * _if et * _at ne peuvent pas être traduites avec dtplyr mais celles-ci sont en préparation.

EdTeD
la source
0

Mettre à jour une colonne lors de la jointure Quelques astuces .SD Beaucoup de fonctions f Et Dieu sait quoi d'autre parce que #rdatatable est plus qu'une simple bibliothèque et il ne peut pas être résumé avec peu de fonctions

C'est un écosystème à part entière

Je n'ai jamais eu besoin de dplyr depuis le jour où j'ai commencé R. Parce que data.table est tellement bon

Vikram
la source