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
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
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
- 01 Wie viel Speicher belegt jede Spalte, wenn der Inhalt mitgezählt wird?
- 02 Sind Typen beim Einlesen angegeben, oder wurden sie geraten?
- 03 Gibt es eine Schleife über Zeilen, die eine Spaltenoperation sein könnte?
- 04 Wächst irgendwo eine Tabelle innerhalb einer Schleife?
- 05 Passen die Daten mit passenden Typen in den Arbeitsspeicher, oder grundsätzlich nicht mehr?
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.
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.
Wo du genau das übst
Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Häufige Fragen
Wie viel Arbeitsspeicher braucht eine Tabelle ungefähr?
Bringt eine Umstellung auf PyArrow-gestützte Typen etwas?
Sollen wir von CSV auf ein anderes Format wechseln?
Hilft es, die Auswertung auf mehrere Kerne zu verteilen?
Wann ist es Zeit für eine Datenbank statt für Dateien?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.
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.
Passt dazu
- Python ohne GIL: was der freie Threading-Modus für euren Code ändert
- Type Hints in Python: was sie bringen und wo mypy anschlägt
- Wann du in Python eine virtuelle Umgebung brauchst und wann nicht
- pytest läuft lokal, scheitert in der CI: die häufigsten Ursachen