Code-Performanz in R: Mit großen Datensätzen arbeiten
Dies ist der vierte und letzte Teil unserer Serie über Code-Performanz in R. Im ersten Part ging es darum, Code-Geschwindigkeit zu messen und herauszufinden, welcher Teil des Codes langsam ist. Der zweite Teil umfasste allgemeine Techniken zur Beschleunigung von R-Code. Im dritten Teil wurde beschrieben, wie man unter Linux, Mac und Windows R-Code parallelisiert. Dieser Teil widmet sich nun den Herausforderungen, die große Datensätze mit sich bringen.
Ob ein Datensatz "groß" ist, hängt nicht nur von der Anzahl der Zeilen ab, sondern vor allem davon, welche Methoden man anwenden möchte. Einen Mittelwert über 10.000 Zahlen zu berechnen ist absolut unproblematisch, aber eine nichtlineare Regression mit vielen Variablen kann bereits mit 1000 Beobachtungen eine Weile dauern.
Manchmal kann hier Parallelisierung weiterhelfen (s. Teil 3 der Serie). Aber gerade bei großen Datensätzen stößt man beim Parallelisieren oft an die Grenzen des Arbeitsspeichers. Außerdem gibt es natürlich Berechnungen, die sich gar nicht parallelisieren lassen. Dann sind möglicherweise die Ansätze aus Teil 2 hilfreich, aber es gibt noch weitere Möglichkeiten:
Auf Stichproben arbeiten bzw. die Daten aufsplitten
Manche Berechnungen werden für große Daten nicht nur sehr langsam, sondern unmöglich, beispielsweise weil der Arbeitsspeicher zum limitierenden Faktor wird. Aber praktischerweise reicht es oft vollkommen aus, auf einer Stichprobe zu arbeiten, beispielsweise um Statistiken wie Mittelwerte zu ermitteln oder eine Regression zu berechnen. Zumindest während man den Code entwickelt, kann das viel Zeit sparen.
Eine andere Möglichkeit ist, die Daten in mehrere Teile aufzusplitten, die Berechnungen auf jedem Teil separat durchzuführen und die Ergebnisse wieder zusammenzuführen (z.B. indem man für Regressionskoeffizienten jeweils den Mittelwert berechnet). Falls der Arbeitsspeicher nicht zum Problem wird, lassen sich diese Berechnungen möglicherweise sogar parallelisieren.
Das klingt auf den ersten Blick kontraintuitiv: Warum lässt sich ein Modell schneller berechnen, wenn man es mehrfach berechnen muss, wenn auch auf weniger Daten? Die Ursache liegt darin, dass viele Methoden (z.B. Regressionsanalysen) mit Matrizen arbeiten. Und diese wachsen nicht linear, sondern quadratisch mit der Anzahl von Beobachtungen - was in der Folge auch für den Rechenaufwand und Arbeitsspeicher gilt. Deswegen kann eine halb so große Datenmenge zu ungefähr viermal weniger benötigten Ressourcen führen.
Arbeitsspeicher freigeben
Wenn der Arbeitsspeicher zum Problem wird, sollte man zunächst prüfen, ob in der R-Umgebung große Objekte herumliegen, die man nicht mehr benötigt. Diese lassen sich mit rm
löschen. Danach empfiehlt es sich, mit gc
eine sogenannte "garbage collection" durchzuführen (also "die Müllabfuhr zu rufen"), sodass der Platz im Arbeitsspeicher auch wirklich wieder anderen Anwendungen zur Verfügung steht:
rm(largeObject)
gc() # garbage collection
Die garbage collection wird zwar ohnehin regelmäßig automatisch ausgeführt, aber indem man sie selbst ausführt, ist sichergestellt, dass der Arbeitsspeicher sofort freigegeben wird.
Base R vs. dplyr vs. data.table
Gerade beim Data Handling finden viele R-Programmierer*innen dplyr eleganter als Base R, und oft ist es auch schneller. Aber es gibt eine noch viel schnellere Alternative: das Paket data.table. Schon bei kleinen Aktionen ist der Unterschied sichtbar, beispielsweise beim Auswählen von Spalten oder einer gruppenweisen Mittelwertberechnung:
library(data.table)
library(dplyr)
cols <- c("Sepal.Length", "Sepal.Width")
irisDt <- as.data.table(iris)
# Spaltenauswahl
microbenchmark("dplyr" = iris %>% select(cols),
"data.table" = irisDt[, cols, with = FALSE])
## Unit: microseconds
## expr min lq mean median uq max neval
## dplyr 1890.744 2099.4445 2770.3403 2401.4760 3132.7005 9259.750 100
## data.table 62.763 76.5215 179.3211 110.4575 147.2455 5923.169 100
# Gruppenweise Mittelwert berechnen
microbenchmark("dplyr" = iris %>%
group_by(Species) %>%
summarise(mean(Sepal.Length)),
"data.table" = irisDt[,.(meanSL = mean(Sepal.Length)),
by = Species])
## Unit: microseconds
## expr min lq mean median uq max neval
## dplyr 3758.252 4686.548 5769.8606 5533.120 6430.0995 14503.304 100
## data.table 415.039 512.455 665.5811 613.622 718.2905 1646.667 100
Bei zeitaufwändigeren Berechnungen - zum Beispiel auf großen Datensätzen - sind die Unterschiede noch beeindruckender.
Datenbanken verwenden
Statt für jede Analyse die kompletten Daten in R zu laden, lassen sie sich auch in einer Datenbank speichern, z.B. einer SQL-Datenbank. Das hat einige Vorteile:
- Beim Abruf der Daten kann sich in der Datenbankabfrage (Query) auf die relevanten Zeilen und Spalten beschränken und muss nicht die kompletten Daten in den Arbeitsspeicher laden.
- Auch ein Teil des Data Handlings lässt sich bereits in der Datenbankabfrage unterbringen, z.B. Sortierung oder gruppierte Berechnungen.
- Vorbereitete Datensätze (z.B. aggregierte Daten oder die Kombinationen aus mehreren Datenquellen) lassen sich praktisch in der Datenbank abspeichern und sind aus R heraus verfügbar. Datenbanken sind generell sehr gut für solche Berechnungen geeignet und damit sehr schnell.