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

760

Aperçu

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

  1. data.tableet dplyrsont comparables en vitesse, sauf lorsqu'il existe de nombreux groupes (c.-à-d.> 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 fera) les interactions DB potentielles
  4. Il y a quelques différences de fonctionnalités mineures (voir "Exemples / Utilisation" ci-dessous)

Dans mon esprit, 2. n'a pas beaucoup de poids car je le connais assez bien data.table, même si je comprends que pour les utilisateurs novices, ce sera un facteur important. Je voudrais éviter un argument 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 est déjà familier 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 package pour les personnes familières avec les packages (c'est-à-dire une combinaison de touches requise par rapport au niveau requis d'ésotérisme, où moins de chacun est une bonne chose).
  2. Existe-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 package par rapport à un autre?

Une question récente sur les SO m'a fait réfléchir un peu plus, car jusqu'à ce moment-là, je ne pensais pas dplyroffrir beaucoup plus que ce que je pouvais déjà faire data.table. Voici la dplyrsolution (données en 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 piratage pour data.tabletrouver une solution. Cela dit, les bonnes data.tablesolutions sont également assez bonnes (merci Jean-Robert, Arun, et notez ici que j'ai préféré une seule déclaration à la solution strictement 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 en avez l'habitude data.table(c'est- à -dire qu'elle n'utilise pas certaines des astuces les plus ésotériques).

Idéalement, ce que j'aimerais voir, ce sont de bons exemples où la manière dplyrou data.tableest beaucoup plus concise ou fonctionne beaucoup mieux.

Exemples

Usage
  • dplyrne permet pas les opérations groupées qui retournent un nombre arbitraire de lignes (à partir de la question d' eddi , remarque: cela semble être implémenté dans dplyr 0.5 , également, @beginneR montre une solution de contournement potentielle à utiliser dodans la réponse à la question de @ eddi).
  • data.tableprend en charge les jointures roulantes (merci @dholstius) ainsi que les jointures à chevauchement
  • data.tableoptimise en interne les expressions du formulaire DT[col == value]ou DT[col %in% values]pour la vitesse grâce à l'indexation automatique qui utilise la recherche binaire tout en utilisant la même syntaxe de base R. Voir ici pour plus de détails et une petite référence.
  • dplyroffre des versions d'évaluation standard de fonctions (par exemple regroup, summarize_each_) qui peuvent simplifier l'utilisation programmatique de dplyr(notez que l'utilisation programmatique de data.tableest certainement possible, nécessite juste une réflexion, une substitution / citation, etc., au moins à ma connaissance)
Repères

Les données

Ceci est 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))
BrodieG
la source
9
La solution qui est similaire en lecture à celle- dplyrci est:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)]
eddi
7
Pour # 1 à la fois dplyret les data.tableéquipes travaillent sur des repères, donc une réponse sera là à un moment donné. # 2 (syntaxe) imO est strictement faux, mais cela s'aventure clairement en territoire d'opinion, donc je vote pour fermer aussi.
eddi
13
eh bien, encore une fois imO, l'ensemble des problèmes qui s'expriment plus clairement dans (d)plyra la mesure 0
eddi
28
@BrodieG la seule chose qui vraiment me dérange à la fois dplyret plyren ce qui concerne la syntaxe et est fondamentalement la principale raison pour laquelle je n'aime pas leur syntaxe, est que je dois apprendre beaucoup trop (lire plus de 1) des fonctions supplémentaires (avec des noms qui encore ça n'a pas de sens pour moi), rappelez-vous ce qu'ils font, quels arguments ils soutiennent, etc.
eddi
43
@eddi [ironique] la seule chose qui me dérange vraiment à propos de la syntaxe data.table est que je dois apprendre comment trop d'arguments de fonction interagissent et ce que signifient les raccourcis cryptiques (par exemple .SD). [sérieusement] Je pense que ce sont des différences de conception légitimes qui
plairont

Réponses:

532

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

Mon intention est de couvrir chacun de ces éléments aussi clairement que possible du point de vue data.table.

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


La syntaxe data.table est cohérente dans sa forme - DT[i, j, by]. Garder i, jet byensemble, c'est par la conception. En gardant les opérations liées ensemble, il permet d' optimiser facilement les opérations pour la vitesse et surtout l' utilisation de la mémoire , et fournit également des fonctionnalités puissantes , tout en maintenant la cohérence de la syntaxe.

1. Vitesse

Un certain nombre de références (bien que principalement sur les opérations de regroupement) ont été ajoutées à la question montrant déjà data.table devient plus rapide que dplyr lorsque le nombre de groupes et / ou de lignes à regrouper augmente, y compris les références de Matt sur le regroupement de 10 millions à 2 milliards de lignes (100 Go de RAM) sur 100 à 10 millions de groupes et différentes colonnes de regroupement, ce qui se compare également pandas. Voir également les repères mis à jour , qui incluent Sparket pydatatableégalement.

Concernant les repères, il serait intéressant de couvrir également ces aspects restants:

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

  • Comparez d'autres opérations telles que la mise à jour et les jointures .

  • Évaluez également l' 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 cet article .

    Notez que le commentaire de Hadley parle de vitesse (ce dplyr est rapide pour lui), alors que la principale préoccupation ici est la 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 mettra jamais à jour par référence. L'équivalent 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 la transparence référentielle . La mise à jour d'un objet data.table par référence, en particulier au sein d'une fonction, n'est pas toujours souhaitable. Mais c'est une fonctionnalité incroyablement utile: consultez ceci et ces articles pour des cas intéressants. Et nous voulons le garder.

    Par conséquent, nous travaillons à l'exportation de la shallow()fonction dans data.table qui fournira à l'utilisateur les deux possibilités . Par exemple, s'il est souhaitable de ne pas modifier le data.table d'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 ne l'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 à l' aide de shallow(), nous comprenons que vous ne souhaitez pas modifier l'objet d'origine. Nous nous occupons de tout en interne pour nous assurer que tout en veillant à ne copier les colonnes que vous les modifiez uniquement lorsque cela est absolument nécessaire . Une fois implémenté, cela devrait régler complètement le problème de transparence référentielle tout en offrant à l'utilisateur les deux possibilités.

    De plus, une fois shallow()exportée, 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 toujours de nombreuses fonctionnalités fournies par data.table, y compris la (sous-) affectation par référence.

  3. Agréger lors de l'adhésion:

    Supposons que vous ayez deux tableaux de données 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) * mulpour chaque ligne DT2tout en se joignant par des colonnes x,y. On peut soit:

    • 1) agréger DT1pour 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 seule fois (en utilisant la by = .EACHIfonction):

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

    Quel est l'avantage?

    • Nous n'avons pas à allouer de 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 la jonction).

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

    Consultez cet article pour une explication détaillée de by = .EACHI. Aucun résultat intermédiaire n'est matérialisé et la jointure + agrégat est effectuée en une seule fois.

    Jetez un œil à ceci , ceci et cela pour des scénarios d'utilisation réels.

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

  4. Mise à jour et jointures:

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

    DT1[DT2, col := i.mul]

    ajoute / met à jour DT1la colonne colavec à mulpartir de DT2sur les lignes où DT2la colonne clé 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 *_joinopération, qui devrait copier l'intégralité DT1juste pour y ajouter une nouvelle colonne, ce qui est inutile.

    Consultez ce post pour un scénario d'utilisation réel.

Pour résumer, il est important de réaliser que chaque optimisation est importante. Comme dirait Grace Hopper , faites attention à vos nanosecondes !

3. Syntaxe

Regardons maintenant la 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, c'est de contraster la cohérence syntaxique . Nous comparerons côte à côte data.table et dplyr syntax.

Nous travaillerons avec les données fictives 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 avons dû utiliser filter()dans dplyr tout en résumant . Mais lors de la mise à jour , 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érons sur les lignes où x > 2, mais dans le premier cas, obtiennent sum(y), alors que dans le second cas mettez à jour ces lignes pour yavec sa somme cumulée.

      C'est ce que nous voulons dire lorsque nous disons que le DT[i, j, by]formulaire est cohérent .

    • De même, dans le cas (c), lorsque nous avons une if-elsecondition, nous pouvons exprimer la logique "telle quelle " dans data.table et dplyr. Cependant, si nous voulons retourner uniquement les lignes où la ifcondition satisfait et ignorer le contraire, nous ne pouvons pas utiliser summarise()directement (AFAICT). Nous devons d' filter()abord et ensuite résumer car summarise()attend toujours une seule valeur .

      Bien qu'elle renvoie le même résultat, l'utilisation 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 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 dplyrintroduit *_each()avec un tas de fonctions funs().

    • data.table :=nécessite que les noms de colonne soient fournis, tandis que dplyr le 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 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 renvoyer une liste j. Chaque élément de la liste deviendra une colonne dans le résultat. Donc, nous pouvons utiliser, encore une fois, la fonction de base familière c()pour concaténer .Nà un listqui retourne a list.

    Remarque: Encore une fois, 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 de résultat. Vous pouvez utiliser c(), as.list(), lapply(), list()etc ... les fonctions de base pour ce faire , sans avoir à apprendre de nouvelles fonctions.

    Vous devrez apprendre uniquement les variables spéciales - .Net .SDau moins. L'équivalent dans dplyr sont n()et.

  3. Rejoint

    dplyr fournit des fonctions distinctes 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 une merge.data.table()fonction équivalente comme 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.), tandis que d'autres pourraient aimer data.table DT[i, j, by], ou merge()qui est similaire à la base R.

    • Cependant, dplyr rejoint exactement cela. Rien de plus. Rien de moins.

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

    • data.tables peut s'agréger lors de la jointure (3) et également se mettre à jour lors de la jointure (4), en utilisant la by = .EACHIfonction. Pourquoi matérialiser l'intégralité du résultat de la jointure pour ajouter / mettre à jour seulement quelques colonnes?

    • data.table est capable de faire rouler les jointures (5) - rouler en avant, LOCF , rouler en arrière, NOCB , le plus proche .

    • data.table a également un mult =argument qui sélectionne la première , la dernière ou toutes les correspondances (6).

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

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

  1. do()...

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

    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 jeter à peu près n'importe quoi j- la seule chose à retenir est qu'il retourne une liste afin que chaque élément de la liste soit converti en colonne.

    • Dans dplyr, je ne peux pas faire ça. Vous devez recourir à la do()façon dont vous êtes sûr de savoir si votre fonction retournera toujours une seule valeur. Et c'est assez lent.

Encore une fois, la syntaxe de data.table est cohérente avec DT[i, j, by]. Nous pouvons simplement continuer à lancer des expressions jsans avoir à nous soucier de ces choses.

Jetez un oeil à cette question SO et celle-ci . Je me demande s'il serait possible d'exprimer la réponse comme simple en utilisant la syntaxe de dplyr ...

Pour résumer, j'ai particulièrement mis en évidence plusieurs cas où la syntaxe de dplyr est soit inefficace, limitée ou échoue à rendre les opérations simples. Cela est particulièrement dû au fait que data.table obtient un peu de jeu 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 super. Mais il est important de réaliser également sa syntaxe et ses limites de fonctionnalités, et je n'ai pas encore vu de message à ce sujet.

data.table a aussi ses bizarreries (dont certaines que j'ai souligné 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 également 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 plus:

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

  • fwrite - un rédacteur de fichiers rapide parallélisé est maintenant disponible. Voir cet article pour une explication détaillée sur la mise en œuvre et # 1664 pour garder une trace des développements ultérieurs.

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

  • Regroupement ad hoc : dplyrtrie automatiquement les résultats en regroupant les variables pendant summarise(), ce qui n'est pas toujours souhaitable.

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

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

  • Des jointures de plage superposées ont été récemment implémentées dans data.table. Consultez cet article pour un aperçu des repères.

  • setorder() fonction dans data.table qui permet une réorganisation très rapide des data.tables par référence.

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

  • data.tablefournit des équivalents plus rapides des opérations réglées (écrit par Jan Gorecki) - fsetdiff, fintersect, funionet fsetequalavec plus allargument (comme dans SQL).

  • data.table se charge proprement sans avertissement de masquage et dispose d'un mécanisme décrit ici pour la [.data.framecompatibilité lorsqu'il est transmis à n'importe quel package R. dplyr modifie les fonctions de base filter, laget [qui peuvent 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 bousculé si les utilisateurs aimeraient beaucoup cette fonctionnalité. Je ne sais pas.

  • Sur le parallélisme - Tout est difficile, jusqu'à ce que quelqu'un aille de l'avant et le fasse. Bien sûr, cela demandera des efforts (en étant thread-safe).

    • Des progrès sont en cours (dans la version v1.9.7) pour paralléliser les parties chronophages connues pour des gains de performances incrémentiels OpenMP.
Arun
la source
9
@bluefeet: Je ne pense pas que vous nous ayez rendu de grands services en déplaçant cette discussion sur le chat. J'avais l'impression qu'Arun était l'un des développeurs et cela aurait pu donner des informations utiles.
IRTFM
2
Lorsque je suis allé discuter en utilisant votre lien, il est apparu que tout le matériel suivant le commentaire commençant par "Vous devriez utiliser un filtre" .. était parti. Suis-je en train de manquer quelque chose sur le mécanisme SO-chat?
IRTFM
6
Je pense que partout où vous utilisez l'affectation par référence ( :=), l' dplyréquivalent devrait également être utilisé <-comme au DF <- DF %>% mutate...lieu de simplementDF %>% mutate...
David Arenburg
4
Concernant la syntaxe. Je pense que cela dplyrpeut être plus facile pour les utilisateurs qui utilisaient la plyrsyntaxe, mais data.tablepeut être plus facile pour les utilisateurs qui ont utilisé pour interroger la syntaxe des langues comme SQL, et l'algèbre relationnelle derrière elle, qui concerne la transformation des données tabulaires. @Arun, vous devez noter que les opérateurs de set sont très faciles à faire en enveloppant la data.tablefonction et apportent bien sûr une accélération significative.
jangorecki
9
J'ai lu ce post tant de fois et cela m'a beaucoup aidé à comprendre data.table et à mieux l'utiliser. Dans la plupart des cas, je préfère data.table à dplyr ou pandas ou PL / pgSQL. Cependant, je ne pouvais pas m'arrêter de penser à comment l'exprimer. La syntaxe n'est pas facile, claire ou verbeuse. En fait, même après avoir beaucoup utilisé data.table, j'ai souvent du mal à comprendre mon propre code, j'ai écrit littéralement il y a une semaine. Ceci est un exemple de vie d'un langage en écriture seule. en.wikipedia.org/wiki/Write-only_language Alors, espérons que nous pourrons un jour utiliser dplyr sur data.table.
Ufos
385

Voici ma tentative d'une réponse complète du point de vue de dplyr, en suivant les grandes lignes de la réponse d'Arun (mais quelque peu réarrangé en fonction de priorités différentes).

Syntaxe

Il y a une certaine subjectivité dans la syntaxe, mais je maintiens mon affirmation selon laquelle la concision de data.table rend plus difficile à apprendre et à 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, c'est qu'elle limite 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, ainsi qu'un adverbe "par groupe". Cette contrainte est d'une grande aide lorsque vous apprenez la manipulation de données, car elle vous aide à réfléchir à votre problème. Dans dplyr, chacun de ces verbes est associé à une seule fonction. Chaque fonction fait un travail et est facile à comprendre isolément.

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

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 (ou même R!), Vous pouvez toujours obtenir l'essentiel de ce qui se passe car les fonctions sont tous 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 à moins que vous ne soyez déjà familier avec data.table. (Je ne pouvais pas non plus comprendre comment mettre en retrait le répété [ d'une manière qui me semble bonne). Personnellement, quand je regarde le code que j'ai écrit il y a 6 mois, c'est comme regarder un code écrit par un inconnu, donc j'en suis venu à préférer un code simple, mais verbeux.

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

  • Étant donné que presque toutes les opérations de table de données utilisent, [vous avez besoin d'un contexte supplémentaire pour comprendre ce qui se passe. Par exemple, x[y] joindre deux tables de données ou extraire 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()c'est une opération distincte dans dplyr. Cela change fondamentalement le calcul, donc je pense que cela devrait être évident lors de l'écrémage du code, et c'est plus facile à repérer group_by()que l' byargument [.data.table.

J'aime aussi que le tuyau ne se limite pas à 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 packages 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 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 canaliser avec %>%ne se limite pas aux seules trames de données et est facilement généralisée à d'autres contextes: graphiques web interactifs , scraping web , gists , contrats d'exécution , ...)

Mémoire et performances

Je les ai regroupés, car, pour moi, ils ne sont pas si importants. La plupart des utilisateurs R travaillent avec bien moins d'un million de lignes de données, et dplyr est suffisamment rapide pour cette taille de données que vous ne connaissez pas le temps de traitement. Nous optimisons dplyr pour l'expressivité sur les 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 de trame de données ne vous conviennent pas, vous pouvez utiliser le backend de data.table (bien qu'avec un ensemble de fonctionnalités quelque peu restreint). Si les données avec lesquelles vous travaillez ne tiennent pas en 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 grandes idées de data.table comme la commande radix et l'utilisation du même index pour les jointures et les filtres. Nous travaillons également sur la parallélisation afin de pouvoir profiter de plusieurs cœurs.

Caractéristiques

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

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

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

  • Regroupement plus flexible comme des échantillons bootstrap, des rollups et plus

J'investis également du temps dans l'amélioration des connecteurs de base de données de R , la possibilité de parler aux API Web et de faciliter le raclage des pages html .

hadley
la source
27
Juste une note latérale, je suis d'accord avec beaucoup de vos arguments (bien que je préfère la data.tablesyntaxe moi-même), mais vous pouvez facilement l'utiliser %>%pour diriger les data.tableopérations si vous n'aimez pas le [style. %>%n'est pas spécifique à dplyr, mais provient plutôt d'un package séparé (dont vous êtes également le co-auteur), donc je ne suis pas sûr de comprendre ce que vous essayez de dire dans la plupart de votre paragraphe de syntaxe .
David Arenburg
11
@DavidArenburg bon point. J'ai réécrit la syntaxe pour, espérons-le, clarifier mes principaux points et souligner que vous pouvez utiliser %>%avec data.table
hadley
5
Merci Hadley, c'est une perspective utile. Ré-indentation, je fais généralement DT[\n\texpression\n][\texpression\n]( gist ) qui fonctionne vraiment bien. Je garde la réponse d'Arun comme réponse car il répond plus directement à mes questions spécifiques qui ne concernent pas tellement l'accessibilité de la syntaxe, mais je pense que c'est une bonne réponse pour les personnes qui essaient d'avoir une idée générale des différences / similitudes entre dplyret data.table.
BrodieG
33
Pourquoi travailler sur Fastread alors qu'il y en a déjà fread()? Le temps ne serait-il pas mieux consacré à améliorer fread () ou à travailler sur d'autres choses (sous-développées)?
EDi
10
L'API de data.tableest fondée sur un abus massif de la []notation. C'est sa plus grande force et sa plus grande faiblesse.
Paul
65

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

dplyr fait définitivement des choses qui data.tablene peuvent pas.

Votre point # 3

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

est une réponse directe à votre propre question mais n'est pas élevée à un niveau suffisamment élevé. dplyrest véritablement un frontal extensible à plusieurs mécanismes de stockage de données, tout comme data.tableune extension à un seul.

Regardez dplyrcomme une interface agnostique dorsale, avec toutes les cibles utilisant la même grammaire, où vous pouvez étendre les cibles et les gestionnaires à volonté. data.tableest, du dplyrpoint de vue, l'un de ces objectifs.

Vous ne verrez jamais (j'espère) un jour qui data.tabletentera 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.

dplyrpeut éventuellement faire les choses ne data.tablesera pas ou pourrait ne pas faire aussi bien.

Basé sur la conception du travail en mémoire, il data.tablepourrait être beaucoup plus difficile de s'étendre au traitement parallèle des requêtes dplyr.


En réponse aux questions internes ...

Usage

Y a-t-il des tâches analytiques qui sont beaucoup plus faciles à coder avec l'un ou l'autre package pour les personnes familières avec les packages (c'est-à-dire une combinaison de touches requise par rapport au niveau requis d'ésotérisme, où moins de chacun est une bonne chose).

Cela peut sembler un coup de volée, mais la vraie réponse est non. Les personnes familiarisées avec les outils semblent utiliser celle qui leur est la plus familière ou celle qui est en fait la bonne pour le travail à accomplir. Cela étant dit, parfois vous voulez 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 rendre les abstractions plus claires .

Performance

Existe-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 package par rapport à un autre?

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

Cela signifie que lorsque vous rencontrez un problème de performances avec, data.tablevous pouvez être sûr qu'il se trouve dans votre fonction de requête et s'il s'agit en fait d'un goulot d'étranglement, data.tablevous vous êtes gagné la joie de déposer un rapport. Cela est également vrai lorsque vous l' dplyrutilisez data.tablecomme back-end; vous pouvez voir des frais généraux dplyrmais il y a de fortes chances que ce soit votre requête.

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

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

Thell
la source
3
Cant dplyr wrap a data.table with tbl_dt? Pourquoi ne pas simplement tirer le meilleur parti des deux mondes?
aaa90210
22
Vous oubliez de mentionner la déclaration inverse "data.table fait définitivement des choses que dplyr ne peut pas", ce qui est également vrai.
jangorecki
25
La réponse d'Arun l'explique bien. Les plus importants (en termes de performances) seraient la peur, la mise à jour par référence, les jointures tournantes, les jointures qui se chevauchent. Je crois qu'il n'y a aucun package (pas seulement dplyr) qui puisse rivaliser avec ces fonctionnalités. Un bel exemple peut être la dernière diapositive de cette présentation.
jangorecki
15
Au total, data.table est la raison pour laquelle j'utilise toujours R. Sinon, j'utiliserais des pandas. C'est encore meilleur / plus rapide que les pandas.
marbel
8
J'aime data.table en raison de sa simplicité et de sa ressemblance avec la structure de la syntaxe SQL. Mon travail consiste à faire des analyses de données ad hoc et des graphiques très intenses tous les jours pour la modélisation statistique, et j'ai vraiment besoin d'un outil assez simple pour faire des choses compliquées. Maintenant, je peux réduire ma boîte à outils à data.table uniquement pour data et lattice pour graph dans mon travail quotidien. Donnez un exemple, je peux même faire des opérations comme celle-ci: $ DT [groupe == 1, y_hat: = prédire (fit1, data = .SD),] $, ce qui est vraiment bien et je le considère comme un grand avantage de SQL dans environnement R classique.
xappppp