web-dev-qa-db-fra.com

data.table vs dplyr: l'un peut-il faire quelque chose de bien l'autre ne peut pas ou fait mal?

Vue d'ensemble

Je suis relativement familier avec data.table, pas tellement avec dplyr. J'ai lu quelques dplyr vignettes et des exemples qui sont apparus à propos de 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 certaines autres circonstances (voir les repères ci-dessous).
  2. dplyr a une syntaxe plus accessible
  3. dplyr résume (ou testera) les interactions de base de données potentielles
  4. Il y a quelques différences de fonctionnalités mineures (voir "Exemples/Utilisation" ci-dessous)

Dans mon esprit, 2. ne porte pas beaucoup de poids parce que je le connais assez bien data.table, bien que je sache que pour les utilisateurs novices, ce sera un facteur important. Je voudrais éviter un argument sur ce qui est plus intuitif, car cela n’est pas pertinent pour ma question spécifique posée du point de vue de quelqu'un qui connaît déjà data.table. J'aimerais également éviter une discussion sur la manière dont "plus intuitif" conduit à une analyse plus rapide (certes, mais encore une fois, ce qui ne m'intéresse pas ici).

Question

Ce que je veux savoir, c'est:

  1. Existe-t-il des tâches analytiques qui sont beaucoup plus faciles à coder avec l’un ou l’autre des packages pour les personnes familiarisées avec ces derniers (c'est-à-dire une combinaison de touches requises par rapport au niveau requis d'ésotérisme, où moins de chacun est une bonne chose).
  2. Existe-t-il des tâches d'analyse qui sont effectuées de manière substantielle (c'est-à-dire plus de 2x) plus efficacement dans un package par rapport à un autre?.

Une récente SO question m'a fait réfléchir un peu plus, car jusqu'alors je ne pensais pas que dplyr offrirait bien plus que ce que je peux déjà faire. dans data.table. Voici la solution dplyr (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 bien mieux que ma tentative de bidouillage d'une solution data.table. Cela dit, les bonnes solutions data.table sont également satisfaisantes (merci, Jean-Robert, Arun, et notez bien que je privilégiais les déclarations uniques plutôt que 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 plutôt simple si vous êtes habitué à data.table (c’est-à-dire qu’il n’utilise pas certaines astuces plus ésotériques).

Idéalement, ce que j'aimerais voir, c’est quelques bons exemples dans lesquels la méthode dplyr ou data.table est nettement plus concise ou fonctionne nettement mieux.

Exemples

  • dplyr n'autorise pas les opérations groupées qui renvoient un nombre arbitraire de lignes (de question d'Eddi, remarque: cela ressemble comme il sera implémenté dans dplyr 0.5, aussi, @beginneR montre une solution potentielle en utilisant do dans la réponse à la question de @ eddi).
  • data.table supporte jointure par roulement (merci @dholstius) ainsi que jointure de chevauchement
  • data.table optimise en interne les expressions de la forme DT[col == value] ou DT[col %in% values] pour la vitesse à travers l'indexation automatique qui utilise la recherche binaire en utilisant la même syntaxe de base R. Voir ici pour plus de détails et un repère minuscule.
  • dplyr propose des versions d'évaluation standard de fonctions (par exemple, regroup, summarize_each_) pouvant simplifier l'utilisation de dplyr par programmation (il est tout à fait possible d'utiliser data.table par programmation. , nécessite juste une réflexion, une substitution/une citation, etc., du moins à ma connaissance)

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

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

Mon intention est de couvrir chacun d’entre eux le plus clairement possible du point de vue de data.table.

Remarque: sauf indication explicite contraire, en faisant référence à 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]_. Conserver i, j et by ensemble est de par leur conception. En gardant les opérations connexes ensemble, cela permet de facilement optimiser opérations pour vitesse et plus important encore tilisation de la mémoire, et aussi de fournir puissant fonctionnalités, tout en maintenant la cohérence de la syntaxe.

1. vitesse

Un certain nombre de points de repère (bien que principalement sur les opérations de regroupement) ont été ajoutés à la question, indiquant déjà que data.table obtient plus rapide que dplyr en nombre de groupes et/ou de lignes à regrouper par augmentation, notamment - benchmarks by Matt sur le regroupement de 10 millions à 2 milliards de lignes (100 Go en RAM) sur 100 à 10 millions de groupes et les colonnes de regroupement variables, qui compare également pandas. Voir aussi benchmarks mis à jour , qui comprend également Spark et pydatatable.

Sur des points de repère, il serait bon de couvrir également ces aspects restants:

  • Regroupement des opérations impliquant un sous-ensemble de lignes - c.-à-d., Opérations de type DT[x > val, sum(y), by = z].

  • Comparez les autres opérations telles que pdate et joins.

  • Également benchmark empreinte mémoire pour chaque opération en plus de l'exécution.

2. Utilisation de la mémoire

  1. Les opérations impliquant filter() ou slice() dans dplyr peuvent être inefficaces en mémoire (à la fois sur data.frames et data.tables). Voir ce post .

    Notez que commentaire de Hadley parle de vitesse (ce qui est rapide pour lui), alors que la préoccupation majeure ici est mémoire.

  2. à l'heure actuelle, l'interface data.table permet de modifier/mettre à jour les colonnes par référence (notez qu'il n'est pas nécessaire de réaffecter le résultat à une variable).

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

    Mais dplyr ne pourra jamais mettre à jour par référence. L'équivalent de dplyr serait (notez que le résultat doit être réaffecté):

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

    Une préoccupation à cet égard est transparence référentielle . Mettre à jour un objet data.table par référence, en particulier dans une fonction, peut ne pas être toujours souhaitable. Mais ceci est une fonctionnalité extrêmement utile: voir this et this postes pour les cas intéressants. Et nous voulons le garder.

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

    _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 une copie superficielle en utilisant shallow(), nous comprenons que vous ne souhaitiez pas modifier l'objet d'origine. Nous nous occupons de tout en interne pour nous assurer que, tout en nous assurant de copier les colonnes, vous modifiez niquement lorsque cela est absolument nécessaire. Une fois implémenté, cela devrait régler le problème transparence référentielle tout en offrant à l'utilisateur les deux possibilités.

    De plus, une fois que shallow() est exporté, l'interface data.table de dplyr devrait éviter presque toutes les copies. Ainsi, ceux qui préfèrent la syntaxe de dplyr peuvent l’utiliser avec data.tables.

    Mais il manquera encore de nombreuses fonctionnalités fournies par data.table, notamment le (sous) -assignment par référence.

  3. Agréger en rejoignant:

    Supposons que vous ayez 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 voudriez obtenir sum(z) * mul pour chaque ligne de _DT2_ en joignant par les colonnes _x,y_. Nous pouvons soit:

    • 1) agrégez _DT1_ pour obtenir sum(z), 2) effectuez une jointure et 3) multipliez (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 la fonction _by = .EACHI_):

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

    Quel est l'avantage?

    • Il n'est pas nécessaire d'allouer de la mémoire pour le résultat intermédiaire.

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

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

    Vérifiez cet article pour une explication détaillée de _by = .EACHI_. Aucun résultat intermédiaire n'est matérialisé et l'assemblage + l'agrégat est effectué en une fois.

    Jetez un oeil à this , this et this posts pour de vrais scénarios d'utilisation.

    Dans dplyr il vous faudrait joindre et agréger ou agréger d'abord et ensuite adhérer , aucun des deux n'étant aussi efficace, en termes de mémoire (qui à son tour se traduit par vitesse).

  4. Mettre à jour et rejoindre:

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

    _DT1[DT2, col := i.mul]
    _

    ajoute/met à jour la colonne de _DT1_ col avec mul de _DT2_ sur les lignes où la colonne de clé de _DT2_ correspond à _DT1_. Je ne pense pas qu'il y ait un équivalent exact de cette opération dans dplyr, c'est-à-dire sans éviter une opération _*_join_, qui devrait copier l'intégralité de _DT1_ simplement pour ajouter une nouvelle colonne. à ce qui est inutile.

    Vérifiez cet article pour un scénario d'utilisation réel.

Pour résumer, il est important de réaliser que chaque optimisation compte. En tant que Grace Hopper dirais, Attention à vos nanosecondes !

3. Syntaxe

Regardons maintenant syntax. Hadley a commenté ici :

Les tables de données sont extrêmement rapides mais je pense que leur concision 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, c’est de contraster cohérence de la syntaxe. Nous comparerons côte à côte la syntaxe data.table et dplyr.

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

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

    _# 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 prolixe. Les choses sont plus ou moins équivalentes dans le cas (a).

    • Dans le cas (b), nous devions utiliser filter() dans dplyr tandis que résumant. Mais alors que pdate, nous avons dû déplacer la logique dans mutate(). Dans data.table cependant, nous exprimons les deux opérations avec la même logique - nous opérons sur des lignes où _x > 2_, mais dans le premier cas, obtenez sum(y), alors que dans le second cas, mettez à jour ces lignes pour y avec sa valeur cumulative. somme.

      C’est ce que nous voulons dire lorsque nous disons que la forme _DT[i, j, by]_ est cohérente.

    • De même dans le cas (c), lorsque nous avons la condition _if-else_, nous pouvons exprimer la logique "as-is" à la fois dans data.table et dplyr. Cependant, si nous souhaitons ne renvoyer que les lignes où la condition if satisfait et ignorer sinon, nous ne pouvons pas utiliser summarise() directement (AFAICT). Nous devons d'abord filter() puis résumer car summarise() attend toujours un valeur unique.

      Bien qu'elle renvoie le même résultat, l'utilisation de filter() ici rend l'opération réelle moins évidente.

      Il pourrait très bien être possible d'utiliser filter() dans le premier cas également (cela ne me semble pas évident), mais ce que je veux dire, c'est que nous ne devrions pas être obligés de 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 la fonction de base familière lapply(), alors que dplyr introduit *_each() avec un tas de fonctions à funs().

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

    • Dans le cas (b), la syntaxe de dplyr est relativement simple. L'amélioration des agrégations/mises à jour sur plusieurs fonctions figure dans la liste de data.table.

    • Dans le cas (c) cependant, dplyr renverrait n() autant de fois que de colonnes, au lieu d’une seule fois. Dans data.table, tout ce que nous avons à faire est de renvoyer une liste dans j. Chaque élément de la liste deviendra une colonne dans le résultat. Nous pouvons donc utiliser à nouveau la fonction de base bien connue c() pour concaténer _.N_ en un list qui retourne un list.

    Remarque: Une fois encore, dans data.table, il suffit de renvoyer une liste dans j. Chaque élément de la liste deviendra une colonne dans le résultat. Vous pouvez utiliser les fonctions de base c(), as.list(), lapply(), list() etc ..., sans avoir à en apprendre de nouvelles.

    Vous devrez apprendre uniquement les variables spéciales - _.N_ et _.SD_ au moins. Les équivalents dans dplyr sont n() et _._

  3. Jointures

    dplyr fournit des fonctions distinctes pour chaque type de jointure, où data.table autorise les jointures avec la même syntaxe _DT[i, j, by]_ (et avec raison). Il fournit également une fonction merge.data.table() équivalente à la place.

    _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 aimeraient le fichier _DT[i, j, by]_ ou merge() de data.table, similaire à la base R.

    • Cependant, Dplyr fait exactement cela. Rien de plus. Rien de moins.

    • data.tables peut sélectionner des colonnes lors de l'adhésion (2). Sous dplyr, vous devrez d'abord select() sur les deux data.frames avant de rejoindre, comme indiqué ci-dessus. Sinon, vous devriez matérialiser la jointure avec des colonnes inutiles uniquement pour les supprimer ultérieurement, ce qui est inefficace.

    • data.tables peut agréger en rejoignant ​​(3) et aussi mettre à jour en rejoignant ​​(4), à l'aide de la fonction _by = .EACHI_. Pourquoi matérialiser tout le résultat de la jointure pour n’ajouter/mettre à jour que quelques colonnes?

    • data.table est capable de jointure par roulement ​​(5) - roll en avant, LOCF , en arrière, NOCB , le plus proche =.

    • data.table possède également un argument _mult =_ qui sélectionne premier, dernier ou tout ​​correspond à (6).

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

Une fois encore, la syntaxe est cohérente avec _DT[i, j, by]_ avec des arguments supplémentaires permettant de contrôler davantage la sortie.

  1. do()...

    dplyr's resume est spécialement conçu pour les fonctions qui renvoient une seule valeur. Si votre fonction retourne plusieurs valeurs/inégales, vous devrez recourir à do(). Vous devez savoir à l'avance que toutes vos fonctions renvoient une valeur.

    _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))))
    _
    • L'équivalent de _.SD_ est _._

    • Dans data.table, vous pouvez créer à peu près n'importe quoi dans j - la seule chose à retenir est de renvoyer une liste afin que chaque élément de la liste soit converti en colonne.

    • En dplyr, ne peut pas faire ça. Vous devez recourir à do() en fonction de votre certitude de savoir si votre fonction renverra toujours une valeur unique. Et c'est assez lent.

Une fois encore, la syntaxe de data.table est conforme à _DT[i, j, by]_. Nous pouvons simplement continuer à lancer des expressions dans j sans avoir à nous soucier de ces choses.

Regardez cette SO question et celui-ci . Je me demande s'il serait possible d'exprimer la réponse aussi simplement en utilisant la syntaxe de dplyr ...

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

data.table a aussi ses bizarreries (quelques-unes de celles-ci que j'ai indiquées que nous essayons 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 manquent à 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 - Un lecteur de fichiers rapide est disponible depuis longtemps.

  • fwrite - un parallélisé un graveur de fichier rapide est maintenant disponible. Voir cet article pour une explication détaillée de la mise en œuvre et # 1664 pour suivre l'évolution de la situation.

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

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

  • Nombreux avantages des jointures data.table (pour la rapidité/efficacité de la mémoire et la syntaxe) mentionnés ci-dessus.

  • Jointures non équitables : Permet les jointures utilisant d'autres opérateurs _<=, <, >, >=_ avec tous les autres avantages des jointures data.table.

  • La plage se chevauchant rejoint a été récemment implémenté dans data.table. Vérifiez cet article pour un aperçu des points de repère.

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

  • dplyr fournit interface avec les bases de données en utilisant la même syntaxe, ce que data.table ne fait pas pour le moment.

  • _data.table_ fournit des équivalents plus rapides de opérations définies (écrit par Jan Gorecki) - fsetdiff, fintersect, funion et fsetequal avec des options supplémentaires all argument (comme en SQL).

  • data.table se charge proprement sans avertissements de masquage et possède un mécanisme décrit ici pour la compatibilité _[.data.frame_ lorsqu'il est transmis à un package R. dplyr change les fonctions de base filter, lag et _[_ qui peuvent causer des problèmes; par exemple. ici et ici .


Finalement:

  • Sur les bases de données, rien ne justifie que data.table ne puisse pas fournir une interface similaire, mais ce n’est pas une priorité pour le moment. Il se peut que les utilisateurs apprécient beaucoup cette fonctionnalité. Vous n'êtes pas sûr.

  • Sur le parallélisme - Tout est difficile, jusqu'à ce que quelqu'un le fasse et le fasse. Bien sûr, il faudra un effort (être en sécurité).

    • Des progrès sont en cours (dans la version 1.9.7) pour la parallélisation des composants connus qui prennent du temps pour des gains de performances incrémentiels à l’aide de OpenMP.
479
Arun

Voici ma tentative de réponse globale du point de vue de dplyr, en suivant les grandes lignes de la réponse d’Arun (mais quelque peu réorganisée en fonction de priorités différentes).

Syntaxe

Il existe une certaine subjectivité dans la syntaxe, mais je maintiens mon affirmation selon laquelle la concision de data.table rend plus difficile l’apprentissage et la lecture. 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 cela contraint vos options. Je prétends que la plupart des problèmes de table unique peuvent être résolus avec seulement cinq verbes clés: filtrer, sélectionner, muter, organiser et résumer, avec un adverbe "par groupe". Cette contrainte est d’une grande aide lorsque vous apprenez à manipuler des données, car elle vous aide à mieux réfléchir au problème. Dans dplyr, chacun de ces verbes est mappé à une seule fonction. Chaque fonction fait un travail et est facile à comprendre de manière isolée.

Vous créez une complexité en associant ces opérations simples à %>%. Voici un exemple tiré de l’un des articles 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 auparavant (ni même R!), Vous pouvez quand même avoir l'essentiel de ce qui se passe car les fonctions sont toutes des verbes anglais. 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 si vous n'êtes pas déjà familiarisé avec data.table. (Je ne pouvais pas non plus comprendre comment indenter le [ répété d'une manière qui soit belle à mes yeux). Personnellement, quand je regarde le code que j’ai écrit il ya 6 mois, c’est comme un code écrit par un inconnu, c’est pourquoi j’ai choisi de préférer le code simple, même verbeux.

Deux autres facteurs mineurs qui, à mon avis, diminuent légèrement la lisibilité:

  • Étant donné que presque chaque opération de table de données utilise [, vous avez besoin d'un contexte supplémentaire pour comprendre ce qui se passe. Par exemple, x[y] joint-il deux tables de données ou extrait-il des colonnes d'un bloc de données? Ce n'est qu'un petit problème, car dans un code bien écrit, les noms de variables devraient suggérer ce qui se passe.

  • J'aime que group_by() soit une opération distincte dans dplyr. Cela change fondamentalement le calcul, donc je pense que cela devrait être évident lorsque vous parcourez le code, et il est plus facile de repérer group_by() que l'argument by de [.data.table.

J'aime aussi que le le tuya ne soit pas limité à un seul paquet. Vous pouvez commencer par ranger vos données avec tidyr , et terminer avec un tracé dans ggvis . Et vous n'êtes pas limité aux paquets que j'écris - n'importe qui peut écrire une fonction qui fait partie intégrante d'un canal de manipulation de données. En fait, je préfère le code précédent data.table 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 blocs de données et s’applique facilement à d’autres contextes: graphiques Web interactifs , Web scraping , gists , contrats d'exécution , ...)

Mémoire et performance

Je les ai regroupés parce que, pour moi, ils ne sont pas si importants. La plupart des utilisateurs de R travaillent avec moins d'un million de lignes de données et dplyr est suffisamment rapide pour cette taille de données, de sorte que vous ne connaissez pas le temps de traitement. Nous optimisons dplyr pour son expressivité sur des données moyennes. n'hésitez pas à utiliser data.table pour une vitesse brute sur des données plus volumineuses.

La flexibilité de dplyr signifie également que vous pouvez facilement modifier les caractéristiques de performance en utilisant la même syntaxe. Si les performances de dplyr avec le backend du cadre de données ne vous suffisent pas, vous pouvez utiliser le backend data.table (bien qu'avec un ensemble de fonctionnalités quelque peu restreint). Si les données que vous utilisez ne tiennent pas dans la mémoire, vous pouvez utiliser un backend de base de données.

Cela dit, les performances de Dplyr s'amélioreront à long terme. Nous allons certainement implémenter certaines des bonnes idées de data.table telles que le tri des bases et l'utilisation du même index pour les jointures et les filtres. Nous travaillons également sur la parallélisation afin de pouvoir tirer parti de plusieurs cœurs.

Caractéristiques

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

  • le package readr, pour faciliter la récupération de fichiers sur disque et dans la mémoire, de manière analogue à fread().

  • Des jointures plus flexibles, notamment la prise en charge des jointures non équi.

  • Regroupement plus souple tel que bootstrap échantillons, rollups, etc.

J'investis également du temps dans l'amélioration de R connecteurs de base de données , la capacité de parler à apis Web , et de le rendre plus facile à supprimer des pages html .

362
hadley

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

dplyr fait certainement des choses que data.table ne peut pas.

Votre point n ° 3

dplyr résume (ou testera) les interactions potentielles entre bases de données

est une réponse directe à votre propre question mais n’est pas suffisamment élevée. dplyr est véritablement une interface frontale extensible à plusieurs mécanismes de stockage de données, où data.table est une extension d'un seul.

Regardez dplyr comme une interface agnostique d'arrière-plan, avec toutes les cibles utilisant la même grammaire, où vous pouvez étendre les cibles et les gestionnaires à votre guise. data.table est, de la perspective dplyr, l'un de ces objectifs.

Vous ne verrez jamais (j'espère) le jour où data.table tentera de traduire vos requêtes afin de créer des instructions SQL fonctionnant avec des magasins de données sur disque ou en réseau.

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

Sur la base de la conception du travail en mémoire, data.table pourrait avoir beaucoup plus de difficulté à s’étendre au traitement en parallèle de requêtes que dplyr.


En réponse aux questions sur le corps ...

Utilisation

Existe-t-il des tâches analytiques beaucoup plus faciles à coder avec l'un ou l'autre package pour les personnes familiarisées avec les packages (c'est-à-dire une combinaison de frappes au clavier requise par rapport à niveau requis d'ésotérisme, où moins de chacun est une bonne chose).

Cela peut sembler être une cagnotte, mais la vraie réponse est non. Les personnes familiarisées avec des outils semblent utiliser soit celui qui leur est le plus familier, soit celui qui leur convient réellement. Cela dit, vous souhaitez parfois présenter une lisibilité particulière, parfois un niveau de performance, et lorsque vous avez besoin d'un niveau suffisamment élevé des deux, vous pouvez simplement avoir besoin d'un autre outil pour accompagner ce que vous avez déjà pour clarifier les abstractions. .

Performance

Existe-t-il des tâches d'analyse qui sont effectuées de manière substantielle (c'est-à-dire plus de 2x) plus efficacement dans un package par rapport à un autre?.

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

Cela signifie que lorsque vous rencontrez un problème de performances avec data.table, vous pouvez être certain que cela se trouve dans votre fonction de requête et que si est réellement goulot d’étranglement avec data.table alors vous avez gagné le plaisir de déposer un rapport. Ceci est également vrai lorsque dplyr utilise data.table comme back-end; vous pouvez voir des frais supplémentaires provenant de dplyr mais les probabilités sont c'est votre requête.

Lorsque dplyr a des problèmes de performances avec les systèmes dorsaux, vous pouvez les contourner en enregistrant une fonction pour une évaluation hybride ou (dans le cas de bases de données) en manipulant la requête générée avant son exécution.

Voir aussi la réponse acceptée à quand plyr est-il meilleur que data.table?

56
Thell