Ich schwärme für data.tables

Über die Vorteile von R's data.tables

von Stefano Ugliano, Lead AI Engineer bei 5Analytics

Ein paar Gründe, warum ich in R's data.table-Klasse verliebt bin

Mit zunehmender Leistungsfähigkeit wird R zu mehr als nur einem einfachen Werkzeug oder einer Programmiersprache – R entwickelt sich zu einer Denkweise, um Probleme anzugehen. Wie die Vielzahl von Antworten auf Stack Exchange zeigen, ist eines der besten Beispiele für diese Kompetenz der Einsatz von Schleifen: Eine gute Programmierung in R vermeidet Schleifen fast vollständig. Wenn auch nicht so sehr wegen Geschwindigkeitsproblemen, sondern weil es normalerweise eine bessere, sauberere und einfachere Methode gibt (die Funktion apply ist nur eines von vielen Beispielen). [1]

Einige Pakete erweitern das Potential von R beträchtlich. Andere schleifen oder vereinfachen das bereits Vorhandene und oft beschleunigen sie sowohl die Programmierarbeit für uns Menschen als auch die Verarbeitungszeit unserer geliebten Arbeitsmaschinen. [2]

In meinem Fall hat das Paket data.table beides getan (und noch einiges mehr), indem es die ohnehin schon brillante native Klasse dataframe über mein Vorstellungsvermögen hinaus beschleunigte, und einen Hauch von funktionaler Programmierung hinzufügte. Was ich hier präsentieren werde, ist definitiv kein ausführlicher Leitfaden und keine ordnungsgemäße Einführung in das Paket. Man kann es eher als Anriss oder als wohlverdientes Lob sehen, nachdem was das Paket in den letzten Monaten für mich getan hat.

Die Klasse data.table, um die herum das Paket vor etwa 10 Jahren zum ersten Mal entwickelt wurde, ist in vielerlei Hinsicht R’s dataframe sehr ähnlich, hat aber den großen Vorteil, dass es möglich ist, ein Objekt per Referenz anstatt per Kopie zu modifizieren. Das allein würde schon ausreichen, um data.table im Umgang mit "big-ish data" zu übertreffen – also jenen Datenobjekten, die R verarbeiten könnte, wenn der Speicher gut verwaltet würde und wir genügend Zeit hätten. Aber data.table kann sogar noch viel mehr. Nehmen wir zum Beispiel die selten angenehme Erfahrung, Daten aus einer großen .csv-Datei zu importieren: Meine persönlichen Messungen zeigen, dass data.table's eigene Funktion fread() um den sagenhaften Faktor 20 schneller ist als die Basisfunktion read.csv() – und andere Anwender berichten von ähnlichen, wenn nicht sogar größeren Verbesserungen!

Ein weiterer Vorteil von data.table ist die nahezu unmittelbare Datenbeschaffung. Indem Sie eine oder mehrere Spalten der Tabelle als key setzen, wird das gesamte Objekt vorsortiert, und ermöglicht es Ihnen so, viel schneller Untergruppierungen vorzunehmen. Möchten Sie Ihre große Tabelle nach allen Transaktionen abfragen, die von diesem oder jenem Kunden durchgeführt wurden? Stellen Sie einfach customer als key ein, und Sie haben sich und Ihrem Code wertvolle Zeit gespart. Wie viel Zeit? Wir sprechen hier von Binärbaum gegen Vector-Scan, also O(log n) gegen O(n). Eine einfache Zeitmessung, wie in der Einleitung beschrieben, ergibt eine Verbesserung um ca. den Faktor 1400. (Und mit einem Male ist der vorhin erwähnte Faktor 20 gar nicht mehr so beeindruckend ...)

Ich bin sicher, dass die Frage nach dem Warum eines solchen Pakets jetzt jedem klar ist, deshalb möchte ich das Wie kurz vorstellen. Wie bereits erwähnt, gehen all diese Geschwindigkeitsverbesserungen mit einer etwas anderen Syntax einher, die es data.table erlaubt, viel mehr zu tun als seine Geschwisterklassen. Das kanonische Beispiel für die Annäherung an ein data.table-Objekt lautet

DT[i, j, by]

wobei

  • DT das Objekt selbst ist;
  • i das Kriterium ist, nach dem die Zeilen untergeordnet werden;
  • j die Spaltenauswahl ist;
  • die Aggregationsregel ist.

Diese Regeln entsprechen denen in SQL-Abfragen, wobei die beiden Sprachen wie folgt übereinstimmen:

R vs SQL
i   = WHERE
j   = SELECT
by = GROUP BY

Sehen wir uns nun deren Einsatz an einem knappen Beispiel an, das den allgegenwärtigen Datensatz mtcars verwendet:

> library(data.table)
> dt <- data.table(mtcars)

Zuerst suchen wir alle Informationen raus für Autos, deren "cyl" gleich 6 ist – anstelle des klassischen dt[dt$cyl == 6, ] können wir uns ein wenig Tippen sparen (ich liebe das) und direkt schreiben:

> dt[cyl == 6]

  mpg cyl disp hp drat wt qsec vs am gear  carb 
1:  21.0  6  160.0  110  3.90  2.620  16.46  0  1  4  4
2:  21.0  6  160.0  110  3.90  2.875  17.02  0  1  4  4
3:  21.4  6  258.0  110  3.08  3.215  19.44   1  0  3  1
4:  18.1  6  225.0  105  2.76  3.460  20.22   1  0  3  1
5:  19.2  6  167.6  123  3.92  3.440  18.30   1  0  4  4
6:  17.8  6  167.6  123  3.92  3.440  18.90  1  0  4  4
7:  19.7  6  145.0  175  3.62  2.770  15.50  0  1  5  6


Was der Abfrage SELECT * FROM dt WHERE cyl = 6; entspricht.

Um das durchschnittliche Gewicht ("wt") aller oben genannten Fahrzeuge zu erhalten, fügen wir einfach innerhalb der Klammern ein:

> dt[cyl == 6, mean(wt)]
[1] 3.117143

Ist das nicht brillant? Und nun stellen wir uns Folgendes vor: Wir wollen das Durchschnittsgewicht der Autos, wenn wir sie nach "gear" gruppieren – und außerdem sollten die Reihen nach steigendem Durchschnittsgewicht geordnet werden:

> dt[cyl == 6, .(avgwt = mean(wt)), by = gear][order(avgwt)]

  gear avgwt
1: 5 2.77000
2: 4 3.09375
3: 3 3.33750


Beachten Sie hier die ungewöhnliche Verwendung des Punktes vor den Klammern, der eine komplexere Anfrage für j einschließt (diese wäre auch notwendig gewesen, um mehrere Spalten gleichzeitig abzufragen). Noch dazu könnten wir direkt ein zweites Paar Klammern hinzufügen, um die Reihenfolge festzulegen. Theoretisch könnten wir ein neues komplettes "i, j, by"-Triplett auf der data.table verwenden, das von dem ersten Paar Klammern zurückgegeben wird – und danach ein neues, und so weiter ...

Über data.table gibt es noch viel mehr zu zeigen und zu erzählen, aber ich werde es hier nicht weiter vertiefen. Vielmehr bin ich zufrieden, wenn es mir gelingt, Ihre Neugierde auf dieses Meisterwerk einer Bibliothek zu wecken, und ich lasse Ihnen den Spaß daran, es selbst zu erforschen. Ich bin mir jedoch sicher, dass Sie nicht bereuen werden, es auszuprobieren!

-------------------------------------------------

Zusätzliches Material und weiterführende Lektüre:

Ich kann nicht so tun, als wäre ich der erste, der über dieses wunderbare Paket geschrieben hat. Auf github.com im data.table Wiki, finden Sie viele Links zu den Werken meiner Vorgänger. Das gleiche Wiki sammelt jede Menge interessantes Material, das als Hilfestellung bei der ersten Annäherung an data.tables verwendet werden kann.

Ein interessanter Ort, um einige Grundlagen von data.table in Form von Videovorlesungen zu erlernen, ist das erste Kapitel des Datacamp-Kurses, der von Matt Dowle (dem Erfinder von data.table selbst!) unterrichtet wird. Nur das erste Kapitel ist kostenlos, aber es ist immer besser als nichts ...

Eine andere Lehrmeinung zieht den modularen Formalismus von dplyr vor, einem Paket – und wer bin ich, um darüber zu urteilen? Hier noch ein interessanter Kommentar über die data.table vs. dplyr Fehde von Hadley Wickham, Vater von dplyr (unter vielen anderen). Auch wenn mich der "pipe formalism" sehr verzaubert, so habe ich doch vor, noch eine Weile auf der Seite der R-Befürworter zu bleiben.



-----------------------------------------------------------------------------------------

Anmerkungen:

[1] Es ist eine Art Mythos, dass eine einzige for-Schleife genügt, um den gesamten Code zum Stillstand zu bringen: R kann Schleifen eigentlich ganz ordentlich handhaben – im Gegensatz zu Schleifen in Mathematica, die mir mit Grauen in Erinnerung geblieben sind ...

[2] Wie Sie wissen, ist es sowieso besser, sich auf die Singularität vorzubereiten.

Kommentare
Einen Kommentar schreiben
Entschuldigung

Ihr Browser ist leider nicht aktuell genug.
Um diese Webseite zu benutzen, benötigen Sie einen aktuellen Browser
Wir empfehlen einen der folgenden Browser in der aktuellsten Version zu installieren.

Auf iOS Geräten sollte mindestens die Betriebssystem Version iOS 9 installiert sein.

Mozilla Firefox Google Chrome Microsoft Edge Internet Explorer