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) ]
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.
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 lej-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 dansi
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 colonnesb,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èsfilter()
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).
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.
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.
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.
Pour répondre à vos questions:
data.table
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)
}
où 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
:
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)