Blog

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.

Weitere Teile der Artikelreihe:

by Mira Céline Klein

Code-Performanz in R: Parallelisierung

by Mira Céline Klein

Code-Performanz in R: Warum ist mein Code langsam

by Mira Céline Klein

Code-Performanz in R: R-Code beschleunigen