dplyr sur data.table, est-ce que j'utilise vraiment data.table?

89

Si j'utilise la syntaxe de dplyr au-dessus d'un datatable , est-ce que j'obtiens tous les avantages de vitesse de datatable tout en utilisant toujours la syntaxe de dplyr? En d'autres termes, est-ce que j'utilise mal la table de données si je l'interroge avec la syntaxe dplyr? Ou dois-je utiliser une pure syntaxe datable pour exploiter toute sa puissance.

Merci d'avance pour tout conseil. Exemple de code:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

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

Résultats:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

Voici l'équivalence datable que j'ai trouvée. Je ne sais pas s'il est conforme aux bonnes pratiques de DT. Mais je me demande si le code est vraiment plus efficace que la syntaxe de dplyr dans les coulisses:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]
Polymérase
la source
7
Pourquoi n'utiliseriez-vous pas la syntaxe de table de données? C'est aussi élégant et efficace. Il n’est pas vraiment possible de répondre à la question car elle est très large. Oui, il existe des dplyrméthodes pour les tableaux de données, mais le tableau de données a également ses propres méthodes comparables
Rich Scriven
7
Je peux utiliser une syntaxe ou un cours datable. Mais d'une manière ou d'une autre, je trouve la syntaxe de dplyr plus élégante. Indépendamment de ma préférence pour la syntaxe. Ce que je veux vraiment savoir, c'est: dois-je utiliser une syntaxe purement datable pour bénéficier à 100% de la puissance datable.
Polymerase
3
Pour un benchmark récent où dplyrest utilisé sur data.frames et data.tables correspondant , voir ici (et les références qui y sont).
Henrik
2
@Polymerase - Je pense que la réponse à cette question est définitivement "Oui"
Rich Scriven
1
@Henrik: J'ai réalisé plus tard que j'avais mal interprété cette page car ils affichaient uniquement le code de la construction du dataframe mais pas le code qu'ils utilisaient pour la construction data.table. Quand je l'ai réalisé, j'ai supprimé mon commentaire (en espérant que vous ne l'aviez pas vu).
IRTFM

Réponses:

75

Il n'y a pas de réponse directe / simple car les philosophies de ces deux packages diffèrent sur certains aspects. Certains compromis sont donc inévitables. Voici quelques-unes des préoccupations que vous devrez peut-être aborder / considérer.

Opérations impliquant i(== filter()et slice()en dplyr)

Supposons DTavec, disons 10 colonnes. Considérez ces expressions data.table:

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1) donne le nombre de lignes dans la DTcolonne where a > 1. (2) renvoie mean(b)groupé par c,dpour la même expression ique (1).

Les dplyrexpressions couramment utilisées seraient:

DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

De toute évidence, les codes data.table sont plus courts. De plus, ils sont également plus efficaces en mémoire 1 . Pourquoi? Parce que dans (3) et (4), filter()retourne d'abord les lignes pour les 10 colonnes , quand dans (3) nous avons juste besoin du nombre de lignes, et dans (4) nous avons juste besoin de colonnes b, c, dpour les opérations successives. Pour surmonter cela, nous devons select()colonnes apriori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

Il est essentiel de souligner une différence philosophique majeure entre les deux packages:

  • Dans data.table, nous aimons garder ces opérations liées ensemble, et cela permet de regarder le j-expression(à partir du même appel de fonction) et de réaliser qu'il n'y a pas besoin de colonnes dans (1). L'expression dans iest calculée et .Nn'est que la somme de ce vecteur logique qui donne le nombre de lignes; le sous-ensemble entier n'est jamais réalisé. Dans (2), seules les colonnes b,c,dsont matérialisées dans le sous-ensemble, les autres colonnes sont ignorées.

  • Mais dans dplyr, la philosophie est qu'une fonction fasse précisément une chose bien . Il n'y a (au moins actuellement) aucun moyen de dire si l'opération après a filter()besoin de toutes les colonnes que nous avons filtrées. Vous devrez penser à l'avance si vous souhaitez effectuer ces tâches efficacement. Personnellement, je trouve cela contre-intutitif dans ce cas.

Notez que dans (5) et (6), nous sous-ensembleons toujours des colonnes adont nous n'avons pas besoin. Mais je ne sais pas comment éviter cela. Si la filter()fonction avait un argument pour sélectionner les colonnes à retourner, nous pourrions éviter ce problème, mais alors la fonction ne fera pas qu'une seule tâche (qui est également un choix de conception de dplyr).

Sous-attribuer par référence

dplyr ne mettra jamais à jour par référence. C'est une autre énorme différence (philosophique) entre les deux packages.

Par exemple, dans data.table, vous pouvez faire:

DT[a %in% some_vals, a := NA]

qui met à jour la colonne a par référence uniquement sur les lignes qui satisfont à la condition. Pour le moment, dplyr copie en profondeur l'intégralité de la table data.table en interne pour ajouter une nouvelle colonne. @BrodieG l'a déjà mentionné dans sa réponse.

Mais la copie profonde peut être remplacée par une copie superficielle lorsque FR # 617 est implémenté. Aussi pertinent: dplyr: FR # 614 . Notez qu'encore, la colonne que vous modifiez sera toujours copiée (donc un peu plus lente / moins efficace en mémoire). Il n'y aura aucun moyen de mettre à jour les colonnes par référence.

Autres fonctionnalités

  • Dans data.table, vous pouvez agréger lors de la jointure, ce qui est plus simple à comprendre et est efficace en mémoire car le résultat de la jointure intermédiaire n'est jamais matérialisé. Consultez cet article pour un exemple. Vous ne pouvez pas (pour le moment?) Faire cela en utilisant la syntaxe data.table / data.frame de dplyr.

  • La fonction de jointures roulantes de data.table n'est pas non plus prise en charge dans la syntaxe de dplyr.

  • Nous avons récemment implémenté des jointures de chevauchement dans data.table pour joindre des plages d'intervalle ( voici un exemple ), qui est une fonction distincte foverlaps()pour le moment, et pourrait donc être utilisée avec les opérateurs de tube (magrittr / pipeR? - je ne l'ai jamais essayé moi-même).

    Mais finalement, notre objectif est de l'intégrer [.data.tableafin que nous puissions récolter les autres fonctionnalités comme le regroupement, l'agrégation en rejoignant etc. qui auront les mêmes limitations décrites ci-dessus.

  • Depuis 1.9.4, data.table implémente l'indexation automatique en utilisant des clés secondaires pour des sous-ensembles basés sur une recherche binaire rapide sur la syntaxe R régulière. Ex: DT[x == 1]et DT[x %in% some_vals]créera automatiquement un index lors de la première exécution, qui sera ensuite utilisé sur des sous-ensembles successifs de la même colonne au sous-ensemble rapide en utilisant la recherche binaire. Cette fonctionnalité continuera d'évoluer. Vérifiez cet essentiel pour un bref aperçu de cette fonctionnalité.

    De la façon dont filter()est implémenté pour data.tables, il ne tire pas parti de cette fonctionnalité.

  • Une fonctionnalité de dplyr est qu'il fournit également une interface aux bases de données en utilisant la même syntaxe, ce que data.table ne fait pas pour le moment.

Ainsi, vous devrez peser ces points (et probablement d'autres) et décider en fonction de si ces compromis sont acceptables pour vous.

HTH


(1) Notez que l'efficacité de la mémoire a un impact direct sur la vitesse (d'autant plus que les données grossissent), car le goulot d'étranglement dans la plupart des cas est de déplacer les données de la mémoire principale vers le cache (et d'utiliser autant que possible les données du cache - réduire les échecs de cache - afin de réduire l'accès à la mémoire principale). Ne pas entrer dans les détails ici.

Arun
la source
4
Absolument brillant. Merci pour cela
David Arenburg
6
C'est une bonne réponse, mais il serait possible (sinon probable) pour dplyr d'implémenter un filter()plus efficace en summarise()utilisant la même approche que dplyr utilise pour SQL - c'est-à-dire créer une expression et ne l'exécuter qu'une seule fois à la demande. Il est peu probable que cela soit implémenté dans un proche avenir car dplyr est assez rapide pour moi et la mise en œuvre d'un planificateur / optimiseur de requêtes est relativement difficile.
hadley
24

Essayez-le.

library(rbenchmark)
library(dplyr)
library(data.table)

benchmark(
dplyr = diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair", 
                        list(AvgPrice = mean(price),
                             MedianPrice = as.numeric(median(price)),
                             Count = .N), by = cut][order(-Count)])[1:4]

Sur ce problème, il semble que data.table soit 2,4 fois plus rapide que dplyr en utilisant data.table:

        test replications elapsed relative
2 data.table          100    2.39    1.000
1      dplyr          100    5.77    2.414

Révisé sur la base du commentaire de Polymerase.

G. Grothendieck
la source
2
En utilisant le microbenchmarkpackage, j'ai constaté que l'exécution du dplyrcode de l'OP sur la version originale (trame de données) de diamondsprenait un temps médian de 0,012 seconde, alors que cela prenait un temps médian de 0,024 seconde après la conversion diamondsen table de données. L'exécution du data.tablecode de G. Grothendieck a pris 0,013 seconde. Au moins sur mon système, il ressemble dplyret data.tablea à peu près les mêmes performances. Mais pourquoi serait- dplyril plus lent lorsque la trame de données est d'abord convertie en table de données?
eipi10
Cher G. Grothendieck, c'est merveilleux. Merci de m'avoir montré cet utilitaire de référence. BTW vous avez oublié [order (-Count)] dans la version datable pour faire l'équivalence de l'arrangement de dplyr (desc (Count)). Après avoir ajouté cela, datatable est encore plus rapide d'environ x1,8 (au lieu de 2,9).
Polymerase
@ eipi10 pouvez-vous réexécuter votre banc avec la version datable ici (tri ajouté par décompte à la dernière étape): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median) (price)), Count = .N), by = cut] [order (-Count)]
Polymerase
Encore 0,013 seconde. L'opération de commande ne prend guère de temps car il s'agit simplement de réorganiser la table finale, qui ne comporte que quatre lignes.
eipi10
1
Il y a une surcharge fixe pour la conversion de la syntaxe dplyr à la syntaxe de la table de données, il peut donc valoir la peine d'essayer différentes tailles de problème. Je n'ai peut-être pas non plus implémenté le code de table de données le plus efficace de dplyr; les correctifs sont toujours les bienvenus
hadley
21

Pour répondre à tes questions:

  • Oui, vous utilisez data.table
  • Mais pas aussi efficacement que vous le feriez avec une data.tablesyntaxe pure

Dans de nombreux cas, ce sera un compromis acceptable pour ceux qui veulent la dplyrsyntaxe, bien qu'il soit peut-être plus lent qu'avec dplyrdes trames de données simples.

Un facteur important semble être que dplyrcopier le data.tablepar défaut lors du regroupement. Considérez (en utilisant microbenchmark):

Unit: microseconds
                                                               expr       min         lq    median
                                diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                          diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                               diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

Le filtrage est d'une vitesse comparable, mais le regroupement ne l'est pas. Je crois que le coupable est cette ligne dans dplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}

où la valeur par copydéfaut est TRUE(et ne peut pas facilement être changé en FALSE que je peux voir). Cela ne représente probablement pas 100% de la différence, mais les frais généraux généraux à eux seuls sur quelque chose de la taille la diamondsplus probable ne sont pas la différence complète.

Le problème est que pour avoir une grammaire cohérente, dplyrle regroupement s'effectue en deux étapes. Il définit d'abord des clés sur une copie de la table de données d'origine qui correspondent aux groupes, et ce n'est que plus tard qu'il regroupe. data.tablealloue simplement de la mémoire pour le plus grand groupe de résultats, qui dans ce cas n'est qu'une ligne, ce qui fait une grande différence dans la quantité de mémoire à allouer.

FYI, si quelqu'un s'en soucie, j'ai trouvé ceci en utilisant treeprof( install_github("brodieg/treeprof")), un visualiseur d'arborescence expérimental (et toujours très alpha) pour la Rprofsortie:

entrez la description de l'image ici

Notez que ce qui précède ne fonctionne actuellement que sur les macs AFAIK. Aussi, malheureusement, Rprofenregistre les appels du type packagename::funnamecomme anonymes, donc il pourrait en fait s'agir de tous les datatable::appels à l'intérieur grouped_dtqui sont responsables, mais d'après des tests rapides, il semblait que c'était datatable::copyle plus gros.

Cela dit, vous pouvez rapidement voir à quel point il n'y a pas beaucoup de frais généraux autour de l' [.data.tableappel, mais il existe également une branche complètement séparée pour le regroupement.


EDIT : pour confirmer la copie:

> tracemem(diamondsDT)
[1] "<0x000000002747e348>"    
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
Source: local data table [5 x 2]

        cut AvgPrice
1      Fair 4358.758
2      Good 3928.864
3 Very Good 3981.760
4   Premium 4584.258
5     Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
         cut       V1
1:     Ideal 3457.542
2:   Premium 4584.258
3:      Good 3928.864
4: Very Good 3981.760
5:      Fair 4358.758
> untracemem(diamondsDT)
BrodieG
la source
C'est génial, merci. Cela signifie-t-il que dplyr :: group_by () doublera la mémoire requise (par rapport à la pure syntaxe datable) à cause de l'étape de copie interne des données? Cela signifie que si la taille de mon objet datatable est de 1 Go et que j'utilise la syntaxe chaînée de dplyr similaire à celle du message d'origine. J'aurai besoin d'au moins 2 Go de mémoire libre pour obtenir les résultats?
Polymerase
2
J'ai l'impression d'avoir corrigé cela dans la version de développement?
hadley
@hadley, je travaillais à partir de la version CRAN. En regardant dev, on dirait que vous avez partiellement résolu le problème, mais la copie réelle reste (pas testée, juste en regardant les lignes c (20, 30:32) dans R / grouped-dt.r. C'est probablement plus rapide maintenant, mais Je parie que le pas lent est la copie.
BrodieG
3
J'attends également une fonction de copie superficielle dans data.table; jusque-là, je pense qu'il vaut mieux être sûr que rapide.
hadley
2

Vous pouvez utiliser dtplyr maintenant, qui fait partie du tidyverse . Il vous permet d'utiliser les déclarations de style dplyr comme d'habitude, mais utilise une évaluation paresseuse et traduit vos déclarations en code data.table sous le capot. La surcharge de traduction est minime, mais vous tirez tous, sinon la plupart des avantages de data.table. Plus de détails sur le repo officiel de git ici et sur la page tidyverse .

Lait noir
la source