Groeperen en aggregeren — één antwoord per groep
Vragen als "per vestiging", "per categorie" en "per dag" omzetten in één regel groupby: de sleutel, de waarde en de aggregatie, meerdere getallen tegelijk met named agg, size tegenover count, twee sleutels en de valkuil van het gemiddelde van gemiddelden.
Hoe wil je leren?
Het probleem dat we oplossen
De eigenaar van de winkel stuurt je een kort berichtje: "Hoeveel stuks hebben we deze week in elke vestiging verkocht?"
Je hebt orders.csv — acht bestellingen, één rij per stuk. Met wat de afgelopen vier hoofdstukken je hebben geleerd, kun je dit al beantwoorden. Filter één vestiging, tel de kolom daarvan op en herhaal:
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 27De getallen kloppen. De methode heeft drie problemen, en elk daarvan wordt erger naarmate de data groeit.
Ten eerste: je moest de vestigingen vooraf weten. Je hebt de lijst zelf getypt. Wanneer het bestand van volgende week een bestelling van een nieuwe vestiging west bevat, ziet deze lus die niet — hij geeft geen foutmelding, maar laat west simpelweg weg, waardoor de totalen niet langer optellen tot het geheel.
Ten tweede: elke nieuwe vraag is een nieuwe lus. "Omzet per categorie" betekent een tweede lus over een tweede handmatig getypte lijst. "Aantal stuks per vestiging en categorie" betekent een lus binnen een lus.
Ten derde: het antwoord is geen tabel. Het zijn regels geprinte tekst. Je kunt het niet sorteren, filteren of naast een ander antwoord zetten zonder het opnieuw op te bouwen.
Wat je eigenlijk wilt, is in één regel zeggen: splits de rijen per vestiging, tel de hoeveelheid binnen elk stuk op en geef me de stukken terug als tabel. Dat is groupby, en dat is hoe vrijwel elke samenvatting in pandas wordt geschreven — een rapport, een getal op een dashboard, het eerste antwoord op "waar komt het geld vandaan?".
Aan het einde van dit hoofdstuk kun je
- Een vraag zoals "stuks per vestiging" omzetten in zijn drie delen — sleutel, waardekolom, aggregatie — en de sleutel controleren voordat je groepeert
df.groupby(key)[column].sum()schrijven en de Series die het teruggeeft uitlezen, met de sleutel als index- Meerdere samenvattingen tegelijk berekenen met
.agg()en met named aggregation - Het onderscheid maken tussen
size()encount(), en bepalen wat er gebeurt met ontbrekende sleutels en ontbrekende waarden - Groeperen op twee kolommen, het resultaat afvlakken of omvormen, en de fout van het "gemiddelde van gemiddelden" voorkomen
Voorkennis: Rijen sorteren.
Het bestand dat we gebruiken
Hetzelfde orders.csv als in de voorgaande hoofdstukken — acht bestellingen van een kleine winkel. Als je het nog niet hebt, maak het dan in je projectmap aan met precies deze regels:
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.0Eén rij is één bestelling: uit welke vestiging deze kwam, welk product, de categorie, hoeveel stuks en de prijs van één stuk.
Voordat je de code schrijft
Elke gegroepeerde vraag heeft hetzelfde skelet. Voordat je groupby typt, ontleed je de vraag.
1. Formuleer de vraag in drie delen
Een gegroepeerde vraag bevat altijd drie elementen, en de bewoording van de vraag vertelt je wat wat is:
- Sleutel — de kolom waarvan de waarden de groepen bepalen. Het is het woord na per, op basis van of voor elke. In "stuks per vestiging" is het
branch. - Waarde — de kolom die wordt samengevat: datgene wat wordt gemeten. Hier
quantity. - Aggregatie — hoe meerdere waarden één worden: totaal, gemiddelde, hoeveel, grootste. Hier
sum.
Nog een paar voorbeelden, zodat het patroon beklijft:
- "Omzet per categorie" — sleutel
category, waarderevenue, aggregatiesum - "Gemiddelde bestelgrootte in elke vestiging" — sleutel
branch, waardequantity, aggregatiemean - "Hoeveel bestellingen per dag" — sleutel
date, geen waardekolom (je telt rijen), aggregatiesize - "Grootste individuele bestelling per product" — sleutel
product, waardequantity, aggregatiemax
Als je niet alle drie de delen kunt benoemen, is de vraag nog niet klaar voor code. "Welke vestiging is het beste?" heeft geen waarde en geen aggregatie — beste qua aantal stuks? qua omzet? qua aantal bestellingen? Vraag het na voordat je begint te rekenen. De drie antwoorden kunnen immers drie verschillende vestigingen zijn.
2. Bekijk de invoer voordat je groepeert
Groeperen verbergt rijen. Zodra de acht bestellingen veranderen in drie totalen, is een beschadigde rij onzichtbaar geworden binnen een optelling. De controles doe je dus vooraf:
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: objectTwee dingen zijn hier van belang. De waardekolom, quantity, is int64 — numeriek, dus sum telt op in plaats van tekst aan elkaar te plakken. De sleutelkolom, branch, is str, wat prima is voor een sleutel.
Kijk vervolgens naar de daadwerkelijke waarden van de sleutel. Dit is de controle die mensen vaak overslaan, en het is juist de belangrijkste:
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: int64Drie verschillende vestigingen, niets ontbreekt in beide kolommen. groupby maakt één groep per unieke waarde, teken voor teken — dus "north" en "north " zouden twee aparte groepen zijn. value_counts() is waar je dat ziet, voordat het in een fout rapport terechtkomt.
Dit is wat er gebeurt wanneer je deze controle overslaat. Drie rijen die allemaal de vestiging north bedoelen — één correct getypt, één met een spatie aan het eind en één met een hoofdletter:
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: int64Geen foutmelding, en drie "vestigingen" waar er maar één hoort te zijn. Het getal van elke groep klopt voor wat pandas te verwerken kreeg, en toch is het rapport fout. Het opschonen van de sleutel — spaties weghalen met strip, omzetten naar kleine letters — vóór het groeperen maakt er één groep van met het werkelijke totaal van 44.
3. Schets de uitvoer voordat je deze maakt
Bepaal hoe het antwoord eruit moet zien, zodat je meteen ziet wanneer het niet klopt:
branch units
east ?
north ?
south ?Uit de bovenstaande controles weet je al drie feiten hierover: het zal precies 3 rijen hebben (één per unieke vestiging), de vestigingen verschijnen in alfabetische volgorde (groupby sorteert de sleutels) en de drie getallen moeten optellen tot het totaal van de hele kolom. Dat laatste is je bewijs achteraf.
4. Kies het juiste gereedschap
- Eén getal per groep, uit één kolom:
df.groupby(key)[col].sum()(ofmean,max, ...) - Meerdere getallen uit één kolom:
df.groupby(key)[col].agg(["sum", "mean"]) - Meerdere getallen uit meerdere kolommen, met je eigen namen:
df.groupby(key).agg(name=(col, "sum"), ...) - Hoeveel rijen elke groep heeft:
df.groupby(key).size() - Een kolom die nog niet bestaat, zoals omzet: maak deze eerst aan en groepeer daarna
Nu de code.
Split, apply, combine
groupby doet drie dingen achter elkaar, en het is de moeite waard om die namen te kennen, omdat elke gegroepeerde vraag uit deze drie stappen bestaat:
- Split de rijen in stukken, één per unieke sleutelwaarde
- Apply (pas toe): voer een berekening uit op elk stuk afzonderlijk
- Combine (combineer): voeg de resultaten samen in één nieuw object, met de sleutel als index
Je kunt de split-stap op zichzelf bekijken. Over een groupby-object kun je namelijk itereren met een lus, wat je elke sleutel en het bijbehorende stuk van de tabel oplevert:
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]Elk stuk is een gewoon DataFrame met alle zeven kolommen — het stuk van north heeft vier rijen, de andere twee hebben er twee. Niemand hoefde de vestigingsnamen in te typen; pandas vond ze zelf in de data. Een bestelling van een nieuwe vestiging west volgende week zou simpelweg een vierde stuk zijn.
Je zult in echte code zelden zo'n lus gebruiken. Het staat hier zodat het concept helder is: alles hieronder is "doe iets met elk stuk en plak de antwoorden vervolgens aan elkaar".
Eén getal per groep
De vraag "stuks per vestiging" in de drie delen — sleutel branch, waarde quantity, aggregatie sum — wordt in precies die volgorde geschreven:
units = orders.groupby("branch")["quantity"].sum()
print(units)branch
east 27
north 48
south 6
Name: quantity, dtype: int64Lees het van links naar rechts: groepeer op branch, neem quantity, tel het op. Dezelfde getallen als de lus, in één regel, zonder handmatig getypte lijst met vestigingen.
Vergelijk het nu met de schets. Drie rijen — ja. Alfabetisch — ja. En het bewijs:
print(units.sum(), orders["quantity"].sum())81 81De groepen tellen op tot het geheel, dus er is geen rij verloren gegaan en geen enkele rij is dubbel geteld. Het is de moeite waard om van deze ene controleregel een vaste gewoonte te maken: vergelijk telkens wanneer je groepeert het totaal van het resultaat met het totaal van de invoer.
Het resultaat is een Series en de sleutel is de index
print(type(units))
print(units.index)
print(units["north"])<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48De combine-stap heeft de vestigingen in de index geplaatst, niet in een kolom. Daarom staat in de uitvoer branch op een eigen regel boven de waarden — het is de naam van de index. Het betekent ook dat je een waarde opzoekt via de vestigingsnaam, units["north"], precies zoals je dat met .loc in hoofdstuk vier deed.
Omdat het een gewone Series is, werkt alles uit eerdere hoofdstukken erop. Sorteren geeft antwoord op "welke vestiging verkocht het meest":
print(units.sort_values(ascending=False))
print(units.idxmax())branch
north 48
east 27
south 6
Name: quantity, dtype: int64
northidxmax() geeft het label van de grootste waarde terug — de vestiging, niet het getal. Voor vragen als "wie staat er bovenaan" is dat meestal wat je wilt.
Waarom je eerst de kolom kiest
Het is verleidelijk om ["quantity"] over te slaan en pandas alles te laten optellen:
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]Geen foutmelding — en vrijwel elk getal erin is onzin. order_id is opgeteld, alsof bestelnummers bedragen waren. De date-strings zijn achter elkaar geplakt, omdat tekst "optellen" betekent dat je het samenvoegt. price is ook opgeteld: de 993.0 voor north is de som van de prijzen van vier verschillende producten bij elkaar opgeteld, wat helemaal niets meet.
orders.groupby("branch").sum() geeft geen foutmelding. Het telt ID-nummers op, plakt datums aan elkaar en berekent het totaal van stuksprijzen, en levert het resultaat af alsof het een echt rapport is.Alleen quantity heeft betekenis, en je had om alleen die kolom kunnen vragen. Dit is de reden voor de vuistregel kies de waardekolom vóór de aggregatie: sum doet wat optellen betekent voor elk gegevenstype, en voor de meeste kolommen is dat geen vraag die iemand ooit heeft gesteld.
mean is strenger en weigert liever dan onzin te produceren:
print(orders.groupby("branch").mean())TypeError: dtype 'str' does not support operation 'mean'Die foutmelding maakt dezelfde fout zichtbaar. Lees het als: je vroeg me het gemiddelde van een tekstkolom te berekenen. De oplossing is niet numeric_only=True — dat zou nog steeds het gemiddelde nemen van order_id en price — maar aangeven welke kolom je bedoelt.
Meerdere getallen tegelijk: .agg()
Eén samenvatting is zelden genoeg. "Hoeveel verkocht elke vestiging, gemiddeld per bestelling, en over hoeveel bestellingen?" zijn drie aggregaties van dezelfde kolom. Geef een lijst van hun namen door aan .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 2Nu is het resultaat een DataFrame — één kolom per aggregatie, één rij per vestiging. Het vertelt een verhaal dat het totaal alleen verborgen hield: east verkocht net iets meer dan de helft van het aantal stuks van north, maar de gemiddelde bestelling is groter. De vestiging north krijgt meer bestellingen; east krijgt minder, iets grotere bestellingen.
De namen in de lijst zijn strings: "sum", "mean", "count", "min", "max", "median", "nunique" (aantal unieke waarden), "first", "last". Het zijn dezelfde methoden die je op een enkele kolom hebt gebruikt, nu toegepast op elk stuk.
Named aggregation: jouw kolommen, jouw namen
Echte rapporten vatten verschillende kolommen op verschillende manieren samen en vereisen leesbare kolomnamen. Named aggregation doet beide in één aanroep. Elk trefwoordargument is de naam van een uitvoerkolom en de waarde ervan is een paar: (input column, aggregation).
Omzet is het meest voor de hand liggende om te rapporteren, en dat is nog geen kolom — het is quantity maal price op elke rij. Kolommen aanmaken krijgt een eigen hoofdstuk hierna; voor nu is één regel voldoende, en het toont stap 4 van het plan in actie: als de waardekolom nog niet bestaat, maak deze dan aan vóór het groeperen.
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.0Dit is een rapport dat je zo kunt versturen. Elke regel van de agg-aanroep leest als een zin: orders is de telling van order_id, units is de som van quantity, revenue is de som van revenue. De namen van de uitvoerkolommen heb je zelf gekozen en zijn niet geërfd als sum en count, waardoor je ze achteraf niet hoeft te hernoemen.
En het verandert het eerdere antwoord. Qua stuks stond east duidelijk op de tweede plaats; qua omzet schuift south er net voorbij — één tas van 850 weegt zwaarder dan twintig pennen. Daarom hamerde stap 1 op het benoemen van de waarde: "beste vestiging" op basis van stuks en "beste vestiging" op basis van omzet zijn twee verschillende vragen.
Het resultaat is een DataFrame, dus je sorteert het als zodanig:
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.0Tellen: size() vergeleken met count()
"Hoeveel bestellingen per vestiging" klinkt als één enkele vraag. Pandas heeft twee antwoorden, en die verschillen precies wanneer er data ontbreekt.
size()telt rijen in elke groep, ongeacht wat erin staat.count()telt niet-ontbrekende waarden van een kolom in elke groep.
In het schone bestand komen ze overeen. Om ze te zien verschillen, is hier een kleine tabel, handmatig opgebouwd naast orders, met één ontbrekende vestiging en één ontbrekend aantal:
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 veranderde in een float vanwege de ontbrekende waarde — hoofdstuk zes heeft uitgelegd waarom.)
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 heeft één bestelling (size = 1), maar nul bekende aantallen (count = 0). Beide zijn waar; ze beantwoorden verschillende vragen. Gebruik size() voor "hoeveel bestellingen" en count() voor "voor hoeveel bestellingen heb ik daadwerkelijk een getal".
Twee dingen die ontbrekende data stilletjes doet
Kijk nog eens naar die uitvoer. Bestelling 1003, die zonder vestiging, staat in geen van beide. Er gingen vijf bestellingen in; er kwamen er vier uit:
print(gaps.groupby("branch").size().sum(), len(gaps))4 5Standaard verwijdert groupby rijen waarvan de sleutel ontbreekt. Er is geen groep genaamd "onbekend", dus verdwijnt de rij uit elk resultaat. De totaalcontrole van daarnet vangt dit direct op. Als die rijen van belang zijn — en een bestelling zonder vestiging is nog steeds een bestelling — vraag er dan om met dropna=False:
print(gaps.groupby("branch", dropna=False)["quantity"].sum())branch
east 0.0
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64Nu zijn alle vier de groepen aanwezig, en de NaN-groep bevat de 2 stuks die niemand kon toewijzen.
Het tweede stilzwijgende gedrag zit in dezelfde uitvoer: east toont 0.0. De vestiging east heeft niet nul verkocht; het enige aantal is simpelweg onbekend. sum behandelt "niets om op te tellen" als nul. Als "onbekend" ook onbekend moet blijven, geef dat dan aan met min_count=1 — "bereken alleen een som als er ten minste één echte waarde bestaat":
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: float64Welke de juiste is, hangt af van de vraag, en dat is precies het punt: beslis dit tijdens de planningsstap, niet per toeval. De echte oplossing voor beide problemen ligt meestal toch al eerder — schoon de ontbrekende waarden op (hoofdstuk zes) voordat je groepeert.
Twee sleutels: groepen binnen groepen
"Omzet per vestiging en categorie" heeft twee sleutels. Geef een lijst mee:
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: float64Eén rij per combinatie die daadwerkelijk voorkomt — zes in dit geval. De index heeft nu twee niveaus, branch en category; pandas noemt dit een MultiIndex en print het buitenste niveau slechts één keer per blok om het leesbaar te houden. De lege ruimte onder east betekent "nog steeds east".
Je kunt eruit selecteren zoals je zou verwachten:
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])category
accessories 2180.0
stationery 420.0
Name: revenue, dtype: float64
420.0Eén label geeft je alles van north; een tuple van beide labels geeft je één enkel getal.
Weer gewone kolommen krijgen
Een MultiIndex is compact, maar veel vervolgstappen — opslaan naar CSV, filteren met een masker, combineren met een andere tabel (wat later aan bod komt) — zijn eenvoudiger met gewone kolommen. Er zijn twee manieren, en ze leveren dezelfde tabel op:
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 vertelt groupby om de sleutels om te beginnen al niet naar de index te verplaatsen; reset_index() verplaatst ze er achteraf weer uit. Gebruik de variant die in jouw situatie het duidelijkst leest.
Hervormen voor mensen: unstack()
Voor een mens die het resultaat leest, is een raster duidelijker dan een lange lijst — vestigingen aan de zijkant, categorieën bovenaan:
print(by_both.unstack())category accessories stationery
branch
east 840.0 300.0
north 2180.0 420.0
south 850.0 300.0unstack() neemt het binnenste indexniveau, category, en zet de waarden daarvan om in kolommen. De getallen zijn exact dezelfde zes als daarnet. Als een vestiging een categorie nooit had verkocht, zou die cel NaN zijn; unstack(fill_value=0) vult daar in plaats daarvan een 0 in.
De valkuil van het gemiddelde van gemiddelden
Een verleidelijke vraag: wat kost een artikel gemiddeld in elke vestiging? Het snelle antwoord is het gemiddelde van de kolom price berekenen:
print(orders.groupby("branch")["price"].mean())branch
east 67.50
north 248.25
south 455.00
Name: price, dtype: float64Dat is het gemiddelde van de prijzen op de bestelregels, en elke regel telt één keer mee — de ene tas van vestiging south (1 stuk à 850) weegt even zwaar als de schriften (5 stuks à 60). Maar klanten kochten niet één exemplaar van elke regel. Ze kochten stuks. De gemiddelde prijs per verkocht stuk is de totale omzet gedeeld door het totale aantal stuks, en die moet worden berekend uit de sommen:
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.67Het getal voor south daalt van 455 naar ongeveer 192. Rekenkundig is geen van beide getallen "fout"; er beantwoordt er maar één de vraag. De vuistregel: als datgene wat je wilt een verhouding is, aggregeer de teller en de noemer dan afzonderlijk en deel ze daarna. Het middelen van iets dat al een gemiddelde is, of een prijs die al per stuk geldt, weegt elke rij even zwaar mee, of het nu om één stuk ging of om dertig.
Een compleet voorbeeld
branch_report.py beantwoordt de vraag van de eigenaar zoals het hoort — per vestiging: aantal bestellingen, stuks, omzet, het aandeel in de totale omzet en het bestverkochte product op basis van stuks — en controleert de eigen totalen met een bewijs.
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 == 8Waarom het op deze manier is geschreven:
- De controles komen eerst en worden geprint. Drie verschillende vestigingen en nul ontbrekende waarden zijn de voorwaarden waaronder het rapport betrouwbaar is. Als het bestand van volgende week vier vestigingen of twee ontbrekende waarden telt, zie je dat direct bovenaan, nog voordat je ook maar één totaal bekijkt.
revenuewordt aangemaakt vóór het groeperen. Het optellen vanquantity * priceper rij en daarna groeperen levert de werkelijke omzet op. Een opgeteld aantal achteraf vermenigvuldigen met een gemiddelde prijs zou opnieuw de valkuil van het gemiddelde van gemiddelden zijn.- Eén
agg-aanroep, benoemde kolommen. Elke uitvoerkolom wordt één keer gedeclareerd, met de naam die de lezer te zien krijgt. Een vierde meetwaarde toevoegen is slechts één extra regel. share_pctwordt berekend op het resultaat, niet op de invoer. Na het groeperen isreporteen gewoon DataFrame met branch als index, dus gewone berekeningen op kolommen werken erop. De percentages tellen op tot 100.0.- Het bestverkochte product gebruikt groepering op twee sleutels en vervolgens een sortering.
groupby(["branch", "product"])geeft het aantal stuks per vestiging-product-paar; sorteren opquantityvan hoog naar laag en de eerste rij per vestiging behouden (drop_duplicates("branch")) houdt de bestseller van elke vestiging over. Doortop.set_index("branch")["product"]toe te wijzen worden de producten uitgelijnd op vestigingslabel en niet op positie — de index zorgt voor de juiste koppeling. - De laatste twee regels vormen het bewijs. De omzet en het aantal bestellingen van de groepen zijn gelijk aan die van de invoer. Als er een vestiging had ontbroken of het bestand een lege vestiging had bevat, zouden de getallen verschillen en wist je dat nog voordat het rapport de deur uitging.
Wanneer het misgaat
KeyError: 'Branch' De sleutelkolom is anders gespeld dan in het bestand — hier met een hoofdletter B. Kolomnamen zijn hoofdlettergevoelig en groupby controleert ze direct. Print list(orders.columns) en neem de naam exact over; de aanhalingstekens in een lijst maken ook spaties aan het einde zichtbaar, zoals 'branch '.
KeyError: 'Column not found: Quantity' Dezelfde fout, één stap later: de sleutel was in orde, maar de waardekolom in groupby("branch")["Quantity"] bestaat niet. De melding zegt "Column not found" in plaats van alleen de naam, wat je vertelt dat de selectie ná groupby is mislukt.
KeyError: "Label(s) ['qty'] do not exist" Bij named aggregation moet het eerste item van elk paar een echte kolom zijn: units=("qty", "sum") mislukt omdat er geen qty is. De uitvoernaam aan de linkerkant (units) mag van alles zijn; de invoer aan de rechterkant niet.
ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. Je hebt orders.groupby("branch")["quantity", "price"] geschreven. Twee namen binnen één paar haken vormen een tuple. Om meerdere kolommen te selecteren heb je een lijst nodig — dubbele haken, precies zoals in hoofdstuk vier: orders.groupby("branch")[["quantity", "price"]].sum().
TypeError: dtype 'str' does not support operation 'mean' Een aggregatie is bij een tekstkolom beland — ofwel omdat er geen kolom was geselecteerd (groupby("branch").mean()), ofwel omdat de gekozen kolom als tekst is binnengekomen. In het tweede geval toont info() een str waar je een getal verwachtte; pd.to_numeric(..., errors="coerce") uit hoofdstuk zes is de reparatie.
KeyError: 'quantity' bij het uitlezen van het resultaat Na units = orders.groupby("branch")["quantity"].sum() vormen de vestigingen de index van units en is quantity alleen de naam ervan. units["quantity"] zoekt naar een vestiging genaamd quantity. Gebruik units["north"], of units.sum() voor het totaal.
Stap 4 van 6 — Voorspellen
Test je begrip
Wat wordt er geprint?
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
Het doel is het totale aantal en de totale prijs per vestiging. Wat gebeurt er?
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.`
- BHet werkt en geeft een tabel met twee kolommen terug
- C`KeyError: ('quantity', 'price')`
- DHet geeft alleen de kolom quantity terug
Wat is het verschil tussen size() en count() na groupby?
- AEr is geen verschil; het zijn twee namen voor hetzelfde
- B`size()` meet het geheugengebruik van elke groep
- C`size()` telt de rijen in elke groep, inclusief ontbrekende waarden; `count()` telt alleen de niet-ontbrekende waarden
- D`count()` telt rijen; `size()` telt unieke waarden
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
De eigenaar wil nu hetzelfde soort antwoord per categorie en per dag. Schrijf met behulp van orders.csv het script category_report.py dat precies deze vier dingen doet:
- Het bestand inleest en
shape, de gesorteerde lijst van unieke categorieën en het aantal ontbrekende waarden incategory,quantityenpricesamen print - Een kolom
revenueaanmaakt en vervolgens één tabel opbouwt met één rij per categorie en de volgende kolommen, gesorteerd op omzet van hoog naar laag:orders(aantal bestellingen),units(totale hoeveelheid),revenue(totale omzet),avg_order_units(gemiddeld aantal stuks per bestelling) enavg_unit_price(omzet gedeeld door stuks, afgerond op 2 decimalen) - De omzet per dag print, en vervolgens de dag met de hoogste omzet
- Een controleregel met het bewijs print: de omzet van de categorietabel, van de dagtabel en van het hele bestand
Maak een plan op papier voordat je code schrijft: benoem voor de voorwaarden 2 en 3 de sleutel, de waarde en de aggregatie voor elke kolom, en noteer hoeveel rijen elk resultaat moet hebben.
Wanneer het klopt, print het programma precies dit:
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.0Laat het daarna met opzet misgaan en bekijk wat er telkens gebeurt:
- Verander de categorie van bestelling 1001 in
"Stationery"(met een hoofdletter S). Hoeveel rijen heeft je tabel nu, en slaagt het bewijs nog steeds? - Vervang
"sum"door"mean"voorunits. Welk getal verandert er, en welke vraag beantwoordt het nu? - Bereken
avg_unit_pricein plaats daarvan als het gemiddelde vanprice. Welke categorie verandert het meest, en waarom?
Oplossing
Eerst het plan.
- Voorwaarde 2: sleutel
category.orders= telling vanorder_id;units= som vanquantity;revenue= som vanrevenue;avg_order_units= gemiddelde vanquantity;avg_unit_price= geen aggregatie van één kolom — het is een verhouding, dus die wordt achteraf berekend uitrevenueenunits. Het bestand heeft twee categorieën, dus de tabel heeft 2 rijen. - Voorwaarde 3: sleutel
date, waarderevenue, aggregatiesum. Vier unieke datums, dus 4 rijen; de beste dag is het indexlabel van het maximum, en dat isidxmax(). - Het bewijs: de omzet van beide tabellen moet optellen tot de 4890.0 van het bestand.
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.0Twee rijen en vier rijen, zoals gepland, en alle drie de omzettotalen komen overeen. Merk op wat de tabel vertelt dat geen van beide kolommen op zichzelf zou laten zien: stationery verplaatst bijna vijf keer zoveel stuks, maar accessories levert bijna vier keer zoveel geld op.
Wat de drie experimenten laten zien.
- Met
"Stationery"op één rij heeft de tabel 3 rijen —Stationeryis een eigen groep met één bestelling. Het bewijs slaagt nog steeds, omdat er geen rij verloren is gegaan; hij is alleen verkeerd ingedeeld. Daarom wordt de regel met unieke categorieën bovenaan geprint: die zou nu drie categorieën vermelden, zodat je het probleem ziet voordat je ook maar één getal leest. - Met
"mean"wordtunits3.5 en 16.75 — hetzelfde alsavg_order_units. Het beantwoordt nu "hoe groot is een typische bestelling", niet "hoeveel hebben we verkocht".avg_unit_priceverandert ook, omdat het de omzet door de verkeerde waarde deelt. - Het middelen van
pricegeeft voor accessories(850 + 120 + 850 + 120) / 4 = 485.0, ver boven de werkelijke 276.43, omdat de twee tas-bestellingen van één en twee stuks even zwaar meetellen als de flessen-bestellingen van zeven en vier stuks. Stationery stijgt van 15.22 naar 24.5 — minder dramatisch, simpelweg omdat de prijzen daarvan allemaal laag zijn. Een getal dat al per stuk geldt middelen over bestelregels is de valkuil; de sommen delen is de oplossing.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken