dplyr on data.table, verwende ich data.table wirklich?

89

Wenn ich dplyr Syntax auf eine Datentabelle , erhalte ich alle die Geschwindigkeitsvorteile der Datentabelle , während immer noch die Syntax von dplyr verwenden? Mit anderen Worten, verwende ich die Datentabelle falsch, wenn ich sie mit der Dplyr-Syntax abfrage? Oder muss ich eine reine datierbare Syntax verwenden, um die gesamte Leistung zu nutzen?

Vielen Dank im Voraus für jeden Rat. Codebeispiel:

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

Ergebnisse:

#         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

Hier ist die datierbare Äquivalenz, die ich mir ausgedacht habe. Ich bin mir nicht sicher, ob es der bewährten DT-Praxis entspricht. Aber ich frage mich, ob der Code hinter den Kulissen wirklich effizienter ist als die Dplyr-Syntax:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]
Polymerase
quelle
7
Warum würden Sie keine Datentabellensyntax verwenden? Es ist auch elegant und effizient. Die Frage ist nicht wirklich beantwortbar, da sie sehr weit gefasst ist. Ja, es gibt dplyrMethoden für Datentabellen, aber die Datentabelle hat auch ihre eigenen vergleichbaren Methoden
Rich Scriven
7
Ich kann datierbare Syntax oder Kurs verwenden. Aber irgendwie finde ich die dplyr-Syntax eleganter. Unabhängig von meiner Präferenz für die Syntax. Was ich wirklich wissen möchte, ist: Muss ich eine reine datierbare Syntax verwenden, um 100% ige Vorteile der datierbaren Leistung zu erzielen?
Polymerase
3
Eine aktuelle Benchmark, in dplyrder data.frames und entsprechende data.tables verwendet werden, finden Sie hier (und die darin enthaltenen Referenzen).
Henrik
2
@ Polymerase - Ich würde denken, die Antwort auf diese Frage ist definitiv "Ja"
Rich Scriven
1
@Henrik: Später wurde mir klar, dass ich diese Seite falsch interpretiert hatte, weil sie nur den Code für die Datenrahmenkonstruktion anzeigten, nicht aber den Code, den sie für die data.table-Konstruktion verwendeten. Als ich es merkte, löschte ich meinen Kommentar (in der Hoffnung, dass Sie ihn nicht gesehen hatten).
IRTFM

Antworten:

75

Es gibt keine einfache Antwort, da sich die Philosophien dieser beiden Pakete in bestimmten Aspekten unterscheiden. Einige Kompromisse sind also unvermeidlich. Hier sind einige der Bedenken, die Sie möglicherweise ansprechen / berücksichtigen müssen.

Operationen mit i(== filter()und slice()in dplyr)

Angenommen, DTmit etwa 10 Spalten. Betrachten Sie diese data.table-Ausdrücke:

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

(1) gibt die Anzahl der Zeilen in der DTSpalte where an a > 1. (2) gibt mean(b)gruppiert nach c,dfür denselben Ausdruck in iwie (1) zurück.

Häufig verwendete dplyrAusdrücke wären:

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

Datentabellencodes sind eindeutig kürzer. Darüber hinaus sind sie auch speichereffizienter 1 . Warum? Da sowohl in (3) und (4), filter()kehrt Zeilen für alle 10 Spalten Zuerst wird , wenn in (3) können wir die Anzahl von Zeilen gerade benötigen, und in (4) haben wir nur Spalten müssen b, c, dfür die aufeinanderfolgenden Operationen. Um dies zu überwinden, müssen wir select()apriori Spalten:

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)

Es ist wichtig, einen großen philosophischen Unterschied zwischen den beiden Paketen hervorzuheben:

  • In data.tablemöchten wir diese verwandten Operationen zusammenhalten, und dies ermöglicht es, die j-expression(aus demselben Funktionsaufruf) zu betrachten und zu erkennen, dass in (1) keine Spalten erforderlich sind. Der Ausdruck in iwird berechnet und .Nist nur die Summe des logischen Vektors, der die Anzahl der Zeilen angibt. Die gesamte Teilmenge wird niemals realisiert. In (2) werden nur Spalten b,c,din der Teilmenge materialisiert, andere Spalten werden ignoriert.

  • Aber in dplyrist die Philosophie, dass eine Funktion genau eines gut macht . Es gibt (zumindest derzeit) keine Möglichkeit festzustellen, ob die Operation danach filter()alle von uns gefilterten Spalten benötigt. Sie müssen vorausdenken, wenn Sie solche Aufgaben effizient ausführen möchten. Ich persönlich finde es in diesem Fall nicht intuitiv.

Beachten Sie, dass wir in (5) und (6) immer noch Spalten unterteilen, adie wir nicht benötigen. Aber ich bin mir nicht sicher, wie ich das vermeiden soll. Wenn die filter()Funktion ein Argument zum Auswählen der zurückzugebenden Spalten hätte, könnten wir dieses Problem vermeiden, aber dann führt die Funktion nicht nur eine Aufgabe aus (was auch eine dplyr-Entwurfsauswahl ist).

Unterzuweisung durch Referenz

dplyr wird niemals durch Referenz aktualisiert. Dies ist ein weiterer großer (philosophischer) Unterschied zwischen den beiden Paketen.

In data.table können Sie beispielsweise Folgendes tun:

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

welche Updates Spalte a unter Bezugnahme auf nur jene Zeilen, die die Bedingung erfüllen. Im Moment kopiert dplyr deep die gesamte data.table intern, um eine neue Spalte hinzuzufügen. @BrodieG hat dies bereits in seiner Antwort erwähnt.

Die tiefe Kopie kann jedoch durch eine flache Kopie ersetzt werden, wenn FR # 617 implementiert ist. Ebenfalls relevant: dplyr: FR # 614 . Beachten Sie, dass die von Ihnen geänderte Spalte immer kopiert wird (daher etwas langsamer / weniger speichereffizient). Es gibt keine Möglichkeit, Spalten durch Referenz zu aktualisieren.

Andere Funktionen

  • In data.table können Sie während des Beitritts aggregieren. Dies ist einfacher zu verstehen und speichereffizient, da das Ergebnis der Zwischenverknüpfung niemals erreicht wird. Überprüfen Sie diesen Beitrag für ein Beispiel. Sie können dies (im Moment?) Nicht mit der data.table / data.frame-Syntax von dplyr tun.

  • Die Funktion für rollierende Verknüpfungen von data.table wird auch in der Syntax von dplyr nicht unterstützt.

  • Wir haben kürzlich Überlappungs-Joins in data.table implementiert, um über Intervallbereiche zu verbinden ( hier ein Beispiel ). Dies ist foverlaps()derzeit eine separate Funktion und kann daher mit den Pipe-Operatoren verwendet werden (magrittr / pipeR? - habe es nie selbst versucht).

    Letztendlich ist es unser Ziel, es [.data.tableso zu integrieren , dass wir die anderen Funktionen wie Gruppieren, Aggregieren während des Beitritts usw. nutzen können, die dieselben oben beschriebenen Einschränkungen aufweisen.

  • Seit 1.9.4 implementiert data.table die automatische Indizierung mithilfe von Sekundärschlüsseln für schnelle, auf binärer Suche basierende Teilmengen mit regulärer R-Syntax. Beispiel: DT[x == 1]und DT[x %in% some_vals]erstellt beim ersten Durchlauf automatisch einen Index, der dann für aufeinanderfolgende Teilmengen aus derselben Spalte bis zur schnellen Teilmenge mithilfe der binären Suche verwendet wird. Diese Funktion wird weiterentwickelt. In dieser Übersicht finden Sie eine kurze Übersicht über diese Funktion.

    Aus dem Weg filter(), der für data.tables implementiert ist, wird diese Funktion nicht genutzt.

  • Eine dplyr-Funktion besteht darin, dass sie auch eine Schnittstelle zu Datenbanken mit derselben Syntax bietet, die data.table derzeit nicht bietet.

Sie müssen also diese (und wahrscheinlich auch andere) Punkte abwägen und basierend darauf entscheiden, ob diese Kompromisse für Sie akzeptabel sind.

HTH


(1) Beachten Sie, dass sich die Speichereffizienz direkt auf die Geschwindigkeit auswirkt (insbesondere wenn die Daten größer werden), da der Engpass in den meisten Fällen darin besteht, die Daten aus dem Hauptspeicher in den Cache zu verschieben (und die Daten im Cache so weit wie möglich zu nutzen - reduzieren Sie die Cache-Fehler - um den Zugriff auf den Hauptspeicher zu reduzieren). Ich gehe hier nicht auf Details ein.

Arun
quelle
4
Absolut brilliant. Danke dafür
David Arenburg
6
Das ist eine gute Antwort, aber es wäre für dplyr möglich (wenn nicht wahrscheinlich), ein effizientes filter()Plus summarise()mit demselben Ansatz zu implementieren , den dplyr für SQL verwendet - dh einen Ausdruck aufzubauen und dann nur einmal bei Bedarf auszuführen. Es ist unwahrscheinlich, dass dies in naher Zukunft implementiert wird, da dplyr für mich schnell genug ist und die Implementierung eines Abfrageplaners / -optimierers relativ schwierig ist.
Hadley
24

Probier es einfach.

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]

Bei diesem Problem scheint data.table mit data.table 2,4-mal schneller zu sein als dplyr:

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

Überarbeitet basierend auf dem Kommentar von Polymerase.

G. Grothendieck
quelle
2
Bei Verwendung des microbenchmarkPakets stellte ich fest, dass das Ausführen des OP- dplyrCodes auf der Originalversion (Datenrahmen) von diamondseine mittlere Zeit von 0,012 Sekunden und eine mittlere Zeit von 0,024 Sekunden nach dem Konvertieren diamondsin eine Datentabelle dauerte . Das Ausführen des data.tableCodes von G. Grothendieck dauerte 0,013 Sekunden. Zumindest auf meinem System sieht es so aus dplyrund data.tablehat ungefähr die gleiche Leistung. Aber warum sollte dplyres langsamer sein, wenn der Datenrahmen zum ersten Mal in eine Datentabelle konvertiert wird?
Eipi10
Lieber G. Grothendieck, das ist wunderbar. Vielen Dank, dass Sie mir dieses Benchmark-Dienstprogramm gezeigt haben. Übrigens haben Sie [order (-Count)] in der datierbaren Version vergessen, um die Äquivalenz der Anordnung von dplyr (desc (Count)) herzustellen. Nach dem Hinzufügen ist die Datentabelle immer noch um etwa 1,8 x schneller (anstelle von 2,9).
Polymerase
@ eipi10 können Sie Ihre Bank mit der datierbaren Version hier erneut ausführen (Sortierung nach desc Count im letzten Schritt hinzugefügt): diamondsDT [cut! = "Fair", Liste (AvgPrice = Mittelwert (Preis), MedianPrice = as.numeric (Median) (Preis)), Count = .N), by = cut] [order (-Count)]
Polymerase
Immer noch 0,013 Sekunden. Der Bestellvorgang nimmt kaum Zeit in Anspruch, da nur der Final Table mit nur vier Zeilen neu angeordnet wird.
Eipi10
1
Es gibt einen festen Overhead für die Konvertierung von der Dplyr-Syntax in die Datentabellensyntax, daher lohnt es sich möglicherweise, verschiedene Problemgrößen auszuprobieren. Außerdem habe ich möglicherweise nicht den effizientesten Datentabellencode in dplyr implementiert. Patches sind immer willkommen
Hadley
21

So beantworten Sie Ihre Fragen:

  • Ja, Sie verwenden data.table
  • Aber nicht so effizient wie mit reiner data.tableSyntax

In vielen Fällen ist dies ein akzeptabler Kompromiss für diejenigen, die die dplyrSyntax wünschen, obwohl sie möglicherweise langsamer ist als dplyrbei einfachen Datenrahmen.

Ein wichtiger Faktor scheint zu sein, dass dplyrdas data.tablebeim Gruppieren standardmäßig kopiert wird. Beachten Sie (unter Verwendung von Mikrobenchmark):

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

Die Filterung ist von vergleichbarer Geschwindigkeit, die Gruppierung jedoch nicht. Ich glaube, der Schuldige ist diese Zeile in dplyr:::grouped_dt:

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

wo copystandardmäßig TRUE(und kann nicht einfach in FALSE geändert werden, was ich sehen kann). Dies macht wahrscheinlich nicht 100% des Unterschieds aus, aber der allgemeine Overhead allein für etwas, dessen Größe diamondshöchstwahrscheinlich nicht der volle Unterschied ist.

Das Problem ist, dass dplyrdie Gruppierung in zwei Schritten erfolgt, um eine konsistente Grammatik zu erhalten . Zuerst werden Schlüssel für eine Kopie der ursprünglichen Datentabelle festgelegt, die den Gruppen entsprechen, und erst später wird eine Gruppe erstellt. data.tableWeist nur Speicher für die größte Ergebnisgruppe zu, die in diesem Fall nur eine Zeile umfasst. Dies macht also einen großen Unterschied darin, wie viel Speicher zugewiesen werden muss.

Zu Ihrer Information, wenn es jemanden interessiert, habe ich dies mithilfe von treeprof( install_github("brodieg/treeprof")) gefunden, einem experimentellen (und immer noch sehr Alpha) Baum-Viewer für die RprofAusgabe:

Geben Sie hier die Bildbeschreibung ein

Beachten Sie, dass das oben genannte derzeit nur auf Macs AFAIK funktioniert. Leider werden RprofAnrufe des Typs auch packagename::funnameals anonym aufgezeichnet, sodass tatsächlich alle darin enthaltenen datatable::Anrufe grouped_dtverantwortlich sein können, aber nach schnellen Tests sah es so aus, als wäre datatable::copyes der große.

Sie können jedoch schnell erkennen, dass der [.data.tableAnruf nicht so viel Aufwand verursacht , aber es gibt auch einen völlig separaten Zweig für die Gruppierung.


BEARBEITEN : um das Kopieren zu bestätigen:

> 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
quelle
Das ist großartig, danke. Bedeutet das, dass dplyr :: group_by () den Speicherbedarf (im Vergleich zur reinen datierbaren Syntax) aufgrund des internen Datenkopierschritts verdoppelt? Das heißt, wenn meine datierbare Objektgröße 1 GB beträgt und ich die verkettete dplyr-Syntax verwende, die der im ursprünglichen Beitrag ähnelt. Ich benötige mindestens 2 GB freien Speicher, um die Ergebnisse zu erhalten.
Polymerase
2
Ich habe das Gefühl, dass ich das in der Entwicklerversion behoben habe?
Hadley
@ Hadley, ich habe mit der CRAN-Version gearbeitet. Wenn Sie sich dev ansehen, sehen Sie so aus, als hätten Sie das Problem teilweise behoben, aber die eigentliche Kopie bleibt erhalten (nicht getestet, nur die Zeilen c (20, 30:32) in R / grouped-dt.r. Es ist jetzt wahrscheinlich schneller, aber Ich wette, der langsame Schritt ist die Kopie.
BrodieG
3
Ich warte auch auf eine flache Kopierfunktion in data.table; Bis dahin denke ich, dass es besser ist, sicher zu sein als schnell.
Hadley
2

Sie können jetzt dtplyr verwenden , das Teil der Tidyverse ist . Sie können wie gewohnt Anweisungen im dplyr-Stil verwenden, verwenden jedoch eine verzögerte Auswertung und übersetzen Ihre Anweisungen in data.table-Code unter der Haube. Der Aufwand für die Übersetzung ist minimal, aber Sie leiten alle, wenn nicht die meisten Vorteile von data.table ab. Weitere Details beim offiziellen Git Repo hier und auf der Tidyverse- Seite .

Schwarze Milch
quelle