post

Datenanalyse mit NumPy, Pandas und Matplotlib

Von Rohdaten zur Erkenntnis: Wie NumPy, pandas und Matplotlib zusammenspielen – vektorisiertes Rechnen, groupby-Aggregation und ein schneller Plot in einer kleinen Pipeline.

In fast jeder Schulung, in der es um Daten geht, kommt derselbe Moment. Jemand hat eine CSV-Datei, öffnet sie in einem Texteditor, sieht ein paar tausend Zeilen und fragt: „Und wie rechne ich da jetzt drüber?" Der erste Reflex ist meistens eine Python-Schleife. Öffnen, Zeile für Zeile durchgehen, Werte summieren, am Ende ausgeben. Das funktioniert – für hundert Zeilen. Bei einer Million Zeilen wartet man dann und fragt sich, warum das so lange dauert.

Genau an dieser Stelle setzt der Werkzeugkasten aus NumPy, pandas und Matplotlib an. Die drei Bibliotheken bilden zusammen einen Weg von den Rohdaten bis zur fertigen Grafik, und jede übernimmt dabei eine klar umrissene Aufgabe. In diesem Artikel gehe ich diesen Weg einmal komplett durch: erst das schnelle Rechnen mit NumPy, dann strukturierte Tabellen mit pandas, schließlich die Visualisierung mit Matplotlib. Am Ende steht eine kleine Pipeline, die lädt, bereinigt, gruppiert und plottet. Wer sich vorher noch fragt, wie die Daten überhaupt ins Programm kommen, findet in Daten einlesen in Python die Grundlagen dazu.

Warum keine Python-Schleife? NumPy und das ndarray

Der Kern von NumPy ist ein einziger Datentyp: das ndarray. Auf den ersten Blick sieht es aus wie eine Liste, aber der Unterschied steht im Speicher. Eine Python-Liste hält Verweise auf beliebige Objekte, die irgendwo verstreut im Speicher liegen. Ein ndarray ist homogen typisiert und zusammenhängend abgelegt – alle Elemente haben denselben Datentyp und stehen direkt hintereinander.

Dieser Aufbau ist die Voraussetzung für das, was NumPy schnell macht: vektorisierte Operationen. Statt in Python über einzelne Elemente zu iterieren, wird die Rechnung als Ganzes an kompilierten C-Code übergeben. Ein Beispiel macht den Unterschied deutlich.

import numpy as np

values = np.arange(1_000_000)   # 0, 1, 2, ... 999999
result = values * 2 + 1         # vectorized, no Python loop
total = result.mean()

print(result.shape)             # (1000000,)
print(result.dtype)             # int64
print(total)                    # 1000000.0

Die Zeile values * 2 + 1 rechnet auf allen Millionen Elementen, ohne dass eine einzige Python-for-Schleife läuft. Der Ausdruck ist nicht nur schneller, er ist auch kürzer und liest sich näher an der mathematischen Absicht. Das ist der eigentliche Gewinn: Man beschreibt, was gerechnet werden soll, nicht wie über die Elemente iteriert wird.

Neben den arithmetischen Operationen gibt es zwei Werkzeuge, die ich in Schulungen immer früh zeige, weil sie ständig gebraucht werden. Das eine sind Reduktionen wie .sum(), .mean(), .std() oder .min() und .max(), die aus einem Array eine Kennzahl machen. Das andere sind Boolean-Masken.

import numpy as np

data = np.array([-3, 7, -1, 4, 9, -8])

mask = data > 0                 # array of True/False
positives = data[mask]          # keep only positive values
clipped = np.where(data < 0, 0, data)   # replace negatives with 0

print(positives)                # [7 4 9]
print(clipped)                  # [0 7 0 4 9 0]

data > 0 erzeugt kein einzelnes True oder False, sondern ein ganzes Array aus Wahrheitswerten. Damit lässt sich dann filtern (data[mask]) oder mit np.where fallweise ersetzen. Auch hier keine Schleife, keine Bedingung im Python-Code – die Logik steckt komplett in der vektorisierten Operation.

Eng verwandt mit der Vektorisierung ist das Broadcasting. Damit lassen sich Arrays unterschiedlicher Form miteinander verrechnen, solange ihre Dimensionen zueinander passen. Ein Skalar wird dabei gedanklich auf die gesamte Form des Arrays ausgedehnt – deshalb funktioniert values * 2 + 1, ohne dass man die 2 und die 1 erst selbst auf eine Million Elemente aufblasen müsste. Bei zweidimensionalen Daten greift dieselbe Regel: Ein Zeilenvektor mit passender Spaltenzahl lässt sich auf jede Zeile einer Matrix anwenden, etwa um von jeder Spalte den Mittelwert abzuziehen. Broadcasting erspart damit viele explizite Schleifen und hält den Code nah an der mathematischen Notation. Wichtig ist nur, die Formen im Kopf zu behalten: Passen die Dimensionen nicht zusammen, quittiert NumPy das mit einem ValueError, statt stillschweigend etwas Falsches zu rechnen.

Pandas: Tabellen mit Beschriftung

NumPy rechnet gut mit Zahlen, aber reale Daten haben Struktur. Spalten haben Namen, Zeilen haben eine Bedeutung, und oft mischen sich Zahlen mit Text und Datumsangaben. Hier kommt pandas ins Spiel, und wichtig zu verstehen: pandas baut direkt auf NumPy auf. Unter den Tabellen von pandas stecken NumPy-Arrays.

Die zwei zentralen Strukturen sind Series und DataFrame. Eine Series ist eine eindimensionale Spalte mit einem Index. Ein DataFrame ist eine zweidimensionale Tabelle mit beschrifteten Zeilen und Spalten – im Grunde eine Sammlung von Series, die sich denselben Index teilen.

import pandas as pd

df = pd.DataFrame({
    "region": ["north", "south", "north", "west", "south"],
    "amount": [120.0, 80.0, 45.0, 200.0, 95.0],
    "channel": ["online", "store", "online", "store", "online"],
})

print(df.shape)                 # (5, 3)
print(df["amount"].mean())      # 108.0

Das Einlesen aus einer Datei läuft meist über pd.read_csv, das eine CSV-Datei in ein DataFrame verwandelt. Ab dann arbeitet man mit derselben Tabelle weiter, egal woher sie stammt.

Auswahl mit loc und iloc – der klassische Stolperstein

Sobald man Zeilen und Spalten gezielt herausgreifen will, tauchen zwei Zugriffsarten auf, die Anfänger zuverlässig durcheinanderbringen: .loc und .iloc. Der Unterschied ist eigentlich einfach, aber man muss ihn einmal bewusst gelernt haben.

.loc ist label-basiert. Man greift über die Beschriftung von Zeilen und Spalten zu, und – das ist die Eigenheit – der Endpunkt eines Bereichs ist inklusiv. .iloc ist positionsbasiert. Man greift über die ganzzahlige Position zu, und der Endpunkt ist exklusiv, genau wie beim normalen Python-Slicing.

import pandas as pd

df = pd.DataFrame(
    {"amount": [120, 80, 45, 200, 95]},
    index=["a", "b", "c", "d", "e"],
)

# label-based, endpoint INCLUDED -> rows a, b, c
print(df.loc["a":"c", "amount"])

# position-based, endpoint EXCLUDED -> rows a, b, c (positions 0, 1, 2)
print(df.iloc[0:3, 0])

Beide Ausdrücke liefern hier dieselben drei Zeilen, aber aus unterschiedlichen Gründen: einmal weil "c" als Label eingeschlossen wird, einmal weil Position 3 gerade nicht mehr mitgenommen wird. Wer das verwechselt, erntet entweder einen Off-by-one-Fehler oder einen KeyError. Mein Tipp aus der Praxis: Labels immer über .loc, Positionen immer über .iloc, und niemals mischen.

Zum Filtern nutzt man Boolean-Masken – genau wie in NumPy, nur dass die Bedingung sich auf eine Spalte bezieht.

import pandas as pd

df = pd.DataFrame({
    "region": ["north", "south", "north", "west"],
    "amount": [120, 80, 45, 200],
})

big = df[df["amount"] > 100]              # keep rows above 100
big_regions = df.loc[df["amount"] > 100, "region"]   # filter + pick column

Der häufigste Fehler: verkettetes Indizieren

Jetzt zu einem Fallstrick, der in fast jeder Schulung einmal auftaucht und der zu einer der meistgesuchten Warnungen im pandas-Umfeld führt: dem SettingWithCopyWarning. Er entsteht durch verkettetes Indizieren – also zwei aufeinanderfolgende Zugriffe in einem Ausdruck.

# problematic: chained indexing
df[df["amount"] > 100]["tier"] = "high"     # may write into a copy!

Das Tückische daran: df[df["amount"] > 100] liefert je nach Situation eine Sicht auf die Originaldaten oder eine Kopie davon – und es ist nicht sicher vorhersagbar, welches von beiden. Schreibt man dann in das Ergebnis, landet die Zuweisung womöglich in einer Kopie, die sofort wieder verworfen wird. Das Original bleibt unverändert, und pandas warnt genau davor. Die korrekte Form nutzt einen einzigen .loc-Zugriff, der Zeilenauswahl und Spalte in einem Schritt adressiert:

# correct: single .loc access
df.loc[df["amount"] > 100, "tier"] = "high"

Und wenn man bewusst mit einer eigenständigen Kopie weiterarbeiten will, sagt man das ausdrücklich mit df.copy(). Dann ist klar, dass Änderungen das Original nicht berühren sollen. Diese eine Regel – Auswahl und Zuweisung in einem .loc – erspart erfahrungsgemäß die meisten dieser Warnungen.

Der zweite Fehler in derselben Kategorie ist das Iterieren über Zeilen. Wer aus der klassischen Programmierung kommt, greift instinktiv zu df.iterrows() und geht Zeile für Zeile durch. Das ist langsam und speicherhungrig, weil pandas für jede Zeile ein neues Objekt bauen muss und der ganze Geschwindigkeitsvorteil des vektorisierten NumPy-Unterbaus verloren geht. Fast immer lässt sich dieselbe Absicht mit einer Spaltenoperation, mit groupby oder mit np.where ausdrücken – und läuft dann um Größenordnungen schneller.

Gruppieren und aggregieren mit groupby

Die vielleicht wichtigste Operation für Analysen ist groupby. Sie folgt einem Muster, das unter dem Namen Split-Apply-Combine bekannt ist: Die Daten werden nach einem Schlüssel in Gruppen aufgeteilt (split), auf jede Gruppe wird eine Aggregatfunktion angewendet (apply), und die Teilergebnisse werden zu einer neuen Tabelle zusammengefügt (combine).

import pandas as pd

df = pd.DataFrame({
    "region": ["north", "south", "north", "west", "south"],
    "amount": [120.0, 80.0, 45.0, 200.0, 95.0],
})

summary = (df.groupby("region")
             .agg(total=("amount", "sum"),
                  avg=("amount", "mean"))
             .reset_index())

print(summary)
#   region  total    avg
# 0  north  165.0   82.5
# 1  south  175.0   87.5
# 2   west  200.0  200.0

Die benannte Aggregation total=("amount", "sum") ist meine bevorzugte Schreibweise: Sie sagt in einer Zeile, welche Spalte mit welcher Funktion aggregiert wird und wie die Ergebnisspalte heißen soll. Das Ergebnis ist eine kompakte Tabelle, die sich direkt weiterverarbeiten lässt. Das .reset_index() am Ende holt den Gruppierungsschlüssel aus dem Index zurück in eine normale Spalte – praktisch, wenn man gleich danach plotten will.

Oft liegen die Daten nicht in einer einzigen Tabelle, sondern verteilt. Verkaufszahlen hier, Regionsinformationen dort. Zum Zusammenführen dient merge, das nach dem Vorbild von Datenbank-Joins arbeitet:

import pandas as pd

sales = pd.DataFrame({
    "region": ["north", "south", "west"],
    "amount": [165, 175, 200],
})
managers = pd.DataFrame({
    "region": ["north", "south", "east"],
    "manager": ["Ada", "Ben", "Cleo"],
})

joined = sales.merge(managers, how="inner", on="region")
print(joined)
#   region  amount manager
# 0  north     165     Ada
# 1  south     175     Ben

Hier lohnt ein wacher Blick auf den Parameter how. Mit how="inner" bleiben nur Regionen, die in beiden Tabellen vorkommen – „west" und „east" fallen heraus. Ein how="outer" würde alle Regionen behalten und die fehlenden Werte mit NaN auffüllen. Wer den falschen Modus wählt, verliert entweder unbemerkt Zeilen oder handelt sich unerwartete Lücken ein. Es lohnt sich, vor und nach einem merge einmal auf .shape zu schauen und zu prüfen, ob die Zeilenzahl zu den Erwartungen passt.

Die Pipeline: laden, bereinigen, gruppieren, plotten

Setzen wir die Teile zusammen. Eine typische Analyse durchläuft immer dieselben Stationen, und es hilft, diesen Ablauf einmal als Bild vor Augen zu haben.

flowchart LR
    A[Rohdaten<br/>CSV-Datei] --> B[Laden<br/>pandas.read_csv]
    B --> C[Bereinigen<br/>dropna / fillna]
    C --> D[Transformieren<br/>groupby / agg]
    D --> E[Plotten<br/>matplotlib]

Vor dem Gruppieren steht meist das Bereinigen. Reale Daten haben Lücken, und groupby oder Rechnungen darauf reagieren empfindlich auf fehlende Werte. Zwei Werkzeuge decken die meisten Fälle ab: dropna wirft Zeilen mit fehlenden Werten weg, fillna ersetzt sie durch einen Vorgabewert. Welches von beiden richtig ist, hängt von der Frage ab – manchmal ist eine Lücke ein echtes Nichts, manchmal eine getarnte Null.

import pandas as pd

df = pd.read_csv("sales.csv")

# clean: drop rows without an amount
df = df.dropna(subset=["amount"])

# transform: aggregate per region
summary = (df.groupby("region")
             .agg(total=("amount", "sum"))
             .reset_index())

Matplotlib: aus Zahlen wird ein Bild

Am Ende der Pipeline steht die Visualisierung. Matplotlib ist die etablierte Bibliothek dafür, und ich empfehle in Schulungen von Anfang an den objektorientierten Stil: erst mit plt.subplots() eine Figur und eine Achse erzeugen, dann alles auf dieser Achse aufbauen. Das ist etwas mehr Tipparbeit als der schnelle plt.plot()-Einzeiler, aber es bleibt übersichtlich, sobald eine Grafik mehr als eine Kurve enthält.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.bar(summary["region"], summary["total"])
ax.set_xlabel("Region")
ax.set_ylabel("Total sales")
ax.set_title("Sales by region")
plt.show()

Für die drei häufigsten Diagrammtypen gibt es je eine passende Funktion auf der Achse. Ein Balkendiagramm mit ax.bar(x, height) vergleicht Kategorien – ideal für das groupby-Ergebnis von eben. Ein Liniendiagramm mit ax.plot(x, y) zeigt Verläufe, etwa eine Kennzahl über mehrere Monate hinweg. Und ein Streudiagramm mit ax.scatter(x, y) zeigt den Zusammenhang zweier Größen. Beschriftungen über set_xlabel, set_ylabel und set_title gehören für mich immer dazu; eine Grafik ohne Achsenbeschriftung ist nur eine halbe Aussage.

Wer macht was? Die Rollenteilung im Überblick

Wenn ich den Werkzeugkasten am Ende einer Schulung zusammenfasse, hilft eine klare Aufgabenteilung, damit niemand die falsche Bibliothek für ein Problem greift:

  • NumPy bildet die numerische Basis: homogene Arrays, vektorisiertes Rechnen, mathematische Operationen. Alles darüber baut darauf auf.
  • pandas kümmert sich um strukturierte, beschriftete Tabellendaten: einlesen, bereinigen, auswählen, filtern, gruppieren, zusammenführen – der gesamte Umgang mit realen, unordentlichen Daten.
  • Matplotlib übernimmt die Visualisierung: Aus den fertig aufbereiteten Zahlen wird ein Diagramm, das eine Aussage transportiert.

Diese Reihenfolge ist kein Zufall. Sie beschreibt genau den Weg, den die Daten nehmen: von rohen Zahlen über eine strukturierte Tabelle bis zum Bild.

Roter Faden

Der eigentliche Denkwechsel bei der Datenanalyse mit Python ist der Abschied von der Schleife. Nicht mehr Element für Element durchgehen, sondern ganze Spalten und ganze Tabellen als Einheit behandeln. NumPy macht das über vektorisierte Operationen auf dem ndarray, pandas hebt dieselbe Idee auf beschriftete Tabellen, und groupby fasst nach dem Split-Apply-Combine-Muster ganze Gruppen auf einmal zusammen. Wer die zwei Anfängerfallen kennt – verkettetes Indizieren und das Iterieren über Zeilen – umgeht die häufigsten Reibungspunkte und schreibt Code, der bei einer Million Zeilen genauso ruhig bleibt wie bei hundert. Der Rest ist Übung: laden, bereinigen, gruppieren, plotten, und diesen Kreislauf so oft gehen, bis er in den Fingern sitzt. Wer noch tiefer verstehen will, warum diese Werkzeuge so gebaut sind, wie sie sind, findet in Python für Entwickler, die Systeme verstehen wollen den passenden Unterbau.

Weiterführende Quellen

Kommentare

Kommentar schreiben