Auswertungen mit pandas

Pandas wird langsam: woran es liegt und was den Speicher frisst

Eine Tabelle von zwei Gigabyte auf der Platte kann im Arbeitsspeicher zehn belegen. Warum das so ist, welche Umbauten den größten Hebel haben und ab wann ihr ehrlicherweise ein anderes Werkzeug braucht.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Erst läuft es, dann dauert es, dann bricht es ab

Der Verlauf ist immer derselbe. Die Auswertung entsteht auf einem Auszug von zehntausend Zeilen und ist in Sekunden fertig. Ein halbes Jahr später sind es zehn Millionen Zeilen, der Lauf braucht zwanzig Minuten, und irgendwann bricht er mit einem Speicherfehler ab oder wird im Container stillschweigend abgeräumt. Am Code hat in der Zwischenzeit niemand etwas geändert, und genau das macht die Fehlersuche schwierig: Es gibt keine Änderung, auf die man zeigen könnte.

Die erste Vermutung geht meist in die falsche Richtung. Gesucht wird nach der einen langsamen Zeile, dabei liegt der Aufwand oft schon beim Einlesen. Wenn die Datentypen nicht angegeben sind, rät pandas sie aus den Daten und landet dabei fast immer bei den großzügigsten: acht Byte pro Zahl, auch wenn drei verschiedene Werte vorkommen. Bei Text war es lange noch ungünstiger, weil jede einzelne Zeichenkette als eigenes Python-Objekt im Speicher lag.

Die zweite Vermutung ist, dass die Datenmenge einfach zu groß geworden sei und man jetzt ein verteiltes System brauche. In der Praxis stimmt das selten. Eine Tabelle mit zehn Millionen Zeilen und dreißig Spalten ist mit passenden Typen ein Fall für einen gewöhnlichen Rechner. Der Wechsel auf ein anderes Werkzeug bringt dann zwar auch etwas, er verdeckt aber nur, dass dieselbe Auswertung mit denselben Fehlern jetzt eben auf größerer Hardware läuft.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Symptom, Ursache, Lösung

Die Fälle, die im Alltag wirklich auflaufen

Sortiert nach dem, was du beobachtest. Zu jedem Fall gehört die Eigenschaft der Bibliothek, aus der er folgt, und der Umbau, der ihn dauerhaft erledigt, statt ihm mit mehr Arbeitsspeicher auszuweichen.

Symptom

Eine Datei von zwei Gigabyte belegt nach dem Einlesen ein Vielfaches davon im Arbeitsspeicher, und der Rechner beginnt auszulagern.

Ursache

Ohne Angabe rät pandas die Typen aus einem Ausschnitt der Daten und wählt dabei die großzügigen: acht Byte für jede Ganzzahl und jede Fließkommazahl, unabhängig vom tatsächlichen Wertebereich. Dazu kommt der Index. Bei Textspalten hing der Verbrauch lange daran, dass jede Zeichenkette ein eigenes Python-Objekt war, mit erheblichem Aufschlag pro Wert.

Lösung

Gib beim Einlesen die Typen mit und beschränke die Spalten auf die tatsächlich benötigten. Wandle Spalten mit wenigen verschiedenen Werten in Kategorien um, das ersetzt jeden Wert durch eine kleine Zahl plus eine Liste der Ausprägungen. Prüf das Ergebnis mit der gründlichen Speicherausgabe, dann siehst du sofort, ob die Umstellung gewirkt hat.

Symptom

Eine Berechnung über alle Zeilen dauert Minuten, obwohl sie pro Zeile nur eine Handvoll Rechenschritte macht.

Ursache

Eine Schleife über die Zeilen oder eine zeilenweise angewendete Funktion arbeitet Zeile für Zeile im Python-Interpreter und legt dabei je Zeile Objekte an. Der Aufwand pro Zeile ist damit um Größenordnungen höher als bei einer Operation, die der kompilierte Kern auf der ganzen Spalte ausführt.

Lösung

Schreib die Berechnung als Operation über Spalten. Bedingungen werden zu Auswahlmasken, Fallunterscheidungen zu einer Auswahl zwischen zwei Spalten, Zuordnungen aus einem Wörterbuch zu einer Abbildung über die ganze Spalte, und Zusammenfassungen je Gruppe zu einer Gruppierung mit Aggregatfunktion. Nur wo sich das wirklich nicht ausdrücken lässt, bleibt eine Schleife, und dann über die schnellere Variante mit benannten Tupeln.

Symptom

Ein Ablauf, der Ergebnisse einsammelt, wird mit jedem Durchlauf langsamer, obwohl die Arbeitspakete gleich groß bleiben.

Ursache

Beim Anhängen an eine bestehende Tabelle entsteht jedes Mal eine neue Tabelle, in die alles Bisherige kopiert wird. Aus einer Schleife über tausend Teile wird damit ein Aufwand, der quadratisch mit der Zahl der Teile wächst. Dasselbe Muster tritt beim einzelnen Hinzufügen vieler Spalten auf, wo die interne Ablage zerstückelt und jede weitere Operation langsamer wird.

Lösung

Sammle die Teilergebnisse in einer gewöhnlichen Liste und füg sie nach der Schleife in einem einzigen Schritt zusammen. Mehrere neue Spalten weist du gemeinsam zu, statt einzeln nacheinander. Wenn eine Tabelle bereits zerstückelt ist, stellt eine vollständige Kopie den zusammenhängenden Zustand wieder her, aber besser ist, sie gar nicht erst so entstehen zu lassen.

Symptom

Eine Zuweisung auf einen ausgewählten Ausschnitt verändert die ursprüngliche Tabelle nicht, ohne dass eine Warnung erscheint.

Ursache

Seit pandas 3.0 gilt Copy-on-Write als Standard. Jede Auswahl und jede Methode, die ein neues Objekt zurückgibt, verhält sich nach außen wie eine Kopie. Eine Zuweisung über zwei aufeinanderfolgende Zugriffe ändert deshalb nur ein Zwischenergebnis, das anschließend verworfen wird. Die frühere Warnung, die auf diesen Fall hingewiesen hat, wurde mit der Umstellung entfernt.

Lösung

Schreib Zuweisungen in einem einzigen Zugriff mit Zeilen- und Spaltenauswahl zusammen, statt sie in zwei Schritte zu zerlegen. Wo eine Methode ein Ergebnis liefert, weise dieses Ergebnis ausdrücklich zu. Der Wechsel ist übrigens kein reiner Verlust: Weil intern nur noch kopiert wird, wenn es nötig ist, verschwinden gleichzeitig viele Kopien, die früher unbemerkt entstanden sind.

Symptom

Eine Verknüpfung zweier Tabellen oder eine Gruppierung braucht auffällig viel Zeit und Speicher, obwohl beide Tabellen für sich unauffällig sind.

Ursache

Häufig liegen die Schlüsselspalten in unterschiedlichen Typen vor oder als allgemeine Objektspalten, dann fällt der Vergleich auf den langsamen Weg zurück. Der teurere Fall sind mehrdeutige Schlüssel: Wenn ein Wert links dreimal und rechts viermal vorkommt, entstehen daraus zwölf Zeilen, und eine Verknüpfung, die eine Million Zeilen liefern sollte, liefert dreißig Millionen.

Lösung

Gleich die Typen der Schlüsselspalten vorher an und prüf die Eindeutigkeit ausdrücklich, statt sie anzunehmen. pandas kann die erwartete Beziehung beim Verknüpfen selbst prüfen und meldet einen Verstoß sofort, statt still ein Vielfaches an Zeilen zu erzeugen. Bei wiederholten Verknüpfungen über dieselbe Spalte lohnt es, sie als Index zu setzen.

Symptom

Auch mit passenden Typen und ohne Schleifen passt die Auswertung nicht mehr in den Arbeitsspeicher.

Ursache

pandas hält alles vollständig im Speicher und braucht für Zwischenergebnisse zusätzlich Platz. Als grobe Orientierung gilt, dass mehrere Zwischenschritte auf einmal existieren können und der Bedarf deshalb ein Mehrfaches der reinen Datenmenge beträgt. Ab einer bestimmten Größe ist das keine Frage der Einstellung mehr.

Lösung

Reduzier zuerst, was überhaupt geladen wird: nur benötigte Spalten, Filter schon beim Lesen, ein spaltenweises Dateiformat. Wenn sich die Auswertung in unabhängige Blöcke zerlegen lässt, verarbeite sie blockweise und führe erst die Ergebnisse zusammen. Bleibt sie unteilbar, ist der Wechsel auf ein Werkzeug fällig, das nicht alles gleichzeitig im Speicher halten muss.

Fünf Fragen, und du weißt, wo die Zeit hingeht

  1. 01 Wie viel Speicher belegt jede Spalte, wenn der Inhalt mitgezählt wird?
  2. 02 Sind Typen beim Einlesen angegeben, oder wurden sie geraten?
  3. 03 Gibt es eine Schleife über Zeilen, die eine Spaltenoperation sein könnte?
  4. 04 Wächst irgendwo eine Tabelle innerhalb einer Schleife?
  5. 05 Passen die Daten mit passenden Typen in den Arbeitsspeicher, oder grundsätzlich nicht mehr?
Was du mitnimmst

Was du danach an einem Vormittag herausholst

Die Reihenfolge ist wichtig, weil die ersten beiden Schritte oft schon reichen und die späteren viel mehr Arbeit machen. Miss nach jedem Schritt, sonst optimierst du an einer Stelle, die nie das Problem war.

Zuerst den tatsächlichen Speicherbedarf ansehen

Lass dir den Verbrauch je Spalte ausgeben, und zwar mit der gründlichen Variante, die auch den Inhalt von Textspalten mitzählt. Erst diese Tabelle zeigt, ob zwei Spalten von dreißig für achtzig Prozent des Speichers verantwortlich sind, und genau das ist der Normalfall.

Die Typen beim Einlesen festlegen statt raten lassen

Gib die Datentypen ausdrücklich an und lies nur die Spalten ein, die du wirklich brauchst. Eine Ganzzahl bis zweihundert braucht ein Byte statt acht, ein Datum gehört in einen Zeitstempeltyp, und eine Spalte mit wenigen verschiedenen Werten gehört als Kategorie eingelesen. Das ist die einzige Änderung, die regelmäßig einen Faktor bringt und nur eine Zeile kostet.

Schleifen über Zeilen ersetzen

Jede Schleife über Zeilen und jede Anwendung einer Funktion zeilenweise ist eine Python-Schleife mit dem entsprechenden Aufwand pro Zeile. Bedingte Zuweisungen, Umrechnungen und Zuordnungen lassen sich fast immer als Operation über die ganze Spalte schreiben, und dann rechnet der kompilierte Kern statt des Interpreters.

Wachsende Tabellen aus der Schleife nehmen

Wer in einer Schleife immer wieder Zeilen anhängt, kopiert die gesamte Tabelle bei jedem Durchlauf. Sammle die Teile in einer Liste und füg sie am Ende in einem Schritt zusammen. Dasselbe gilt für viele einzeln hinzugefügte Spalten, die den Speicher zerstückeln und danach jede Operation ausbremsen.

Das Dateiformat wechseln, wenn oft eingelesen wird

CSV muss bei jedem Lauf vollständig gelesen und geparst werden, und die Typinformation fehlt darin komplett. Ein spaltenweise organisiertes Format wie Parquet speichert Typen mit, komprimiert deutlich besser und erlaubt, nur die benötigten Spalten zu lesen. Bei wiederkehrenden Auswertungen ist das der zweitgrößte Hebel nach den Datentypen.

Die Grenze kennen, ab der ein anderes Werkzeug richtig ist

pandas arbeitet vollständig im Arbeitsspeicher und rechnet die meisten Schritte auf einem Kern. Wenn die Daten auch mit passenden Typen nicht hineinpassen oder die Auswertung sich aus vielen Verknüpfungen und Aggregationen zusammensetzt, ist das kein Feineinstellungsproblem mehr, sondern eine Werkzeugfrage.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Datentypen sind der größte Hebel, und zwar mit Abstand

Eine Spalte mit ganzen Zahlen belegt in der Voreinstellung acht Byte pro Wert, auch wenn darin nur Werte zwischen null und hundert stehen. Mit dem passenden kleineren Typ wird daraus ein Byte, also ein Achtel. Bei Fließkommazahlen halbiert der Wechsel auf einfache Genauigkeit den Bedarf, sofern die Genauigkeit für den Zweck reicht, und das ist bei Messwerten und Kennzahlen fast immer der Fall.

Der größte Einzelposten sind aber meist Textspalten mit wenigen verschiedenen Ausprägungen: Länderkürzel, Statuswerte, Produktgruppen. Als Kategorie gespeichert, liegt jeder Wert nur noch als kleine Zahl vor, dazu einmal die Liste der Ausprägungen. Bei einer Million Zeilen mit zwanzig verschiedenen Werten ist das der Unterschied zwischen zweistelligen Megabyte und einem Bruchteil davon. Gruppierungen und Verknüpfungen über solche Spalten werden nebenbei ebenfalls schneller.

Seit pandas 3.0 hat sich die Ausgangslage bei Text spürbar verbessert, weil Zeichenketten standardmäßig in einem eigenen Typ landen statt als allgemeine Objekte, und dieser Typ setzt auf PyArrow auf, wenn die Bibliothek installiert ist. Damit fällt ein großer Teil des früheren Aufschlags weg. Für Spalten mit wenigen Ausprägungen bleibt die Kategorie trotzdem die sparsamere Wahl, und Vergleiche über Kategorien sind weiterhin schneller als über freien Text.

Was sich mit pandas 3.0 sonst noch geändert hat

Die auffälligste Änderung ist Copy-on-Write als Standardverhalten. Jede Auswahl und jede Methode, die ein neues Objekt zurückgibt, verhält sich nach außen wie eine Kopie, intern wird aber nur kopiert, wenn tatsächlich geschrieben wird. Das räumt eine alte Unklarheit aus, denn früher war oft nicht vorhersehbar, ob eine Zuweisung auf einem Ausschnitt die Ursprungstabelle mitverändert. Der Preis ist, dass Zuweisungen über zwei aufeinanderfolgende Zugriffe wirkungslos bleiben und die bekannte Warnung dazu entfallen ist.

Für die Laufzeit ist die Umstellung überwiegend eine gute Nachricht. Viele Methoden, die früher aus Vorsicht vollständig kopiert haben, kommen jetzt ohne Kopie aus, solange niemand schreibt. Bei Abläufen mit vielen Zwischenschritten macht sich das direkt bemerkbar. Wer allerdings Code aus der Zeit davor übernimmt, sollte gezielt nach zweistufigen Zuweisungen suchen, denn die scheitern jetzt lautlos.

Daneben stehen zwei Änderungen, die bei einem Sprung auf die neue Fassung Arbeit machen können: Die Mindestanforderungen sind gestiegen, sowohl bei der Python-Fassung als auch bei NumPy, und für Zeitzonen wird die Standardbibliothek verwendet statt der bisherigen Zusatzbibliothek. Wer Zeitzonenobjekte irgendwo vergleicht oder serialisiert, sollte diese Stellen ausdrücklich prüfen.

Erst messen, dann umbauen

Ohne Messung optimiert man zuverlässig die falsche Stelle. Der erste Blick gehört der Aufteilung in drei Abschnitte: Wie lange dauert das Einlesen, wie lange das Umformen und Rechnen, wie lange das Schreiben des Ergebnisses. Häufig steckt mehr als die Hälfte im Einlesen, und dann ist jede Feinarbeit an der Rechenlogik verschenkt.

Der zweite Blick gehört dem Speicher. Die gründliche Speicherausgabe pro Spalte deckt fast immer auf, dass wenige Spalten den größten Teil belegen, und liefert damit gleich die Reihenfolge für die Umstellung. Ein nützlicher Zwischenschritt ist, die Auswertung auf einem Ausschnitt von einem Prozent der Daten laufen zu lassen und die Zeiten hochzurechnen: Wächst etwas deutlich stärker als linear, hast du eine Kopie in einer Schleife gefunden.

Für einzelne Ausdrücke lohnt der direkte Vergleich zweier Formulierungen mit einer Zeitmessung, die mehrfach ausführt. Dabei zeigt sich regelmäßig, dass die naheliegende Reihenfolge nicht die schnellste ist: Erst filtern und dann rechnen ist fast immer günstiger als umgekehrt, und eine Gruppierung über eine Kategoriespalte schlägt dieselbe Gruppierung über freien Text deutlich.

Wann ein anderes Werkzeug die ehrlichere Antwort ist

pandas ist für Daten gemacht, die vollständig in den Arbeitsspeicher passen, und rechnet die meisten Schritte auf einem Kern. Beides sind bewusste Entscheidungen, und beide sind die Grenze. Wenn die Daten auch mit passenden Typen nicht hineinpassen, hilft keine weitere Feinarbeit mehr, sondern nur ein Werkzeug, das blockweise arbeitet oder die Rechenarbeit dorthin verlagert, wo die Daten liegen.

Für Auswertungen, die im Kern aus Verknüpfungen, Filtern und Gruppierungen bestehen, ist die naheliegende Antwort eine Abfragesprache statt einer DataFrame-Bibliothek. Das kann die Datenbank sein, in der die Daten ohnehin liegen, oder eine Auswertungsmaschine, die Dateien direkt liest und dabei nur die benötigten Spalten und Zeilen anfasst. Der Gewinn kommt dann nicht aus schnellerem Code, sondern daraus, dass große Teile der Daten gar nicht erst gelesen werden.

Daneben gibt es DataFrame-Bibliotheken, die von vornherein auf mehrere Kerne und auf verzögerte Auswertung setzen und deshalb bei denselben Operationen deutlich schneller sind. Der Wechsel dorthin ist keine Portierung von Zeilen, sondern eine Umgewöhnung im Denken, und er lohnt sich vor allem für Auswertungen, die regelmäßig laufen. Für eine einmalige Analyse ist der vertraute Weg mit besseren Typen fast immer die günstigere Rechnung.

Dazu passende Kurse

Wenn im Team Auswertungen entstehen, die später jemand betreiben muss, geben Python-Kurse für Datenauswertung dafür die Grundlage.

Weil viele Auswertungen dort schneller laufen, wo die Daten ohnehin liegen, gehören Datenbankkurse für Abfragen und Auswertungen zur selben Überlegung.

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Einführung in die Programmierung für Programmiereinsteiger
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Groovy Grundkurs
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Unreal Engine Kurs für Einsteiger

Häufige Fragen

Wie viel Arbeitsspeicher braucht eine Tabelle ungefähr?
Das lässt sich nicht pauschal sagen, aber es lässt sich messen. Verlass dich nicht auf die Dateigröße, denn CSV ist Text und komprimiert sich in Zahlenspalten stark, während Textspalten im Speicher aufgehen. Lass dir den Verbrauch je Spalte mit der gründlichen Variante ausgeben, dann hast du die tatsächliche Zahl und gleichzeitig die Reihenfolge, in der du umstellen solltest.
Bringt eine Umstellung auf PyArrow-gestützte Typen etwas?
Bei Text deutlich, weil die Werte kompakt hintereinander liegen statt als einzelne Objekte, und seit pandas 3.0 ist das für Zeichenketten der Normalfall. Bei Zahlenspalten ist der Unterschied kleiner und hängt an der Operation. Was gleich bleibt: Eine Spalte mit wenigen verschiedenen Werten fährt als Kategorie in beiden Welten am besten.
Sollen wir von CSV auf ein anderes Format wechseln?
Wenn dieselbe Datei mehr als einmal gelesen wird, ja. CSV kennt keine Typen, muss vollständig geparst werden und speichert spaltenweise gar nichts. Ein spaltenweises Format speichert die Typen mit, komprimiert besser und erlaubt, nur die benötigten Spalten zu lesen. Für den einmaligen Austausch mit Menschen bleibt CSV natürlich sinnvoll.
Hilft es, die Auswertung auf mehrere Kerne zu verteilen?
Nur, wenn die Zeit wirklich in der Rechenarbeit steckt und nicht im Einlesen oder im Speicherzugriff. Vorher lohnen sich die Datentypen und das Beseitigen von Kopien fast immer mehr, und sie kosten weniger. Wenn danach noch Rechenzeit übrig ist, kommen blockweise Verarbeitung oder eine Bibliothek mit eingebauter Parallelität in Frage.
Wann ist es Zeit für eine Datenbank statt für Dateien?
Sobald dieselben Daten regelmäßig gefiltert, verknüpft und aggregiert werden und mehrere Personen darauf zugreifen. Der Gewinn kommt dabei nicht aus schnellerem Code, sondern daraus, dass Filter und Verknüpfungen dort ausgeführt werden, wo die Daten liegen, und nur das Ergebnis den Weg zu eurer Auswertung antritt.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Auswertungen bauen, die auch bei zehn Millionen Zeilen tragen

Im Data-Science-Kurs bei cmt arbeitest du mit Datentypen, Gruppierungen und Verknüpfungen an Datenmengen, bei denen der Unterschied sichtbar wird, statt an Beispielen mit hundert Zeilen.