Rijen sorteren — en beslissen wat er gebeurt bij gelijke waarden
Rijen op volgorde zetten met sort_values: één kolom of meerdere, elk in zijn eigen richting, ontbrekende waarden en tekst die op getallen lijkt — plus nlargest voor "top N", en waarom een gelijke stand op de selectiegrens een bewuste beslissing moet zijn en geen toeval.
Hoe wil je leren?
Het probleem dat we oplossen
De eigenaar van de winkel stuurt een kort berichtje: "Wat waren onze grootste bestellingen deze maand? Stuur me de top drie."
De data is het vertrouwde orders.csv — acht bestellingen uit de drie vestigingen van de winkel — north, south en east. Met acht rijen zou je het met het blote oog kunnen beantwoorden. Kijk omlaag in de kolom quantity, vind de 30, dan de 20, en dan de 12. Klaar.
Stel je nu het echte bestand voor: vierduizend bestellingen. Een kolom van boven naar beneden doorlezen om de drie grootste getallen te vinden is geen methode meer, maar ijdele hoop. En het volgende bericht van de eigenaar is al onderweg: "Kun je me de lijst gegroepeerd per vestiging sturen, met de grootste eerst binnen elke vestiging? En de nieuwste bestellingen bovenaan, alsjeblieft."
Al die verzoeken komen neer op dezelfde bewerking: zet de rijen in een bruikbare volgorde, en lees dan vanaf de bovenkant. Dat is sorteren, en in pandas doe je dat met één methode: sort_values().
Maar de methode zelf is het makkelijke deel. De echte vragen spelen eromheen, en dat is waar rapporten de mist in gaan:
- "Grootste" op basis waarvan? Aantal, prijs of het bedrag dat de bestelling heeft opgeleverd? Elk levert een andere top drie op.
- Wanneer twee bestellingen gelijk eindigen, welke komt dan eerst — en verandert dat antwoord elke keer dat je de code uitvoert?
- Wat gebeurt er met een bestelling waarvan het aantal leeg is?
- Als een kolom met getallen als tekst is ingelezen, sorteert
"9"na"30", zonder dat iets je waarschuwt.
Dit hoofdstuk leert je de methode en, nog belangrijker, die beslissingen.
Aan het einde van dit hoofdstuk kun je
- Een tabel sorteren op één of meerdere kolommen met
sort_values(), elk in zijn eigen richting, en het resultaat bewaren in plaats van het kwijt te raken - Uitleggen waarom indexlabels meereizen met hun rijen, en kiezen tussen ze behouden,
sort_index()enreset_index(drop=True) - Gelijke waarden doelbewust oplossen, en bepalen waar ontbrekende waarden terechtkomen met
na_position - "Top N"-vragen beantwoorden met
head(),nlargest()ennsmallest()— inclusief "top N van een deelverzameling" en een gelijke stand op de afkapgrens - Tekst sorteren ongeacht hoofdletters met
key=, en getallen die als tekst zijn opgeslagen herkennen aan de manier waarop ze sorteren
Voorkennis: Ontbrekende data — gaten vinden, verwijderen en invullen.
Maak het bestand eerst aan
Elk voorbeeld in dit hoofdstuk leest orders.csv in, hetzelfde bestand dat sinds hoofdstuk vier wordt gebruikt. Als je het nog niet hebt, maak het dan aan in je projectmap 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 sectie verderop gebruikt een tweede bestand, orders_gaps.csv. Het is dezelfde tabel met twee aantallen die leeg zijn gelaten — bestellingen 1002 en 1007 — het soort gat dat je in hoofdstuk zes hebt leren vinden:
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,,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,,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Voordat je de code schrijft
Sorteren lijkt te eenvoudig om vooraf te plannen. Dat is het niet, want een sortering mislukt nooit — die levert altijd een volgorde op. Als je op het verkeerde hebt gesorteerd, ziet het resultaat er net zo netjes uit als het juiste. Het denkwerk moet dus gebeuren vóór de code.
1. Schrijf de vraag op in één zin, met de kolom erin. "Grootste bestellingen" is geen vraag die pandas kan beantwoorden. "De drie bestellingen met de grootste quantity" wel. Net als "de drie bestellingen met de hoogste price". Ze geven verschillende antwoorden — de tas van 850 is het duurste artikel, maar er zijn maar twee tassen verkocht. Bepaal welke de eigenaar bedoelt. Als je het niet weet, vraag het dan; het kost één berichtje, en een verkeerde top drie kost vertrouwen.
2. Bekijk de invoer voordat je sorteert. Drie zaken zijn van belang voor een sortering: het aantal rijen, de types van de kolommen waarop je gaat sorteren, en of die kolommen lege plekken bevatten.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)
print(orders["quantity"].isna().sum())(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: object
0Acht rijen, zeven kolommen. quantity is int64 en price is float64, dus die sorteren als getallen. branch en product zijn str — tekst, wat alfabetisch sorteert. date is ook str. Dat is hier geen probleem, en de reden is goed om te weten: datums geschreven als YYYY-MM-DD sorteren als tekst correct, omdat het meest significante deel vooraan staat. Datums geschreven als 4/3/2024 zouden dat niet doen. En quantity heeft geen lege cellen, dus we hoeven nog niet te beslissen waar ontbrekende waarden terechtkomen.
3. Maak een schets van de uitvoer. Voor "top drie op aantal" moet het antwoord drie rijen zijn, grootste eerst: 30 (eraser, bestelling 1005), dan 20 (pen, 1007), dan 12 (pen, 1001). Door dat op te schrijven voordat je iets uitvoert, heb je iets om de code aan te toetsen.
4. Bepaal de details die een sortering nodig heeft.
- Sleutel — welke kolom, of kolommen, bepalen de volgorde? Dat wordt
by=. - Richting — kleinste eerst of grootste eerst, voor elke sleutel? Dat wordt
ascending=. - Gelijke waarden — als twee rijen dezelfde waarde hebben, welke komt dan eerst? Dat wordt een tweede sleutel, of
kind="stable". - Lege waarden — moeten ontbrekende waarden eerst of laatst komen? Dat wordt
na_position=. - Hoeveel — alle rijen, of alleen de top N? Dat wordt
.head(n), ofnlargest(). - Index — betekenen de oude rijlabels achteraf nog iets? Behoud ze, of gooi ze weg met
reset_index(drop=True).
5. Kies het hulpmiddel.
- Elke rij, op volgorde van een of meer kolommen:
sort_values() - Alleen de bovenste of onderste N volgens een numerieke kolom:
nlargest()/nsmallest() - De rijen terug in volgorde van de index:
sort_index() - De top N binnen een deelverzameling ("in de vestiging north"): eerst filteren, dan sorteren, dan
head()
6. Weet hoe je het controleert. Na een sortering moeten de eerste rijen overeenkomen met je schets, moet shape ongewijzigd zijn (een sortering voegt nooit rijen toe en verwijdert ze nooit), en bij een numerieke sleutel moet de kolom monotoon naar beneden lopen.
sort_values() — één kolom
Om de afdrukken compact te houden, tonen de voorbeelden vijf van de zeven kolommen. cols is gewoon een lijst met namen, gebruikt met de dubbele haken uit hoofdstuk vier.
cols = ["order_id", "branch", "product", "quantity", "price"]
by_qty = orders.sort_values("quantity")
print(by_qty[cols])order_id branch product quantity price
5 1006 south bag 1 850.0
2 1003 north bag 2 850.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
3 1004 east bottle 7 120.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0De standaardinstelling is oplopend — kleinste eerst. Lees de kolom quantity van boven naar beneden: 1, 2, 4, 5, 7, 12, 20, 30. Dat is de controle uit het plan: een numerieke sleutel mag alleen maar oplopen.
Kijk nu naar de linkerkant. De index is niet langer 0, 1, 2, …. Er staat 5, 2, 7, 1, 3, 0, 6, 4.
Waarom de index meereist met de rij
Pandas heeft de rijen niet hernummerd, en dat is opzettelijk. Een indexlabel is de naam van de rij, niet zijn positie. Label 5 was bestelling 1006 vóór de sortering en is nog steeds bestelling 1006 erna. Als sorteren de rijen zou hernummeren, zou elk label plotseling naar een andere bestelling wijzen, en alles wat je over "rij 5" had genoteerd zou stilletjes niet meer kloppen.
Na een sortering zijn er dus twee verschillende manieren om "de eerste rij" aan te duiden, en ze komen niet meer overeen:
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])1006
1001iloc[0] betekent positie nul — de eerste rij op de pagina, bestelling 1006. loc[0] betekent de rij met label 0 — bestelling 1001, die nu zesde staat. Hoofdstuk vier maakte dit onderscheid al; sorteren is de bewerking waarbij het echt uitmaakt. Als je "de bovenste rij na het sorteren" wilt, gebruik dan iloc of head(), die beide posities tellen.
Grootste eerst: ascending=False
print(orders.sort_values("quantity", ascending=False)[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0De top drie bestaat uit 1005, 1007 en 1001 — precies de schets uit het plan. Om alleen die drie te versturen, voeg je head(3) toe:
top3 = orders.sort_values("quantity", ascending=False).head(3)
print(top3[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0Als je de keten van links naar rechts leest, zie je het plan weer terug: neem orders, zet het grootste aantal vooraan, behoud de eerste drie. Dit patroon van "sorteren, dan head" beantwoordt de meeste "top N"-vragen.
Tekst sorteert alfabetisch
Een str-kolom sorteert in woordenboekvolgorde:
print(orders.sort_values("product")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
4 1005 north eraser 30 8.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0bag vóór bottle omdat a vóór o komt. Twee tassen, twee flessen en twee pennen — dit zijn gelijke waarden, en we komen zo terug op de vraag welke van zo'n gelijk paar als eerste komt.
Een sortering geeft een nieuwe tabel terug
Dit is de fout die bijna iedereen een keer maakt, en er verschijnt geen enkele foutmelding:
orders.sort_values("quantity", ascending=False)
print(orders[cols].head(3))order_id branch product quantity price
0 1001 north pen 12 15.0
1 1002 south notebook 5 60.0
2 1003 north bag 2 850.0Er is niets gesorteerd. sort_values() verandert orders niet; het bouwt en retourneert een nieuw DataFrame in de nieuwe volgorde. De eerste regel maakte die gesorteerde tabel aan en gooide hem meteen weg, omdat niemand hem opsloeg. orders is nog precies wat read_csv heeft opgeleverd.
Waarom werkt pandas zo? Omdat de oorspronkelijke volgorde ook informatie is. In orders.csv is het de volgorde waarin de bestellingen zijn binnengekomen. Als elke sortering je enige exemplaar zou herschikken, zou je nooit meer terug kunnen naar "hoe het bestand was", en een sortering geschreven voor het ene rapport zou stilletjes de invoer voor het volgende rapport veranderen. Doordat een nieuw object wordt teruggegeven, is sorteren altijd veilig om uit te proberen.
sort_values() verandert nooit de tabel waarop je het aanroept. Als je het resultaat niet toewijst, heeft de sortering wel plaatsgevonden maar is deze weggegooid — en niets waarschuwt je.Je moet het resultaat dus bewaren, hetzij onder een nieuwe naam of weer onder dezelfde naam:
by_qty_desc = orders.sort_values("quantity", ascending=False) # keep both
print(by_qty_desc["order_id"].head(3).tolist())
print(orders["order_id"].head(3).tolist())[1005, 1007, 1001]
[1001, 1002, 1003]De gesorteerde kopie heeft de grote bestellingen bovenaan; het origineel is onaangeroerd gebleven. orders = orders.sort_values(...) schrijven is ook prima wanneer je zeker weet dat je de oude volgorde niet meer nodig hebt.
In oudere code kom je nog wel eens inplace=True tegen. Dit wijzigt de tabel ter plekke en geeft None terug, wat tot een eigen bug leidt (zie Wanneer het misgaat). Met Copy-on-Write in pandas 3 bespaart inplace=True ook geen geheugen meer. Het resultaat toewijzen is duidelijker: je ziet meteen op de regel waar de gesorteerde tabel naartoe gaat.
Meerdere kolommen: prioriteit en richting
Het tweede verzoek van de eigenaar: "gegroepeerd per vestiging, met de grootste eerst binnen elke vestiging." Dat zijn twee sleutels, en de volgorde van de lijst bepaalt de prioriteit:
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])
print(by_branch[cols])order_id branch product quantity price
6 1007 east pen 20 15.0
3 1004 east bottle 7 120.0
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
1 1002 south notebook 5 60.0
5 1006 south bag 1 850.0Lees het zoals pandas het heeft opgebouwd. Eerst wordt elke rij geplaatst op basis van branch, van A tot Z: east, north, south. De tweede sleutel, quantity, wordt alleen geraadpleegd wanneer de eerste sleutel gelijk is — binnen east, binnen north, binnen south. Binnen north loopt het 30, 12, 4, 2: grootste eerst, omdat het tweede element van ascending gelijk is aan False.
ascending accepteert één True/False per sleutel, in dezelfde volgorde als by. Eén enkele False zou gelden voor beide sleutels en south vooraan zetten.
Het omdraaien van de lijst verandert de vraag volledig:
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0Nu geeft aantal de doorslag, en zou branch er alleen toe doen als twee bestellingen hetzelfde aantal hadden — wat hier bij geen enkele bestelling het geval is. De vestigingen liggen verspreid. De vraag die je moet stellen voordat je de lijst schrijft is dus: "waarop is het rapport geordend?" Die kolom komt als eerste.
Gelijke waarden: maak de volgorde een beslissing, geen toeval
Sorteer op price, duurste eerst:
print(orders.sort_values("price", ascending=False)[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
7 1008 north bottle 4 120.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0Kijk naar de twee flessen van 120. In het bestand komt bestelling 1004 vóór 1008. In de gesorteerde tabel komt 1008 als eerste. Er is niets stuk: bij één enkele sleutel belooft het standaardsorteeralgoritme van pandas (quicksort) niet dat rijen met gelijke waarden hun oorspronkelijke volgorde behouden, en hier gebeurde dat dan ook niet. De twee tassen van 850 bleven in bestandsvolgorde, de twee flessen zijn omgewisseld — er is geen regel die je had kunnen voorspellen.
Meestal maakt dat niet uit. Het begint pas uit te maken op het moment dat je de lijst afkapt. Als het rapport vraagt om "de drie duurste bestellingen", is de derde plaats een gelijke stand tussen 1004 en 1008, en welke van de twee op de lijst belandt, werd bepaald door het algoritme, niet door jou. Voer dezelfde code uit op een net iets ander bestand en het antwoord kan veranderen.
Er zijn twee manieren om die beslissing weer in eigen hand te nemen.
Optie 1: een stabiele sortering. kind="stable" belooft dat gelijke rijen de volgorde behouden die ze vóór het sorteren hadden — hier de bestandsvolgorde:
stable = orders.sort_values("price", ascending=False, kind="stable")
print(stable["order_id"].tolist())[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]Optie 2 (meestal beter): een expliciete tiebreaker. Bepaal wat de doorslag moet geven bij een gelijke stand. Voor een verkooprapport is "bij gelijke prijs de grootste bestelling eerst" een verstandige regel:
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])
print(tie_broken[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
4 1005 north eraser 30 8.0Kijk opnieuw naar de flessen: 1004 staat boven 1008, omdat 7 wint van 4. En de twee pennen van 15: voorheen kwam 1001 als eerste; nu wint 1007, omdat 20 wint van 12 — en die zal altijd winnen, op elke machine, in welke volgorde het bestand ook binnenkomt. Een tiebreaker verandert een toevalligheid in een regel die je aan de eigenaar kunt uitleggen. (Wanneer je op meer dan één kolom sorteert, is de sortering van pandas sowieso stabiel, dus een lijst met sleutels husselt echte gelijke waarden ook nooit door elkaar.)
Als je geen zakelijke regel hebt, is order_id een prima laatste sleutel: deze is uniek, dus daarna blijven er gegarandeerd geen gelijke waarden meer over.
Ontbrekende waarden: na_position
Lees de versie van het bestand in met twee lege aantallen:
gaps = pd.read_csv("orders_gaps.csv")
print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])2
order_id branch product quantity price
4 1005 north eraser 30.0 8.0
0 1001 north pen 12.0 15.0
3 1004 east bottle 7.0 120.0
7 1008 north bottle 4.0 120.0
2 1003 north bag 2.0 850.0
5 1006 south bag 1.0 850.0
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0Twee dingen vallen op. Ten eerste wordt quantity nu afgedrukt als 30.0, 12.0 — de lege plekken veranderden de kolom in float64, zoals hoofdstuk zes uitlegde, omdat NaN een float is. Ten tweede kwamen de rijen met NaN onderaan te staan, ook al vroegen we om de grootste eerst. Dat is de standaardinstelling, na_position="last", en dat geldt in beide richtingen: een ontbrekende waarde is niet "klein" of "groot", maar onbekend, dus pandas zet deze apart in plaats van te gokken.
Let op wat dit met de top drie deed. Bestelling 1007 verkocht in werkelijkheid 20 pennen, maar het aantal is leeg in dit bestand, waardoor deze uit de top drie viel en 1004 zijn plaats innam. Een sortering kan ontbrekende data niet herstellen; die kan je alleen laten zien waar de gaten zijn gebleven. Daarom telt stap 2 van het plan de lege cellen vóór het sorteren.
Wanneer je data controleert, wil je vaak het tegenovergestelde — de problemen bovenaan, waar je ze direct ziet:
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))order_id branch product quantity price
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0
5 1006 south bag 1.0 850.0na_position is dus geen opmaakkeuze. "last" past bij een rapport, waar lege waarden de echte waarden niet mogen verdringen. "first" past bij een controle, waar lege waarden precies zijn waar je naar op zoek bent.
Terug naar het begin: sort_index() en reset_index()
Doordat elke rij zijn label heeft behouden, gaat de oorspronkelijke volgorde nooit verloren. Sorteren op de index brengt deze weer terug:
restored = by_qty.sort_index()
print(restored["order_id"].tolist())[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]sort_index() accepteert ook ascending=False, waarmee je een tabel kunt weergeven met het "nieuwste label eerst".
Soms betekenen de oude labels niets meer. Als je sorteert om een ranglijst te maken en door te sturen, zijn de labels 4, 6, 0 aan de zijkant alleen maar verwarrend. reset_index(drop=True) gooit ze weg en nummert de rijen 0, 1, 2, … in hun nieuwe volgorde:
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)
print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])order_id branch product quantity price
0 1005 north eraser 30 8.0
1 1007 east pen 20 15.0
2 1001 north pen 12 15.0
1005Nu komen loc[0] en iloc[0] weer met elkaar overeen: beide zijn bestelling 1005, de grootste. Zonder drop=True zou pandas de oude labels behouden als een nieuwe kolom genaamd index — handig als je rijen wilt kunnen terugvinden, maar overbodige ballast als je dat niet hoeft.
Welke moet je kiezen? Behoud de labels zolang je nog met de data werkt en rijen wellicht nog moet koppelen aan het origineel. Reset ze wanneer de volgorde zelf het eindresultaat is — een rangschikking of een top-tienlijst.
De kortere weg voor "top N": nlargest() en nsmallest()
"Top drie op aantal" komt zo vaak voor dat pandas een methode heeft die precies dat uitdrukt:
print(orders.nlargest(3, "quantity")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0Dezelfde rijen als sort_values("quantity", ascending=False).head(3). Waarom zou je hier de voorkeur aan geven? De regel benoemt de bedoeling — "de drie grootste" — in plaats van het mechanisme. En bij een grote tabel is het sneller, omdat het niet alle rijen op volgorde hoeft te zetten om er slechts drie te bewaren.
nsmallest() is het spiegelbeeld: de goedkoopste drie artikelen in de winkel:
print(orders.nsmallest(3, "price")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0Gelijke waarden op de grens: keep=
Vraag om de drie duurste artikelen en er gebeurt iets ongemakkelijks:
print(orders.nlargest(3, "price")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0De derde plaats is 120 — maar twee bestellingen kosten 120: 1004 en 1008. pandas gaf er één van terug — met de standaard keep="first" degene die als eerste in de tabel staat — en liet de andere stilletjes weg. Als je een prijs uitreikt aan "de top drie", is dat oneerlijk tegenover 1008. keep="all" behoudt elke rij die gelijk eindigt op de selectiegrens, zelfs als dat meer dan N rijen betekent:
print(orders.nlargest(3, "price", keep="all")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0Vier rijen voor een "top drie". Dat is geen bug: het is het eerlijke antwoord wanneer de derde plaats wordt gedeeld. Of je dat wilt, is wederom een beslissing in je planning.
nlargest en nsmallest werken alleen op numerieke kolommen. Gebruik voor tekst sort_values en head.
Top N van een deelverzameling: eerst filteren, dan sorteren, dan afkappen
"Wat waren de twee grootste bestellingen in de vestiging north?" Dit combineert hoofdstuk vijf met dit hoofdstuk, en de volgorde van de stappen maakt uit:
north = orders[orders["branch"] == "north"]
top_north = north.sort_values("quantity", ascending=False).head(2)
print(top_north[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0Filter eerst, want de vraag gaat alleen over de vestiging north. Als je de hele tabel zou sorteren en eerst head(2) zou nemen, zou je 1005 en 1007 krijgen — en 1007 hoort bij de vestiging east. Als je daarna pas filtert, blijft er nog maar één rij over, en rapporteer je een "top twee" met slechts één vermelding. De regel: beperk eerst tot de rijen waar de vraag over gaat, rangschik ze dan pas, en kap daarna af.
Dezelfde keten werkt met nlargest: orders[orders["branch"] == "north"].nlargest(2, "quantity").
Tekst sorteren zonder op hoofdletters te letten: key=
Bij het sorteren van tekst worden tekens vergeleken op basis van hun karaktercode, en elke hoofdletter komt vóór elke kleine letter. Gegevens die door mensen zijn ingetypt bevatten vaak een mix van beide:
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})
print(names.sort_values("product"))product
3 Bottle
1 Notebook
2 bag
4 eraser
0 penBottle en Notebook springen vóór bag enkel en alleen omdat ze met een hoofdletter beginnen. Geen enkele lezer zou dat alfabetisch noemen.
Je zou de data kunnen opschonen, maar vaak moet je dat juist niet doen — de schrijfwijze kan precies zijn zoals de klant het heeft ingevoerd. Met het argument key= kun je sorteren op een getransformeerde versie van de kolom, terwijl de oorspronkelijke waarden behouden blijven. Het verwacht een functie die de hele kolom ontvangt en de versie teruggeeft waarop gesorteerd moet worden:
print(names.sort_values("product", key=lambda col: col.str.lower()))product
2 bag
3 Bottle
4 eraser
1 Notebook
0 penDe kopie in kleine letters wordt alleen gebruikt voor het vergelijken en daarna weggegooid. De tabel toont Bottle en Notebook nog steeds precies zoals ze zijn ingetypt. (lambda col: col.str.lower() is een functie van één regel: gegeven de kolom col, geef deze terug in kleine letters.)
Wanneer getallen als tekst zijn opgeslagen
Hier is de valkuil waarvoor de planningsstap over dtypes bedoeld is. Stel dat quantity als tekst binnenkwam — een verdwaalde "N/A" in het bestand, of een systeem dat alles als strings exporteert. Om te zien wat dat doet, maken we met opzet zo'n kolom aan met astype:
as_text = orders.astype({"quantity": "str"})
print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])str
order_id quantity
5 1006 1
0 1001 12
2 1003 2
6 1007 20
4 1005 30
7 1008 4
1 1002 5
3 1004 71, 12, 2, 20, 30, 4, 5, 7. Geen foutmelding, en een volgorde die bijna juist lijkt. Tekst wordt teken voor teken vergeleken: "12" begint met een 1, wat kleiner is dan 2, dus komt "12" vóór "2" — net zoals "ab" in een woordenboek vóór "b" komt. De sortering deed precies wat er werd opgedragen. Er werd alleen het verkeerde opgedragen.
Twee aanwijzingen verraden het: quantity is links uitgelijnd in de afdruk, de manier waarop pandas tekst toont, en dtype vermeldt str. De echte oplossing is om de kolom om te zetten naar getallen (pd.to_numeric uit hoofdstuk zes). Als je de kolom per se moet laten zoals hij is, kan key= in plaats daarvan op de numerieke waarde sorteren:
fixed = as_text.sort_values("quantity", key=pd.to_numeric)
print(fixed["quantity"].tolist())['1', '2', '4', '5', '7', '12', '20', '30']Nu is de volgorde correct — maar de waarden zijn nog steeds strings, dus je zou ze niet bij elkaar kunnen optellen. Converteer de kolom bij voorkeur één keer, direct na het inlezen van het bestand. Dan klopt elke sortering, elk filter en elke optelling die volgt automatisch, zonder dat je eraan hoeft te denken.
Een compleet voorbeeld
Het volledige verzoek van de eigenaar, beantwoord in één programma: (1) de top drie bestellingen naar opbrengst, (2) de nieuwste bestellingen eerst, (3) een lijst per vestiging met de grootste bestelling eerst binnen elke vestiging.
"Opbrengst" is aantal vermenigvuldigd met prijs. Kolommen toevoegen wordt later in de cursus uitgebreid behandeld; voor nu volstaat één regel — toewijzen aan een nieuwe kolomnaam maakt deze automatisch aan.
sorted_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check the input before ranking anything
print("Rows, columns:", orders.shape)
print("Blank quantities:", orders["quantity"].isna().sum())
print()
orders["revenue"] = orders["quantity"] * orders["price"]
cols = ["order_id", "date", "branch", "product", "revenue"]
# 1. Top three by revenue, ties broken by the larger quantity, renumbered as a ranking
top3 = (
orders.sort_values(["revenue", "quantity"], ascending=[False, False])
.head(3)
.reset_index(drop=True)
)
print("Top three orders by revenue")
print(top3[cols])
print()
# 2. Newest first; within the same day, the bigger order first
newest = orders.sort_values(["date", "revenue"], ascending=[False, False])
print("Newest first")
print(newest[cols].head(4))
print()
# 3. By branch A-Z, biggest order first inside each branch
by_branch = orders.sort_values(["branch", "revenue"], ascending=[True, False])
print("By branch")
print(by_branch[cols])
print()
print("Still eight rows:", by_branch.shape[0] == orders.shape[0])Rows, columns: (8, 7)
Blank quantities: 0
Top three orders by revenue
order_id date branch product revenue
0 1003 2024-03-02 north bag 1700.0
1 1006 2024-03-03 south bag 850.0
2 1004 2024-03-02 east bottle 840.0
Newest first
order_id date branch product revenue
7 1008 2024-03-04 north bottle 480.0
6 1007 2024-03-04 east pen 300.0
5 1006 2024-03-03 south bag 850.0
4 1005 2024-03-03 north eraser 240.0
By branch
order_id date branch product revenue
3 1004 2024-03-02 east bottle 840.0
6 1007 2024-03-04 east pen 300.0
2 1003 2024-03-02 north bag 1700.0
7 1008 2024-03-04 north bottle 480.0
4 1005 2024-03-03 north eraser 240.0
0 1001 2024-03-01 north pen 180.0
5 1006 2024-03-03 south bag 850.0
1 1002 2024-03-01 south notebook 300.0
Still eight rows: TrueWaarom het zo is geschreven:
- De controle komt eerst.
shapeen het aantal lege plekken worden afgedrukt vóór enige rangschikking. Een top drie berekend op basis van een half ingelezen bestand, of met lege cellen naar beneden geduwd, zou er net zo overtuigend uitzien. - "Grootste" werd omgezet in een kolom. Rangschikken op
quantityzou de 30 gummen bovenaan zetten — 240 aan omzet. Rangschikken oprevenuezet de twee tassen ter waarde van 1.700 bovenaan. De eerste stap van het plan — schrijf de vraag op met de kolom erin — bepaalt welk antwoord de eigenaar krijgt. - Elke sortering heeft een tiebreaker. Op 2024-03-04 zijn er twee bestellingen;
revenuebeslist welke eerst wordt vermeld, zodat het rapport bij elke run hetzelfde is. - Alleen de ranglijst is hernummerd.
top3is een afgeronde lijst, dus de labels worden 0, 1, 2.newestenby_branchbehouden hun oorspronkelijke labels, zodat elke rij nog steeds kan worden herleid naar het bestand. orderszelf wordt nooit opnieuw gesorteerd. Elk rapport is een nieuwe tabel opgebouwd uit dezelfde onaangetaste invoer, zodat de drie antwoorden elkaar niet kunnen verstoren.- De laatste regel is een goedkope controle. Een sortering mag het aantal rijen nooit veranderen.
Trueprinten kost niets en zou bijvoorbeeld een per ongeluk achtergeblevenhead()op de verkeerde plek direct opmerken.
Wanneer het misgaat
KeyError: 'Quantity'
orders.sort_values("Quantity")KeyError: 'Quantity'De naam in by= moet exact overeenkomen met een kolom — inclusief hoofdletters, kleine letters en spaties. De kolom is quantity, in kleine letters. Print list(orders.columns) om de echte namen te zien, inclusief aanhalingstekens en eventuele verdwaalde spaties, zoals hoofdstuk drie aanraadde.
AttributeError: 'NoneType' object has no attribute 'head'
top = orders.sort_values("quantity", inplace=True)
top.head()AttributeError: 'NoneType' object has no attribute 'head'Met inplace=True wijzigt sort_values de tabel orders en geeft het None terug. Dus top is None, en None heeft geen head(). Kies één stijl: óf orders.sort_values("quantity", inplace=True) op een eigen regel, óf — beter — top = orders.sort_values("quantity") zonder inplace.
ValueError: Length of ascending (1) != length of by (2)
orders.sort_values(["branch", "quantity"], ascending=[False])ValueError: Length of ascending (1) != length of by (2)Een lijst voor ascending heeft één element per sleutel nodig. Twee sleutels, twee booleans: ascending=[True, False]. (Een enkele losse False, niet in een lijst, is toegestaan — die geldt dan voor elke sleutel.)
ValueError: For argument "ascending" expected type bool, received type str.
orders.sort_values("branch", ascending="False")ValueError: For argument "ascending" expected type bool, received type str."False" tussen aanhalingstekens is een string, niet de boolean False. Verwijder de aanhalingstekens.
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype
orders.nlargest(3, "branch")TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtypenlargest en nsmallest rangschikken alleen getallen. Voor "de laatste drie vestigingen alfabetisch" gebruik je orders.sort_values("branch", ascending=False).head(3). Als je deze fout kreeg bij een kolom waarvan je verwachtte dat deze numeriek was, is het werkelijke probleem dat deze als tekst is ingelezen — controleer dtypes.
TypeError: '<' not supported between instances of 'str' and 'int'
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()TypeError: '<' not supported between instances of 'str' and 'int'Deze kolom bevat echte getallen en een stuk tekst, waardoor het dtype object is en Python niet kan bepalen of "unknown" groter of kleiner is dan 12. Schoon de kolom eerst op: pd.to_numeric(mixed, errors="coerce") zet "unknown" om in NaN, waarna de sortering werkt en na_position bepaalt waar het gat terechtkomt.
Stap 4 van 6 — Voorspellen
Test je begrip
Twee tassen delen de hoogste prijs, 850.0. 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))
top = orders.sort_values("price", ascending=False)
print(list(top.index[:3]))- A[2, 5, 7]
- B[0, 1, 2]
- C[2, 5, 3]
- D[5, 2, 7]
Het doel is om de bestelling met de meeste stuks te printen. 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))
orders.sort_values("quantity", ascending=False)
print(orders.iloc[0]["order_id"])- A1001 — `sort_values` gaf een gesorteerde kopie terug en niemand heeft die bewaard
- B1007
- CEen `TypeError`: het resultaat moet worden toegewezen
- D1005
Je wilt bestellingen gegroepeerd op vestiging A–Z, en binnen elke vestiging het grootste aantal eerst. Welke regel doet dit?
- Aorders.sort_values("branch").sort_values("quantity", ascending=False)
- Borders.sort_values(["branch", "quantity"], ascending=False)
- Corders.sort_values(["quantity", "branch"], ascending=[False, True])
- Dorders.sort_values(["branch", "quantity"], ascending=[True, False])
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
Een docent stuurt je de cijfers van een toets. Sla dit op als results.csv. Twee studenten hebben nog geen cijfer, en sommige namen zijn met een kleine letter getypt:
name,section,marks
Rafi,B,78
anika,A,91
Tanvir,A,
Mitu,B,91
zara,A,65
Karim,B,
Nadia,A,88
Sabbir,B,54Schrijf ranking.py. Schrijf vóór enige code het plan als commentaar bovenaan het bestand: voor elke lijst de sleutelkolom, de richting, hoe gelijke waarden worden opgelost en waar lege waarden heengaan. Zorg er vervolgens voor dat het programma aan deze vier voorwaarden voldoet:
- Vóór het sorteren print het
shape,dtypesen het aantal ontbrekende cijfers. - Het print een ranglijst (merit list): hoogste cijfer eerst, studenten zonder cijfer onderaan, een gelijke stand op cijfers beslist op naam in alfabetische volgorde ongeacht hoofdletters, en de rijen hernummerd naar 0, 1, 2, …
- Het print de top drie met
nlargest, waarbij iedereen die op de derde plaats gelijk staat behouden blijft. - Het print de studenten zonder cijfer, gesorteerd op naam ongeacht hoofdletters, zodat de docent achter hen aan kan gaan — en daarna de oorspronkelijke tabel, om te bewijzen dat die nooit is gewijzigd.
Je programma moet precies dit printen:
(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0Voer daarna twee experimenten uit op de ranglijst:
- Verwijder
key=. Welke van de twee studenten met 91 komt nu als eerste, en waarom? - Verwijder
"name"helemaal uit de sleutels en sorteer alleen opmarks. Wie komt als eerste met 91 — en is dat resultaat gegarandeerd?
Oplossing
Het plan. Het bestand heeft 8 studenten en 3 kolommen. marks bevat lege cellen, dus het wordt ingelezen als float64, en elke lijst die op cijfers sorteert moet aangeven waar lege waarden heengaan. name mengt hoofd- en kleine letters, dus elke vergelijking van namen heeft key= nodig.
- Ranglijst (merit list) — sleutels
marks, daarnaname; cijfers van hoog naar laag, namen van A naar Z; een gelijke stand op cijfers wordt beslist op naam; lege waarden als laatste (de standaard); hernummerd metreset_index(drop=True). - Top drie —
nlargest(3, "marks")metkeep="all", zodat een gedeelde derde plaats niet wordt afgekapt.nlargestlaat ontbrekende waarden uit zichzelf weg. - Nog geen cijfer — filter de rijen waarin
marksontbreekt, zoals in de eerdere hoofdstukken, en sorteer daarna op naam. Eerst filteren, dan sorteren. - Origineel — nooit opnieuw toegewezen, dus
resultsprinten aan het einde toont de bestandsvolgorde.
Eén detail: een key=-functie wordt toegepast op elke sleutelkolom. Voor de ranglijst betekent dat ook marks, en .str.lower() zou mislukken op getallen. Daarom zet de functie alleen tekstkolommen om in kleine letters en laat ze elke andere kolom ongewijzigd.
ranking.py:
import pandas as pd
# Merit list : marks high->low, ties by name A->Z ignoring case, blanks last, renumber
# Top three : nlargest on marks, keep="all" so a shared third place is not cut
# No mark yet : filter rows with no mark, then name A->Z ignoring case
# Original : never reassigned
results = pd.read_csv("results.csv")
print(results.shape)
print(results.dtypes)
print("Missing marks:", results["marks"].isna().sum())
print()
def lower_text(col):
return col.str.lower() if col.dtype == "str" else col
merit = results.sort_values(
["marks", "name"], ascending=[False, True], key=lower_text
).reset_index(drop=True)
print("Merit list")
print(merit)
print()
print("Top three")
print(results.nlargest(3, "marks", keep="all"))
print()
print("No mark yet")
missing = results[results["marks"].isna()]
print(missing.sort_values("name", key=lower_text))
print()
print("Original, unchanged")
print(results)(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0De uitvoer vergelijken met het plan:
marksisfloat64, nietint64, enMissing marks: 2— de lege plekken werden gevonden vóór enige rangschikking, zodat de lezer van de ranglijst weet dat er twee studenten op ontbreken, en niet dat ze onderaan staan.- De gelijke stand op 91 wordt beslist op naam.
anikakomt vóórMituomdat de vergelijking is gemaakt op basis van"anika"en"mitu". - De top drie telt hier drie rijen, omdat de derde plaats (88) niet wordt gedeeld. Als Nadia ook 91 had gehad, zou
keep="all"vier rijen hebben opgeleverd in plaats van stilletjes een student weg te laten. - De lijst zonder cijfer filtert eerst en sorteert daarna — dezelfde volgorde van "beperken, dan pas rangschikken" als het patroon voor de top-N-van-een-deelverzameling.
- De laatste tabel is de volgorde van het bestand. Er zijn drie lijsten gegenereerd en
resultsis nooit opnieuw toegewezen.
De experimenten:
no_key = results.sort_values(["marks", "name"], ascending=[False, True])
marks_only = results.sort_values("marks", ascending=False)
print(no_key["name"].head(2).tolist())
print(marks_only["name"].head(2).tolist())['Mitu', 'anika']
['anika', 'Mitu']Zonder key= worden namen teken voor teken vergeleken, en elke hoofdletter komt vóór elke kleine letter, waardoor "Mitu" wint van "anika" — een volgorde gebaseerd op hoe een naam is getypt, niet op het alfabet. Met alleen marks komt anika toevallig als eerste omdat ze eerder in het bestand staat. Dat is hetzelfde antwoord als de juiste ranglijst, maar puur door toeval: bij één enkele sleutel belooft de standaardsortering niet dat gelijke rijen in bestandsvolgorde blijven, zoals de twee flessen van 120 eerder lieten zien. Een regel die je zelf hebt vastgelegd is gegarandeerd; een resultaat dat er toevallig goed uitziet is dat niet.
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