Das CSV-Format hat sich für viele Anwendungen überlebt. Im zweiten Teil dieser kleinen Blogserie möchte ich ausführen, wie Parquet – eine prominente Alternative zu CSV für die Speicherung tabellarischer Daten – im Vergleich mit CSV abschneidet. Ich setze dafür einige beispielhafte Datenverarbeitungsschritte in R um und erstelle quantitative Vergleiche.
Im ersten Teil dieses Blogposts bin ich auf Vor- und Nachteile des überall anzutreffenden CSV-Formats eingegangen und habe anschliessend eine aus meiner Sicht valable Alternative vorgestellt:
Testverfahren
Um die dort postulierten Vorteile von Parquet zu demonstrieren, habe ich etwas R-Code geschrieben. und einen Datensatz der Verkehrsbetriebe der Stadt Zürich (VBZ) zum Testen verwendet. Das Skript Testing-CSV-vs-Parquet.R
lädt initial eine Woche von Ist-/Soll-Fahrzeiten der VBZ aus dem Internet herunter (die Download-Geschwindigkeit soll das spätere Benchmarking nicht beeinflussen, ansonsten hätte ich das CSV auch direkt vom Web einlesen können). Bei diesen Fahrzeiten-Daten handelt sich um eine ziemlich umfangreiche CSV-Datei.
Die CSV-Datei wird dann mit der Library readr
in eine sogenannte Tibble df
(eine Art Dataframe) geladen. Die anschliessende Ausgabe in R zeigt sehr schön, dass die Funktion readr::read_csv()
ohne spezielle Anweisungen die meisten Werte in der Datei als Double und vereinzelt als String (bzw. Character) auffasst:
Wenn man die Tibble oder die CSV-Datei näher anschaut, sieht man aber, dass zum Beispiel das Attribut betriebsdatum
– wie der Name sagt – Datumswerte enthält. Diese liegen in der Form <dd.mm.yy>
(zum Beispiel 31.12.22
) vor – leider also nicht ISO 8601-konform. Der CSV-Parser der Library readr
hat (wahrscheinlich aus diesem Grund?) nicht erkannt, dass es sich um Datumswerte handelt und hat diese stattdessen als String verarbeitet – unbequem, wenn man mit den Datumswerten arbeiten will, zum Beispiel um die Fahrzeiten des Montags in der fraglichen Woche zu extrahieren. Auch unschön: Zahlreiche Attribute mit ganzzahligen Werten wurden fälschlicherweise als Doubles (also als Gleitkommazahlen) eingelesen. Das ist eine Verschwendung von Speicherplatz und kann potenziell auch numerische Probleme nach sich ziehen.
readr::read_csv()
erlaubt uns, mit dem Argument col_types
sogenannte Spaltenspezifikation mitzugeben. Das habe ich bei den nächsten Import-Operationen gemacht. Für die Tibble df2
wurden nur die drei Attribute mit Datumswerten explizit als solche spezifiziert. Für df3
habe ich dann alle Attributtypen explizit spezifiziert und die diversen unnötigen Doubles platzsparend als Integers (Ganzzahlen) importiert. Diese Schritte sind notabene nur wegen der im ersten Blogpost geschilderten Unzulänglichkeiten des CSV-Formats nötig…
Anschliessend:
- prüfe ich die Grösse der Dateien on disk und die Grösse der Tibbles in memory,
- exportiere die Tibbles in Parquet-Dateien und
- lese eine der Parquet-Dateien wiederum in eine Tibble ein.
Aus Neugier habe ich die CSV-Datei auch mal importiert mit der readr::read_csv()
-Option guess_max = Inf
. Diese Option instruiert den CSV-Parser in read_csv()
, sämtliche Spalten komplett einzulesen, um so den am besten geeigneten Datentyp pro Spalte zu bestimmen. Angesichts der Dateigrösse dauert diese Option im Vergleich zum normalen Vorgehen sehr lange. Das Default-Vorgehen von readr
ist nämlich, «nur» eine Stichprobe zu analysieren: If you don’t explicitly specify column types with the col_types
argument, readr will attempt to guess them using some simple heuristics. By default, it will inspect 1000 values, evenly spaced from the first to the last row. This is a heuristic designed to always be fast (no matter how large your file is) and, in our experience, does a good job in most cases.
Meine ganze Programmier-Übung gibt uns folgendes Zahlenmaterial:
Speicherbedarf
Speicherbedarf on disk
Die (unkomprimierte) CSV-Datei ist deutlich grösser als die wie oben beschrieben erstellten Parquet-Dateien mit demselben Dateninhalt. Wir sparen mit Parquet vs. CSV in diesem Beispiel 85% Disk Space ein! Das ist praktisch im Fall einer Desktop-basierten Nutzung. In einer Cloud-Umgebung würde es uns zudem potenziell Hosting-, Ingress- sowie Egress-Kosten sparen.
Speicherbedarf in memory
Die umständlicher (mit vollständiger Spaltenspezifikation) eingelesene CSV-Datei und die einfach eingelesene (aber davor gut erstellte) Parquet-Datei benötigen beide über 40% weniger Platz im Arbeitsspeicher als die naiv aus der CSV-Datei importierten Daten.
Lesegeschwindigkeit
Als nächstes habe ich die Geschwindigkeit untersucht, mit der die serialisierten Daten von R in eine Tibble eingelesen werden können. Im ersten Plot sieht man (über 10 Ausführungen gemittelt) die Zeit, die R benötigt, um Daten aus der Beispiel-CSV-Datei in eine Tibble zu lesen – einmal ganz ohne Spezifikation der Datentypen der Spalten, einmal mit Spezifikation der Spalten mit Datumswerten und einmal mit Spezifikation aller Spalten. Die Zeiteinheit ist Sekunden, die y-Achse ist logarithmisch. Die Statistiken wurden auf einem Apple M1 Max ermittelt.
Wie man sieht, ist die dritte Variante – die für den Anwender bzw. die Anwender aber die komplizierteste ist – mit gut 1 Sekunde Dauer etwa einen Drittel schneller als die erste (einfachste) Variante., bei der die CSV-Datei ohne Spaltenspezifikation eingelesen worden ist.
Der zweite Plot vergleicht die benötigte Zeit für das Laden der Beispiel-CSV-Datei mit der readr
-Option guess_max = Inf
(also so, dass die Spalten erst einmal komplett gescannt werden, um den geeigneten Datentyp pro Spalte zu bestimmen) mit der benötigten Zeit für das «naive» Laden der CSV-Datei. Die erste Variante dauert massiv länger: im Mittel sind es 15.8 Sekunden versus 1.4 Sekunden – also mehr als Faktor 10 Unterschied.
Der dritte Plot zeigt die Dauer des Einlesens der drei Parquet-Dateien, die aus der CSV-Datei abgeleitet worden sind: einmal vom naiv eingelesenen CSV, einmal vom CSV mit Spezifikation der Spalten mit Datumswerten und einmal mit kompletter Spaltenspezifikation. Achtung: Das Benchmarking hat hier die Zeiteinheit zu Millisekunden geswitcht, statt wie oben Sekunden. Die erste Variante dauert im Mittel 77 Millisekunden, die zweite 66 Millisekunden und die dritte (schnellste) gar nur 53 Millisekunden!
Im vierten Plot vergleiche ich der Vollständigkeit halber das «naive» Einlesen der CSV-Datei mit dem Einlesen einer voll und korrekt spezifizierten Parquet-Datei. Wichtig: Für die Endnutzerin oder den Endnutzer sind beide Operationen gleich einfach. Das Einlesen der CSV-Datei dauert über 10 Ausführungen gemittelt aber 1.39 Sekunden, das Einlesen der Parquet-Datei lediglich 0.055 Sekunden. Letzteres ist also circa 25-mal schneller!
Komfort und Vorbeugung von Datenverlust
Werden die Daten aus einer (richtig erstellten) Parquet-Datei eingelesen, haben sie automatisch die korrekten Datentypen:
Damit entfallen sämtliche Ratespiele des CSV-Readers, die Daten verfälschen können oder den benötigten Speicher (on disk oder im Arbeitsspeicher) unnötig aufblasen können.
Weg ist auch der Zusatzaufwand beim Data Engineer für das Ermitteln und Schreiben von Spaltenspezifikationen und/oder das nachträgliche Parsen von Daten (beispielsweise von Datumswerten, die als Strings eingelesen wurden) in sinnvolle Datentypen.
Fazit: Nicht schlecht, dieses Parquet.
Haben Sie Data Engineering- oder Data Science-Herausforderungen, bei denen ein effizientes Datenformat nützlich sein könnte? Oder wo das Problem etwas komplizierter ist und Sie eventuell froh um einen Austausch wären? Kontaktieren Sie mich unverbindlich.