Was kann ich mit dtplyr nicht machen, was ich in data.table kann?

9

Soll ich meinen Lernaufwand für die Daten Gerangel in R, und zwar zwischen investieren dplyr, dtplyrund data.table?

  • Ich benutze dplyrmeistens, aber wenn die Daten dafür zu groß sind, werde ich sie verwenden data.table, was selten vorkommt. Nun, da dtplyrv1.0 als Schnittstelle für herauskommt data.table, scheint es, als müsste ich mir nie wieder Gedanken über die Verwendung der data.tableSchnittstelle machen.

  • Was sind die nützlichsten Funktionen oder Aspekte data.table, die derzeit nicht verwendet werden könnendtplyr und mit denen dies wahrscheinlich nie geschehen wird dtplyr?

  • Auf dplyrden ersten Blick data.tableklingt es so, als würde dtplyres überholen dplyr. Wird es einen Grund geben, es zu verwenden, dplyrwenn dtplyres vollständig ausgereift ist?

Hinweis: Ich frage nicht nach dplyrvs data.table(wie in data.table vs dplyr: Kann einer etwas gut machen, was der andere nicht kann oder schlecht macht? ), Aber da einer für ein bestimmtes Problem dem anderen vorgezogen wird, warum nicht? t dtplyrdas zu verwendende Werkzeug sein.

dule arnaux
quelle
1
Gibt es etwas , in dplyrdem du gut abschneiden kannst und in dem du nicht gut abschneiden kannst data.table? Wenn nicht, ist der Wechsel data.tablezu besser als dtplyr.
Sindri_baldur
2
Aus der dtplyrReadme-Datei: „Einige data.tableAusdrücke haben keine direkte dplyrEntsprechung. Zum Beispiel gibt es keine Möglichkeit, Cross- oder Rolling-Joins mit auszudrücken dplyr. ' und 'Um der dplyrSemantik zu entsprechen, wird mutate() standardmäßig nicht geändert. Dies bedeutet, dass die meisten Ausdrücke mutate()eine Kopie erstellen müssen, die bei data.tabledirekter Verwendung nicht erforderlich wäre . ' Es gibt einen Weg um diesen zweiten Teil herum, aber wenn man bedenkt, wie oft er mutateverwendet wird, ist das in meinen Augen ein ziemlich großer Nachteil.
ClancyStats

Antworten:

15

Ich werde versuchen, meine besten Anleitungen zu geben, aber es ist nicht einfach, weil man mit allen {data.table}, {dplyr}, {dtplyr} und auch {base R} vertraut sein muss. Ich benutze {data.table} und viele {tidy-world} -Pakete (außer {dplyr}). Ich liebe beide, obwohl ich die Syntax von data.table gegenüber dplyr's bevorzuge. Ich hoffe, dass alle Tidy-World-Pakete {dtplyr} oder {data.table} als Backend verwenden, wann immer dies erforderlich ist.

Wie bei jeder anderen Übersetzung (denken Sie an dplyr-to-sparkly / SQL) gibt es Dinge, die zumindest vorerst übersetzt werden können oder nicht. Ich meine, vielleicht kann {dtplyr} es eines Tages zu 100% übersetzen, wer weiß. Die folgende Liste ist weder vollständig noch zu 100% korrekt, da ich mein Bestes geben werde, um auf der Grundlage meines Wissens zu verwandten Themen / Paketen / Problemen / etc. Zu antworten.

Für die Antworten, die nicht ganz korrekt sind, hoffe ich, dass es Ihnen einige Anleitungen gibt, auf welche Aspekte von {data.table} Sie achten sollten, und vergleichen Sie sie mit {dtplyr} und finden Sie die Antworten selbst heraus. Nehmen Sie diese Antworten nicht als selbstverständlich an.

Und ich hoffe, dieser Beitrag kann als eine der Ressourcen für alle {dplyr}, {data.table} oder {dtplyr} Benutzer / Ersteller für Diskussionen und Kooperationen verwendet werden und #RStats noch besser machen.

{data.table} wird nicht nur für schnelle und speichereffiziente Operationen verwendet. Es gibt viele Leute, einschließlich mir, die die elegante Syntax von {data.table} bevorzugen. Es enthält auch andere schnelle Operationen wie Zeitreihenfunktionen wie frollapplydie in C geschriebene Rolling-Family (dh ). Es kann mit allen Funktionen verwendet werden, einschließlich Tidyverse. Ich benutze {data.table} + {purrr} viel!

Komplexität der Operationen

Dies kann leicht übersetzt werden

library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)

# dplyr 
diamonds %>%
  filter(cut != "Fair") %>% 
  group_by(cut) %>% 
  summarize(
    avg_price    = mean(price),
    median_price = as.numeric(median(price)),
    count        = n()
  ) %>%
  arrange(desc(count))

# data.table
data [
  ][cut != 'Fair', by = cut, .(
      avg_price    = mean(price),
      median_price = as.numeric(median(price)),
      count        = .N
    )
  ][order( - count)]

{data.table} ist sehr schnell und speichereffizient, da (fast?) alles von Grund auf von C mit den Schlüsselkonzepten Update-by-Reference erstellt wird , Key (Think SQL) und deren unermüdlicher Optimierung überall im Paket erstellt wird (dh fifelse, fread/freadRadix - Sortierreihenfolge von Basis R angenommen), wobei er darauf achtete die Syntax ist präzise und konsistent, das ist , warum ich denke , es ist elegant ist.

Von der Einführung in data.table werden die wichtigsten Datenmanipulationsvorgänge wie Teilmenge, Gruppe, Aktualisierung, Verknüpfung usw. für zusammengehalten

  • prägnante und konsistente Syntax ...

  • flüssige Analyse ohne die kognitive Belastung, jede Operation abbilden zu müssen ...

  • Automatische und sehr effektive Optimierung von Vorgängen durch genaue Kenntnis der für jeden Vorgang erforderlichen Daten, was zu einem sehr schnellen und speichereffizienten Code führt

Der letzte Punkt als Beispiel:

# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
        .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
  • Wir haben zuerst eine Teilmenge in i, um übereinstimmende Zeilenindizes zu finden, bei denen der Ursprungsflughafen gleich "JFK" und der Monat gleich 6L ist. Wir setzen noch nicht die gesamte Datentabelle, die diesen Zeilen entspricht, unter.

  • Nun schauen wir uns j an und stellen fest, dass es nur zwei Spalten verwendet. Und was wir tun müssen, ist ihren Mittelwert zu berechnen (). Daher setzen wir nur die Spalten unter, die den übereinstimmenden Zeilen entsprechen, und berechnen ihren Mittelwert ().

Da sich die drei Hauptkomponenten der Abfrage (i, j und by) zusammen in [...] befinden , kann data.table alle drei anzeigen und die Abfrage vor der Auswertung insgesamt optimieren, nicht jeweils einzeln . Wir sind daher in der Lage, die gesamte Teilmenge (dh die Teilmenge der Spalten neben arr_delay und dep_delay) sowohl für die Geschwindigkeit als auch für die Speichereffizienz zu vermeiden.

Um die Vorteile von {data.table} nutzen zu können, muss die Übersetzung von {dtplr} in dieser Hinsicht korrekt sein. Je komplexer die Operationen sind, desto schwieriger sind die Übersetzungen. Für einfache Operationen wie oben kann es sicherlich leicht übersetzt werden. Für komplexe oder solche, die nicht von {dtplyr} unterstützt werden, müssen Sie sich wie oben erwähnt selbst herausfinden, die übersetzte Syntax und den Benchmark vergleichen und vertraute verwandte Pakete kennen.

Für komplexe oder nicht unterstützte Operationen kann ich im Folgenden einige Beispiele nennen. Wieder versuche ich nur mein Bestes. Sei sanft zu mir.

Update-by-Reference

Ich werde nicht auf das Intro / die Details eingehen, aber hier sind einige Links

Hauptressource: Referenzsemantik

Weitere Details: Genau verstehen, wann eine data.table auf eine andere data.table verweist (im Vergleich zu einer Kopie davon)

Update-by-Reference ist meiner Meinung nach das wichtigste Merkmal von {data.table} und das macht es so schnell und speichereffizient. dplyr::mutateunterstützt es standardmäßig nicht. Da ich mit {dtplyr} nicht vertraut bin, bin ich mir nicht sicher, wie viel und welche Vorgänge von {dtplyr} unterstützt werden können oder nicht. Wie oben erwähnt, hängt dies auch von der Komplexität der Operationen ab, die sich wiederum auf die Übersetzungen auswirken.

Es gibt zwei Möglichkeiten, Update-by-Reference in {data.table} zu verwenden

  • Zuweisungsoperator von {data.table} :=

  • set-Familie: set, setnames, setcolorder, setkey, setDT, fsetdiff, und viele mehr

:=wird im Vergleich zu häufiger verwendet set. Bei komplexen und großen Datenmengen durch Referenz aktualisieren der Schlüssel, um Höchstgeschwindigkeit und Speichereffizienz zu erzielen. Die einfache Denkweise (nicht 100% genau, da die Details viel komplizierter sind, da es sich um Hard- / Flachkopien und viele andere Faktoren handelt) besagt, dass es sich um einen großen Datensatz von 10 GB mit 10 Spalten und jeweils 1 GB handelt . Um eine Spalte zu bearbeiten, müssen Sie nur 1 GB verarbeiten.

Der entscheidende Punkt ist, dass Sie mit Update-by-Reference nur die erforderlichen Daten verarbeiten müssen. Aus diesem Grund verwenden wir bei der Verwendung von {data.table}, insbesondere bei großen Datenmengen, nach Möglichkeit immer die Aktualisierung per Referenz . Beispiel: Bearbeiten eines großen Modellierungsdatensatzes

# Manipulating list columns

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)

# data.table
dt [,
    by = Species, .(data   = .( .SD )) ][,  # `.(` shorthand for `list`
    model   := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
    summary := map(model, summary) ][,
    plot    := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                           geom_point())]

# dplyr
df %>% 
  group_by(Species) %>% 
  nest() %>% 
  mutate(
    model   = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
    summary = map(model, summary),
    plot    = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                          geom_point())
  )

Der Verschachtelungsvorgang wird list(.SD)möglicherweise von {dtlyr} nicht unterstützt, wenn tidyverse Benutzer tidyr::nest? Ich bin mir also nicht sicher, ob die nachfolgenden Operationen so übersetzt werden können, dass {data.table} schneller und weniger Speicherplatz ist.

HINWEIS: Das Ergebnis von data.table ist in "Millisekunde", dplyr in "Minute".

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))

bench::mark(
  check = FALSE,

  dt[, by = Species, .(data = list(.SD))],
  df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
#   expression                                   min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#   <bch:expr>                              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms   2.49      705.8MB     1.24     2     1
# 2 df %>% group_by(Species) %>% nest()        6.85m    6.85m   0.00243     1.4GB     2.28     1   937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# #   gc <list>

Es gibt viele Anwendungsfälle von Update-by-Reference und selbst {data.table} -Benutzer verwenden die erweiterte Version nicht immer, da mehr Codes erforderlich sind. Ob {dtplyr} diese Out-of-the-Box unterstützt, müssen Sie selbst herausfinden.

Mehrere Referenzaktualisierungen für dieselben Funktionen

Hauptressource : Elegantes Zuweisen mehrerer Spalten in data.table mit lapply ()

Dies betrifft entweder die am häufigsten verwendete :=oder set.

dt <- data.table( matrix(runif(10000), nrow = 100) )

# A few variants

for (col in paste0('V', 20:100))
  set(dt, j = col, value = sqrt(get(col)))

for (col in paste0('V', 20:100))
  dt[, (col) := sqrt(get(col))]

# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])

Laut Ersteller von {data.table} Matt Dowle

(Beachten Sie, dass es möglicherweise üblicher ist, eine Schleife über eine große Anzahl von Zeilen als über eine große Anzahl von Spalten zu erstellen.)

Join + Setkey + Update-by-Reference

Ich brauchte in letzter Zeit eine schnelle Verknüpfung mit relativ großen Datenmengen und ähnlichen Verknüpfungsmustern, daher verwende ich anstelle der normalen Verknüpfungen die Möglichkeit, durch Referenz zu aktualisieren . Da sie mehr Codes benötigen, verpacke ich sie in ein privates Paket mit einer nicht standardmäßigen Bewertung für Wiederverwendbarkeit und Lesbarkeit, wo ich es nennesetjoin .

Ich habe hier einen Benchmark durchgeführt: data.table join + update-by-reference + setkey

Zusammenfassung

# For brevity, only the codes for join-operation are shown here. Please refer to the link for details

# Normal_join
x <- y[x, on = 'a']

# update_by_reference
x_2[y_2, on = 'a', c := c]

# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]

HINWEIS: dplyr::left_joinwurde ebenfalls getestet und ist mit ~ 9.000 ms am langsamsten. Verwenden Sie mehr Speicher als beide {data.table} update_by_referenceund setkey_n_update, aber weniger Speicher als den normalen_join von {data.table}. Es verbrauchte ungefähr ~ 2,0 GB Speicher. Ich habe es nicht aufgenommen, da ich mich ausschließlich auf {data.table} konzentrieren möchte.

Wichtigste Ergebnisse

  • setkey + updateund updatesind ~ 11 und ~ 6,5 - mal schneller als normal joinjeweils
  • Beim ersten Join setkey + updateähnelt die Performance von dem updateOverhead, der setkeydie eigenen Performancegewinne weitgehend ausgleicht
  • bei zweiten und nachfolgenden Verknüpfungen ist, wie setkeynicht erforderlich, setkey + updateschneller als das update~ 1,8-fache (oder schneller als das normal join~ 11-fache)

Bild

Beispiele

Verwenden Sie für performante und speichereffiziente Verknüpfungen entweder updateoder setkey + update, wenn letzteres auf Kosten von mehr Codes schneller ist.

Der Kürze halber sehen wir uns einige Pseudocodes an . Die Logik ist die gleiche.

Für eine oder mehrere Spalten

a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)

# `update`
a[b, on = .(x), y := y]
a[b, on = .(x),  `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x),  `:=` (y = y, z = z, ...) ]

Für viele Spalten

cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]

Wrapper für schnelle und speichereffiziente Verknüpfungen ... viele von ihnen ... mit ähnlichem Verknüpfungsmuster, wickeln Sie sie wie setjoinoben - mit update - mit oder ohnesetkey

setjoin(a, b, on = ...)  # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop   = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
  setjoin(...) %>%
  setjoin(...)

Mit setkeyArgumenton weggelassen werden. Es kann auch zur besseren Lesbarkeit enthalten sein, insbesondere für die Zusammenarbeit mit anderen.

Große Reihenoperation

  • wie oben erwähnt verwenden set
  • Füllen Sie Ihre Tabelle vorab aus und verwenden Sie Update-by-Reference- Techniken
  • Teilmenge mit Schlüssel (dh setkey)

Verwandte Ressource: Fügen Sie am Ende eines data.table-Objekts eine Zeile als Referenz hinzu

Zusammenfassung der Aktualisierung durch Referenz

Dies sind nur einige Anwendungsfälle von Update-by-Reference . Es gibt viele mehr.

Wie Sie sehen können, gibt es für die erweiterte Verwendung des Umgangs mit großen Datenmengen viele Anwendungsfälle und -techniken, bei denen Update-by-Reference verwendet wird für große Datenmengen verwendet wird. Es ist nicht so einfach in {data.table} zu verwenden und ob {dtplyr} es unterstützt, können Sie selbst herausfinden.

Ich konzentriere mich auf Update-by-Reference in diesem Beitrag da ich denke, dass dies die leistungsstärkste Funktion von {data.table} für schnelle und speichereffiziente Vorgänge ist. Das heißt, es gibt viele, viele andere Aspekte, die es auch so effizient machen, und ich denke, die von {dtplyr} nicht nativ unterstützt werden.

Andere Schlüsselaspekte

Was unterstützt wird / nicht, hängt auch von der Komplexität der Vorgänge ab und davon, ob es sich um die native Funktion von data.table handelt, z. B. Update-by-Reference oder setkey. Und ob der übersetzte Code der effizientere ist (einer, den Benutzer von data.table schreiben würden), ist auch ein weiterer Faktor (dh der Code wird übersetzt, aber ist es die effiziente Version?). Viele Dinge sind miteinander verbunden.

  • setkey. Siehe Schlüssel und schnelle Teilmenge der binären Suche
  • Sekundärindizes und automatische Indizierung
  • Verwenden von .SD für die Datenanalyse
  • Zeitreihenfunktionen: Denken frollapply. Walzfunktionen, Walzaggregate, Schiebefenster, gleitender Durchschnitt
  • Rolling Join , Non-Equi Join , (einige) "Cross" Join
  • {data.table} hat die Grundlage für Geschwindigkeit und Speichereffizienz geschaffen. In Zukunft kann es viele Funktionen umfassen (z. B. die Implementierung der oben genannten Zeitreihenfunktionen).
  • im Allgemeinen, die komplexere Operationen auf data.table der i, joder byOperationen (können Sie fast alle Ausdrücke dort verwenden), ich denke , die die Übersetzungen härter, vor allem wenn es mit kombinieren Update-by-reference , setkeyund anderen einheimischen data.table funktioniert wiefrollapply
  • Ein weiterer Punkt bezieht sich auf die Verwendung von Basis R oder Tidyverse. Ich benutze beide data.table + tidyverse (außer dplyr / readr / tidyr). Bei großen Operationen vergleiche ich häufig die stringr::str_*Funktionen von Familie und Basis R, und ich finde, dass Basis R bis zu einem gewissen Grad schneller ist, und verwende diese. Der Punkt ist, halten Sie sich nicht nur an tidyverse oder data.table oder ..., sondern erkunden Sie andere Optionen, um die Arbeit zu erledigen.

Viele dieser Aspekte hängen mit den oben genannten Punkten zusammen

  • Komplexität der Operationen

  • Update-by-Reference

Sie können herausfinden, ob {dtplyr} diese Vorgänge unterstützt, insbesondere wenn sie kombiniert werden.

Ein weiterer nützlicher Trick beim Umgang mit kleinen oder großen Datenmengen während einer interaktiven Sitzung: {data.table} erfüllt wirklich das Versprechen, die Programmier- und Rechenzeit enorm zu reduzieren .

Einstellschlüssel für wiederholt verwendete Variablen für Geschwindigkeit und 'aufgeladene Rownamen' (Teilmenge ohne Angabe des Variablennamens).

dt <- data.table(iris)
setkey(dt, Species) 

dt['setosa',    do_something(...), ...]
dt['virginica', do_another(...),   ...]
dt['setosa',    more(...),         ...]

# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders. 
# It's simply elegant
dt['setosa', do_something(...), Species, ...]

Wenn Ihre Operationen nur einfache wie im ersten Beispiel umfassen, kann {dtplyr} die Aufgabe erledigen. Für komplexe / nicht unterstützte können Sie dieses Handbuch verwenden, um die übersetzten von {dtplyr} damit zu vergleichen, wie erfahrene data.table-Benutzer mit der eleganten Syntax von data.table schnell und speichereffizient codieren würden. Die Übersetzung bedeutet nicht, dass dies der effizienteste Weg ist, da es möglicherweise unterschiedliche Techniken gibt, um mit unterschiedlichen Fällen großer Datenmengen umzugehen. Für noch größere Datenmengen können Sie {data.table} mit {disk.frame} , {fst} und {drake} und anderen fantastischen Paketen kombinieren , um das Beste daraus zu machen. Es gibt auch eine {big.data.table} , die derzeit jedoch inaktiv ist.

Ich hoffe es hilft allen. Einen schönen Tag noch ☺☺

K22
quelle
2

Nicht-Equi-Joins und Rolling-Joins kommen in den Sinn. Es scheint keine Pläne zu geben, überhaupt äquivalente Funktionen in dplyr aufzunehmen, daher gibt es für dtplyr nichts zu übersetzen.

Es gibt auch eine Umformung (optimierter Dcast und Schmelze, die den gleichen Funktionen in reshape2 entspricht), die nicht auch in dplyr enthalten ist.

Alle * _if- und * _at-Funktionen können derzeit nicht mit dtplyr übersetzt werden, aber diese sind in Arbeit.

EdTeD
quelle
0

Aktualisieren einer Spalte beim Beitritt Einige .SD-Tricks Viele f-Funktionen Und Gott weiß was noch, denn #rdatatable ist mehr als nur eine einfache Bibliothek und kann nicht mit wenigen Funktionen zusammengefasst werden

Es ist ein ganzes Ökosystem für sich

Ich habe dplyr seit dem Tag, an dem ich mit R angefangen habe, nie mehr gebraucht. Weil data.table so verdammt gut ist

Vikram
quelle