Rijen filteren — elke rij dezelfde vraag stellen
Alleen de rijen behouden die ja antwoorden: boolean maskers, ze combineren met &, | en ~, isin, between en de .str-methoden, gefilterde rijen met .loc — en precies die rijen aanpassen zonder de chained assignment die stilletjes niets doet.
Hoe wil je leren?
Het probleem dat we oplossen
De winkelmanager stuurt een bericht: "Welke bestellingen van de vestiging north bevatten meer dan drie artikelen? Ik wil ze zien."
Bij de acht bestellingen uit het vorige hoofdstuk kon je dit nog met het blote oog zien. Ga met je vinger langs de kolom branch, stop bij elke north en kijk opzij naar quantity. Drie rijen. Klaar.
Stel je nu het echte bestand voor — twaalfduizend bestellingen, een heel jaar aan data. Met het blote oog lukt dat niet meer, en het eerste waar de meeste Python-programmeurs aan denken is een lus:
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]Het antwoord klopt. Maar kijk eens wat het heeft gekost: een teller, een lege lijst, een if, een append, en de tabel die rij voor rij uit elkaar wordt gehaald. En het resultaat is geen tabel meer — het is een kale lijst met bestelnummers. Om de manager ook de producten en prijzen te laten zien, zou je opnieuw een lus moeten schrijven. Bij twaalfduizend rijen is dit bovendien traag, omdat elke orders.iloc[i] een nieuwe Series bouwt om er slechts twee waarden uit te lezen.
Pandas heeft een andere manier om dit te vragen, en het is het meest voorkomende wat je ooit met een DataFrame zult doen: elke rij tegelijk dezelfde ja-of-nee-vraag stellen en vervolgens de rijen behouden die ja zeiden. Dat is filteren, en daarover gaat dit hele hoofdstuk. De lus hierboven wordt één enkele regel, het resultaat blijft een tabel, en het leest bijna net als de zin van de manager.
Aan het einde van dit hoofdstuk kun je
- Een vraag omzetten in een boolean masker — een kolom met
True/False, één per rij — en dit gebruiken om rijen te behouden, of om ze te tellen metmask.sum()enmask.mean() - Voorwaarden combineren met
&,|en~, en uitleggen waarom de haakjes verplicht zijn isin,betweenen de.str-methoden gebruiken voor lijsten, bereiken en tekstpatronen- Rijen filteren en kolommen selecteren in één stap met
.loc[mask, columns] - Waarden in de gefilterde rijen veilig aanpassen met
.loc[mask, column] = value, en deChainedAssignmentErrorherkennen die de onveilige manier in pandas 3 oplevert
Voorkennis: Kolommen en rijen selecteren.
Maak het bestand eerst aan
Dit hoofdstuk gebruikt hetzelfde orders.csv als het vorige. Als je het nog niet hebt, maak het dan aan in je projectmap met precies deze negen 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.0Voordat je de code schrijft
Een filter is een vraag geschreven in code. De meeste filterbugs zijn geen bugs in pandas — het zijn vragen die nooit scherp zijn gedefinieerd, of kolommen die niet bevatten wat je dacht. Voer daarom vóór de eerste filterregel deze vijf stappen uit.
1. Schrijf de vraag uit als voorwaarden, één per kolom. Haal de zin van de manager uit elkaar:
- "bestellingen van de vestiging north" → kolom
branch, test is gelijk aan, waarde"north" - "meer dan drie artikelen" → kolom
quantity, test groter dan, waarde3 - "Ik wil ze zien" → geen test; behoud de hele rij
Deze lijst dwingt je om twee beslissingen te nemen. "Meer dan drie" is > 3, niet >= 3 — een bestelling van precies drie artikelen telt niet mee. En de twee voorwaarden zijn verbonden door en: een bestelling moet aan beide voldoen. Die twee beslissingen vormen het hele filter; de code is alleen de spelling ervan.
2. Controleer de invoer — vorm (shape), types en de exacte waarden waarmee je gaat vergelijken.
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: objectquantity is int64, dus > 3 vergelijkt getallen. Als het was binnengekomen als str — omdat iemand in één cel "3 pcs" had getypt — zou de vergelijking mislukken met een TypeError, en dat wil je nu weten, niet halverwege. branch is str, dus dit wordt vergeleken als tekst, en een tekstvergelijking is exact: "north", "North" en "north " zijn drie verschillende waarden. Kijk dus naar de waarden die daadwerkelijk bestaan:
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strDrie vestigingen, elk op één manier gespeld, zonder verdwaalde spaties. Nu weet je zeker dat "north" in de code ergens mee overeenkomt.
3. Schets het resultaat voordat je het produceert. Beantwoord bij een klein bestand de vraag eerst met de hand. De bestellingen van vestiging north zijn 1001 (12 artikelen), 1003 (2), 1005 (30) en 1008 (4). Meer dan drie artikelen: 1001, 1005, 1008. Het verwachte resultaat is dus 3 rijen, alle 7 kolommen, en de bijbehorende bestelnummers zijn bekend. Als de code later 4 rijen of 0 rijen oplevert, weet je meteen dat de code fout is in plaats van erop te vertrouwen. Bij een groot bestand kun je dit niet allemaal met de hand doen, maar je kunt nog steeds een schatting maken: "vestiging north verwerkt ongeveer de helft van de bestellingen, dus het antwoord moet ruim onder de helft van het aantal rijen liggen."
4. Kies het juiste hulpmiddel op basis van de vorm van de voorwaarde.
- één vergelijking → een masker:
df["col"] > value - meerdere voorwaarden → maskers gecombineerd met
&(en),|(of),~(niet) - "een van deze waarden" →
df["col"].isin([...]) - "tussen twee waarden" →
df["col"].between(low, high) - een patroon in tekst →
df["col"].str.contains(...),.str.startswith(...) - rijen én slechts enkele kolommen →
df.loc[mask, ["col1", "col2"]] - waarden in die rijen wijzigen →
df.loc[mask, "col"] = new_value
De vraag van de manager bestaat uit twee vergelijkingen verbonden door en, dus: twee maskers en &.
5. Bepaal hoe je het antwoord gaat controleren. Na het filteren vangen drie goedkope controles bijna alles op: het aantal rijen komt overeen met je schets; de voorwaarde geldt echt voor elke overgebleven rij ((result["quantity"] > 3).all() moet True printen); en niets wat je verwachtte ontbreekt (zoek naar 1008 in het resultaat).
Nu de code — één idee tegelijk.
Een vergelijking levert een masker op
Vergelijk een kolom met een waarde, en pandas geeft je niet één enkele True of False. Het vergelijkt elke rij en geeft je een hele kolom met antwoorden:
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolDit is een Series van het type bool — een boolean masker. Het heeft dezelfde index als orders (0 tot en met 7), met één antwoord per rij: rij 0 heeft 12 artikelen, dus True; rij 1 heeft er 5, en 5 is niet groter dan 5, dus False.
Waarom werkt pandas op deze manier, in plaats van je een lus te laten gebruiken? Omdat een kolom wordt opgeslagen als één aaneengesloten blok getallen van hetzelfde type, en een heel blok vergelijken met 5 is één bewerking die in snelle gecompileerde code wordt uitgevoerd. Jouw lus vroeg Python om elke rij afzonderlijk te bekijken, acht keer — of twaalfduizend keer. Het masker vraagt het één keer.
Elke vergelijkingsoperator werkt op dezelfde manier: ==, !=, <, <=, >, >=. Ze werken ook op tekstkolommen — orders["branch"] == "north" is een masker met True in rijen 0, 2, 4 en 7.
Er is nog niets gefilterd. Een masker bevat alleen de antwoorden. De volgende stap is om ze te gebruiken.
Het masker behoudt rijen
Plaats het masker tussen vierkante haken, en pandas behoudt de rijen waarin het masker True is:
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.0Vier van de acht rijen. Lees de index aan de linkerkant: 0, 3, 4, 6. De overgebleven rijen behouden hun oorspronkelijke labels. Filteren hernummert niets — rij 3 is nog steeds rij 3, dezelfde bestelling 1004 die het in de volledige tabel was. Dat blijkt belangrijk te zijn, en we komen er nog op terug.
Hoe lijnt pandas het masker uit met de rijen? Niet op positie — maar op indexlabel. Label 0 van het masker beslist over rij 0, label 3 beslist over rij 3. Hier zijn de twee indexen identiek, omdat het masker is gemaakt op basis van orders zelf, dus dit valt niet op. Het wordt pas zichtbaar wanneer je een masker gebruikt dat op basis van een andere tabel is gemaakt — dat behandelen we in "Wanneer het misgaat".
Meestal zie je het masker inline geschreven, zonder aparte naam:
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Lees het van binnen naar buiten: `orders["branch"] == "north"` bouwt het masker, `orders[ … ]` behoudt de True-rijen. Het masker een naam geven (big = …) loont de moeite wanneer de voorwaarde lang is of wanneer je hem twee keer gebruikt; inline is prima voor korte voorwaarden.
Tellen met een masker, zonder te filteren
Vaak wil de manager niet de rijen hebben, maar alleen een getal: hoeveel bestellingen bevatten meer dan vijf artikelen? Daar hoef je niet voor te filteren. In berekeningen telt True als 1 en False als 0, dus:
print(big.sum())
print(big.mean())4
0.5sum() telt de enen bij elkaar op: 4 bestellingen. mean() deelt door het aantal rijen: 4 van de 8 is 0.5, dus de helft van de bestellingen bevatte meer dan vijf artikelen. "Hoeveel" is .sum() van een masker; "welk aandeel" is .mean() van een masker. Beide kosten één regel en geen van beide bouwt een nieuwe tabel.
En wanneer je een totaal wilt berekenen over de gefilterde rijen — het aantal artikelen in die grote bestellingen — filter je eerst, kies je daarna de kolom en bereken je vervolgens de som:
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69. Een kolom uit een gefilterde tabel op deze manier uitlezen is prima. Er op deze manier naar schrijven niet — dat behandelen we in de laatste sectie van dit hoofdstuk.
Meerdere voorwaarden: &, |, ~
De vraag van de manager bevat twee voorwaarden. Pandas voegt maskers samen met drie operatoren:
&betekent en — de rij wordt behouden wanneer beide maskersTruezijn|betekent of — de rij wordt behouden wanneer minstens één maskerTrueis~betekent niet — dit keert elkeTrueom naarFalseen omgekeerd
Hier is eindelijk de vraag van de manager:
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Drie rijen: 1001, 1005, 1008 — precies de schets uit de planningsstap. Nu de controle uit stap 5:
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
TrueHet aantal klopt, en beide voorwaarden gelden voor elke overgebleven rij. .all() vraagt "is elke waarde in dit masker True?" — het is de eenvoudigste manier om te bewijzen dat een filter deed wat je bedoelde.
Een of-vraag: bestellingen van vestiging east, of elke bestelling van iets dat 850 of meer kost.
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Vier rijen: de twee bestellingen van vestiging east, en de twee bestellingen van een tas uit north en south. Een rij die aan beide zou voldoen (hier geen enkele) verschijnt één keer, niet twee keer — een filter behoudt of verwijdert een rij immers alleen maar.
Een niet-vraag: alles behalve kantoorartikelen.
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0~ zet elke True om in False en omgekeerd. Voor een enkele gelijkheid kun je in plaats daarvan != schrijven, en dat is eenvoudiger; ~ bewijst zijn nut wanneer datgene wat je ontkent een langere voorwaarde is of een isin (dat zie je zo meteen).
Waarom de haakjes verplicht zijn
Elke voorwaarde hierboven staat tussen eigen haakjes. Dat is geen stijlkwestie. Haal ze weg en de code breekt:
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]Wat je hierboven ziet, is de laatste regel van de traceback, de regel die de fout noemt; de regels erboven geven alleen aan waar het gebeurde. De reden is operatorprecedentie — de volgorde waarin Python operatoren toepast. In Python bindt & sterker dan > en <. Dat is lang voor het ontstaan van pandas besloten, omdat & is ontworpen voor gehele getallen (bitwise "and"), waar het als eerste moet gebeuren. Python leest de regel dus als volgt:
orders["quantity"] > (3 & orders["price"]) < 100Het probeert eerst 3 & orders["price"] te berekenen — een bitwise "and" tussen het getal 3 en een kolom met decimale getallen — en dat is waar de foutmelding over gaat. De melding noemt rand_ (de "right-hand and") en een array, maar de oorzaak is altijd hetzelfde: een ontbrekend paar haakjes.
Een foutmelding is nog het gunstigste scenario. Zet je alleen om de eerste voorwaarde haakjes en vergeet je de tweede, dan treedt er helemaal geen fout op:
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4De eerste regel wordt gelezen als ((quantity > 3) & price) < 100. De & combineert een masker met de prijzen, en elk resultaat ligt toevallig onder de 100, waardoor het "filter" alle acht rijen behoudt. Het juiste filter behoudt er vier. Dezelfde kolommen, dezelfde getallen, één ontbrekend paar haakjes — en de verkeerde versie draait geruisloos door. Zet elke voorwaarde tussen eigen haakjes, elke keer weer. Dan kan noch de fout noch de geruisloze foute versie voorkomen.
Waarom niet and?
Het andere natuurlijke instinct is om het Engelse woord te schrijven:
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().Pythons and, or en not hebben één enkele True of False aan elke kant nodig — ze zijn gebouwd voor if-statements. Om and te gebruiken, vraagt Python aan het eerste masker "ben je waar?", en een kolom met acht antwoorden kan daar niet met één antwoord op reageren. Is het waar als minstens één waarde True is? Als ze dat allemaal zijn? Pandas weigert te gokken, en de foutmelding somt de manieren op waarop je zou kunnen beslissen (a.any(), a.all()). Maar je wilde niet één antwoord; je wilde "en" rij voor rij. Dat is &. De regel om te onthouden: binnen een filter worden and/or/not vervangen door &/|/~.
Een van meerdere waarden: isin
Bestellingen van de vestigingen south of east. Je zou twee gelijkheden kunnen schrijven die met | zijn verbonden. Bij vijf vestigingen worden dat vijf vergelijkingen. isin accepteert een lijst en vraagt "staat de waarde van deze rij in de lijst?":
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0En hier bewijst ~ zijn waarde — elke vestiging behalve north en east is een ~ vóór een isin:
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]Twee bestellingen, beide van vestiging south. Merk op dat ~orders["branch"].isin([...]) geen extra haakjes nodig heeft: ~ is van toepassing op het resultaat van de methode-aanroep, en er is geen & of | om mee te botsen. Zodra je het combineert met een andere voorwaarde, komen de haakjes terug: (~orders["branch"].isin([...])) & (orders["quantity"] > 3).
Een bereik: between
Bestellingen met een prijs van 15 tot 120. Dat zijn twee vergelijkingen, >= 15 en <= 120. between zegt het in één keer:
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0between sluit standaard beide grenzen in — de pennen van precies 15.0 en de flessen van precies 120.0 zitten erbij. Dat sluit aan bij hoe mensen meestal "van 15 tot 120" bedoelen, maar controleer dit altijd aan de hand van de vraag. Als de vraag strikt daartussen bedoelt, geef dat dan aan:
print(orders["price"].between(15, 120, inclusive="neither").sum())1Alleen het notitieboekje van 60.0 valt er strikt tussen. inclusive accepteert ook "left" en "right" voor halfopen bereiken — de gebruikelijke keuze voor prijscategorieën, waarbij je 0–100, 100–200 enzovoort wilt hebben zonder dat een grenswaarde in twee categorieën terechtkomt.
Tekstpatronen: de .str-methoden
Gelijkheid bij tekst is exact. De .str-accessor geeft een tekstkolom de stringmethoden die je kent uit Python, toegepast op elke rij, en elke methode retourneert een masker.
Producten waarvan de naam begint met "b":
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0Producten die de letter "o" bevatten, ongeacht hoofd- of kleine letters:
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0Er staat nergens een hoofdletter O in de data, en toch worden het notitieboekje en de twee flessen gevonden, omdat case=False het verschil tussen hoofd- en kleine letters negeert. Zonder die optie zou .str.contains("O") niets vinden.
Exacte gelijkheid heeft niet zo'n optie. Dit is de meest voorkomende "mijn filter geeft niets terug"-bug:
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []Geen foutmelding, geen rijen. De tabel is niet fout en pandas is niet fout — er is simpelweg geen vestiging gespeld als "North" met een hoofdletter. Daarom werd in stap 2 van het plan unique() geprint: de waarden waarmee je vergelijkt, moet je overnemen van wat er daadwerkelijk in de kolom staat, niet uit je hoofd typen.
Tekstdatums worden correct vergeleken — als ze ISO zijn
De kolom date kwam binnen als str. Kun je die filteren met >=? Bestellingen vanaf 3 maart:
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: boolHet werkt, maar begrijp goed waarom. Dit zijn strings, dus >= vergelijkt ze als tekst, teken voor teken, zoals een woordenboek geordend is. Dat geeft alleen het juiste antwoord omdat de datums zijn geschreven als jaar-maand-dag met voorloopnullen (2024-03-03), waardoor tekstvolgorde toevallig gelijk is aan tijdsvolgorde. Een kolom geschreven als 3/3/2024 zou verkeerd worden vergeleken — "10/1/2024" sorteert vóór "9/1/2024" omdat "1" vóór "9" komt. Zet voor alles wat verder gaat dan eenvoudige afkapgrenzen de gegevens om naar echte datums met pd.to_datetime, wat aan bod komt in het hoofdstuk over kolommen toevoegen.
Rijen en kolommen tegelijk: .loc[mask, columns]
De filters tot nu toe behielden elke kolom. De manager wilde de bestellingen van vestiging north, maar het rapport heeft alleen het product en het aantal nodig. Je hebt in het vorige hoofdstuk .loc[rows, columns] leren kennen met labels. Het rijen-gedeelte accepteert ook een masker:
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4Eén stap, één object, en de lezer van je code ziet de hele vraag — welke rijen, welke kolommen — op één plek. .loc[mask, "quantity"] (een enkele kolomnaam, geen lijst) levert in plaats daarvan een Series op, klaar voor berekeningen:
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48 artikelen verkocht door de vestiging north.
query(): hetzelfde filter als een zin
Er is een tweede schrijfwijze voor filters, query(), die de voorwaarde als een string accepteert:
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Dezelfde drie rijen. Binnen de string schrijf je gewone kolomnamen, en and/or/not zijn toegestaan, omdat query de string zelf ontleedt in plaats van deze door te geven aan de operatoren van Python. Om een Python-variabele te gebruiken, zet je er een @ voor:
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]query leest prettig bij lange voorwaarden. De nadelen: een typfout in de string ontdek je pas wanneer de regel wordt uitgevoerd, je editor kan je er niet bij helpen, en kolomnamen met spaties vereisen backticks. Deze cursus gebruikt maskers als het belangrijkste hulpmiddel, omdat maskers de basis vormen voor al het andere in pandas — .loc, toewijzingen, tellen. Zorg dat je query kent, zodat je het kunt lezen in de code van anderen.
De gefilterde tabel behoudt zijn oude labels
Terug naar de index. Sla de bestellingen van vestiging north op en bekijk ze:
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]Labels 0, 2, 4, 7. Wat is north.loc[1] dan? Er is geen label 1 in deze tabel — bestelling 1002 kwam uit de vestiging south en is weggefilterd:
print(north.loc[1])KeyError: 1.loc zoekt naar een label, en label 1 bestaat hier niet. Als je "de tweede bestelling van vestiging north" bedoelde, dan is dat een positie, en posities zijn voor .iloc:
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectDe tweede rij op positie is de bestelling voor de tas — en zijn Name is 2, zijn label uit de oorspronkelijke tabel. Dit is precies het onderscheid tussen loc/iloc uit het vorige hoofdstuk, en bij het filteren breekt dat je op, omdat labels en posities na een filter niet meer overeenkomen.
Het behouden van de labels is een bewuste keuze: hiermee kun je elke rij in een resultaat herleiden naar de oorspronkelijke tabel. Wanneer je dat niet meer nodig hebt — bijvoorbeeld als de gefilterde tabel het eindrapport is — hernummer je de tabel:
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0drop=True gooit de oude labels weg. Zonder dat zouden ze worden behouden als een nieuwe kolom genaamd index, wat zelden is wat je in een rapport wilt.
Een leeg resultaat is geen fout
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
TrueEr is geen vestiging west, dus zijn er geen rijen — en geen foutmelding. Dat is correct gedrag (nul is een geldig antwoord), maar het betekent dat een verkeerd gespeld filter geruisloos mislukt. Sommen over een leeg resultaat zijn 0 en gemiddelden zijn nan, wat ver kan doorsijpelen in een rapport voordat iemand het merkt. Wanneer een filter de invoer vormt voor iets belangrijks, controleer dan result.empty of het aantal rijen, en vermeld dit duidelijk in de uitvoer: "No orders from the west branch." is beter dan een lege tabel.
De gefilterde rijen wijzigen: .loc[mask, column] = value
Soms filter je om iets te wijzigen. De pennen die door vestiging east werden verkocht, waren verkeerd geprijsd; ze hadden 13.5 moeten zijn, niet 15.0. De verleidelijke code voert het filter uit en selecteert vervolgens de kolom — twee paar haken, precies zoals je eerder waarden uitlas:
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignmentLees de prijzen: er is niets veranderd. En pandas heeft je uitvoerig gewaarschuwd. (In een terminal gaat de waarschuwing naar stderr, dus verschijnt deze boven de lijst op het moment dat de toewijzingsregel wordt uitgevoerd, en begint deze met de bestandsnaam en het regelnummer. Hier wordt deze zonder die gegevens achter de uitvoer getoond.) Dit is chained assignment — twee indexeerstappen, orders[mask] en daarna ["price"], met de toewijzing aan het einde van de keten. De eerste stap, orders[mask], bouwt een nieuwe tabel met de rijen van vestiging east. De tweede stap stelt prijzen in die nieuwe tabel in. Vervolgens wordt de nieuwe tabel weggegooid, omdat niets ernaar verwijst. orders zelf is nooit aangeraakt.
In pandas 3 is dit gegarandeerd gedrag, genaamd Copy-on-Write: elke tabel die je via indexeren verkrijgt, gedraagt zich als een eigen kopie, zodat schrijven ernaar nooit kan teruggrijpen op het origineel. De ChainedAssignmentError is een waarschuwing, geen exception — het programma blijft draaien — en dat is precies waarom het gevaarlijk is: een script met deze regel eindigt "succesvol" terwijl de oude prijzen gewoon blijven staan.
(Als je oudere tutorials leest, zie je deze situatie beschreven met een SettingWithCopyWarning en de opmerking dat het "mogelijk wel of niet" werkt. In pandas 3 is het antwoord eenvoudig: het werkt nooit, dus schrijf het nooit zo.)
De oplossing is het eigen advies van de waarschuwing: één enkele indexeerstap, .loc[rows, column], met de toewijzing erop:
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]Wacht even — dat veranderde beide bestellingen van vestiging east, de pen én de fles. De fles was niet verkeerd geprijsd. De code deed precies wat er stond; de vraag in de code was verkeerd. Hier komt stap 1 van het plan weer terug: "pennen verkocht door vestiging east" bestaat uit twee voorwaarden, en er was er maar één opgeschreven. Begin opnieuw met een schone kopie van het bestand en schrijf ze allebei:
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5Twee gewoonten worden hier zichtbaar. Het masker krijgt een naam (fix) omdat het voor iets belangrijks wordt gebruikt; en vóór het schrijven geeft fix.sum() aan hoeveel rijen er op het punt staan te worden gewijzigd — één, wat overeenkomt met onze verwachting. Tel altijd voordat je overschrijft, elke keer weer.
Om te lezen isdf[mask]["col"]prima. Om te schrijven gebruik je altijddf.loc[mask, "col"] = value. De geketende vorm werpt geen exception op, en de wijziging verdwijnt geruisloos.
Een gefilterde tabel opslaan om mee te werken is prima
Hoe zit het hiermee?
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]Geen waarschuwing, en dat klopt. Hier was het de bedoeling om een aparte tabel te maken: je hebt hem de naam east gegeven, en het wijzigen ervan past alleen east aan. orders blijft onaangeroerd. Copy-on-Write maakt dit gedrag voorspelbaar — een tabel die je door filteren hebt verkregen, is altijd onafhankelijk van het origineel. De bug zit hem uitsluitend in de geketende vorm, waarbij de tussentijdse tabel geen naam heeft en de wijziging ermee verloren gaat.
Een compleet voorbeeld
De manager wil nu een kort rapport. report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.Waarom het zo is geschreven:
- Eerst controleren, dan vragen.
shapeen de lijst met vestigingen worden geprint vóór elk filter. Als het bestand was binnengekomen metNorthmet een hoofdletter, zou de vestigingenregel dat laten zien voordat het rapport een lege tabel print. .loc[mask, columns]voor de hoofdvraag. De rijen en de kolommen van het antwoord worden op één plek bepaald, enreset_index(drop=True)hernummert het resultaat omdat dit het eindrapport is, geen tussenstap.- Een benoemd masker, drie keer gebruikt.
accessorieswordt één keer berekend en vervolgens geteld (sum), gemeten (mean) en gebruikt om een kolom te selecteren. Door een masker een naam te geven voorkom je dat je dezelfde voorwaarde drie keer moet schrijven en dat er eentje net iets anders uitpakt. - Elke voorwaarde tussen eigen haakjes, zelfs bij de korte, zodat het later toevoegen van een derde voorwaarde niet per ongeluk de voorrangsfout kan veroorzaken.
- Het lege geval wordt expliciet afgehandeld. Nul rijen is een geldig antwoord, en het rapport verwoordt dat in tekst.
Wanneer het misgaat
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). Je hebt and, or of not gebruikt tussen twee maskers — of een if op een heel masker. De sleutelwoorden van Python hebben één enkele True/False nodig, en een masker heeft er één per rij. Schrijf binnen een filter &, |, ~. Bepaal binnen een if wat je bedoelt: if mask.any(): ("is minstens één rij waar?") of if mask.all(): ("zijn alle rijen waar?").
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (of 'ror_', of [int64]) Ontbrekende haakjes om een voorwaarde naast & of |. Python heeft & toegepast vóór >, waardoor het een bitwise "and" probeerde uit te voeren tussen een getal en een kolom. Zet elke voorwaarde tussen eigen haakjes: (a > 3) & (b < 100).
Het filter geeft een lege tabel terug, zonder foutmelding Je waarde komt in de kolom niet precies voor zoals getypt. Print df["col"].unique() — of beter nog: list(df["col"].unique()), wat elke waarde tussen aanhalingstekens toont, zodat een spatie aan het einde in 'north ' zichtbaar wordt. Corrigeer de data met df["col"].str.strip(), of zoek minder strikt met .str.contains(..., case=False).
KeyError: 1 (of een ander getal) direct na het filteren Je hebt .loc[n] gebruikt op een gefilterde tabel alsof deze nog steeds de labels 0, 1, 2… had. De tabel behoudt echter de oorspronkelijke labels. Gebruik .iloc[n] voor "de n-de rij", of gebruik eerst reset_index(drop=True) als de oude labels niet meer van pas komen.
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. Je hebt df[mask]["col"] = value geschreven. Dit wijzigt df in pandas 3 nooit — de waarschuwing vertelt je dat de regel niets heeft gedaan. Schrijf df.loc[mask, "col"] = value.
UserWarning: Boolean Series key will be reindexed to match DataFrame index. Het masker is gemaakt op basis van een andere tabel dan de tabel die je filtert — typisch een masker uit de volledige tabel orders dat wordt toegepast op het gefilterde north. Pandas lijnt het uit op label en laat labels die het niet kan vinden geruisloos vallen:
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(In een terminal verschijnt de waarschuwing boven de tabel.) Het levert hier toevallig een zinnig antwoord op, maar puur dankzij het geluk met de labels. Maak het masker op basis van de tabel die je filtert: north[north["quantity"] > 5].
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))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A5 5
- B4 8
- C4 4
- DTrue 4
Dit is bedoeld om bestellingen van de vestiging north met meer dan drie artikelen te behouden. Wat gebeurt er wanneer dit wordt uitgevoerd?
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))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- AEen `ValueError: The truth value of a Series is ambiguous`
- BHet levert een lege tabel op
- CHet werkt en behoudt bestellingen 1001, 1005 en 1008
- DEen `TypeError`: zonder haakjes wordt `&` toegepast vóór `==` en `>`
Je moet de prijs van elke tas in orders zelf halveren. Welke regel doet dat?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Bbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Corders["price"][orders["product"] == "bag"] /= 2
- Dorders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
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
Gebruik hetzelfde bestand orders.csv. Schrijf het programma filters.py dat vier vragen beantwoordt, elk geprint onder een korte kop:
- Hoeveel bestellingen waren voor 10 artikelen of meer? Print een getal, geen tabel.
- Bestellingen van accessoires buiten vestiging north — alleen
order_id,branch,productenprice, gehernummerd vanaf 0. - Bestellingen van vestiging south of east waarvan de prijs onder de 100 ligt. Gebruik
isinvoor de vestigingen. - Het hoofdkantoor geeft 10% korting op elke bestelling van kantoorartikelen van 20 artikelen of meer. Stel de
pricevan precies die rijen in op 90% van hun huidige prijs, maar print eerst hoeveel rijen je op het punt staat te wijzigen. Print vervolgensorder_id,quantityenpricevoor alle bestellingen van kantoorartikelen.
Plan vóórdat je code schrijft elke vraag op papier: welke kolommen, welke tests, waardoor verbonden, en welke rijen je verwacht door naar het bestand te kijken. Wanneer je programma klopt, print het precies dit:
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Laat het daarna met opzet crashen, één keer per situatie:
- Verwijder de haakjes in vraag 3. Welke fout treedt op?
- Schrijf vraag 4 als
orders[mask]["price"] = .... Wat print pandas, en is er een prijs veranderd? - Verander
"east"in"East"in vraag 3. Wat gebeurt er, en waarom is dat erger dan een foutmelding?
Oplossing
Plan eerst. Elke vraag uit elkaar gehaald:
quantity >= 10, één voorwaarde. Met het blote oog: 12, 30 en 20, dus 3.category == "accessories"enbranch != "north". Met het blote oog: 1004 (east, fles) en 1006 (south, tas).branchin south/east enprice < 100. Met het blote oog: 1002 (notitieboek, 60.0) en 1007 (pen, 15.0).category == "stationery"enquantity >= 20. Met het blote oog: 1005 (gum, 30) en 1007 (pen, 20), dus 2 rijen.
Vraag 4 vereist oplettendheid: "20 artikelen of meer" is >= 20, dus bestelling 1007, met precies 20 artikelen, hoort erbij. En er wordt geschreven, dus gebruikt het .loc[mask, "price"] met een benoemd masker, dat eerst geteld is.
filters.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Elk antwoord komt overeen met het plan. Toelichting bij de keuzes:
- Vraag 1 gebruikt de
sum()van een masker, nietlen(orders[...]). Beide leveren 3 op, maar het masker zegt direct "tel" en bouwt geen tabel. - Vraag 2 gebruikt
!=voor "buiten vestiging north".~(orders["branch"] == "north")betekent hetzelfde;!=is eenvoudiger voor één enkele waarde. - Vraag 3 behoudt de oorspronkelijke labels
1en6, omdat de opdracht niet vroeg om te hernummeren. Die labels vertellen je welke rijen uitordershet waren. - Vraag 4 telt voordat het schrijft, en de nieuwe waarde wordt berekend op basis van dezelfde rijen aan de rechterkant:
orders.loc[discount, "price"] * 0.9. Beide zijden gebruiken hetzelfde masker, waardoor elke rij 90% van zijn eigen prijs krijgt: 8.0 wordt 7.2, 15.0 wordt 13.5. De bestelling voor pennen1001(12 artikelen) en het notitieboek (5 artikelen) behouden hun prijs.
En de drie manieren waarop het misgaat:
- Zonder haakjes geeft vraag 3
TypeError: Cannot perform 'rand_' …, omdat&vóór<werd toegepast. orders[discount]["price"] = …print de waarschuwingChainedAssignmentError, het script draait door, en de uiteindelijke tabel toont nog steeds de oude prijzen8.0en15.0. De regel heeft niets gedaan.- Met
"East"verliest vraag 3 geruisloos de pen van vestiging east en print één rij in plaats van twee. Geen fout, geen waarschuwing: gewoon een verkeerd antwoord dat er geloofwaardig uitziet. Daarom noteert het plan de verwachte rijen vóórdat de code wordt uitgevoerd. Dit probleem wordt alleen opgemerkt door iemand die vooraf al wist dat het antwoord twee rijen moest bevatten.
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