Question data.table vs dplyr: peut-on faire quelque chose bien que l'autre ne peut pas ou fait mal?


Aperçu

Je suis relativement familier avec data.table, pas tellement avec dplyr. J'ai lu quelques passages dplyr vignettes et des exemples qui ont surgi sur SO, et jusqu'à présent, mes conclusions sont les suivantes:

  1. data.table et dplyr sont comparables en vitesse, sauf lorsqu'il y a beaucoup de groupes (c'est-à-dire> 10-100K), et dans d'autres circonstances (voir les repères ci-dessous)
  2. dplyr a une syntaxe plus accessible
  3. dplyr résumés (ou volonté) interactions potentielles DB
  4. Il existe quelques différences mineures de fonctionnalité (voir "Exemples / Utilisation" ci-dessous)

Dans mon esprit 2. ne supporte pas beaucoup de poids parce que je suis assez familier avec data.table, bien que je comprenne que pour les utilisateurs nouveaux à la fois, ce sera un facteur important. Je voudrais éviter un argument sur lequel est plus intuitive, comme cela est sans pertinence pour ma question spécifique posée du point de vue de quelqu'un déjà familier avec data.table. Je voudrais également éviter une discussion sur la façon dont "plus intuitif" conduit à une analyse plus rapide (certainement vrai, mais encore une fois, pas ce qui m'intéresse le plus ici).

Question

Ce que je veux savoir c'est:

  1. Y a-t-il des tâches analytiques qui sont beaucoup plus faciles à coder avec l'un ou l'autre paquet pour les personnes familiarisées avec les paquetages (c'est-à-dire une combinaison de touches requises par rapport au niveau d'ésotérisme requis).
  2. Y a-t-il des tâches analytiques qui sont exécutées de manière substantielle (c'est-à-dire plus de 2x) plus efficacement dans un paquet par rapport à un autre.

Un question SO récente m'a fait réfléchir un peu plus à ce sujet, car jusqu'à ce moment je ne pensais pas dplyr offrirait beaucoup plus que ce que je peux déjà faire dans data.table. Voici la dplyr solution (données à la fin de Q):

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

Ce qui était beaucoup mieux que ma tentative de piratage à un data.table Solution. Cela dit, bon data.table les solutions sont également assez bonnes (merci Jean-Robert, Arun, et notez ici que j'ai privilégié la déclaration unique par rapport à la solution la plus optimale):

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

La syntaxe de ce dernier peut sembler très ésotérique, mais elle est en fait assez simple si vous avez l'habitude de data.table (c'est-à-dire n'utilise pas certaines des astuces les plus ésotériques).

Idéalement, ce que j'aimerais voir, c'est que de bons exemples dplyr ou data.table manière est sensiblement plus concis ou fonctionne sensiblement mieux.

Exemples

Usage
  • dplyr n'autorise pas les opérations groupées qui renvoient un nombre arbitraire de lignes La question d'eddi, note: cela semble être implémenté dans dplyr 0.5, aussi, @beginneR montre un potentiel de contournement en utilisant do dans la réponse à la question de @ eddi).
  • data.table les soutiens jointures roulantes (merci @dholstius) ainsi que chevaucher les jointures
  • data.table optimise en interne les expressions du formulaire DT[col == value] ou DT[col %in% values] pour la vitesse par indexation automatique qui utilise recherche binaire en utilisant la même syntaxe de base R. Vois ici pour plus de détails et une petite référence.
  • dplyr propose des versions d'évaluation standard des fonctions (par ex. regroup, summarize_each_) qui peut simplifier l'utilisation programmatique de dplyr (Notez l'utilisation programmatique de data.table est certainement possible, nécessite juste une réflexion minutieuse, substitution / citation, etc, au moins à ma connaissance)
Benchmarks

Les données

C'est pour le premier exemple que j'ai montré dans la section des questions.

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

568
2018-01-29 15:21


origine


Réponses:


Nous devons couvrir au moins ces aspects pour fournir une réponse / comparaison complète (sans ordre particulier d'importance): Speed, Memory usage, Syntax et Features.

Mon intention est de couvrir chacune d'entre elles le plus clairement possible du point de vue de data.table.

Note: sauf mention contraire explicite, en se référant à dplyr, nous nous référons à l'interface data.frame de dplyr dont les internes sont en C ++ en utilisant Rcpp.


La syntaxe data.table est cohérente dans sa forme - DT[i, j, by]. Garder i, j et by ensemble est par conception. En conservant les opérations connexes, cela permet de facilement optimiser opérations pour la vitesse et plus important utilisation de la mémoire, et aussi fournir quelques fonctionnalités puissantes, tout en maintenant la cohérence dans la syntaxe.

1. Vitesse

Un certain nombre de benchmarks (mais principalement sur les opérations de groupement) ont été ajoutés à la question montrant déjà data.table plus rapide que dplyr que le nombre de groupes et / ou de rangées à grouper par augmentation, y compris benchmarks de Matt sur le regroupement de 10 millions à 2 milliards de lignes (100 Go en RAM) sur 100 - 10 millions de groupes et diverses colonnes de regroupement, qui compare également pandas.

Sur les points de référence, il serait bon de couvrir également ces aspects restants:

  • Regrouper des opérations impliquant sous-ensemble de lignes - c'est à dire., DT[x > val, sum(y), by = z] opérations de type.

  • Repérer d'autres opérations telles que mettre à jour et rejoint.

  • Également référencé empreinte mémoire pour chaque opération en plus de runtime.

2. Utilisation de la mémoire

  1. Opérations impliquant filter() ou slice() dans dplyr peut être inefficace de la mémoire (sur data.frames et data.tables). Voir cet article.

    Notez que Le commentaire de Hadley parle de la vitesse (ce dplyr est rapide pour lui), alors que la principale préoccupation ici est Mémoire.

  2. L'interface data.table permet actuellement de modifier / mettre à jour les colonnes par référence (Notez que nous n'avons pas besoin de ré-affecter le résultat à une variable).

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

    Mais dplyr ne sera jamais mise à jour par référence. L'équivalent dplyr serait (notez que le résultat doit être réattribué):

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

    Une préoccupation pour cela est transparence référentielle. La mise à jour d'un objet data.table par référence, en particulier dans une fonction, n'est peut-être pas toujours souhaitable. Mais ceci est une fonctionnalité incroyablement utile: voir ce et ce messages pour des cas intéressants. Et nous voulons le garder.

    Par conséquent, nous travaillons à l'exportation shallow() fonction dans data.table qui fournira à l'utilisateur les deux possibilités. Par exemple, s'il est souhaitable de ne pas modifier l'entrée data.table dans une fonction, on peut alors:

    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.
    }
    

    En n'utilisant pas shallow(), l'ancienne fonctionnalité est conservée:

    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.
    }
    

    En créant un copie superficielle en utilisant shallow(), nous comprenons que vous ne voulez pas modifier l'objet original. Nous nous occupons de tout en interne pour nous assurer que tout en assurant la copie des colonnes vous modifiez seulement quand c'est absolument nécessaire. Une fois mis en œuvre, cela devrait régler le transparence référentielle problème tout en fournissant à l'utilisateur les deux possibilités.

    Aussi, une fois shallow() est exporté L'interface data.table de dplyr devrait éviter la quasi-totalité des copies. Donc ceux qui préfèrent la syntaxe de dplyr peuvent l'utiliser avec data.tables.

    Mais il lui manquera encore beaucoup de fonctionnalités que fournit data.table, y compris (sous-) affectation par référence.

  3. Agréger en rejoignant:

    Supposons que vous avez deux data.tables comme suit:

    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
    

    Et vous souhaitez obtenir sum(z) * mul pour chaque rangée de DT2 en se joignant à des colonnes x,y. Nous pouvons:

    • 1) agrégat DT1 obtenir sum(z), 2) effectuer une jointure et 3) multiplier (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) tout faire en une fois (en utilisant by = .EACHI fonctionnalité):

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

    Quel est l'avantage?

    • Nous n'avons pas besoin d'allouer de la mémoire pour le résultat intermédiaire.

    • Nous n'avons pas à grouper / hacher deux fois (un pour l'agrégation et l'autre pour l'adhésion).

    • Et plus important encore, l'opération que nous voulions effectuer est claire en regardant j en 2).

    Vérifier ce post pour une explication détaillée de by = .EACHI. Aucun résultat intermédiaire n'est matérialisé et l'agrégat join + est effectué en une fois.

    Jettes un coup d'oeil à ce, ce et ce messages pour des scénarios d'utilisation réelle.

    Dans dplyrvous auriez à rejoindre et agréger ou agréger d'abord, puis rejoindre, dont aucun n'est aussi efficace, en termes de mémoire (qui à son tour se traduit par la vitesse).

  4. Mettre à jour et rejoindre:

    Considérez le code data.table montré ci-dessous:

    DT1[DT2, col := i.mul]
    

    ajoute / met à jour DT1La colonne col avec mul de DT2 sur ces lignes où DT2Les correspondances des colonnes clés DT1. Je ne pense pas qu'il y ait un équivalent exact de cette opération dplyr, c'est-à-dire, sans éviter *_join opération, qui devrait copier l'ensemble DT1 juste pour ajouter une nouvelle colonne, ce qui est inutile.

    Vérifier ce post pour un scénario d'utilisation réelle.

Pour résumer, il est important de réaliser que chaque élément d'optimisation est important. Comme Grace Hopper dirait, Attention à vos nanosecondes!

3. Syntaxe

Regardons maintenant syntaxe. Hadley a commenté ici:

Les tableaux de données sont extrêmement rapides mais je pense que leur concision le rend plus difficile à apprendre et le code qui l'utilise est plus difficile à lire après l'avoir écrit ...

Je trouve cette remarque inutile car elle est très subjective. Ce que nous pouvons peut-être essayer est de contraster cohérence dans la syntaxe. Nous comparerons la syntaxe data.table et la syntaxe dplyr côte à côte.

Nous allons travailler avec les données factices ci-dessous:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. Opérations d'agrégation / mise à jour de base.

    # 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])
    
    • La syntaxe data.table est compacte et dplyr est assez verbeuse. Les choses sont plus ou moins équivalentes dans le cas (a).

    • Dans le cas (b), nous devions utiliser filter() à dplyr tout en résumer. Mais en même temps mise à, nous avons dû déplacer la logique à l'intérieur mutate(). Dans data.table cependant, nous exprimons les deux opérations avec la même logique - opérer sur les lignes où x > 2, mais dans le premier cas, sum(y), alors que dans le second cas, mettre à jour ces lignes pour y avec sa somme cumulée.

      C'est ce que nous voulons dire quand nous disons DT[i, j, by] forme est consistent.

    • De même dans le cas (c), quand nous avons if-else condition, nous sommes en mesure d'exprimer la logique "comme si" dans data.table et dplyr. Cependant, si nous voulons retourner juste ces lignes où le if condition satisfait et sauter autrement, nous ne pouvons pas utiliser summarise() directement (AFAICT). Nous devons filter() d'abord, puis résumer parce que summarise() attend toujours un valeur unique.

      Alors qu'il renvoie le même résultat, en utilisant filter() ici rend l'opération réelle moins évidente.

      Il pourrait très bien être possible d'utiliser filter() dans le premier cas aussi (cela ne me semble pas évident), mais mon point est que nous ne devrions pas avoir à le faire.

  2. Agrégation / mise à jour sur plusieurs colonnes

    # 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))
    
    • Dans le cas (a), les codes sont plus ou moins équivalents. data.table utilise une fonction de base familière lapply(), tandis que dplyr introduit *_each() avec un tas de fonctions à funs().

    • data.table := nécessite que les noms de colonnes soient fournis, alors que dplyr le génère automatiquement.

    • Dans le cas (b), la syntaxe de dplyr est relativement simple. Améliorer les agrégations / mises à jour sur plusieurs fonctions est sur la liste de data.table.

    • Dans le cas (c) cependant, dplyr retournerait n() autant de fois que de colonnes, au lieu d'une seule fois. Dans data.table, tout ce que nous devons faire est de retourner une liste dans j. Chaque élément de la liste deviendra une colonne dans le résultat. Ainsi, nous pouvons utiliser, encore une fois, la fonction de base familier c() concaténer .N à un list qui renvoie un list.

    Remarque: Une fois de plus, dans data.table, tout ce que nous devons faire est de renvoyer une liste dans j. Chaque élément de la liste deviendra une colonne dans le résultat. Vous pouvez utiliser c(), as.list(), lapply(), list() etc ... fonctions de base pour accomplir cela, sans avoir à apprendre de nouvelles fonctions.

    Vous aurez besoin d'apprendre seulement les variables spéciales - .N et .SD au moins. L'équivalent en dplyr sont n() et .

  3. Rejoint

    dplyr fournit des fonctions séparées pour chaque type de jointure où data.table autorise les jointures en utilisant la même syntaxe DT[i, j, by] (et avec raison). Il fournit également un équivalent merge.data.table() fonctionner comme une alternative.

    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"]
    ??
    
    • Certains pourraient trouver une fonction distincte pour chaque jointure beaucoup plus agréable (gauche, droite, intérieure, anti, semi etc), alors que d'autres pourraient aimer data.table DT[i, j, by], ou merge() qui est similaire à la base R.

    • Cependant dplyr se joint à cela. Rien de plus. Rien de moins.

    • data.tables peut sélectionner des colonnes lors de la connexion (2), et dans dplyr vous devrez select() d'abord sur les deux data.frames avant de rejoindre comme indiqué ci-dessus. Sinon, vous devrez matérialiser la jointure avec des colonnes inutiles pour les supprimer plus tard, ce qui est inefficace.

    • data.tables peut agréger tout (3) et aussi mettre à jour tout (4), en utilisant by = .EACHI fonctionnalité. Pourquoi materialse le résultat entier de jointure pour ajouter / mettre à jour juste quelques colonnes?

    • data.table est capable de jointures roulantes (5) - rouleau transmettre, LOCF, reculer, NOCB, la plus proche.

    • data.table a également mult = argument qui sélectionne premier, dernier ou tout allumettes (6).

    • data.table a allow.cartesian = TRUE argument pour protéger des jointures invalides accidentelles.

Encore une fois, la syntaxe est compatible avec DT[i, j, by] avec des arguments supplémentaires permettant de contrôler la sortie plus loin.

  1. do()...

    Le résumé de dplyr est spécialement conçu pour les fonctions qui retournent une seule valeur. Si votre fonction renvoie des valeurs multiples / inégales, vous devrez recourir à do(). Vous devez savoir à l'avance toutes vos fonctions sur la valeur de retour.

    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))))
    
    • .SDL'équivalent est .

    • Dans data.table, vous pouvez lancer à peu près tout dans j - La seule chose à retenir est de renvoyer une liste pour que chaque élément de la liste soit converti en colonne.

    • En dplyr, ne peut pas faire ça. Avoir recours à do() selon que vous êtes sûr que votre fonction renvoie toujours une seule valeur. Et c'est assez lent.

Une fois de plus, la syntaxe de data.table est cohérente avec DT[i, j, by]. Nous pouvons juste continuer à jeter des expressions dans j sans avoir à s'inquiéter de ces choses.

Jettes un coup d'oeil à cette question SO et celui-là. Je me demande s'il serait possible d'exprimer la réponse en utilisant la syntaxe de dplyr ...

Pour résumer, j'ai particulièrement souligné nombreuses les cas où la syntaxe de dplyr est soit inefficace, limitée ou ne parvient pas à rendre les opérations simples. Ceci est dû en particulier au fait que data.table reçoit beaucoup de contrecoup sur la syntaxe "plus difficile à lire / apprendre" (comme celle collée / liée ci-dessus). La plupart des articles qui couvrent dplyr parlent des opérations les plus simples. Et c'est génial. Mais il est également important de comprendre les limites de sa syntaxe et de ses fonctionnalités, et je n'ai pas encore vu de post à ce sujet.

data.table a aussi ses bizarreries (dont certaines que j'ai essayé de corriger). Nous essayons également d'améliorer les jointures de data.table comme je l'ai souligné ici.

Mais il faut aussi considérer le nombre de fonctionnalités qui manque à dplyr par rapport à data.table.

4. Caractéristiques

J'ai souligné la plupart des fonctionnalités ici et aussi dans ce post. En outre:

  • fread - Le lecteur de fichiers rapide est disponible depuis longtemps maintenant.

  • écrire - NOUVEAU dans le courant devel, v1.9.7, un parallélisé Le rédacteur de dossier rapide est maintenant disponible. Voir ce post pour une explication détaillée sur la mise en œuvre et # 1664 pour suivre les développements ultérieurs.

  • Indexation automatique - une autre fonctionnalité pratique pour optimiser la syntaxe de base R telle quelle, en interne.

  • Groupement ad-hoc: dplyr trie automatiquement les résultats en regroupant les variables summarise(), ce qui peut ne pas être toujours souhaitable.

  • De nombreux avantages dans les jointures data.table (pour l'efficacité de la vitesse / mémoire et la syntaxe) mentionnés ci-dessus.

  • Jointures non-équi: est une nouvelle fonctionnalité disponible à partir de v1.9.7 +. Il permet des jointures en utilisant d'autres opérateurs <=, <, >, >= avec tous les autres avantages des jointures data.table.

  • Chevauchement des plages de chevauchement a été implémenté dans data.table récemment. Vérifier ce post pour un aperçu avec des repères.

  • setorder() Fonctionne dans data.table qui permet de réorganiser très rapidement des tables de données par référence.

  • dplyr fournit interface aux bases de données en utilisant la même syntaxe, que data.table n'a pas pour le moment.

  • data.table fournit des équivalents plus rapides de définir les opérations de v1.9.7 + (écrit par Jan Gorecki) - fsetdiff, fintersect, funion et fsetequal avec supplémentaire all argument (comme dans SQL).

  • data.table charge proprement sans avertissements de masquage et dispose d'un mécanisme décrit ici pour [.data.frame compatibilité lors de la transmission à un paquet R. dplyr change les fonctions de base filter, lag et [ ce qui peut causer des problèmes; par exemple. ici et ici.


Finalement:

  • Sur les bases de données - il n'y a aucune raison pour que data.table ne puisse pas fournir une interface similaire, mais ce n'est pas une priorité maintenant. Il pourrait être bumped up si les utilisateurs aimeraient beaucoup cette fonctionnalité .. pas sûr.

  • Sur le parallélisme - Tout est difficile, jusqu'à ce que quelqu'un va de l'avant et le fait. Bien sûr, cela demandera des efforts (être sûr pour les threads).

    • Des progrès sont en cours actuellement (en v1.9.7 devel) vers la parallélisation des pièces consommant beaucoup de temps pour des gains de performance OpenMP.

387
2018-01-08 12:39



Voici ma tentative d'une réponse complète de la perspective dplyr, suivant les grandes lignes de la réponse d'Arun (mais quelque peu réarrangé basé sur des priorités différentes).

Syntaxe

Il y a une certaine subjectivité à la syntaxe, mais je maintiens ma déclaration que la concision de data.table rend plus difficile à apprendre et plus difficile à lire. C'est en partie parce que dplyr résout un problème beaucoup plus facile!

Une chose vraiment importante que dplyr fait pour vous est que contraintes vos options. Je prétends que la plupart des problèmes de table unique peuvent être résolu avec seulement cinq verbes clés filtrer, sélectionner, muter, organiser et résumer, avec un adverbe "par groupe". Cette contrainte est une aide précieuse lorsque vous apprenez la manipulation de données, car il permet de commander votre penser au problème. Dans dplyr, chacun de ces verbes est mappé à un fonction unique. Chaque fonction fait un travail, et est facile à comprendre en isolation.

Vous créez de la complexité en canalisant ces opérations simples avec %>%. Voici un exemple d'un des messages Arun lié à:

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

Même si vous n'avez jamais vu dplyr avant (ou même R!), Vous pouvez toujours obtenir l'essentiel de ce qui se passe parce que les fonctions sont tous en anglais verbes. L'inconvénient des verbes anglais est qu'ils nécessitent plus de dactylographie que [, mais je pense que cela peut être largement atténué par une meilleure saisie semi-automatique.

Voici le code data.table équivalent:

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

Il est plus difficile de suivre ce code, sauf si vous connaissez déjà data.table. (Je ne pouvais pas non plus comprendre comment mettre en retrait les [ d'une manière qui a l'air bonne à mes yeux). Personnellement, quand je regarde le code I écrit il y a 6 mois, c'est comme regarder un code écrit par un étranger, donc je suis venu à préférer le code simple, si verbeux.

Deux autres facteurs mineurs qui, selon moi, diminuent légèrement la lisibilité:

  • Comme presque toutes les opérations de table de données utilisent [ vous avez besoin de plus contexte pour comprendre ce qui se passe. Par exemple, est x[y] joindre deux tables de données ou extraire des colonnes d'une trame de données? Ce n'est qu'un petit problème, car dans le code bien écrit le Les noms de variables devraient suggérer ce qui se passe.

  • J'aime ça group_by() est une opération séparée dans dplyr. Il fondamentalement change le calcul donc je pense que devrait être évident lors de l'écrémage du code, et il est plus facile de repérer group_by() que la by argument à [.data.table.

J'aime aussi que le le tuyau n'est pas limité à un seul paquet. Vous pouvez commencer par ranger votre données avec tidyr, et finir avec un complot ggvis. Et tu es pas limité aux paquets que j'écris - n'importe qui peut écrire une fonction Cela forme une partie transparente d'un tuyau de manipulation de données. En fait, je préfère plutôt le code data.table précédent réécrit avec %>%:

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

Et l'idée de la tuyauterie avec %>% ne se limite pas à des trames de données et est facilement généralisé à d'autres contextes: site web interactif graphique, web grattage, gists, temps d'exécution contrats, ...)

Mémoire et performance

Je les ai regroupés, parce que, pour moi, ils ne sont pas si importants. La plupart des utilisateurs R travaillent avec moins de 1 million de lignes de données, et dplyr est suffisamment rapide pour cette taille de données que vous n'êtes pas au courant de temps de traitement. Nous optimisons dplyr pour l'expressivité sur les données moyennes; n'hésitez pas à utiliser data.table pour la vitesse brute sur des données plus volumineuses.

La flexibilité de dplyr signifie également que vous pouvez facilement ajuster la performance caractéristiques utilisant la même syntaxe. Si la performance de dplyr avec le backend de trame de données n'est pas assez bon pour vous, vous pouvez utiliser le backend data.table (mais avec un ensemble de fonctionnalités quelque peu restreint). Si les données que vous utilisez ne correspondent pas à la mémoire, vous pouvez utiliser un backend de base de données.

Tout cela dit, la performance dplyr s'améliorera à long terme. Bien certainement mettre en œuvre certaines des grandes idées de data.table comme radix commander et utiliser le même index pour les jointures et les filtres. Nous sommes aussi travailler sur la parallélisation afin que nous puissions tirer parti de plusieurs cœurs.

Caractéristiques

Quelques points sur lesquels nous prévoyons de travailler en 2015:

  • la readr paquet, pour le rendre facile à obtenir des fichiers sur le disque et dans à la mémoire, analogue à fread().

  • Jointures plus flexibles, y compris la prise en charge des non-équi-jointures.

  • Groupage plus flexible, comme les échantillons bootstrap, les rollups et plus

J'investis également du temps dans l'amélioration de R base de données connecteurs, la capacité de parler à web apiset en facilitant le gratter les pages html.


305
2017-11-16 22:39



En réponse directe à la titre de question...

dplyr  absolument fait des choses qui data.table ne peux pas.

Votre point # 3

dplyr résume (ou va) les interactions potentielles DB

est une réponse directe à votre propre question mais n'est pas élevée à un niveau suffisamment élevé. dplyr est vraiment un frontend extensible à plusieurs mécanismes de stockage de données où data.table est une extension à un seul.

Regarder dplyr en tant qu'interface agnostique back-end, avec toutes les cibles utilisant le même grammeur, où vous pouvez étendre les cibles et les gestionnaires à volonté. data.table est, de la dplyr perspective, l'un de ces objectifs.

Vous ne verrez jamais (j'espère) un jour data.table tente de traduire vos requêtes pour créer des instructions SQL qui fonctionnent avec des magasins de données sur disque ou en réseau.

dplyr peut éventuellement faire des choses data.table ne fera pas ou pourrait ne pas faire aussi bien.

Basé sur la conception du travail en mémoire, data.table pourrait avoir un temps beaucoup plus difficile s'étendre dans le traitement parallèle des requêtes que dplyr.


En réponse aux questions du corps ...

Usage

Existe-t-il des tâches analytiques beaucoup plus faciles à coder avec l'un ou l'autre paquet? pour les personnes familières avec les forfaits (c'est-à-dire une combinaison de frappes requises par rapport au niveau requis d'ésotérisme, où moins de chacun est une bonne chose).

Cela peut sembler un punt mais la vraie réponse est non. Gens familier avec des outils semblent utiliser le plus familier pour eux ou celui qui est en fait le bon pour le travail à portée de main. Cela étant dit, parfois vous voulez présenter une lisibilité particulière, parfois un niveau de performance, et quand vous avez besoin d'un niveau assez élevé des deux, vous pouvez juste avoir besoin d'un autre outil pour faire ce que vous avez déjà besoin de faire des abstractions plus claires .

Performance

Y a-t-il des tâches analytiques qui sont exécutées de manière substantielle (c'est-à-dire plus de 2x) plus efficacement dans un paquet par rapport à un autre.

Encore une fois, non. data.table excelle à être efficace dans tout il fait où dplyr a le fardeau d'être limité à certains égards au magasin de données sous-jacent et aux gestionnaires enregistrés.

Cela signifie que vous rencontrez un problème de performance avec data.table vous pouvez être sûr qu'il est dans votre fonction de requête et si elle est en fait un goulot d'étranglement avec data.table alors vous avez gagné vous-même la joie de déposer un rapport. Ceci est également vrai quand dplyr utilise data.table en tant que back-end; toi mai voir certains frais généraux de dplyr mais les chances sont c'est votre question.

Quand dplyr a des problèmes de performance avec les back-ends, vous pouvez les contourner en enregistrant une fonction pour l'évaluation hybride ou (dans le cas des bases de données) manipuler la requête générée avant l'exécution.

Voir aussi la réponse acceptée à quand est mieux que data.table?


42