Daten mit R in Blöcken verarbeiten mit iotools: Big Data-Werkzeug

Beim Verarbeiten großen Datenmengen mit R kann man an Grenzen des Arbeitsspeichers stoßen. In R kann das früher geschehen, als man meinen möchte. Wer beispielsweise über 16 GB RAM verfügt und einen 10 GB-Datensatz analysieren möchte, wird feststellen, dass R bei bestimmten Operationen langsam und ineffizient wird. Laut R-Handbuch kann das bereits geschehen, wenn etwa 10% – 20% des Arbeitsspeichers belegt sind. Ein wesentlicher Grund für diese Schwelle dürfte sein, dass R beim Bearbeiten von Objekten häufig interne Kopien anlegt.

Eine Lösung für diese Herausforderung besteht darin, Daten in Blöcken (chunks) zu verarbeiten, sodass jeweils nur ein solcher Block in den Arbeitsspeicher geladen werden muss. Das iotools-Paket von Simon Urbanek und Taylor Arnold bietet eine passende Funktion dafür: chunk.apply() – ähnlich den apply-Funktionen in Base R oder den map-Funktionen in purrr.

New York City Flights 2013

Für unser Beispiel nutzen wir die flights-Daten aus dem nycflights13-Paket von Hadley Wickham. Sie sind etwa 38 MB groß und damit nicht im Big Data-Bereich, aber groß genug, um die Verarbeitung in Blöcken sinnvoll zu testen.

library(nycflights13)
library(data.table)
library(tidyverse)
library(microbenchmark)
library(iotools)

data("flights")
str(flights)
format(object.size(flights), units = "auto")

Um die Daten in Blöcken zu verarbeiten, müssen sie in einem Verzeichnis gespeichert sein:

saveRDS(flights, "flights.rds")
fwrite(flights, file = "flights.csv")

fwrite aus dem data.table-Paket ist die schnellste mir bekannte Funktion zum Speichern von .csv-Dateien. Der Vergleich der beiden Dateien zeigt, dass das R-interne Format .rds wesentlich effizienter gespeichert wird: Es belegt nur knapp 7 MB, während die .csv-Datei fast 30 MB belegt.

Im Folgenden arbeiten wir dennoch mit den .csv-Daten, da in manchen Projekten keine andere Wahl besteht, als mit einem R-externen Format zu beginnen.

csv-Dateien laden: Geschwindigkeitsvergleich Base R, readr (tidyverse) und data.table

Wie stark unterscheiden sich populäre Funktionen zum Laden von .csv-Dateien?

microbenchmark(
   readcsv_base = read.csv("flights.csv"),
   readcsv_readr = read_csv("flights.csv"),
   fread_datatable = fread("flights.csv"),
times = 3
)

datatable::fread() ist auf meiner Maschine fast 10x schneller als readr::read_csv() aus dem tidyverse, welches wiederum fast 3x schneller ist als Base Rs read.csv(). Empfehlung ist also, zumindest Base R zu vermeiden, wenn es ums Laden von .csv-Dateien geht.

Daten in Blöcken verarbeiten

Um die Daten mit chunk.apply() in Blöcken zu verarbeiten, müssen wir die Spaltentypen vorgeben. Wir entnehmen sie einfach dem Originaldatensatz aus dem nycflights13-Paket.

col_types <- sapply(flights, typeof)

Die Verarbeitung in Blöcken sieht dann so aus:

chunk.apply("flights.csv", function(x) {
   d <- dstrsplit(x, col_types = col_types, sep = ",")
   c(quantile(d[, 16], c(0.25, 0.5, 0.75), na.rm = TRUE), n = nrow(d))
   # Hier können andere Funktionen zur Datenaufbereitung oder Datenanalyse stehen
   },
   CH.MAX.SIZE = 8e6, parallel = 1)

Umwandlung der eingelesenen Daten in ein R-Objekt

Ein Unterschied zur gewohnten Analyse von Daten, die komplett im Arbeitsspeicher liegen, besteht darin, dass hier das Lesen der Daten von der Umwandlung in ein R-Objekt getrennt wird. Die Daten werden zunächst in einen einzelnen Vektor (Roh-Vektor / raw vector) eingelesen und dann mit dstrsplit() in einen Datensatz (data frame) umgewandelt. Alternativ könnte man sie auch in eine Matrix konvertierten mit mstrsplit(). Matrizen werden schneller verarbeitet als data frames, weil sie auf einen einheitlichen Datentyp festgelegt sind – was auch gleichzeitig ihren Nachteil beschreibt.

Verarbeitung in Blöcken – Beispiel

Die oben gezeigte Funktion berechnet nur Quantile und zeigt die Fallzahl (Anzahl Zeilen) pro Datenblock (chunk):

     25% 50% 75%  n
[1,] 502 872 1389 85507
[2,] 502 937 1389 86296
[3,] 502 872 1389 86385
[4,] 502 828 1400 78589

Anstelle der simplen Zeile mit der quantile()– und nrow()-Funktion kann man komplexere Schritte der Datenaufbereitung und / oder Datenanalyse einsetzen. Durch Veränderung der maximalen Chunk-Größe (CH.MAX.SIZE) kann man die Anzahl der Blöcke steuern – hier gilt es zu testen, was im konkreten Anwendungsfall am besten funktioniert – ggf. mit Hilfe von Benchmarks wie dem oben gezeigten microbenchmark, oder dem neueren bench::mark().

Die Ergebnisse werden hier als Matrix zurückgeliefert, sie können natürlich auch einem Objekt zugewiesen und ins data.frame-Format umgewandelt werden.

Welche Erfahrungen haben Sie mit Big Data-Analysen in R gemacht? Freue mich über Erfahrungen, Paket-Empfehlungen etc.

Viel Erfolg mit R!

Freue mich über Kommentare!