data.table vs dplyr: Kann einer etwas gut machen, der andere nicht oder schlecht?

759

Überblick

Ich bin relativ vertraut mit data.table, nicht so sehr mit dplyr. Ich habe einige dplyrVignetten und Beispiele gelesen , die auf SO aufgetaucht sind, und bisher sind meine Schlussfolgerungen folgende:

  1. data.tableund dplyrsind in der Geschwindigkeit vergleichbar, außer wenn es viele (dh> 10-100K) Gruppen gibt, und unter einigen anderen Umständen (siehe Benchmarks unten)
  2. dplyr hat eine zugänglichere Syntax
  3. dplyr abstrahiert (oder wird) mögliche DB-Interaktionen
  4. Es gibt einige geringfügige Funktionsunterschiede (siehe "Beispiele / Verwendung" unten).

In meinen Augen hat 2. nicht viel Gewicht, weil ich ziemlich vertraut damit bin data.table, obwohl ich verstehe, dass es für Benutzer, die beide neu sind, ein großer Faktor sein wird. Ich möchte ein intuitiveres Argument vermeiden, da dies für meine spezifische Frage, die aus der Perspektive einer bereits vertrauten Person gestellt wird, irrelevant ist data.table. Ich möchte auch eine Diskussion darüber vermeiden, wie "intuitiver" zu einer schnelleren Analyse führt (sicherlich wahr, aber auch hier nicht das, woran ich hier am meisten interessiert bin).

Frage

Was ich wissen möchte ist:

  1. Gibt es analytische Aufgaben, die für Personen, die mit den Paketen vertraut sind, viel einfacher mit dem einen oder anderen Paket zu codieren sind (dh eine Kombination von erforderlichen Tastenanschlägen im Vergleich zum erforderlichen Grad an Esoterik, wobei weniger von jedem eine gute Sache ist).
  2. Gibt es analytische Aufgaben, die in einem Paket wesentlich effizienter (dh mehr als zweimal) effizienter ausgeführt werden als in einem anderen.

Eine aktuelle SO-Frage brachte mich dazu, ein bisschen mehr darüber nachzudenken, denn bis zu diesem Zeitpunkt dachte ich nicht, dass ich dplyrviel mehr bieten würde, als ich bereits tun kann data.table. Hier ist die dplyrLösung (Daten am Ende von Q):

dat %.%
  group_by(name, job) %.%
  filter(job != "Boss" | year == min(year)) %.%
  mutate(cumu_job2 = cumsum(job2))

Das war viel besser als mein Hackversuch nach einer data.tableLösung. Trotzdem sind gute data.tableLösungen auch ziemlich gut (danke Jean-Robert, Arun, und beachten Sie, dass ich hier eine einzelne Aussage der streng optimalen Lösung vorgezogen habe):

setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

Die Syntax für Letzteres mag sehr esoterisch erscheinen, aber es ist tatsächlich ziemlich einfach, wenn Sie es gewohnt sind data.table(dh einige der esoterischeren Tricks werden nicht verwendet).

Im Idealfall , was ich möchte sehen , ist einige gute Beispiele waren die dplyroder data.tableArt und Weise wesentlich prägnanter oder führt wesentlich besser.

Beispiele

Verwendungszweck
  • dplyrerlaubt keine gruppierten Operationen, die eine beliebige Anzahl von Zeilen zurückgeben (aus der Frage von eddi , Anmerkung: Dies sieht so aus, als würde es in dplyr 0.5 implementiert , außerdem zeigt @beginneR eine mögliche Problemumgehung doin der Antwort auf die Frage von @ eddi).
  • data.tableunterstützt rollierende Joins (danke @dholstius) sowie überlappende Joins
  • data.tableintern optimiert Ausdrücke der Form DT[col == value]oder DT[col %in% values]für die Geschwindigkeit durch die automatische Indexierung , die verwendet binäre Suche , während der gleichen Grund R Syntax. Hier finden Sie weitere Details und einen kleinen Benchmark.
  • dplyrbietet Standard - Testversionen von Funktionen (zB regroup, summarize_each_) dass die programmatische Verwendung vereinfachen kann dplyr(Hinweis programmatische Verwendung von data.tablezumindest meines Wissens ist auf jeden Fall möglich, einige sorgfältige Überlegungen erfordert nur, Substitution / zitieren, etc,)
Benchmarks

Daten

Dies ist das erste Beispiel, das ich im Fragenbereich gezeigt habe.

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
quelle
9
Die Lösung, die beim Lesen ähnlich dplyrist, ist:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)]
Eddi
7
Für # 1 arbeiten beide dplyrund data.tableTeams an Benchmarks, daher wird irgendwann eine Antwort da sein. # 2 (Syntax) imO ist streng falsch, aber das wagt sich eindeutig in das Meinungsgebiet, also stimme ich auch dafür, zu schließen.
Eddi
13
Nun, wieder imO, die Menge der Probleme, die sauberer ausgedrückt werden, (d)plyrhat Maßnahme 0
eddi
28
@BrodieG Das einzige, was mich an beiden dplyrund plyrin Bezug auf die Syntax wirklich nervt und im Grunde der Hauptgrund ist, warum ich ihre Syntax nicht mag, ist, dass ich viel zu viele (mehr als 1) zusätzliche Funktionen lernen muss (mit Namen, die immer noch sind macht für mich keinen Sinn), erinnere dich daran, was sie tun, welche Argumente sie nehmen usw. Das war für mich immer eine große Abkehr von der Plyr-Philosophie.
Eddi
43
@eddi [ironisch] Das einzige, was mich an der Syntax von data.table wirklich nervt, ist, dass ich lernen muss, wie viel zu viele Funktionsargumente interagieren und was kryptische Verknüpfungen bedeuten (z .SD. B. ). [ernsthaft] Ich denke, dies sind legitime Designunterschiede, die unterschiedliche Menschen ansprechen werden
Hadley

Antworten:

532

Wir müssen Abdeckung zumindest diese Aspekte eine umfassende Antwort / Vergleich zur Verfügung zu stellen (in keiner bestimmten Reihenfolge ihrer Bedeutung): Speed, Memory usage, Syntaxund Features.

Meine Absicht ist es, jedes dieser Themen aus Sicht der Datentabelle so klar wie möglich zu behandeln.

Hinweis: Sofern nicht ausdrücklich anders angegeben, verweisen wir unter Bezugnahme auf dplyr auf die data.frame-Schnittstelle von dplyr, deren Interna in C ++ mit Rcpp gespeichert sind.


Die data.table-Syntax ist in ihrer Form konsistent - DT[i, j, by]. Zu halten i, jund bygemeinsam ist konstruktionsbedingt . Durch das Zusammenhalten verwandter Vorgänge können Vorgänge auf einfache Weise hinsichtlich Geschwindigkeit und vor allem der Speichernutzung optimiert und einige leistungsstarke Funktionen bereitgestellt werden , während die Konsistenz der Syntax erhalten bleibt.

1. Geschwindigkeit

Nicht wenige Benchmarks (obwohl hauptsächlich zu Gruppierungsvorgängen) wurden zu der Frage hinzugefügt, die bereits Daten anzeigt. Die Tabelle wird schneller als dplyr, da die Anzahl der zu gruppierenden Gruppen und / oder Zeilen zunimmt, einschließlich der Benchmarks von Matt für die Gruppierung von 10 Millionen auf 2 Milliarden Zeilen ( 100 GB im RAM) in 100 - 10 Millionen Gruppen und unterschiedlichen Gruppierungsspalten, was ebenfalls vergleichbar ist pandas. Siehe auch aktualisierte Benchmarks , einschließlich Sparkund pydatatable.

Bei Benchmarks wäre es großartig, auch diese verbleibenden Aspekte abzudecken:

  • Gruppierungsoperationen, die eine Teilmenge von Zeilen umfassen, dh DT[x > val, sum(y), by = z]Operationen vom Typ.

  • Benchmark andere Operationen wie Update und schließt sich .

  • Zusätzlich zur Laufzeit wird auch der Speicherbedarf für jeden Vorgang gemessen.

2. Speichernutzung

  1. Operationen mit filter()oder slice()in dplyr können speichereffizient sein (sowohl auf data.frames als auch auf data.tables). Siehe diesen Beitrag .

    Beachten Sie, dass Hadleys Kommentar über Geschwindigkeit spricht (dass dplyr für ihn reichlich schnell ist), während das Hauptanliegen hier die Erinnerung ist .

  2. Die Schnittstelle data.table ermöglicht es derzeit, Spalten anhand von Referenzen zu ändern / zu aktualisieren (beachten Sie, dass wir das Ergebnis nicht erneut einer Variablen zuweisen müssen).

    # sub-assign by reference, updates 'y' in-place
    DT[x >= 1L, y := NA]

    Dplyr wird jedoch niemals durch Referenz aktualisiert. Das dplyr-Äquivalent wäre (beachten Sie, dass das Ergebnis neu zugewiesen werden muss):

    # copies the entire 'y' column
    ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))

    Ein Anliegen hierfür ist die referenzielle Transparenz . Das Aktualisieren eines data.table-Objekts durch Referenz, insbesondere innerhalb einer Funktion, ist möglicherweise nicht immer wünschenswert. Dies ist jedoch eine unglaublich nützliche Funktion: In diesem und diesen Beiträgen finden Sie interessante Fälle. Und wir wollen es behalten.

    Daher arbeiten wir daran, shallow()Funktionen in data.table zu exportieren , die dem Benutzer beide Möglichkeiten bieten . Wenn es beispielsweise wünschenswert ist, die Eingabedatentabelle innerhalb einer Funktion nicht zu ändern, kann man Folgendes tun:

    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.
    }

    Wenn Sie nicht verwenden shallow(), bleibt die alte Funktionalität erhalten:

    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.
    }

    Wenn Sie eine flache Kopie mit erstellen shallow(), verstehen Sie, dass Sie das Originalobjekt nicht ändern möchten. Wir kümmern uns intern um alles, um sicherzustellen, dass Sie Spalten, die Sie ändern, nur dann kopieren, wenn dies unbedingt erforderlich ist . Bei der Implementierung sollte dies das Problem der referenziellen Transparenz vollständig lösen und dem Benutzer beide Möglichkeiten bieten.

    Auch, wenn shallow()exportiert data.table Schnittstelle des dplyr sollten fast alle Kopien vermeiden. Wer also die Syntax von dplyr bevorzugt, kann sie mit data.tables verwenden.

    Es fehlen jedoch noch viele Funktionen, die data.table bietet, einschließlich der (Unter-) Zuweisung durch Referenz.

  3. Beim Beitritt aggregieren:

    Angenommen, Sie haben zwei data.tables wie folgt:

    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

    Und Sie möchten sum(z) * mulfür jede Zeile DT2beim Verbinden durch Spalten erhalten x,y. Wir können entweder:

    • 1) aggregieren DT1, um zu erhalten sum(z), 2) einen Join durchführen und 3) multiplizieren (oder)

      # 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) Machen Sie alles auf einmal (mit der by = .EACHIFunktion):

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

    Was ist der Vorteil?

    • Wir müssen keinen Speicher für das Zwischenergebnis zuweisen.

    • Wir müssen nicht zweimal gruppieren / hashen (eine für die Aggregation und eine für das Beitreten).

    • Und was noch wichtiger ist, die Operation, die wir durchführen wollten, wird anhand von j(2) deutlich.

    Überprüfen Sie diesen Beitrag für eine detaillierte Erklärung von by = .EACHI. Es werden keine Zwischenergebnisse erzielt, und das Join + Aggregat wird auf einmal ausgeführt.

    Schauen Sie sich diesen , diesen und diesen Beitrag für reale Nutzungsszenarien an.

    In dplyrmüsste man zuerst beitreten und aggregieren oder aggregieren und dann beitreten , von denen keines in Bezug auf den Speicher so effizient ist (was sich wiederum in Geschwindigkeit niederschlägt).

  4. Aktualisieren und beitreten:

    Betrachten Sie den unten gezeigten data.table-Code:

    DT1[DT2, col := i.mul]

    fügt / updates DT1‚s Spalte colmit mulaus DT2auf diesen Zeilen , in denen DT2‘ s Schlüsselspalte Streichhölzer DT1. Ich glaube nicht, dass es ein genaues Äquivalent zu dieser Operation gibt dplyr, dh ohne eine *_joinOperation zu vermeiden , die das Ganze kopieren müsste, DT1nur um eine neue Spalte hinzuzufügen, was unnötig ist.

    Überprüfen Sie diesen Beitrag auf ein reales Nutzungsszenario.

Zusammenfassend ist es wichtig zu wissen, dass jede Optimierung wichtig ist. Wie Grace Hopper sagen würde: Achten Sie auf Ihre Nanosekunden !

3. Syntax

Schauen wir uns nun die Syntax an . Hadley kommentierte hier :

Datentabellen sind extrem schnell, aber ich denke, ihre Präzision macht es schwieriger zu lernen und Code, der sie verwendet, ist schwieriger zu lesen, nachdem Sie sie geschrieben haben ...

Ich finde diese Bemerkung sinnlos, weil sie sehr subjektiv ist. Was wir vielleicht versuchen können, ist, die Konsistenz in der Syntax gegenüberzustellen . Wir werden data.table und dplyr-Syntax nebeneinander vergleichen.

Wir werden mit den unten gezeigten Dummy-Daten arbeiten:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. Grundlegende Aggregations- / Aktualisierungsvorgänge.

    # 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])
    • Die Syntax von data.table ist kompakt und dplyr ist ziemlich ausführlich. In Fall (a) sind die Dinge mehr oder weniger gleichwertig.

    • In Fall (b) mussten wir filter()beim Zusammenfassen in dplyr verwenden . Aber während der Aktualisierung mussten wir die Logik nach innen verschieben mutate(). In data.table drücken wir jedoch beide Operationen mit derselben Logik aus - arbeiten Sie mit Zeilen x > 2, in denen , aber im ersten Fall, get sum(y), während im zweiten Fall diese Zeilen ymit ihrer kumulativen Summe aktualisiert werden .

      Das meinen wir, wenn wir sagen, dass die DT[i, j, by]Form konsistent ist .

    • In ähnlicher Weise können wir in Fall (c), wenn wir eine if-elseBedingung haben, die Logik "wie sie ist" sowohl in data.table als auch in dplyr ausdrücken. Wenn wir jedoch nur die Zeilen zurückgeben möchten, in denen die ifBedingung erfüllt ist, und ansonsten überspringen summarise()möchten , können wir nicht direkt verwenden (AFAICT). Wir müssen filter()zuerst und dann zusammenfassen, weil summarise()immer ein einziger Wert erwartet wird .

      Während es das gleiche Ergebnis zurückgibt, filter()macht die Verwendung hier die tatsächliche Operation weniger offensichtlich.

      Es könnte sehr gut möglich sein, es auch filter()im ersten Fall zu verwenden (scheint mir nicht offensichtlich zu sein), aber mein Punkt ist, dass wir es nicht müssen sollten.

  2. Aggregation / Aktualisierung für mehrere Spalten

    # 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))
    • In Fall (a) sind die Codes mehr oder weniger äquivalent. data.table verwendet die bekannte Basisfunktion lapply(), während dplyres *_each()zusammen mit einer Reihe von Funktionen eingeführt wird funs().

    • Für data.table :=müssen Spaltennamen angegeben werden, während dplyr diese automatisch generiert.

    • In Fall (b) ist die Syntax von dplyr relativ einfach. Das Verbessern von Aggregationen / Aktualisierungen für mehrere Funktionen steht in der Liste von data.table.

    • In Fall (c) würde dplyr jedoch nicht n()nur einmal, sondern so oft wie viele Spalten zurückgeben. In data.table müssen wir lediglich eine Liste in zurückgeben j. Jedes Element der Liste wird zu einer Spalte im Ergebnis. Wir können also wieder die bekannte Basisfunktion verwenden c(), .Num eine zu verketten , listdie a zurückgibt list.

    Hinweis: In data.table müssen wir erneut nur eine Liste in zurückgeben j. Jedes Element der Liste wird zu einer Spalte im Ergebnis. Sie können verwendet werden c(), as.list(), lapply(), list()etc ... Basisfunktionen , dies zu erreichen, ohne dass neue Funktionen erlernen zu müssen.

    Sie müssen nur die speziellen Variablen lernen - .Nund .SDzumindest. Das Äquivalent in dplyr sind n()und.

  3. Tritt bei

    dplyr bietet separate Funktionen für jeden Join-Typ, wobei data.table Joins mit derselben Syntax DT[i, j, by](und mit gutem Grund) zulässt . merge.data.table()Alternativ bietet es auch eine äquivalente Funktion.

    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"]
    ??
    • Einige finden möglicherweise eine separate Funktion für jede Verknüpfung viel besser (links, rechts, inner, anti, semi usw.), während andere möglicherweise data.table's mögen DT[i, j, by]oder merge()die Basis R ähneln.

    • Dplyr-Joins tun genau das. Nichts mehr. Nicht weniger.

    • data.tables kann Spalten beim Verbinden auswählen (2), und in dplyr müssen Sie select()zuerst beide data.frames bearbeiten, bevor Sie wie oben gezeigt verbinden können. Andernfalls würden Sie den Join mit unnötigen Spalten materialisieren, um sie später zu entfernen, und das ist ineffizient.

    • data.tables können mithilfe der Funktion beim Beitritt (3) aggregiert und beim Beitritt (4) aktualisiert werden by = .EACHI. Warum das gesamte Join-Ergebnis materialisieren, um nur wenige Spalten hinzuzufügen / zu aktualisieren?

    • data.table der Lage ist , Rollen verbindet (5) - Rolle vorwärts, LOCF , Rolle rückwärts, NOCB , am nächsten .

    • data.table hat auch ein mult =Argument, das die erste , letzte oder alle Übereinstimmungen auswählt (6).

    • data.table hat ein allow.cartesian = TRUEArgument zum Schutz vor versehentlichen ungültigen Verknüpfungen .

Auch hier stimmt die Syntax DT[i, j, by]mit zusätzlichen Argumenten überein, mit denen die Ausgabe weiter gesteuert werden kann.

  1. do()...

    Die Zusammenfassung von dplyr wurde speziell für Funktionen entwickelt, die einen einzelnen Wert zurückgeben. Wenn Ihre Funktion mehrere / ungleiche Werte zurückgibt, müssen Sie darauf zurückgreifen do(). Sie müssen im Voraus über alle Ihre Funktionen Rückgabewert wissen.

    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))))
    • .SDDas Äquivalent ist .

    • In data.table können Sie so ziemlich alles einwerfen. Sie müssen sich jnur daran erinnern, dass eine Liste zurückgegeben wird, damit jedes Element der Liste in eine Spalte konvertiert wird.

    • In dplyr kann das nicht. do()Je nachdem, wie sicher Sie sind, ob Ihre Funktion immer einen einzelnen Wert zurückgeben würde, müssen Sie darauf zurückgreifen . Und es ist ziemlich langsam.

Auch hier stimmt die Syntax von data.table überein DT[i, j, by]. Wir können einfach weiter Ausdrücke einwerfen, johne uns um diese Dinge kümmern zu müssen.

Schauen Sie sich diese und diese SO-Frage an . Ich frage mich, ob es möglich wäre, die Antwort mit der Syntax von dplyr als einfach auszudrücken ...

Zusammenfassend habe ich einige Fälle besonders hervorgehoben, in denen die Syntax von dplyr entweder ineffizient oder eingeschränkt ist oder Operationen nicht einfach macht. Dies liegt insbesondere daran, dass data.table einiges an Gegenreaktionen bezüglich der Syntax "schwerer zu lesen / lernen" (wie die oben eingefügte / verknüpfte) erhält. Die meisten Beiträge, die dplyr behandeln, sprechen über die einfachsten Operationen. Und das ist großartig. Aber es ist wichtig, auch die Syntax und die Funktionseinschränkungen zu kennen, und ich muss noch einen Beitrag dazu lesen.

data.table hat auch seine Macken (von denen ich einige darauf hingewiesen habe, dass wir versuchen, sie zu beheben). Wir versuchen auch, die Verknüpfungen von data.table zu verbessern, wie ich hier hervorgehoben habe .

Man sollte aber auch die Anzahl der Features berücksichtigen, die dplyr im Vergleich zu data.table fehlen.

4. Funktionen

Ich habe auf die meisten Funktionen hier und auch in diesem Beitrag hingewiesen . In Ergänzung:

  • fread - schneller Dateireader ist seit langem verfügbar.

  • fwrite - ein parallelisierter schneller Dateischreiber ist jetzt verfügbar. In diesem Beitrag finden Sie eine detaillierte Erläuterung zur Implementierung und in # 1664 weitere Entwicklungen.

  • Automatische Indizierung - eine weitere praktische Funktion zur internen Optimierung der Basis-R-Syntax.

  • Ad-hoc-Gruppierung : dplyrSortiert die Ergebnisse automatisch nach Gruppierung von Variablen während summarise(), was möglicherweise nicht immer wünschenswert ist.

  • Zahlreiche Vorteile der oben genannten data.table-Joins (für Geschwindigkeits- / Speichereffizienz und Syntax).

  • Non-Equi-Joins : Ermöglicht Joins mit anderen Operatoren <=, <, >, >=sowie alle anderen Vorteile von data.table-Joins.

  • Überlappende Bereichsverknüpfungen wurden kürzlich in data.table implementiert. Überprüfen Sie diesen Beitrag für eine Übersicht mit Benchmarks.

  • setorder() Funktion in data.table, die eine sehr schnelle Neuordnung von data.tables anhand von Referenzen ermöglicht.

  • dplyr bietet eine Schnittstelle zu Datenbanken mit derselben Syntax, die data.table derzeit nicht verwendet.

  • data.tablebietet Äquivalente schnelle Mengenoperationen (von Jan Gorecki geschrieben) - fsetdiff, fintersect, funionund fsetequalmit zusätzlichem allArgumente (wie in SQL).

  • data.table wird sauber und ohne Maskierungswarnungen geladen und verfügt über einen hier beschriebenen Mechanismus zur [.data.frameKompatibilität, wenn er an ein R-Paket übergeben wird. dplyr ändert Basisfunktionen filter, lagund [was zu Problemen führen kann; zB hier und hier .


Schließlich:

  • In Datenbanken - es gibt keinen Grund, warum data.table keine ähnliche Schnittstelle bereitstellen kann, aber dies hat derzeit keine Priorität. Es könnte zu Problemen kommen, wenn Benutzer diese Funktion sehr mögen würden. Ich bin mir nicht sicher.

  • Über Parallelität - Alles ist schwierig, bis jemand weitermacht und es tut. Natürlich wird es Mühe kosten (threadsicher zu sein).

    • Derzeit (in Version 1.9.7) werden Fortschritte bei der Parallelisierung bekannter zeitaufwändiger Teile für inkrementelle Leistungssteigerungen erzielt OpenMP.
Arun
quelle
9
@bluefeet: Ich glaube nicht, dass Sie dem Rest von uns einen großartigen Dienst erwiesen haben, indem Sie diese Diskussion in den Chat verschoben haben. Ich hatte den Eindruck, dass Arun einer der Entwickler war, und dies könnte zu nützlichen Erkenntnissen geführt haben.
IRTFM
2
Als ich über Ihren Link zum Chat ging, schien das gesamte Material nach dem Kommentar "Sie sollten einen Filter verwenden" weg zu sein. Vermisse ich etwas über den SO-Chat-Mechanismus?
IRTFM
6
Ich denke, dass überall, wo Sie Zuweisung durch Referenz ( :=) verwenden, dplyrÄquivalent auch <-wie in DF <- DF %>% mutate...anstelle von nur verwenden sollteDF %>% mutate...
David Arenburg
4
In Bezug auf die Syntax. Ich glaube, dplyrdass dies für Benutzer, die früher plyrSyntax data.tableverwendet haben, einfacher sein kann , aber für Benutzer, die früher die Syntax von Sprachen abfragten SQL, und für die dahinter stehende relationale Algebra, bei der es um die tabellarische Datentransformation geht, einfacher sein kann . @Arun Sie sollten beachten, dass Set-Operatoren sehr einfach durch die Wrapping- data.tableFunktion ausgeführt werden können und natürlich eine erhebliche Beschleunigung bringen.
Jangorecki
9
Ich habe diesen Beitrag so oft gelesen und er hat mir sehr geholfen, data.table zu verstehen und ihn besser nutzen zu können. In den meisten Fällen bevorzuge ich data.table gegenüber dplyr oder pandas oder PL / pgSQL. Ich konnte jedoch nicht aufhören darüber nachzudenken, wie ich es ausdrücken sollte. Die Syntax ist nicht einfach, klar oder ausführlich. Selbst nachdem ich data.table häufig verwendet habe, habe ich oft noch Schwierigkeiten, meinen eigenen Code zu verstehen, den ich vor einer Woche buchstäblich geschrieben habe. Dies ist ein Lebensbeispiel für eine reine Schreibsprache. en.wikipedia.org/wiki/Write-only_language Hoffen wir also, dass wir eines Tages dplyr für data.table verwenden können.
Ufos
385

Hier ist mein Versuch, eine umfassende Antwort aus der Perspektive von dplyr zu finden, die dem breiten Umriss von Aruns Antwort folgt (aber aufgrund unterschiedlicher Prioritäten etwas neu geordnet).

Syntax

Die Syntax ist etwas subjektiv, aber ich stehe zu meiner Aussage, dass die Prägnanz von data.table das Lernen und Lesen erschwert. Dies liegt zum Teil daran, dass dplyr ein viel einfacheres Problem löst!

Eine wirklich wichtige Sache, die dplyr für Sie tut, ist, dass es Ihre Optionen einschränkt . Ich behaupte, dass die meisten Einzeltabellenprobleme mit nur fünf Schlüsselverben gelöst werden können, die gefiltert, ausgewählt, mutiert, angeordnet und zusammengefasst werden, zusammen mit einem Adverb "nach Gruppe". Diese Einschränkung ist eine große Hilfe, wenn Sie Datenmanipulation lernen, da sie Ihnen hilft, über das Problem nachzudenken. In dplyr wird jedes dieser Verben einer einzelnen Funktion zugeordnet. Jede Funktion erledigt einen Job und ist isoliert leicht zu verstehen.

Sie schaffen Komplexität, indem Sie diese einfachen Operationen zusammen mit %>%. Hier ist ein Beispiel aus einem der Beiträge, mit denen Arun verlinkt ist :

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

Selbst wenn Sie dplyr noch nie zuvor gesehen haben (oder sogar R!), Können Sie sich dennoch ein Bild davon machen, was passiert, da die Funktionen alle englische Verben sind. Der Nachteil von englischen Verben ist, dass sie mehr Eingabe erfordern als [, aber ich denke, dass dies durch eine bessere automatische Vervollständigung weitgehend gemildert werden kann.

Hier ist der entsprechende data.table-Code:

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

Es ist schwieriger, diesem Code zu folgen, wenn Sie nicht bereits mit data.table vertraut sind. (Ich konnte auch nicht herausfinden, wie ich das Wiederholte [ auf eine Weise einrücken kann, die für mein Auge gut aussieht). Wenn ich mir Code anschaue, den ich vor 6 Monaten geschrieben habe, ist das wie ein Code, der von einem Fremden geschrieben wurde. Daher bevorzuge ich einfachen, wenn auch ausführlichen Code.

Zwei weitere kleine Faktoren, die meiner Meinung nach die Lesbarkeit leicht beeinträchtigen:

  • Da fast jede Datentabellenoperation verwendet wird [, benötigen Sie zusätzlichen Kontext, um herauszufinden, was passiert. x[y] Verbinden Sie beispielsweise zwei Datentabellen oder extrahieren Sie Spalten aus einem Datenrahmen? Dies ist nur ein kleines Problem, da in gut geschriebenem Code die Variablennamen darauf hinweisen sollten, was passiert.

  • Ich mag das group_by()ist eine separate Operation in dplyr. Es ändert die Berechnung grundlegend, so dass ich denke, dass es beim Überfliegen des Codes offensichtlich sein sollte, und es ist einfacher zu erkennen group_by()als das byArgument dafür [.data.table.

Mir gefällt auch, dass die Pipe nicht nur auf ein Paket beschränkt ist. Sie können beginnen, indem Sie Ihre Daten mit tidyr aufräumen und mit einem Plot in ggvis abschließen . Und Sie sind nicht auf die Pakete beschränkt, die ich schreibe - jeder kann eine Funktion schreiben, die einen nahtlosen Teil einer Datenmanipulations-Pipe bildet. Tatsächlich bevorzuge ich lieber den vorherigen data.table-Code, der umgeschrieben wurde mit %>%:

diamonds %>% 
  data.table() %>% 
  .[cut != "Fair", 
    .(AvgPrice = mean(price),
      MedianPrice = as.numeric(median(price)),
      Count = .N
    ), 
    by = cut
  ] %>% 
  .[order(-Count)]

Und die Idee des Piping mit %>%beschränkt sich nicht nur auf Datenrahmen und lässt sich leicht auf andere Kontexte verallgemeinern: interaktive Webgrafiken , Web Scraping , Kernel , Laufzeitverträge , ...)

Speicher und Leistung

Ich habe diese zusammengefasst, weil sie für mich nicht so wichtig sind. Die meisten R-Benutzer arbeiten mit weit unter 1 Million Datenzeilen, und dplyr ist für diese Datengröße ausreichend schnell genug, um die Verarbeitungszeit nicht zu kennen. Wir optimieren dplyr auf Ausdruckskraft bei mittleren Daten. Fühlen Sie sich frei, data.table für die Rohgeschwindigkeit bei größeren Datenmengen zu verwenden.

Die Flexibilität von dplyr bedeutet auch, dass Sie die Leistungsmerkmale mit derselben Syntax problemlos anpassen können. Wenn die Leistung von dplyr mit dem Datenrahmen-Backend für Sie nicht gut genug ist, können Sie das data.table-Backend verwenden (allerdings mit etwas eingeschränkter Funktionalität). Wenn die Daten, mit denen Sie arbeiten, nicht in den Speicher passen, können Sie ein Datenbank-Backend verwenden.

Trotzdem wird die Leistung von dplyr langfristig besser. Wir werden auf jeden Fall einige der großartigen Ideen von data.table implementieren, z. B. die Radix-Bestellung und die Verwendung des gleichen Index für Joins und Filter. Wir arbeiten auch an der Parallelisierung, damit wir mehrere Kerne nutzen können.

Eigenschaften

Ein paar Dinge, an denen wir 2015 arbeiten wollen:

  • das readrPaket, um es einfach zu machen, Dateien von der Festplatte in den Speicher zu bekommen, analog zu fread().

  • Flexiblere Verknüpfungen, einschließlich Unterstützung für Nicht-Equi-Verknüpfungen.

  • Flexiblere Gruppierung wie Bootstrap-Beispiele, Rollups und mehr

Ich investiere auch Zeit in die Verbesserung der Datenbankkonnektoren von R , die Möglichkeit, mit Web-APIs zu kommunizieren und das Scrapen von HTML-Seiten zu vereinfachen .

Hadley
quelle
27
Nur eine Randnotiz, ich stimme vielen Ihrer Argumente zu (obwohl ich die data.tableSyntax selbst bevorzuge ), aber Sie können sie leicht verwenden, %>%um data.tableOperationen zu leiten , wenn Sie keinen [Stil mögen . %>%ist nicht spezifisch für dplyr, sondern stammt aus einem separaten Paket (von dem Sie zufällig auch Mitautor sind), daher bin ich mir nicht sicher, ob ich verstehe, was Sie in den meisten Abschnitten Ihres Syntax- Absatzes sagen wollen .
David Arenburg
11
@ DavidArenburg guter Punkt. Ich habe die Syntax neu geschrieben, um hoffentlich klarer zu machen, was meine Hauptpunkte sind, und um hervorzuheben, dass Sie %>%mit data.table verwenden können
hadley
5
Danke Hadley, das ist eine nützliche Perspektive. Einrückung mache ich normalerweise DT[\n\texpression\n][\texpression\n]( Kern ), was eigentlich ziemlich gut funktioniert. Ich behalte Aruns Antwort als Antwort, da er meine spezifischen Fragen, die weniger die Zugänglichkeit der Syntax betreffen, direkter beantwortet, aber ich denke, dies ist eine gute Antwort für Leute, die versuchen, ein allgemeines Gefühl für die Unterschiede / Gemeinsamkeiten zwischen dplyrund zu bekommen data.table.
BrodieG
33
Warum an Fastread arbeiten, wenn es das schon gibt fread()? Wäre es nicht besser, Zeit für die Verbesserung von fread () oder die Arbeit an anderen (unterentwickelten) Dingen aufzuwenden?
EDi
10
Die API von data.tablebasiert auf einem massiven Missbrauch der []Notation. Das ist seine größte Stärke und seine größte Schwäche.
Paul
65

In direkter Antwort auf den Fragentitel ...

dplyr macht definitiv Dinge, data.tabledie nicht können.

Dein Punkt # 3

dplyr abstrahiert (oder wird) potenzielle DB-Interaktionen

ist eine direkte Antwort auf Ihre eigene Frage, aber nicht hoch genug. dplyrist wirklich ein erweiterbares Front-End für mehrere Datenspeichermechanismen, ebenso data.tablewie eine Erweiterung für einen einzelnen.

Betrachten Sie es dplyrals eine agnostische Back-End-Schnittstelle, bei der alle Ziele dasselbe Grammatikprogramm verwenden, über das Sie die Ziele und Handler nach Belieben erweitern können. data.tableist aus der dplyrPerspektive eines dieser Ziele.

Sie werden (ich hoffe) nie einen Tag sehen, an dem data.tableversucht wird, Ihre Abfragen zu übersetzen, um SQL-Anweisungen zu erstellen, die mit Datenträgern auf der Festplatte oder im Netzwerk arbeiten.

dplyrkann möglicherweise Dinge tun, data.tabledie nicht oder möglicherweise nicht so gut tun.

Aufgrund des Entwurfs der Arbeit im Arbeitsspeicher data.tablekönnte es viel schwieriger sein, sich auf die parallele Verarbeitung von Abfragen auszudehnen als dplyr.


Als Antwort auf die Fragen im Körper ...

Verwendungszweck

Gibt es analytische Aufgaben, die für Personen, die mit den Paketen vertraut sind, viel einfacher mit dem einen oder anderen Paket zu codieren sind (dh eine Kombination von erforderlichen Tastenanschlägen im Vergleich zum erforderlichen Grad an Esoterik, wobei weniger von jedem eine gute Sache ist).

Dies mag wie ein Kahn erscheinen, aber die wirkliche Antwort ist nein. Personen, die mit Werkzeugen vertraut sind, scheinen entweder dasjenige zu verwenden, das ihnen am vertrautesten ist, oder dasjenige, das tatsächlich das richtige für den jeweiligen Job ist. Wenn dies gesagt ist, möchten Sie manchmal eine bestimmte Lesbarkeit, manchmal ein Leistungsniveau präsentieren, und wenn Sie ein ausreichend hohes Niveau von beiden benötigen, benötigen Sie möglicherweise nur ein anderes Werkzeug, um das zu tun, was Sie bereits benötigen, um klarere Abstraktionen zu erstellen .

Performance

Gibt es analytische Aufgaben, die in einem Paket wesentlich effizienter (dh mehr als zweimal) effizienter ausgeführt werden als in einem anderen.

Wieder nein. data.tablezeichnet sich dadurch aus, dass es bei allem , was es tut , effizient ist, wenn dplyrdie Last in gewisser Hinsicht auf den zugrunde liegenden Datenspeicher und die registrierten Handler beschränkt ist.

Wenn Sie also auf ein Leistungsproblem stoßen data.table, können Sie ziemlich sicher sein, dass es in Ihrer Abfragefunktion enthalten ist. Wenn es sich tatsächlich um einen Engpass handelt data.table, haben Sie die Freude gewonnen, einen Bericht einzureichen. Dies gilt auch, wenn dplyres data.tableals Back-End verwendet wird. Möglicherweise sehen Sie einen gewissen Overhead, dplyraber wahrscheinlich ist es Ihre Anfrage.

Wenn dplyrLeistungsprobleme mit Backends auftreten, können Sie diese umgehen, indem Sie eine Funktion für die Hybridauswertung registrieren oder (bei Datenbanken) die generierte Abfrage vor der Ausführung bearbeiten.

Sehen Sie auch die akzeptierte Antwort auf wann ist plyr besser als data.table?

Thell
quelle
3
Kann dplyr eine data.table nicht mit tbl_dt umbrechen? Warum nicht einfach das Beste aus beiden Welten bekommen?
aaa90210
22
Sie vergessen, die umgekehrte Aussage "data.table macht definitiv Dinge, die dplyr nicht kann" zu erwähnen, was auch wahr ist.
Jangorecki
25
Arun Antwort erklärt es gut. Am wichtigsten (in Bezug auf die Leistung) wären Fread, Aktualisierung durch Referenz, rollierende Joins, überlappende Joins. Ich glaube, es gibt kein Paket (nicht nur dplyr), das mit diesen Funktionen konkurrieren kann. Ein schönes Beispiel kann die letzte Folie dieser Präsentation sein.
Jangorecki
15
Insgesamt ist data.table der Grund, warum ich immer noch R verwende. Andernfalls würde ich Pandas verwenden. Es ist noch besser / schneller als Pandas.
Marbel
8
Ich mag data.table wegen seiner Einfachheit und Ähnlichkeit mit der SQL-Syntaxstruktur. Meine Aufgabe besteht darin, jeden Tag sehr intensive Ad-hoc-Datenanalysen und Grafiken für die statistische Modellierung durchzuführen, und ich brauche wirklich ein Tool, das einfach genug ist, um komplizierte Dinge zu erledigen. Jetzt kann ich mein Toolkit in meinem täglichen Job auf nur data.table für Daten und Gitter für Diagramme reduzieren. Geben Sie ein Beispiel, mit dem ich sogar Operationen wie diese ausführen kann: $ DT [group == 1, y_hat: = prognostizieren (fit1, data = .SD),] $, was wirklich ordentlich ist und ich als großen Vorteil von SQL in betrachte klassische R-Umgebung.
xappppp