web-dev-qa-db-fra.com

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

Si j'utilise la syntaxe dplyr en plus d'un datatable, est-ce que j'obtiens tous les avantages de vitesse de datatable tout en utilisant 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 syntaxe purement 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 datatable que j'ai trouvée. Je ne sais pas si elle est conforme aux bonnes pratiques de DT. Mais je me demande si le code est vraiment plus efficace que la syntaxe dplyr derrière la scène:

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

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() dans dplyr)

Supposons DT avec 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 DT où la colonne a > 1. (2) renvoie mean(b) regroupée par c,d Pour la même expression dans i que (1).

Les expressions dplyr 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() renvoie les lignes pour les 10 colonnes en premier, quand en (3) nous juste besoin du nombre de lignes, et en (4) nous avons juste besoin des colonnes b, c, d pour 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 i est calculée et .N N'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,d Sont matérialisées dans le sous-ensemble, les autres colonnes sont ignorées.

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

Notez qu'en (5) et (6), nous sous-ensembleons toujours la colonne a dont nous n'avons pas besoin. Mais je ne sais pas comment éviter cela. Si la fonction filter() avait un argument pour sélectionner les colonnes à renvoyer, nous pourrions éviter ce problème, mais la fonction ne fera pas qu'une seule tâche (ce qui est également un choix de conception 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 sur les seules lignes qui remplissent la condition. Pour le moment, dplyr deep copie l'intégralité du data.table en interne pour ajouter une nouvelle colonne. @BrodieG l'a déjà mentionné dans sa réponse.

Mais la copie complète peut être remplacée par une copie superficielle lorsque FR # 617 est implémenté. Également pertinent: dplyr: FR # 614 . Notez que toujours, 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é. Vérifiez cet article pour un exemple. Vous ne pouvez pas (pour le moment?) Utiliser la syntaxe data.table/data.frame de dplyr.

  • la fonction 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 à chevauchement dans data.table pour joindre sur des plages d'intervalles ( voici un exemple ), qui est une fonction distincte foverlaps() pour le moment, et pourrait donc être utilisée avec le opérateurs de pipe (magrittr/pipeR? - je ne l'ai jamais essayé moi-même).

    Mais en fin de compte, notre objectif est de l'intégrer dans [.data.table Afin que nous puissions récolter les autres fonctionnalités comme le regroupement, l'agrégation tout en se joignant, etc. qui auront les mêmes limitations décrites ci-dessus.

  • Depuis 1.9.4, data.table implémente l'indexation automatique à l'aide de clés secondaires pour des sous-ensembles de recherche binaire rapide sur la syntaxe R régulière. Ex: DT[x == 1] Et DT[x %in% some_vals] Créeront automatiquement un index lors de la première exécution, qui sera ensuite utilisé sur des sous-ensembles successifs de la même colonne pour un sous-ensemble rapide à l'aide de la recherche binaire. Cette fonctionnalité continuera d'évoluer. Vérifiez ce Gist 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 interface avec les bases de données en utilisant la même syntaxe, ce que data.table n'a 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 (en particulier lorsque les données deviennent plus volumineuses), car dans la plupart des cas, le goulot d'étranglement consiste à déplacer les données de la mémoire principale vers le cache (et à utiliser les données du cache autant que possible - réduire les erreurs de cache - afin de réduire l'accès à la mémoire principale). Ne pas entrer dans les détails ici.

73
Arun

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 utilisant data.table:

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

Révisé basé sur le commentaire de Polymerase.

22
G. Grothendieck

Pour répondre à vos questions:

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

Dans de nombreux cas, ce sera un compromis acceptable pour ceux qui souhaitent la syntaxe dplyr, bien qu'elle soit probablement plus lente que dplyr avec des trames de données simples.

Un gros facteur semble être que dplyr copiera le data.table Par défaut lors du regroupement. Considérez (à l'aide de la référence microbienne):

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 pas le regroupement. Je crois que le coupable est cette ligne dans dplyr:::grouped_dt:

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

copy par défaut est TRUE (et ne peut pas facilement être changé en FAUX que je peux voir). Cela ne représente probablement pas 100% de la différence, mais les frais généraux seuls sur quelque chose de la taille de diamonds ne sont probablement pas la différence complète.

Le problème est que pour avoir une grammaire cohérente, dplyr effectue le regroupement en deux étapes. Il définit d'abord les 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 les regroupe. data.table Alloue simplement de la mémoire au 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 qui doit être allouée.

Pour info, si quelqu'un s'en soucie, j'ai trouvé cela en utilisant treeprof (install_github("brodieg/treeprof")), une visionneuse expérimentale (et toujours très alpha) pour la sortie Rprof:

enter image description here

Notez que ce qui précède ne fonctionne actuellement que sur les macs AFAIK. De plus, malheureusement, Rprof enregistre les appels du type packagename::funname Comme anonymes, ce qui pourrait en fait être tous les appels datatable:: Dans grouped_dt Qui sont responsables , mais d'après des tests rapides, il semblait que datatable::copy était le plus gros.

Cela dit, vous pouvez rapidement voir comment il n'y a pas beaucoup de surcharge autour de l'appel [.data.table, Mais il y a aussi une branche complètement distincte pour le regroupement.


ÉDITEZ : 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)
20
BrodieG