Gruppierung und Aggregation — eine Antwort pro Gruppe
Fragen wie „pro Filiale“, „pro Kategorie“ und „pro Tag“ in eine einzige Zeile groupby verwandeln: der Schlüssel, der Wert und die Aggregation, mehrere Kennzahlen auf einmal mit Named Aggregation, size im Vergleich zu count, zwei Schlüssel und die Falle des Durchschnitts von Durchschnitten.
Wie möchtest du lernen?
Das Problem, das wir lösen
Der Ladenbesitzer schickt dir eine kurze Zeile: „Wie viele Einheiten haben wir diese Woche in jeder Filiale verkauft?“
Du hast orders.csv — acht Bestellungen, jeweils eine Zeile. Mit dem Wissen aus den letzten vier Kapiteln kannst du das bereits beantworten. Eine Filiale filtern, ihre Spalte aufsummieren, wiederholen:
import pandas as pd
orders = pd.read_csv("orders.csv")
for branch in ["north", "south", "east"]:
units = orders[orders["branch"] == branch]["quantity"].sum()
print(branch, units)north 48
south 6
east 27Die Zahlen stimmen. Die Methode hat jedoch drei Probleme, und jedes davon wird mit wachsenden Datenmengen gravierender.
Erstens: Du musstest die Filialen im Voraus kennen. Du hast die Liste selbst eingetippt. Wenn die Datei der nächsten Woche eine Bestellung aus einer neuen Filiale west enthält, sieht diese Schleife sie nicht — sie bricht nicht ab, sie lässt west einfach aus, und die Summen ergeben nicht mehr das Ganze.
Zweitens: Jede neue Frage erfordert eine neue Schleife. „Umsatz pro Kategorie“ bedeutet eine zweite Schleife über eine zweite manuell eingetippte Liste. „Einheiten pro Filiale und Kategorie“ bedeutet eine Schleife innerhalb einer Schleife.
Drittens: Die Antwort ist keine Tabelle. Es sind Zeilen aus ausgegebenem Text. Du kannst sie weder sortieren noch filtern oder neben eine andere Antwort stellen, ohne sie neu aufzubauen.
Was du eigentlich sagen willst, lässt sich in einer einzigen Zeile formulieren: Teile die Zeilen nach Filiale auf, summiere die Menge in jedem Teil und gib mir die Teile als Tabelle zurück. Das ist groupby, und so wird nahezu jede Zusammenfassung in pandas geschrieben — ein Bericht, eine Dashboard-Kennzahl, die erste Antwort auf „Woher kommt das Geld?“.
Am Ende dieses Kapitels kannst du
- Eine Frage wie „Einheiten pro Filiale“ in ihre drei Bestandteile zerlegen — Schlüssel, Wertespalte, Aggregation — und den Schlüssel vor dem Gruppieren prüfen
df.groupby(key)[column].sum()schreiben und die zurückgegebene Series verstehen, deren Index der Schlüssel ist- Mehrere Zusammenfassungen auf einmal mit
.agg()und mit Named Aggregation berechnen size()voncount()unterscheiden und entscheiden, was mit fehlenden Schlüsseln und fehlenden Werten geschieht- Nach zwei Spalten gruppieren, das Ergebnis vereinfachen oder umformen und den Fehler „Durchschnitt von Durchschnitten“ vermeiden
Voraussetzungen: Zeilen sortieren.
Die Datei, die wir verwenden
Dieselbe orders.csv wie in den vorherigen Kapiteln — acht Bestellungen aus einem kleinen Geschäft. Wenn du sie nicht hast, erstelle sie in deinem Projektordner mit genau diesen Zeilen:
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Eine Zeile entspricht einer Bestellung: aus welcher Filiale sie stammt, welches Produkt, dessen Kategorie, wie viele Einheiten und der Preis einer Einheit.
Bevor du den Code schreibst
Jede Gruppierungsfrage hat dasselbe Grundgerüst. Bevor du groupby tippst, zerlege die Frage in ihre Einzelteile.
1. Formuliere die Frage in drei Teilen
Eine Gruppierungsfrage enthält immer drei Dinge, und die Wörter der Frage verraten dir, welches was ist:
- Schlüssel (Key) — die Spalte, deren Werte die Gruppen definieren. Es ist das Wort nach pro, nach oder für jedes. In „Einheiten pro Filiale“ ist es
branch. - Wert (Value) — die Spalte, die zusammengefasst wird: das, was gemessen wird. Hier
quantity. - Aggregation — wie aus vielen Werten einer wird: Gesamtsumme, Durchschnitt, Anzahl, Maximum. Hier
sum.
Ein paar weitere Beispiele, damit sich das Muster einprägt:
- „Umsatz pro Kategorie“ — Schlüssel
category, Wertrevenue, Aggregationsum - „Durchschnittliche Bestellgröße in jeder Filiale“ — Schlüssel
branch, Wertquantity, Aggregationmean - „Wie viele Bestellungen an jedem Tag“ — Schlüssel
date, keine Wertespalte (du zählst Zeilen), Aggregationsize - „Größte Einzelbestellung pro Produkt“ — Schlüssel
product, Wertquantity, Aggregationmax
Wenn du nicht alle drei Teile benennen kannst, ist die Frage noch nicht bereit für Code. „Welche Filiale ist die beste?“ hat weder Wert noch Aggregation — die beste nach Einheiten? nach Umsatz? nach Anzahl der Bestellungen? Kläre das, bevor du rechnest. Die drei Antworten können drei unterschiedliche Filialen sein.
2. Sieh dir die Eingabe vor dem Gruppieren an
Gruppieren blendet Zeilen aus. Sobald aus den acht Bestellungen drei Summen werden, ist eine fehlerhafte Zeile in einer Summe unsichtbar. Die Prüfungen finden daher vorher statt:
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectHier sind zwei Dinge wichtig. Die Wertespalte, quantity, ist int64 — numerisch, sodass sum addiert, anstatt Text zusammenzuhängen. Die Schlüsselspalte, branch, ist str, was für einen Schlüssel völlig in Ordnung ist.
Sieh dir dann die tatsächlichen Werte des Schlüssels an. Das ist die Prüfung, die die meisten überspringen, und sie ist die wichtigste:
print(orders["branch"].value_counts())
print(orders[["branch", "quantity"]].isna().sum())branch
north 4
south 2
east 2
Name: count, dtype: int64
branch 0
quantity 0
dtype: int64Drei unterschiedliche Filialen, keine fehlenden Werte in beiden Spalten. groupby bildet eine Gruppe pro eindeutigem Wert, Zeichen für Zeichen — "north" und "north " wären also zwei verschiedene Gruppen. In value_counts() würdest du das sehen, bevor daraus ein falscher Bericht wird.
Hier siehst du, was passiert, wenn diese Prüfung übersprungen wird. Drei Zeilen, die alle die Filiale north meinen — eine korrekt geschrieben, eine mit einem Leerzeichen am Ende, eine großgeschrieben:
messy = pd.DataFrame({"branch": ["north", "north ", "North"], "quantity": [12, 2, 30]})
print(messy.groupby("branch")["quantity"].sum())
messy["branch"] = messy["branch"].str.strip().str.lower()
print(messy.groupby("branch")["quantity"].sum())branch
North 30
north 12
north 2
Name: quantity, dtype: int64
branch
north 44
Name: quantity, dtype: int64Kein Fehler, und drei „Filialen“, wo eigentlich nur eine sein sollte. Die Zahl jeder Gruppe ist für die Eingaben korrekt, die pandas übergeben wurden — und der Bericht ist trotzdem falsch. Das Bereinigen des Schlüssels — Leerzeichen entfernen mit strip, in Kleinbuchstaben umwandeln — vor dem Gruppieren fasst sie zu einer einzigen Gruppe mit der wahren Gesamtsumme von 44 zusammen.
3. Skizziere die Ausgabe, bevor du sie erstellst
Entscheide, wie das Ergebnis aussehen soll, damit du merkst, wenn es das nicht tut:
branch units
east ?
north ?
south ?Aus den obigen Prüfungen weißt du bereits drei Dinge darüber: Es wird genau 3 Zeilen haben (eine pro eindeutiger Filiale), die Filialen erscheinen in alphabetischer Reihenfolge (groupby sortiert seine Schlüssel) und die drei Zahlen müssen in der Summe der Gesamtsumme der Spalte entsprechen. Letzteres ist danach dein Beweis.
4. Wähle das passende Werkzeug
- Eine Zahl pro Gruppe, aus einer Spalte:
df.groupby(key)[col].sum()(odermean,max, ...) - Mehrere Zahlen aus einer Spalte:
df.groupby(key)[col].agg(["sum", "mean"]) - Mehrere Zahlen aus mehreren Spalten, mit eigenen Namen:
df.groupby(key).agg(name=(col, "sum"), ...) - Wie viele Zeilen jede Gruppe hat:
df.groupby(key).size() - Eine Spalte, die noch nicht existiert, wie der Umsatz: erstelle sie zuerst, gruppiere danach
Jetzt zum Code.
Split, apply, combine
groupby führt drei Schritte nacheinander aus, und es lohnt sich, die Begriffe zu kennen, denn jede Gruppierungsfrage besteht aus diesen drei Schritten:
- Split (Aufteilen): Die Zeilen in Teilstücke zerlegen, eines pro eindeutigem Schlüsselwert
- Apply (Anwenden): Eine Berechnung separat auf jedes Teilstück anwenden
- Combine (Zusammenführen): Die Ergebnisse zu einem neuen Objekt zusammenfügen, indexiert nach dem Schlüssel
Du kannst dir das Aufteilen für sich allein ansehen. Über ein groupby-Objekt kann iteriert werden, was dir jeden Schlüssel und dessen Teilstück der Tabelle liefert:
for branch, part in orders.groupby("branch"):
print(branch, part.shape, list(part["order_id"]))east (2, 7) [1004, 1007]
north (4, 7) [1001, 1003, 1005, 1008]
south (2, 7) [1002, 1006]Jedes Teilstück ist ein gewöhnlicher DataFrame mit allen sieben Spalten — das Teilstück für north hat vier Zeilen, die beiden anderen jeweils zwei. Niemand musste die Filialnamen eintippen; pandas hat sie in den Daten gefunden. Eine Bestellung aus einer neuen Filiale west nächste Woche wäre einfach ein viertes Teilstück.
In echtem Code wirst du selten so eine Schleife schreiben. Sie steht hier, damit das Prinzip klar wird: Alles Folgende bedeutet „mache etwas mit jedem Teilstück und klebe die Antworten anschließend zusammen“.
Eine Zahl pro Gruppe
Die Frage „Einheiten pro Filiale“ in ihren drei Teilen — Schlüssel branch, Wert quantity, Aggregation sum — wird in genau dieser Reihenfolge geschrieben:
units = orders.groupby("branch")["quantity"].sum()
print(units)branch
east 27
north 48
south 6
Name: quantity, dtype: int64Lies es von links nach rechts: Gruppiere nach branch, nimm quantity, summiere es. Dieselben Zahlen wie in der Schleife, in einer einzigen Zeile, ganz ohne manuell getippte Liste von Filialen.
Prüfe es nun gegen die Skizze. Drei Zeilen — ja. Alphabetisch — ja. Und die Gegenprobe:
print(units.sum(), orders["quantity"].sum())81 81Die Gruppen ergeben zusammen das Ganze, es ging also keine Zeile verloren und keine wurde doppelt gezählt. Diese eine Zeile zur Kontrolle sollte zur Gewohnheit werden: Wann immer du gruppierst, vergleiche die Gesamtsumme des Ergebnisses mit der Gesamtsumme der Eingabe.
Das Ergebnis ist eine Series, und der Schlüssel ist ihr Index
print(type(units))
print(units.index)
print(units["north"])<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48Der Schritt Combine hat die Filialen in den Index verschoben, nicht in eine Spalte. Deshalb zeigt die Ausgabe branch in einer eigenen Zeile über den Werten — es ist der Name des Index. Das bedeutet auch, dass du einen Wert über den Filialnamen abfragst, units["north"], genau wie mit .loc in Kapitel vier.
Weil es eine normale Series ist, funktioniert alles aus den vorherigen Kapiteln darauf. Das Sortieren beantwortet „Welche Filiale hat am meisten verkauft?“:
print(units.sort_values(ascending=False))
print(units.idxmax())branch
north 48
east 27
south 6
Name: quantity, dtype: int64
northidxmax() gibt das Label des größten Werts zurück — die Filiale, nicht die Zahl. Für „Wer liegt an der Spitze?“ ist es meist genau das, was du suchst.
Warum du zuerst die Spalte auswählst
Es ist verlockend, das ["quantity"] wegzulassen und pandas einfach alles aufsummieren zu lassen:
print(orders.groupby("branch").sum())order_id date ... quantity price
branch ...
east 2011 2024-03-022024-03-04 ... 27 135.0
north 4017 2024-03-012024-03-022024-03-032024-03-04 ... 48 993.0
south 2008 2024-03-012024-03-03 ... 6 910.0
[3 rows x 6 columns]Kein Fehler — und fast jede Zahl darin ist Unsinn. order_id wurde aufaddiert, als wären Bestellnummern Mengen. Die date-Strings wurden aneinandergehängt, weil das „Summieren“ von Text bedeutet, ihn zusammenzufügen. price wurde ebenfalls summiert: Die 993.0 für north sind die zusammengezählten Preise von vier verschiedenen Produkten, was überhaupt nichts aussagt.
orders.groupby("branch").sum() löst keinen Fehler aus. Es addiert ID-Nummern, hängt Datumsangaben aneinander, summiert Einzelpreise und gibt das Ergebnis zurück, sodass es genau wie ein Bericht aussieht.Nur quantity ist sinnvoll, und du hättest ausschließlich danach fragen können. Aus diesem Grund lautet die Regel: Wähle die Wertespalte vor der Aggregation aus. sum führt das aus, was Addition für den jeweiligen Typ bedeutet — und für die meisten Spalten hat niemand nach einer Addition gefragt.
mean ist strenger und bricht ab, anstatt Unsinn zu produzieren:
print(orders.groupby("branch").mean())TypeError: dtype 'str' does not support operation 'mean'Dieser Fehler macht dasselbe Versehen sichtbar. Lies ihn so: Du hast mich gebeten, den Mittelwert einer Textspalte zu berechnen. Die Lösung ist nicht numeric_only=True — das würde immer noch den Durchschnitt von order_id und price bilden —, sondern klar anzugeben, welche Spalte du meinst.
Mehrere Zahlen auf einmal: .agg()
Eine einzelne Zusammenfassung reicht selten aus. „Wie viel hat jede Filiale verkauft, wie viel durchschnittlich pro Bestellung und über wie viele Bestellungen hinweg?“ sind drei Aggregationen derselben Spalte. Übergib eine Liste ihrer Namen an .agg():
print(orders.groupby("branch")["quantity"].agg(["sum", "mean", "count"]))sum mean count
branch
east 27 13.5 2
north 48 12.0 4
south 6 3.0 2Nun ist das Ergebnis ein DataFrame — eine Spalte pro Aggregation, eine Zeile pro Filiale. Er erzählt eine Geschichte, die die Gesamtsumme allein verborgen hat: east verkaufte knapp über die Hälfte der Einheiten von north, aber die durchschnittliche Bestellung ist größer. Die Filiale north erhält mehr Bestellungen; east erhält weniger, dafür etwas größere.
Die Namen in der Liste sind Strings: "sum", "mean", "count", "min", "max", "median", "nunique" (Anzahl eindeutiger Werte), "first", "last". Es sind dieselben Methoden, die du bereits auf einer einzelnen Spalte verwendet hast, angewendet auf jedes Teilstück.
Named Aggregation: Deine Spalten, deine Namen
Echte Berichte fassen unterschiedliche Spalten auf unterschiedliche Weise zusammen und verlangen lesbare Spaltennamen. Named Aggregation erledigt beides in einem einzigen Aufruf. Jedes Schlüsselwortargument ist der Name einer Ausgabespalte, und sein Wert ist ein Paar: (input column, aggregation).
Der Umsatz ist das naheliegendste Ergebnis für einen Bericht, liegt aber noch nicht als Spalte vor — er ergibt sich aus Menge mal Preis in jeder Zeile. Dem Erstellen von Spalten ist ein eigenes Kapitel gewidmet, das als Nächstes folgt; fürs Erste genügt eine einzelne Zeile, und sie zeigt Schritt 4 des Plans in der Praxis: Wenn die Wertespalte noch nicht existiert, erstelle sie vor dem Gruppieren.
orders["revenue"] = orders["quantity"] * orders["price"]
summary = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
print(summary)orders units revenue
branch
east 2 27 1140.0
north 4 48 2600.0
south 2 6 1150.0Das ist ein Bericht, den du direkt verschicken könntest. Jede Zeile des agg-Aufrufs liest sich wie ein Satz: orders ist die Anzahl von order_id, units ist die Summe von quantity, revenue ist die Summe von revenue. Die Ausgabespaltennamen bestimmst du selbst, anstatt Standardnamen wie sum und count zu übernehmen — ein nachträgliches Umbenennen entfällt.
Und das verändert die frühere Antwort. Nach Einheiten lag east klar auf Platz zwei; nach Umsatz zieht south knapp vorbei — eine Tasche zu 850 wiegt mehr als zwanzig Stifte. Genau deshalb bestand Schritt 1 darauf, den Wert exakt zu benennen: „Beste Filiale“ nach Einheiten und „beste Filiale“ nach Umsatz sind zwei völlig unterschiedliche Fragen.
Das Ergebnis ist ein DataFrame, lässt sich also genauso sortieren:
print(summary.sort_values("revenue", ascending=False))orders units revenue
branch
north 4 48 2600.0
south 2 6 1150.0
east 2 27 1140.0Zählen: size() im Vergleich zu count()
„Wie viele Bestellungen pro Filiale“ klingt nach einer einzigen Frage. Pandas hat zwei Antworten darauf, und sie weichen genau dann voneinander ab, wenn Daten fehlen.
size()zählt die Zeilen in jeder Gruppe, unabhängig davon, was darin steht.count()zählt die nicht fehlenden Werte einer Spalte in jeder Gruppe.
Bei einer fehlerfreien Datei stimmen beide überein. Um zu sehen, wie sie sich unterscheiden, erstellen wir neben orders eine kleine Tabelle von Hand mit einer fehlenden Filiale und einer fehlenden Menge:
gaps = pd.DataFrame(
{
"order_id": [1001, 1002, 1003, 1004, 1005],
"branch": ["north", "south", None, "east", "north"],
"quantity": [12, 5, 2, None, 30],
}
)
print(gaps)order_id branch quantity
0 1001 north 12.0
1 1002 south 5.0
2 1003 NaN 2.0
3 1004 east NaN
4 1005 north 30.0(quantity wurde wegen des fehlenden Werts zu einem Float — Kapitel sechs hat erklärt, warum.)
print(gaps.groupby("branch").size())
print(gaps.groupby("branch")["quantity"].count())branch
east 1
north 2
south 1
dtype: int64
branch
east 0
north 2
south 1
Name: quantity, dtype: int64east hat eine Bestellung (size = 1), aber null bekannte Mengen (count = 0). Beides stimmt; sie beantworten unterschiedliche Fragen. Verwende size() für „Wie viele Bestellungen gibt es?“ und count() für „Für wie viele Bestellungen liegt mir tatsächlich ein Wert vor?“.
Zwei Dinge, die fehlende Daten stillschweigend tun
Sieh dir diese Ausgaben noch einmal an. Bestellung 1003, diejenige ohne Filiale, taucht in keiner von beiden auf. Fünf Bestellungen gingen hinein; vier kamen heraus:
print(gaps.groupby("branch").size().sum(), len(gaps))4 5Standardmäßig verwirft groupby Zeilen, deren Schlüssel fehlt. Es gibt keine Gruppe namens „unknown“, daher verschwindet die Zeile aus jedem Ergebnis. Die Summenprüfung von vorhin deckt das sofort auf. Wenn diese Zeilen wichtig sind — und eine Bestellung ohne Filiale ist immer noch eine Bestellung —, fordere sie mit dropna=False an:
print(gaps.groupby("branch", dropna=False)["quantity"].sum())branch
east 0.0
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64Jetzt sind alle vier Gruppen vorhanden, und die NaN-Gruppe enthält die 2 Einheiten, die niemand zuordnen konnte.
Das zweite stillschweigende Verhalten steckt in derselben Ausgabe: east zeigt 0.0 an. Die Filiale east hat nicht null Einheiten verkauft; ihre einzige Menge ist schlicht unbekannt. sum behandelt „nichts zu addieren“ als Null. Wenn „unbekannt“ auch unbekannt bleiben soll, lege das mit min_count=1 fest — „bilde nur dann eine Summe, wenn mindestens ein echter Wert existiert“:
print(gaps.groupby("branch", dropna=False)["quantity"].sum(min_count=1))branch
east NaN
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64Was davon richtig ist, hängt von der Fragestellung ab — und genau darum geht es: Entscheide das im Planungsschritt, nicht durch Zufall. Die Lösung für beide Probleme liegt meist ohnehin früher — bereinige fehlende Werte (Kapitel sechs), bevor du gruppierst.
Zwei Schlüssel: Gruppen innerhalb von Gruppen
„Umsatz pro Filiale und Kategorie“ hat zwei Schlüssel. Übergib eine Liste:
by_both = orders.groupby(["branch", "category"])["revenue"].sum()
print(by_both)branch category
east accessories 840.0
stationery 300.0
north accessories 2180.0
stationery 420.0
south accessories 850.0
stationery 300.0
Name: revenue, dtype: float64Eine Zeile pro Kombination, die tatsächlich vorkommt — hier sind es sechs. Der Index hat nun zwei Stufen, branch und category; pandas nennt das einen MultiIndex und gibt die äußere Stufe nur einmal pro Block aus, damit es übersichtlich bleibt. Das leere Feld unter east bedeutet „immer noch east“.
Du kannst daraus genau so auswählen, wie du es erwarten würdest:
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])category
accessories 2180.0
stationery 420.0
Name: revenue, dtype: float64
420.0Ein einzelnes Label liefert dir alles von north; ein Tupel beider Labels liefert dir eine einzelne Zahl.
Normale Spalten zurückerhalten
Ein MultiIndex ist kompakt, aber viele Folgeschritte — das Speichern als CSV, das Filtern mit einer Maske oder das Zusammenführen mit einer anderen Tabelle (was später folgt) — fallen mit normalen Spalten leichter. Es gibt zwei Wege, und beide führen zur selben Tabelle:
flat = orders.groupby(["branch", "category"], as_index=False)["revenue"].sum()
print(flat)
print(flat.equals(by_both.reset_index()))branch category revenue
0 east accessories 840.0
1 east stationery 300.0
2 north accessories 2180.0
3 north stationery 420.0
4 south accessories 850.0
5 south stationery 300.0
Trueas_index=False weist groupby an, die Schlüssel gar nicht erst in den Index zu verschieben; reset_index() verschiebt sie nachträglich wieder heraus. Nutze das, was an deiner Stelle im Code besser lesbar ist.
Für Menschen umformen: unstack()
Für eine Person, die das Ergebnis liest, ist eine Kreuztabelle übersichtlicher als eine lange Liste — Filialen an der Seite, Kategorien oben quer:
print(by_both.unstack())category accessories stationery
branch
east 840.0 300.0
north 2180.0 420.0
south 850.0 300.0unstack() nimmt die innere Indexstufe, category, und wandelt ihre Werte in Spalten um. Die Zahlen sind exakt dieselben sechs wie zuvor. Hätte eine Filiale eine Kategorie nie verkauft, stünde in dieser Zelle NaN; unstack(fill_value=0) schreibt stattdessen 0 dorthin.
Die Falle des Durchschnitts von Durchschnitten
Eine verlockende Frage: Was kostet eine Einheit im Durchschnitt in jeder Filiale? Die schnelle Antwort wäre, den Mittelwert der Preisspalte zu bilden:
print(orders.groupby("branch")["price"].mean())branch
east 67.50
north 248.25
south 455.00
Name: price, dtype: float64Das ist der Durchschnitt der Preise auf den Bestellzeilen, und jede Zeile zählt einfach einmal — die einzelne Tasche der Filiale south (1 Einheit zu 850) wiegt genauso viel wie ihre Notizbücher (5 Einheiten zu 60). Kunden haben jedoch nicht je eine Zeile gekauft. Sie haben Einheiten gekauft. Der durchschnittliche Preis pro verkaufter Einheit ist der Gesamtumsatz geteilt durch die Gesamtzahl der Einheiten, und das muss aus Summen berechnet werden:
per_unit = orders.groupby("branch").agg(
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
per_unit["avg_unit_price"] = (per_unit["revenue"] / per_unit["units"]).round(2)
print(per_unit)units revenue avg_unit_price
branch
east 27 1140.0 42.22
north 48 2600.0 54.17
south 6 1150.0 191.67Der Wert für south fällt von 455 auf etwa 192. Keine der beiden Zahlen ist rein rechnerisch „falsch“; aber nur eine beantwortet die tatsächliche Frage. Die Regel lautet: Wenn das gewünschte Ergebnis ein Verhältnis ist, aggregiere Zähler und Nenner separat und teile erst danach. Wenn du den Durchschnitt von etwas bildest, das bereits ein Durchschnitt ist, oder von einem Preis, der bereits pro Einheit gilt, wird jede Zeile gleich gewichtet — egal, ob es sich um eine Einheit oder dreißig handelte.
Ein vollständiges Beispiel
branch_report.py beantwortet die Frage des Ladenbesitzers sauber — pro Filiale: Anzahl der Bestellungen, Einheiten, Umsatz, Anteil am Gesamtumsatz und das meistverkaufte Produkt nach Einheiten — und verifiziert seine eigenen Summen.
import pandas as pd
orders = pd.read_csv("orders.csv")
# 1. Check the input before grouping hides anything.
print("Rows, columns:", orders.shape)
print("Branches:", sorted(orders["branch"].unique()))
print("Missing in key/value:", int(orders[["branch", "quantity", "price"]].isna().sum().sum()))
print()
# 2. Create the value column that does not exist yet.
orders["revenue"] = orders["quantity"] * orders["price"]
# 3. One grouped call, one row per branch.
report = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
report["share_pct"] = (report["revenue"] / report["revenue"].sum() * 100).round(1)
# 4. Best product per branch: group by both keys, then keep each branch's top row.
by_product = orders.groupby(["branch", "product"], as_index=False)["quantity"].sum()
top = by_product.sort_values("quantity", ascending=False).drop_duplicates("branch")
report["top_product"] = top.set_index("branch")["product"]
report = report.sort_values("revenue", ascending=False)
print(report)
print()
# 5. Prove nothing was lost or double-counted.
print("Revenue check:", report["revenue"].sum(), "==", orders["revenue"].sum())
print("Orders check :", report["orders"].sum(), "==", len(orders))Rows, columns: (8, 7)
Branches: ['east', 'north', 'south']
Missing in key/value: 0
orders units revenue share_pct top_product
branch
north 4 48 2600.0 53.2 eraser
south 2 6 1150.0 23.5 notebook
east 2 27 1140.0 23.3 pen
Revenue check: 4890.0 == 4890.0
Orders check : 8 == 8Warum es so geschrieben ist:
- Die Prüfungen stehen an erster Stelle und werden ausgegeben. Drei eindeutige Filialen und null fehlende Werte sind die Bedingungen, unter denen der Bericht vertrauenswürdig ist. Wenn die Datei der nächsten Woche vier Filialen oder zwei fehlende Werte aufweist, siehst du das ganz oben, bevor du irgendeine Summe liest.
revenuewird vor dem Gruppieren berechnet. Das Aufsummieren vonquantity * pricepro Zeile und anschließendes Gruppieren liefert den tatsächlichen Umsatz. Eine aufsummierte Menge nachträglich mit einem gemittelten Preis zu multiplizieren, wäre erneut die Falle des Durchschnitts von Durchschnitten.- Ein einziger
agg-Aufruf, benannte Spalten. Jede Ausgabespalte wird einmal deklariert, mit dem Namen, den die Leser sehen werden. Eine vierte Kennzahl hinzuzufügen, erfordert lediglich eine weitere Zeile. share_pctwird auf dem Ergebnis berechnet, nicht auf den Eingabedaten. Nach dem Gruppieren istreportein ganz normaler DataFrame mit der Filiale als Index, sodass reguläre Spaltenarithmetik darauf funktioniert. Seine Prozentwerte summieren sich zu 100.0.- Das Top-Produkt nutzt eine Gruppierung nach zwei Schlüsseln mit anschließender Sortierung.
groupby(["branch", "product"])liefert die Einheiten pro Filiale-Produkt-Paar; das absteigende Sortieren nachquantityund Behalten der jeweils ersten Zeile pro Filiale (drop_duplicates("branch")) hinterlässt den Bestseller jeder Filiale. Die Zuweisung übertop.set_index("branch")["product"]richtet die Produkte am Filial-Label aus, nicht an der Zeilenposition — der Index sorgt für den korrekten Abgleich. - Die letzten beiden Zeilen sind die Gegenprobe. Der Umsatz und die Anzahl der Bestellungen der Gruppen stimmen mit denen der Eingabedaten überein. Wäre eine Filiale verloren gegangen oder hätte die Datei eine leere Filiale enthalten, würden die Zahlen voneinander abweichen und du wüsstest Bescheid, bevor der Bericht verschickt wird.
Wenn etwas schiefgeht
KeyError: 'Branch' Die Schlüsselspalte ist anders geschrieben als in der Datei — hier mit einem großen B. Spaltennamen beachten Groß- und Kleinschreibung (case-sensitive), und groupby prüft sie sofort. Gib list(orders.columns) aus und kopiere den Namen exakt; die Anführungszeichen in einer Liste machen auch nachgestellte Leerzeichen wie 'branch ' sichtbar.
KeyError: 'Column not found: Quantity' Derselbe Fehler, einen Schritt später: Der Schlüssel war in Ordnung, aber die Wertespalte in groupby("branch")["Quantity"] existiert nicht. Die Fehlermeldung enthält „Column not found“ statt nur den Namen, was dir verrät, dass die Auswahl nach dem groupby fehlgeschlagen ist.
KeyError: "Label(s) ['qty'] do not exist" Bei der Named Aggregation muss das erste Element jedes Paars eine reale Spalte sein: units=("qty", "sum") schlägt fehl, weil es kein qty gibt. Der Ausgabename auf der linken Seite (units) kann beliebig gewählt werden; die Eingabe auf der rechten Seite nicht.
ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. Du hast orders.groupby("branch")["quantity", "price"] geschrieben. Zwei Namen in einem einzelnen Klammerpaar ergeben ein Tupel. Um mehrere Spalten auszuwählen, brauchst du eine Liste — doppelte eckige Klammern, genau wie in Kapitel vier: orders.groupby("branch")[["quantity", "price"]].sum().
TypeError: dtype 'str' does not support operation 'mean' Eine Aggregation ist auf eine Textspalte gestoßen — entweder weil keine Spalte ausgewählt wurde (groupby("branch").mean()) oder weil die ausgewählte Spalte als Text eingelesen wurde. Im zweiten Fall zeigt info() den Typ str, wo du eine Zahl erwartet hast; die Lösung ist pd.to_numeric(..., errors="coerce") aus Kapitel sechs.
KeyError: 'quantity' beim Lesen des Ergebnisses Nach units = orders.groupby("branch")["quantity"].sum() sind die Filialen der Index von units und quantity ist lediglich der Name der Series. units["quantity"] sucht nach einer Filiale namens quantity. Verwende units["north"] oder units.sum() für die Gesamtsumme.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Was wird ausgegeben?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
print(orders.groupby("branch")["quantity"].sum())- A branch quantity 0 east 27 1 north 48 2 south 6
- B81
- Cbranch north 48 south 6 east 27 Name: quantity, dtype: int64
- Dbranch east 27 north 48 south 6 Name: quantity, dtype: int64
Das Ziel ist die Gesamtmenge und der Gesamtpreis pro Filiale. Was passiert?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
by_branch = orders.groupby("branch")["quantity", "price"].sum()- A`ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead.`
- BEs funktioniert und gibt eine Tabelle mit zwei Spalten zurück
- C`KeyError: ('quantity', 'price')`
- DEs gibt nur die quantity-Spalte zurück
Was ist der Unterschied zwischen size() und count() nach groupby?
- AEs gibt keinen; es sind zwei Namen für dieselbe Sache
- B`size()` misst den von jeder Gruppe belegten Speicherplatz
- C`size()` zählt Zeilen in jeder Gruppe, einschließlich fehlender Werte; `count()` zählt nur die nicht fehlenden Werte
- D`count()` zählt Zeilen; `size()` zählt eindeutige Werte
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Der Ladenbesitzer möchte nun dieselbe Art von Antwort nach Kategorie und nach Tag. Schreibe unter Verwendung von orders.csv ein Skript namens category_report.py, das genau diese vier Dinge tut:
- Die Datei einliest und
shape, die sortierte Liste der eindeutigen Kategorien sowie die Anzahl der fehlenden Werte incategory,quantityundpricezusammen ausgibt - Eine
revenue-Spalte erstellt und anschließend eine Tabelle mit einer Zeile pro Kategorie und den folgenden Spalten aufbaut, sortiert nach Umsatz (höchster zuerst):orders(Anzahl der Bestellungen),units(Gesamtmenge),revenue(Gesamtumsatz),avg_order_units(durchschnittliche Menge pro Bestellung) undavg_unit_price(Umsatz geteilt durch Einheiten, gerundet auf 2 Nachkommastellen) - Den Umsatz pro Tag ausgibt und danach den einzelnen Tag mit dem höchsten Umsatz
- Eine Kontrollzeile ausgibt: den Umsatz der Kategorientabelle, den der Tagestabelle und den der gesamten Datei
Plane vor dem Schreiben des Codes auf dem Papier: Benenne für die Bedingungen 2 und 3 jeweils Schlüssel, Wert und Aggregation für jede Spalte und notiere, wie viele Zeilen jedes Ergebnis haben muss.
Wenn alles stimmt, gibt das Programm genau Folgendes aus:
Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0Provoziere danach absichtlich Fehler und beobachte, was jeweils passiert:
- Ändere die Kategorie von Bestellung 1001 in
"Stationery"(großes S). Wie viele Zeilen hat deine Tabelle jetzt, und stimmt die Gegenprobe immer noch? - Ersetze
"sum"durch"mean"beiunits. Welche Zahl ändert sich und welche Frage wird nun beantwortet? - Berechne
avg_unit_pricestattdessen als Mittelwert vonprice. Welche Kategorie verändert sich am stärksten und warum?
Lösung
Zuerst planen.
- Bedingung 2: Schlüssel
category.orders= Anzahl vonorder_id;units= Summe vonquantity;revenue= Summe vonrevenue;avg_order_units= Mittelwert vonquantity;avg_unit_price= keine Aggregation einer einzelnen Spalte — es ist ein Verhältnis, wird also nachträglich ausrevenueundunitsberechnet. Die Datei hat zwei Kategorien, also hat die Tabelle 2 Zeilen. - Bedingung 3: Schlüssel
date, Wertrevenue, Aggregationsum. Vier eindeutige Datumsangaben, also 4 Zeilen; der beste Tag ist das Index-Label des Maximums, alsoidxmax(). - Die Gegenprobe: Der Umsatz beider Tabellen muss der Gesamtsumme der Datei von 4890.0 entsprechen.
category_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("Rows, columns:", orders.shape)
print("Categories:", sorted(orders["category"].unique()))
print("Missing:", int(orders[["category", "quantity", "price"]].isna().sum().sum()))
print()
orders["revenue"] = orders["quantity"] * orders["price"]
table = orders.groupby("category").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
avg_order_units=("quantity", "mean"),
)
table["avg_unit_price"] = (table["revenue"] / table["units"]).round(2)
table = table.sort_values("revenue", ascending=False)
print(table)
print()
daily = orders.groupby("date")["revenue"].sum()
print(daily)
print("Best day:", daily.idxmax(), "with", daily.max())
print()
print("Proof:", table["revenue"].sum(), daily.sum(), orders["revenue"].sum())Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0Zwei Zeilen und vier Zeilen, wie geplant, und alle drei Umsatzsummen stimmen überein. Beachte, was die Tabelle aussagt, was keine der Spalten allein verraten würde: Schreibwaren bewegen fast fünfmal so viele Einheiten, aber Zubehör bringt fast viermal so viel Geld ein.
Was die drei Fehlerfälle zeigen.
- Mit
"Stationery"in einer Zeile hat die Tabelle 3 Zeilen —Stationerybildet eine eigene Gruppe mit einer Bestellung. Die Gegenprobe stimmt weiterhin, weil keine Zeile verloren ging; sie wurde lediglich falsch zugeordnet. Genau deshalb wird die Zeile mit den eindeutigen Kategorien ganz oben ausgegeben: Sie würde nun drei Kategorien auflisten, und du würdest das Problem erkennen, bevor du eine einzige Zahl liest. - Mit
"mean"wirdunitszu 3.5 und 16.75 — identisch mitavg_order_units. Es beantwortet nun „Wie groß ist eine typische Bestellung?“, nicht „Wie viel haben wir verkauft?“. Auchavg_unit_priceändert sich, da der Umsatz durch den falschen Wert geteilt wird. - Die Bildung des Mittelwerts von
priceergibt für Zubehör(850 + 120 + 850 + 120) / 4 = 485.0, weit über dem tatsächlichen Wert von 276.43, weil die beiden Taschenbestellungen mit ein und zwei Einheiten genauso viel zählen wie die Flaschenbestellungen mit sieben und vier Einheiten. Bei Schreibwaren steigt der Wert von 15.22 auf 24.5 — weniger drastisch nur deshalb, weil alle Preise dort niedrig sind. Den Durchschnitt einer Pro-Einheit-Kennzahl über Bestellzeilen zu bilden, ist die Falle; Summen durcheinander zu teilen, ist die Lösung.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen