Grosse Datenmengen

Data Wrangling
Tooling
R
Python
Wenn CSV und Arbeitsspeicher an Grenzen kommen: Parquet, nur benötigte Spalten lesen und verzögerte Auswertung mit Arrow und Polars.

Kernideen

  • Gross heisst hier: Die Datei ist zu langsam zu lesen oder passt kaum in den Arbeitsspeicher, nicht „Big Data” im Rechenzentrum
  • Parquet speichert spaltenweise, komprimiert und mit Datentypen; CSV speichert Text
  • Wer nur drei von fünfzig Spalten braucht, liest nur diese drei
  • Verzögerte Auswertung: Filter und Gruppierung werden zuerst beschrieben und dann in einem Durchgang ausgeführt, möglichst nah an der Datei
  • Die Reihenfolge der Mittel: weniger lesen, besseres Format, verzögert auswerten, und erst dann ein grösserer Rechner
  • DuckDB, Arrow und Polars lesen dieselben Parquet-Dateien; die Wahl ist keine Einbahnstrasse

Erklärung

Vorwissen: Datenimport und Formate für CSV und die Formatübersicht, Data Wrangling für Filtern und Gruppieren. Mit SQL auf denselben Dateien arbeitet DuckDB.

Wo die Zeit verloren geht

Engpass Ursache Abhilfe
Lesen dauert lange CSV ist Text; jede Zahl wird beim Lesen geparst Parquet, einmal umwandeln
Arbeitsspeicher voll alle Spalten und Zeilen werden geladen nur benötigte Spalten, Filter vor dem Laden
Gruppieren ist langsam pandas arbeitet auf einem Kern, mit Zwischenkopien Polars oder Arrow, mehrere Kerne, verzögert
Datei zu gross für einen Rechner echte Big-Data-Grösse Spark, Datenbank, Cloud-Dienste

CSV und Parquet

CSV Parquet
Aufbau Zeilen als Text Spalten, binär, komprimiert
Datentypen müssen beim Lesen erraten werden gespeichert
Nur einige Spalten lesen nicht möglich, die Datei wird ganz durchlaufen direkt
Lesbar mit jedem Editor Arrow, pandas, Polars, DuckDB, Spark
Gut für Austausch mit Menschen und alter Software alles Weitere

Die Werkzeuge

Werkzeug R Python Stärke
Arrow arrow pyarrow gemeinsames Speicherformat, Parquet lesen und schreiben, dplyr-Syntax auf Dateien
Polars polars (jünger) polars sehr schnelle Data Frames, verzögerte Auswertung mit scan_*
DuckDB duckdb duckdb SQL direkt auf Dateien
data.table data.table schnelles R ohne neues Format

Beispiele

Frage und Datenlage

200 000 Messungen mit vier Spalten werden einmal als CSV und einmal als Parquet geschrieben. Wie gross sind die Dateien, und kommen die Datentypen beim Zurücklesen richtig an?

Rechnung

pfad_csv <- file.path(ordner, "messungen.csv")
pfad_parquet <- file.path(ordner, "messungen.parquet")

write.csv(messungen, pfad_csv, row.names = FALSE, quote = FALSE)
write_parquet(messungen, pfad_parquet)

groesse <- c(csv = file.size(pfad_csv), parquet = file.size(pfad_parquet)) / 1e6
round(groesse, 2)
    csv parquet 
   4.12    1.63 
round(groesse["csv"] / groesse["parquet"], 1)
csv 
2.5 
sapply(read_parquet(pfad_parquet), class)
    messung    standort        wert      status 
  "integer" "character"   "numeric" "character" 
pfad_csv = os.path.join(ordner, "messungen.csv")
pfad_parquet = os.path.join(ordner, "messungen.parquet")

messungen.to_csv(pfad_csv, index=False)
messungen.to_parquet(pfad_parquet, index=False)

csv_mb = os.path.getsize(pfad_csv) / 1e6
parquet_mb = os.path.getsize(pfad_parquet) / 1e6
print("CSV MB:", round(csv_mb, 2), " Parquet MB:", round(parquet_mb, 2),
      " Verhaeltnis:", round(csv_mb / parquet_mb, 1))
CSV MB: 4.12  Parquet MB: 1.48  Verhaeltnis: 2.8
print(pd.read_parquet(pfad_parquet).dtypes)
messung       int64
standort     object
wert        float64
status       object
dtype: object

Output Zeile für Zeile

Ausgabe Wie sie zu lesen ist
Dateigrössen Parquet ist deutlich kleiner als CSV; das genaue Verhältnis hängt von Bibliothek, Version und Kompression ab und steht in der Ausgabe
Verhältnis wie viele Parquet-Dateien in eine CSV-Datei passen
Datentypen integer, character, numeric in R, int64, object, float64 in pandas: wie geschrieben, ohne Raten

Interpretation

Die beiden Sprachen zeigen verschiedene Dateigrössen. Das ist erwartet: Sie schreiben CSV mit unterschiedlicher Formatierung, und die Parquet-Schreiber wählen je nach Version andere Voreinstellungen. Gleich ist die Grössenordnung des Gewinns. Wichtiger als die Grösse sind die Typen: Eine Parquet-Datei weiss, dass wert eine Zahl ist. Eine CSV-Datei weiss es nicht, und genau dort entstehen die Probleme aus Datenimport und Formate.

Frage und Datenlage

Für eine Auswertung werden nur standort und wert gebraucht. Parquet kann die anderen Spalten beim Lesen überspringen.

Rechnung

teil <- read_parquet(pfad_parquet, col_select = c("standort", "wert"))
dim(teil)
[1] 200000      2
table(teil$standort)

Basel  Bern  Chur  Genf  Sion 
39764 39925 40059 40019 40233 
teil = pd.read_parquet(pfad_parquet, columns=["standort", "wert"])
print(teil.shape)
(200000, 2)
print(teil["standort"].value_counts().sort_index().to_dict())
{'Basel': 39764, 'Bern': 39925, 'Chur': 40059, 'Genf': 40019, 'Sion': 40233}

Output Zeile für Zeile

Ausgabe Wert hier Wie er zu lesen ist
Dimension 200000 Zeilen, 2 Spalten nur zwei der vier Spalten wurden gelesen
Basel 39764 Messungen je Standort
Bern 39925
Chur 40059
Genf 40019
Sion 40233

Interpretation

Bei vier Spalten ist der Gewinn klein. Bei einer Tabelle mit fünfzig Spalten, von denen drei gebraucht werden, liest Parquet einen Bruchteil der Daten. Mit CSV ist das nicht möglich: usecols in pandas oder col_select in readr sparen Speicher, aber die Datei wird trotzdem vollständig durchlaufen.

Frage und Datenlage

Gesucht sind je Standort die Zahl der gültigen Messungen und ihr Mittelwert. Statt die ganze Datei zu laden, wird die Auswertung beschrieben und erst am Ende ausgeführt: in R mit Arrow und dplyr, in Python mit Polars.

Rechnung

abfrage <- open_dataset(pfad_parquet) |>
  filter(status == "ok") |>
  group_by(standort) |>
  summarise(n = n(), mittel = mean(wert)) |>
  arrange(standort)

abfrage            # noch nichts gerechnet, nur beschrieben
FileSystemDataset (query)
standort: string
n: int64
mittel: double

* Sorted by standort [asc]
See $.data for the source Arrow object
ergebnis <- collect(abfrage)
ergebnis$mittel <- round(ergebnis$mittel, 3)
as.data.frame(ergebnis)
  standort     n mittel
1    Basel 38951 19.993
2     Bern 39139 19.981
3     Chur 39239 20.020
4     Genf 39217 20.007
5     Sion 39396 20.023
sum(messungen$status == "Fehler")
[1] 4058
abfrage = (
    pl.scan_parquet(pfad_parquet)
    .filter(pl.col("status") == "ok")
    .group_by("standort")
    .agg(pl.len().alias("n"), pl.col("wert").mean().alias("mittel"))
    .sort("standort")
)

print(abfrage.explain())   # der Plan, noch nichts gerechnet
SORT BY [col("standort")]
  AGGREGATE[maintain_order: false]
    [len().alias("n"), col("wert").mean().alias("mittel")] BY [col("standort")]
    FROM
    simple π 2/2 ["standort", "wert"]
      Parquet SCAN [/tmp/tmpxdyz4ogy/messungen.parquet]
      PROJECT 3/4 COLUMNS
      SELECTION: col("status") == "ok"
      ESTIMATED ROWS: 200000
ergebnis = abfrage.collect().with_columns(pl.col("mittel").round(3))
print(ergebnis)
shape: (5, 3)
┌──────────┬───────┬────────┐
│ standort ┆ n     ┆ mittel │
│ ---      ┆ ---   ┆ ---    │
│ str      ┆ u32   ┆ f64    │
╞══════════╪═══════╪════════╡
│ Basel    ┆ 38951 ┆ 19.993 │
│ Bern     ┆ 39139 ┆ 19.981 │
│ Chur     ┆ 39239 ┆ 20.02  │
│ Genf     ┆ 39217 ┆ 20.007 │
│ Sion     ┆ 39396 ┆ 20.023 │
└──────────┴───────┴────────┘
print("Fehler:", int((messungen["status"] == "Fehler").sum()))
Fehler: 4058

Output Zeile für Zeile

Standort gültige Messungen Mittelwert Wie es zu lesen ist
Basel 38951 19.993
Bern 39139 19.981
Chur 39239 20.02
Genf 39217 20.007
Sion 39396 20.023
Fehlerzeilen 4058 vor der Gruppierung herausgefiltert

Die erste Ausgabe ist kein Ergebnis, sondern der Plan: Arrow zeigt die beschriebene Abfrage, Polars den Ausführungsplan mit dem Filter direkt beim Lesen der Datei.

Interpretation

Der Vorteil liegt im Plan. Weil die ganze Abfrage bekannt ist, bevor gerechnet wird, kann das Werkzeug den Filter an die Datei weiterreichen und nur die Spalten lesen, die in Filter, Gruppierung und Mittelwert vorkommen. Beide Sprachen liefern dieselben Zahlen, weil ihre Dateien aus denselben Daten entstanden sind, auch wenn die Dateien selbst nicht Byte für Byte gleich sind. Die Werkzeuge sind austauschbar, das Format verbindet sie.

Typische Aufgaben

Eine grosse CSV einmal in Parquet umwandeln, ohne sie ganz zu laden

open_dataset("gross.csv", format = "csv") |> write_dataset("parquet_ordner")
pl.scan_csv("gross.csv").sink_parquet("gross.parquet")

Viele Dateien als eine Tabelle abfragen

open_dataset("parquet_ordner/") |> filter(jahr == 2025) |> collect()
pl.scan_parquet("parquet_ordner/*.parquet").filter(pl.col("jahr") == 2025).collect()

Nach einer Spalte partitioniert schreiben

write_dataset(messungen, "nach_standort", partitioning = "standort")
messungen.to_parquet("nach_standort", partition_cols=["standort"])

CSV in Stücken verarbeiten, wenn kein anderes Format möglich ist

summe = 0
for stueck in pd.read_csv("gross.csv", chunksize=100_000):
    summe += stueck["wert"].sum()

Verständnisfragen

Eine CSV-Datei mit 60 Spalten und 50 Millionen Zeilen soll nach zwei Spalten ausgewertet werden. Was bringt am meisten?

Einmal in Parquet umwandeln und danach nur die zwei Spalten lesen
Richtig. Parquet speichert spaltenweise; die übrigen 58 Spalten werden gar nicht angefasst.
Mehr Arbeitsspeicher
Hilft beim Laden, aber die Datei wird weiterhin vollständig geparst.
Die CSV-Datei komprimieren
Spart Platz auf der Festplatte, macht das Lesen aber eher langsamer.

Was passiert beim Aufruf von pl.scan_parquet(...).filter(...) ohne .collect()?

Es wird nur die Abfrage beschrieben, noch nichts gelesen oder gerechnet
Richtig. Erst .collect() führt den Plan aus.
Die Datei wird gelesen und gefiltert im Speicher gehalten
Das wäre pl.read_parquet(...) mit anschliessendem Filter.
Es entsteht ein Fehler
Die Beschreibung ist gültig, sie wird nur nicht ausgeführt.

Warum zeigen R und Python in Beispiel 1 verschiedene Dateigrössen?

Sie schreiben mit verschiedenen Voreinstellungen, etwa Formatierung der Zahlen und Kompression
Richtig. Der Inhalt ist derselbe, die Darstellung auf der Festplatte nicht.
Die Daten sind verschieden
Beide rechnen mit demselben Lehmer-Strom, Beispiel 2 und 3 zeigen identische Zahlen.
Parquet ist nicht standardisiert
Das Format ist standardisiert, lässt aber Wahlmöglichkeiten bei Kompression und Kodierung.

Verlinkte Ressourcen