CSBootcampHSG

Lektion 08

Pandas — DataFrames in der Praxis

Lade CSV-Daten in einen DataFrame, navigiere mit loc/iloc, behandle NaN sauber, gruppiere mit groupby und sortiere — die fünf Operationen, die in Quiz 08 immer wieder geprüft werden.

Dauer:
60 Min. geschätzt
Voraussetzungen:
Funktionen (Lektion 03) · Listen und Dictionaries (Lektion 05)
Illustration: ein stilisierter Pandabär neben einer Tabelle, deren Werte durch Filter-, Lupen- und Summenzeichen-Symbole fließen

Theorie & Konzepte

Phase 1 / 4

Pandas verwandelt eine CSV-Datei in einen DataFrame — eine zweidimensionale Tabelle mit benannten Spalten und einem Index. Im Unterschied zu einer Liste von Dicts kannst du mit einer einzigen Zeile filtern, gruppieren, aggregieren und sortieren. Wer das beherrscht, schreibt zehnzeiligen for-Schleifen-Code in einer Zeile um.

Diese Lektion zeigt dir die vier Operationen, die Quiz 08 abprüft — boolesche Maske, loc vs. iloc, Datentyp-Reasoning auf einer Spalte und eine kleine Datenpipeline. Wenn du das Übungsquiz am Ende bestehst, sitzen die Ideen.

Eine CSV-Datei laden und erkunden

Im HSG-Notebook beginnt jede Pandas-Aufgabe mit einer Zeile wie dieser:

import pandas as pd

DF = pd.read_csv('movies.csv')

pd.read_csv liest die Datei und gibt dir einen DataFrame zurück. Konvention: DF (groß geschrieben) ist der unveränderte Original-DataFrame — den fässt du nie wieder an. Alle Analysen arbeiten auf einer Kopie.

Bevor du rechnest, schau dir die Daten an. Diese vier Befehle solltest du reflexartig kennen:

DF.head(5)        # die ersten 5 Zeilen — erste Orientierung
DF.sample(5)      # 5 zufällige Zeilen — erkennt man Muster? Ausreisser?
DF.shape          # (n_zeilen, n_spalten)
DF.columns        # Liste der Spaltennamen — welche gibt es überhaupt?
DF.dtypes         # welcher Datentyp pro Spalte?

Wer das zuerst aufruft, spart sich Rätselraten darüber, welche Spalten existieren und wie sie heißen.

Anatomie eines DataFrames

Ein DataFrame hat drei Bausteine, die du dir farblich merken kannst — die gleichen Farben tauchen in der Abbildung und im Code unten wieder auf:

  • Index — die Zeilenlabels ganz links. Jede Zeile hat einen Label, den du mit loc ansprichst.
  • Spaltennamen — die Kopfzeile oben. Jede Spalte hat einen Namen; du holst sie mit df['name'].
  • Werte-Block — die eigentlichen Daten in der Mitte. Ein 2D-Raster, das du aggregieren, filtern und transformieren kannst.

Eine einzelne Spalte ist eine Series: ein 1D-Array, das denselben Index wie der DataFrame behält.

import pandas as pd

df = pd.DataFrame(
    {"land": ["CH", "DE", "AT"],
     "einwohner": [8.7, 84.0, 9.0]},
    index=["alpha", "beta", "gamma"],
)

df.index       # Index(['alpha', 'beta', 'gamma'])
df.columns     # Index(['land', 'einwohner'])
df.values      # 2D-Array mit dem Werte-Block
df.shape        # (3, 2) — Zeilen × Spalten

Der Index muss nicht 0, 1, 2, … sein — er kann Strings, Datumsangaben oder beliebige Hashables enthalten. Genau da entsteht der Unterschied zwischen loc und iloc in der nächsten Sektion.

DataFrame, Series und Index

DataFrame mit Index links, Spaltennamen oben und einer hervorgehobenen Series.df — pd.DataFrameIndex links, Spaltennamen oben.indexlandeinwohnerbip_pcalphaCH8.792.4betaDE84.053.1gammaAT9.055.8df['einwohner'] — eine pd.Seriesdf.shape(3, 3)
Ein DataFrame ist ein Container mit Index (links), Spaltennamen (oben) und Werteblock. Eine Series ist eine einzelne Spalte mit demselben Index.

Indizieren — loc vs. iloc

Das wichtigste Begriffspaar in Pandas:

  • df.loc[label] — Zugriff per Label (das, was im Index steht).
  • df.iloc[i] — Zugriff per integer Position (wie ein normaler Listen-Index).
df.loc["beta"]              # Zeile mit Label 'beta'
df.iloc[1]                  # zweite Zeile (Position 1)

df.loc["beta", "einwohner"] # 84.0
df.iloc[1, 1]               # 84.0

df.loc["beta":"gamma"]      # inklusive Endlabel — 'gamma' ist dabei!
df.iloc[1:3]                # exklusive End-Position — wie Python-Slices

Der Klassiker im Quiz: df.loc[25]. Das ist kein Aufruf an Position 25, sondern an die Zeile mit Label 25 — falls du den Index nicht selbst gesetzt hast und Pandas standardmäßig 0,1,2,… vergibt, sind Label und Position zufällig identisch. Sobald du set_index(...) benutzt oder eine Spalte als Index lädst, fallen sie auseinander.

loc vs. iloc — Label vs. Position

loc folgt dem Label im Index, iloc zählt Positionen ab 0.Index aus Labels — df.loc und df.iloc liefern unterschiedliche Zeilen.df.loc[label]df.iloc[i]25a07b112c24d333e4df.loc[2]KeyError — keinLabel "2"df.iloc[2]dritte Zeile →cSelbe Zahl, unterschiedliches Ergebnis — loc liest aus dem Index, iloc aus der Position.
loc folgt dem Index-Label (oben). iloc zählt Positionen ab 0 (unten). Bei einem Index mit eigenen Labels liefert dieselbe Zahl in loc und iloc unterschiedliche Zeilen.

Cheat-Sheet zum Mitnehmen

Cheat-Sheet: loc vs. iloc. Tabelle mit Index-Labels 25, 7, 12, 4 und iloc-Positionen 0–3 daneben. Beispiele: df.loc[2] → KeyError, df.iloc[2] → AT.
Speichere oder drucke dir das aus — bevor du das nächste Mal vor einem Pandas-Indizierungsbug sitzt.

Boolesche Masken — Filtern in einer Zeile

Eine boolesche Series kannst du als Maske zum Indizieren benutzen. Pandas behält nur die Zeilen, in denen die Maske True ist:

mask = df["einwohner"] > 10        # Series[bool] mit demselben Index
big = df[mask]                     # nur DE
small = df[~mask]                  # alle anderen

Mehrere Bedingungen kombinierst du mit & (UND) und | (ODER) — immer in Klammern, weil & enger bindet als >:

df[(df["einwohner"] > 5) & (df["land"] != "DE")]

Die Kombination Maske + Aggregation ist die wichtigste Pandas-Idiomatik überhaupt:

df["einwohner"][df["einwohner"] > 10].mean()   # Mittelwert nur über große Länder

Maske richtig anwenden

`.mean()` rechnet nach dem Filter — nicht davor.

So geht's

# Maske auf Spalte, dann aggregieren:
df['value1'][df['value1'] > 10].mean()

# oder: ganze Zeilen filtern, dann eine Spalte:
df[df['value1'] > 10]['value1'].mean()

Beide Varianten ergeben denselben Wert — sie unterscheiden sich nur in der Reihenfolge der Operationen.

Falle

# alle Werte mitteln, dann mit 10 vergleichen — gibt einen bool, keinen Mittelwert:
df['value1'].mean() > 10

# Bedingung ohne Klammern — Operator-Präzedenz beißt:
df[df['value1'] > 10 & df['value0'] < 5]   # TypeError

NaN-Propagation in einer Pipeline

NaN propagiert durch Operationen, dropna entfernt betroffene Zeilen vollständig.df — Originaldatenab125NaN87NaN39.dropna()gesäubertab12539df['a'].mean() = 7.5Aggregate ignorieren NaN, aber Arithmetik propagiert sie. .dropna() entfernt ganze Zeilen.
Operationen reichen NaN durch (oben). Erst dropna entfernt die Zeile vollständig — danach rechnen Aggregate auf einem sauberen Block.

Vier Regeln, die du nie wieder vergessen solltest

Cheat-Sheet mit den vier wichtigsten NaN-Regeln in Pandas: NaN == NaN ist False, NaN propagiert in Arithmetik, mean() ignoriert NaN, dropna entfernt ganze Zeilen.
Die vier Aussagen, die du im Quiz und im Alltag brauchst.

Eine Spalte herausziehen — `df['col'].iloc[i]`

Sehr häufig willst du einen einzelnen Wert aus einer Spalte:

x = df['einwohner'].iloc[1]   # 84.0 — ein Skalar

Wichtig: df['einwohner'] ist eine Series. .iloc[1] auf einer Series liefert immer einen Skalar (kein DataFrame, keine weitere Series). Welcher konkrete Datentyp das ist, hängt vom Inhalt der Spalte ab:

SpalteninhaltTyp von x
nur ganze Zahlennumpy.int64 (oder pd.Int64)
ganze Zahlen + NaNfloat64 (NaN zwingt Pandas auf float)
Stringsstr
gemischt mit objectwas halt drin steht — int, str, None, …

Und: wenn i ≥ len(df), wirft iloc einen IndexError.

Gruppieren und aggregieren

groupby('spalte') zerlegt den DataFrame in Gruppen. Aggregationen wie mean, sum, count rechnen pro Gruppe und liefern eine Series (bei einer Spalte) oder einen DataFrame (bei mehreren):

filme = pd.DataFrame({
    'regie': ['Nolan', 'Nolan', 'Anderson', 'Anderson', 'Anderson'],
    'dauer': [148, 169, 99, 102, None],
})
filme.groupby('regie')['dauer'].mean()
# regie
# Anderson    100.5     # NaN ignoriert, (99+102)/2
# Nolan       158.5
# Name: dauer, dtype: float64

Das Standardverhalten von groupby ist: nur Schlüssel mit mindestens einem nicht-NaN Wert in der aggregierten Spalte erscheinen. Wenn du alle Schlüssel willst, gibt es Optionen wie dropna=False.

groupby + mean — split, apply, combine

groupby zerlegt den DataFrame nach Schlüssel, mean rechnet pro Gruppe.dfA10B20A30B40A20schlüsselwertgroupbygruppe A103020.mean() = 20.0gruppe B2040.mean() = 30.0combineergebnisA20.0B30.0Split — Apply — Combine. Eine Series mit Schlüsseln als Index.
groupby zerlegt den DataFrame in Gruppen (split). Auf jeder Gruppe wird mean berechnet (apply). Die Ergebnisse werden zu einer Series zusammengeführt (combine).

Cheat-Sheet: split · apply · combine

Cheat-Sheet zu groupby: Drei Phasen split, apply mean(), combine mit konkreten Werten und der zugehörigen Codezeile df.groupby('gruppe')['wert'].mean().
Die drei Phasen mit konkreten Werten und der Codezeile, die sie umsetzt.

Eine kleine Pipeline lesen

Pandas-Pipelines bauen sich Schritt für Schritt auf. Lies sie wie eine Zutatenliste, von oben nach unten:

def average_movie_lengths():
    df = DF.copy()                                    # 1. Original nicht verändern
    df = df[['director_name', 'duration']]            # 2. nur was du brauchst
    df = df.dropna(axis='index')                      # 3. Zeilen mit NaN raus
    df = df.set_index('director_name')                # 4. Schlüssel an den Index
    df = df.groupby('director_name').mean()           # 5. pro Regie aggregieren
    return df['duration'].sort_index()                # 6. nach Index sortieren

Fünf Best Practices:

  1. Immer kopieren, wenn du den Eingabe-DataFrame veränderst (copy()), sonst mutierst du den Aufrufer.
  2. Spalten zuerst reduzieren — kleinere Tabellen sind einfacher zu lesen und schneller.
  3. NaN explizit behandelndropna, fillna, oder bewusst stehen lassen.
  4. Index sinnvoll setzen — viele Operationen werden danach trivial.
  5. Sortieren am Ende — sonst bringt jede weitere Operation die Reihenfolge wieder durcheinander.

Assignment-Walkthrough

Phase 2 / 4

Hier gehen wir typspezifisch durch die 4 Aufgabenformen von Assignment 08: was gefragt ist, wie man es angeht, und wo man sich typischerweise verrennt. Die konkreten Namen und Daten siehst du im HSG-Notebook — die Muster hier passen auf jede Variante.

Assignment-Aufgabe 1

Ein Element an einer Position auslesen

Was ist gefragt

Die erste Aufgabe ist eine Einzelwert-Abfrage: du sollst den Wert einer bestimmten Spalte für die n-te Zeile des DataFrames zurückgeben. Das HSG-Notebook gibt dir einen DataFrame DF und eine Position als int. Deine Funktion soll den passenden Zellwert (z. B. einen Titel) liefern — genau einen Skalar, keine Series, kein DataFrame.

Du brauchst genau eine Zeile Code.

Strategie

  1. Position → DF.iloc[position] liefert dir die Zeile an integer Position als Series.
  2. Spalte → ['spaltenname'] auf dieser Series greift den einen Zellwert ab.
  3. In einer Zeile: DF.iloc[position]['spaltenname'].

Das ist Position-zuerst, Spalte-danach. Du könntest auch DF['spaltenname'].iloc[position] schreiben — funktional identisch, HSG bevorzugt die erste Form.

Lösungsskelett

def meine_funktion(position):
    return DF.iloc[position]['name_der_spalte']

Typische Stolperstellen

  • .loc statt .iloc — wenn der Index des DataFrames keine fortlaufenden Integer sind, adressiert .loc[position] nach Label (und wirft KeyError, falls kein Label mit dieser Zahl existiert). Faustregel: Position → iloc, Label → loc.
  • Zeile 1 vs. Zeile 2 vergessenDF.iloc[position] allein liefert die ganze Zeile; du brauchst den zusätzlichen Spaltenzugriff für einen einzelnen Wert.
  • Falsche SpaltennamenDF.columns gibt dir die Liste; tippfehlersicher kopieren.

Assignment-Aufgabe 2

Einen String in einer Zelle aufsplitten (NaN-sicher)

Was ist gefragt

Die zweite Aufgabe gibt dir dieselbe Zugriffsform wie Aufgabe 1, aber der Zellwert ist ein String mit Trennzeichen (z. B. "a|b|c"). Du sollst daraus eine list machen. Sonderfall: ist der Wert NaN (fehlt), soll die Funktion eine leere Liste [] zurückgeben.

Strategie

  1. Zellwert holen — wie in Aufgabe 1: wert = DF.iloc[position]['spalte'].
  2. NaN-Check mit pd.isna(wert) — niemals mit ==, weil NaN == NaN immer False ist. Wenn ja: return [].
  3. Sonst wert.split('|') (oder welches Trennzeichen auch immer die Aufgabe vorgibt).

Drei Zeilen reichen.

Lösungsskelett

import pandas as pd

def meine_funktion(position):
    wert = DF.iloc[position]['spalte_mit_string']
    if pd.isna(wert):
        return []
    return wert.split('|')

Typische Stolperstellen

  • wert == None oder wert == pd.NA statt pd.isna(wert) — funktioniert nicht zuverlässig für NaN-Floats. pd.isna() ist die eine verlässliche Prüfung.
  • Trennzeichen vergessenwert.split() ohne Argument splittet an Whitespace. Lies die Aufgabe genau: oft ist es | oder ;.
  • Rückgabetypsplit liefert bereits eine list. Du brauchst kein list(...) drumherum.

Assignment-Aufgabe 3

Pro Gruppe aggregieren und sortiert zurückgeben (Series)

Was ist gefragt

Die dritte Aufgabe ist der Klassiker: eine Kennzahl pro Gruppe berechnen. Du sollst eine pd.Series zurückgeben, in der jede Gruppe einmal im Index steht und der Wert die Aggregation (meist Mittelwert) über diese Gruppe ist. Zeilen mit NaN in relevanten Spalten werden weggeworfen, das Ergebnis ist nach dem Gruppenschlüssel aufsteigend sortiert.

Strategie

Sechs-Schritte-Pipeline, die du dir auswendig merken solltest — sie kommt in fast jeder Data-Science-Aufgabe wieder vor:

  1. df = DF.copy() — Original nicht anfassen.
  2. df = df[['gruppen_spalte', 'wert_spalte']] — auf das reduzieren, was du brauchst.
  3. df = df.dropna(axis='index') — Zeilen mit NaN komplett raus.
  4. df = df.set_index('gruppen_spalte') — Schlüssel wird zum Index.
  5. df = df.groupby(level=0).mean() — pro Index-Wert mitteln.
  6. return df['wert_spalte'].sort_index() — als sortierte Series zurückgeben.

Lösungsskelett

def meine_funktion():
    df = DF.copy()
    df = df[['gruppen_spalte', 'wert_spalte']]
    df = df.dropna(axis='index')
    df = df.set_index('gruppen_spalte')
    df = df.groupby(level=0).mean()
    return df['wert_spalte'].sort_index()

Typische Stolperstellen

  • DF.copy() vergessen — wenn die Testzelle die Funktion mehrmals aufruft oder andere Zellen denselben DF verwenden, führt jede nicht-kopierende Operation zu rätselhaften Fehlern. Erste Zeile, immer.
  • return df statt return df['wert_spalte'] — die Aufgabe verlangt eine Series. Nach groupby().mean() hast du noch einen DataFrame mit einer Spalte — erst df['wert_spalte'] macht daraus eine Series.
  • Sortieren vergessensort_index() ist eigene Zeile. Die Reihenfolge, in der groupby die Gruppen liefert, ist nicht garantiert.
  • Nach NaN-Filter leere Gruppegroupby zeigt Gruppen ohne Werte standardmäßig nicht. Das ist normalerweise gewünscht.

Assignment-Aufgabe 4

Abgeleitete Spalte berechnen und als sortierten DataFrame zurückgeben

Was ist gefragt

Die vierte Aufgabe bleibt beim DataFrame als Rückgabeform, aber mit zwei Extras: du sollst eine neue Spalte aus zwei bestehenden berechnen (typisch: eine Ratio oder Differenz) und das Ergebnis absteigend nach dieser neuen Spalte sortieren. Die Spaltenreihenfolge im Ergebnis ist oft vorgegeben.

Strategie

Variante der Sechs-Schritte-Pipeline, aber mit abgeleiteter Spalte statt Aggregation:

  1. df = DF.copy() — wie immer.
  2. df = df[['spalte_a', 'spalte_b', 'spalte_c', ...]] — exakt die geforderten Spalten, in der geforderten Reihenfolge.
  3. df = df.dropna(axis='index') — NaN raus, sonst pflanzt sich das in die Division fort.
  4. df['neue_spalte'] = df['a'] / df['b'] — abgeleitete Spalte zuweisen (landet rechts).
  5. df = df.sort_values(by='neue_spalte', ascending=False) — absteigend sortieren.
  6. return df.

Lösungsskelett

def meine_funktion():
    df = DF.copy()
    df = df[['spalte_a', 'spalte_b', 'spalte_c']]
    df = df.dropna(axis='index')
    df['neue_spalte'] = df['spalte_a'] / df['spalte_b']
    return df.sort_values(by='neue_spalte', ascending=False)

Typische Stolperstellen

  • Spaltenreihenfolge falsch — die Aufgabe gibt meist eine exakte Reihenfolge vor. Der Test vergleicht oft mit assertEqual(list(result.columns), [...]). Reihenfolge zählt.
  • ascending=True (Default) statt ascending=False — absteigend muss explizit gesetzt werden.
  • Division durch 0 → inf im Ergebnis — manche Tests verbieten auch inf-Werte. Falls relevant: df = df[df['nenner'] != 0] vor der Division.
  • Neue Spalte vor dem dropna — erst auf nicht-null-Zeilen rechnen, dann die abgeleitete Spalte anhängen. Sonst rechnet die Arithmetik auf NaN und liefert NaN zurück.

Aufgaben

Phase 3 / 4
Aufgabe

Skalar an einer Position lesen

2 Punkte

Schreibe eine Funktion column_at(df, position, column), die aus dem DataFrame df an der integer Position position den Wert der Spalte column zurückgibt — als Skalar (kein DataFrame, keine Series).

Beispiel: für einen DataFrame mit einer Spalte title und Zeilen "Dune", "Solaris", "1984" liefert column_at(df, 1, "title") den String "Solaris".

Du brauchst genau eine Zeile Pandas-Code im Funktionsrumpf.

Hinweise

    Aufgabe

    NaN-sichere String-Aufteilung

    2 Punkte

    Schreibe eine Funktion keywords_at(df, position, column, separator="|"), die den String aus der Spalte column an integer Position position an separator teilt und die Liste der Teile zurückgibt.

    Sonderfälle:

    • Ist der Wert an dieser Stelle NaN (fehlend), gib eine leere Liste [] zurück.
    • Ist der String leer (""), gib ebenfalls [] zurück (nicht [""]).
    • Sonst: "a|b|c".split("|")["a", "b", "c"].

    Prüfe NaN mit pd.isna(...) — niemals mit ==, weil NaN == NaN immer False ist.

    Hinweise

      Aufgabe

      Pro Gruppe mitteln, sortiert nach Schlüssel

      3 Punkte

      Schreibe eine Funktion mean_by(df, group_col, value_col), die eine pd.Series zurückgibt:

      • Index = die eindeutigen Werte aus group_col (aufsteigend sortiert)
      • Werte = der Mittelwert von value_col pro Gruppe

      Bau die Pipeline in dieser Reihenfolge, damit du die HSG-Idiomatik trainierst:

      1. df.copy() — die Eingabe nie direkt anfassen.
      2. Nur die beiden relevanten Spalten auswählen.
      3. Mit dropna(axis='index') die Zeilen wegwerfen, in denen group_col oder value_col fehlt.
      4. Mit set_index(group_col) die Gruppierungsspalte zum Index machen.
      5. Mit groupby(level=0).mean() auf dem Index gruppieren und mitteln.
      6. Am Ende sort_index() für aufsteigende Reihenfolge.

      Verändere den Eingabe-DataFrame nicht. Das Ergebnis ist eine pd.Series mit group_col-Werten als Index.

      Hinweise

        Aufgabe

        Kennzahl ableiten, filtern, sortieren

        3 Punkte

        Schreibe eine Funktion ratio_table(df, numerator, denominator), die einen neuen DataFrame zurückgibt mit:

        • genau drei Spalten in dieser Reihenfolge: numerator, denominator, "ratio"
        • die Spalte "ratio" ist die zeilenweise Division df[numerator] / df[denominator]
        • keine NaN-Werte im Ergebnis (auch nicht durch Division durch 0 — solche Zeilen rausfiltern)
        • absteigend sortiert nach "ratio"
        • der Eingabe-DataFrame darf nicht verändert werden

        Hinweise

          Übungsquiz

          Phase 4 / 4

          Teste dein Verständnis in 15 Minuten mit 4 Frage(n), direkt an den HSG-Quizstolperfallen ausgerichtet.

          Quiz starten