Ontbrekende data — de gaten vinden en bepalen wat ze betekenen
Elk gat in een tabel vinden, inclusief degene die als tekst verborgen zitten zoals - of ?, begrijpen hoe NaN totalen, gemiddelden en types verandert, en doelbewust — kolom voor kolom — kiezen of je elk gat verwijdert, invult of behoudt.
Hoe wil je leren?
Het probleem dat we oplossen
De bestellingen van de winkel voor de eerste week van maart werden bij drie vestigingen met de hand ingevoerd en geëxporteerd als orders_raw.csv. De eigenaar wil tegen de avond twee getallen zien: hoeveel stuks er zijn verkocht en hoeveel geld er is binnengekomen.
Je leest het bestand in en telt, uit gewoonte van de eerdere hoofdstukken, eerst de kolom price op om een idee van de omvang te krijgen:
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0Dat is geen getal. Het zijn alle prijzen aan elkaar geplakt als tekst — en pandas gaf geen foutmelding om je dat te vertellen. Nu de kolom quantity:
print(orders["quantity"].sum())
print(orders["quantity"].mean())76.0
10.857142857142858Dit ziet er goed uit, en dat is eigenlijk erger. Er staan acht bestellingen in het bestand, maar 76 / 8 is 9.5, niet 10.86. Eén aantal is leeg, en pandas liet dit stilletjes buiten zowel het totaal als het gemiddelde. Ook dat heeft niemand je verteld.
Het bestand bevat dus gaten, en die gaten zijn er in twee soorten. Sommige zijn zichtbaar: een lege cel, die pandas omzet in een speciale "missing"-markering en vervolgens stilletjes overslaat bij berekeningen. Andere zijn verborgen: iemand typte - in plaats van de cel leeg te laten, pandas nam dat letterlijk, en de hele kolom price werd tekst.
Geen van beide soorten geeft een foutmelding. Beide veranderen het antwoord. Dit hoofdstuk gaat over het vinden van elk gat, begrijpen wat pandas ermee doet, en vervolgens een weloverwogen beslissing nemen — verwijderen, invullen of behouden — in plaats van de standaardinstelling die beslissing voor je te laten nemen.
Aan het einde van dit hoofdstuk kun je
- Uitleggen wat
NaNis, waaromNaN == NaNgelijk is aanFalse, en waarom een kolom met gehele getallen in decimalen verandert zodra één cel leeg is - Ontbrekende waarden per kolom tellen met
isna().sum()en de onvolledige rijen eruit filteren - Verborgen markeringen zoals
-of?omzetten in echte ontbrekende waarden, metna_values=ofpd.to_numeric(errors="coerce") - Voorspellen hoe
sum,mean,counten rekenkundige bewerkingen tussen kolommen omgaan met ontbrekende waarden - Ontbrekende waarden verwijderen met
dropna(), waarbij jesubset=enhow=gebruikt om alleen te verwijderen wat je echt wilt verwijderen - Ontbrekende waarden vervangen met
fillna()— één waarde, een verschillende waarde per kolom, of een statistische maat - Kolom voor kolom beslissen of je verwijdert, invult of behoudt — en onderbouwen waarom
- De
inplace=True-valkuil vermijden die onder pandas 3 niets doet
Voorkennis: Rijen filteren.
Maak het bestand eerst aan
Maak in je projectmap orders_raw.csv aan met precies deze regels. De gaten zijn met opzet aangebracht — kijk goed naar de derde, vijfde en zevende regel:
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,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Het zijn dezelfde acht bestellingen als in orders.csv uit de eerdere hoofdstukken, met vier dingen die niet kloppen: bestelling 1002 heeft geen aantal, bestelling 1004 heeft geen vestiging en bij de prijs staat N/A, en bestelling 1006 heeft - als prijs.
Voordat je de code schrijft
Ontbrekende data is het enige onderwerp waarbij eerst code schrijven ronduit gevaarlijk is: dropna() en fillna() worden beide zonder protest uitgevoerd en beide veranderen je antwoord. Het werk begint daarom met kijken en beslissen.
1. Formuleer de vraag in één zin. "Wat was het totale verkochte aantal en de totale omzet (aantal × prijs) voor de week — en hoeveel bestellingen konden niet worden meegeteld?" Dat laatste deel doet ertoe. Een totaal dat stilletjes twee bestellingen weglaat is een ander antwoord dan een totaal dat vermeldt: "6 van de 8 bestellingen meegeteld".
2. Bekijk wat er binnenkwam, vóór al het andere. shape en info() vertellen je samen alles over elke kolom tegelijk:
print(orders.shape)
orders.info()(8, 7)
<class 'pandas.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 8 non-null int64
1 date 8 non-null str
2 branch 7 non-null str
3 product 8 non-null str
4 category 8 non-null str
5 quantity 7 non-null float64
6 price 7 non-null str
dtypes: float64(1), int64(1), str(5)
memory usage: 580.0 bytesLees dit af tegen wat je verwachtte, kolom voor kolom:
branch— verwachtte 8 non-nullstr, kreeg 7 non-null. Er ontbreekt één vestiging.quantity— verwachtte 8 non-nullint64, kreeg 7 non-nullfloat64. Er ontbreekt één aantal, en dat is waarom hetfloat64werd (hieronder uitgelegd).price— verwachtte 8 non-nullfloat64, kreeg 7 non-nullstr. Er ontbreekt één prijs en er is tekst in de kolom terechtgekomen.
Twee dingen in die lijst zijn aanwijzingen in plaats van feiten. Een kolom met gehele getallen die float64 toont, wijst bijna altijd op een gat. Een getallenkolom die str toont, betekent altijd dat er tekst doorheen gemengd zit — en tekst in een getallenkolom is meestal een verborgen markering voor "ontbrekend".
3. Schets de gewenste uitvoer. Iets als:
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known): 76
Total revenue (counted): ...Merk op dat dit zowel de gaten als de totalen rapporteert. Wie het rapport leest, moet weten dat de omzet een ondergrens is.
4. Bepaal het gereedschap voor elk gat — vóórdat je het schrijft. Vraag je bij elke kolom af: wat betekent een lege cel hier, en kan de vraag nog steeds zonder worden beantwoord?
branch— een gat betekent dat de bestelling heeft plaatsgevonden, maar dat de vestiging niet is geregistreerd. Beslissing: invullen met"Unknown", omdat de bestelling echt is en de omzet nog steeds meetelt.quantity— een gat betekent dat we niet weten hoeveel er zijn verkocht. Beslissing: behouden als ontbrekend en de rij uitsluiten van de omzet, want een getal verzinnen (0? het gemiddelde?) zou verkopen verzinnen.price— een gat betekent dat we niet weten waarvoor het is verkocht. Beslissing: behouden als ontbrekend en de rij uitsluiten van de omzet, om dezelfde reden — en rapporteren hoeveel rijen zijn uitgesloten.
5. Bepaal wat je achteraf controleert. Print na elke stap die waarden verwijdert of vervangt opnieuw de aantallen ontbrekende waarden en de shape. Als een dropna() je van 8 rijen naar 2 brengt, wil je dat meteen zien, niet pas nadat het rapport de deur uit is.
De rest van het hoofdstuk behandelt elk gereedschap afzonderlijk, en aan het einde voegen we dit plan samen in één programma.
Hoe "ontbrekend" eruitziet: NaN
Wanneer pandas een lege cel aantreft, slaat het geen lege string of nul op. Het slaat een speciale floating-point-waarde op genaamd NaN — "not a number". Je kunt er zelf een maken door None in een Series te stoppen:
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)0 12.0
1 NaN
2 2.0
dtype: float64Kijk naar het dtype. Je gaf twee gehele getallen en een gat, en kreeg float64 terug, met 12.0 en 2.0. Dit is de reden achter de aanwijzing in info(): NaN is een float-waarde, en een klassieke integer-kolom kan die op geen enkele manier opslaan. Zodra er dus één cel ontbreekt, bevordert pandas de hele kolom naar float64 om ruimte te maken voor NaN. Daarom werd quantity in ons bestand geprint als 12.0, 2.0 enzovoort.
Tekstkolommen gedragen zich anders. Een str-kolom kan een ontbrekende waarde bevatten zonder van type te veranderen:
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strHet wordt nog steeds geprint als NaN, en het dtype blijft str. In pandas 3 kom je NaN dus tegen in zowel getallen- als tekstkolommen, en hetzelfde gereedschap vindt het in beide.
NaN is nergens aan gelijk — zelfs niet aan zichzelf
Dit is het meest verrassende feit over ontbrekende waarden, en het zorgt ervoor dat de meest natuurlijke manier om ze te zoeken niet werkt:
import numpy as np
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(np.nan == np.nan)
print((orders["branch"] == np.nan).sum())False
0NaN betekent "een waarde die we niet weten". Van twee onbekende waarden is niet bekend of ze gelijk zijn, dus de vergelijking levert False op — elke keer weer. Daardoor is == np.nan een test die nooit ergens mee overeenkomt: de kolom branch heeft een gat, en de telling geeft 0. Vraag het in plaats daarvan rechtstreeks aan pandas.
Zoek nooit naar ontbrekende waarden met==.== np.nanwordt uitgevoerd zonder foutmelding en vindt altijd niets, waardoor een kolom vol gaten compleet lijkt. Gebruikisna().
Ontbrekende waarden vinden: isna()
isna() stelt de vraag die == niet kan stellen: "ontbreekt deze cel?" Op een kolom levert het een kolom van True/False op — een masker, precies zoals die uit het filterhoofdstuk:
print(orders["branch"].isna())0 False
1 False
2 False
3 True
4 False
5 False
6 False
7 False
Name: branch, dtype: boolOp het hele DataFrame doet het hetzelfde voor elke cel. Je wilt echter zelden acht rijen met True/False lezen. Je wilt een aantal, en aangezien True als 1 telt, levert .sum() dat op:
print(orders.isna().sum())order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 1
dtype: int64Dit is de regel die je na info() op elk nieuw bestand uitvoert. Het is dezelfde informatie als de Non-Null Count, maar dan omgedraaid om de gaten direct te tellen.
Maar kijk eens naar price: daar staat 1. We weten dat twee prijzen niet kloppen — N/A en -. Houd dat in gedachten; we komen er in de volgende sectie op terug.
Welke rijen zijn onvolledig?
Een telling vertelt je hoeveel; om te zien welke, gebruik je het masker als filter. isna().any(axis=1) vraagt voor elke rij: "ontbreekt er iets in deze rij?":
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery NaN 60.0
3 1004 2024-03-02 NaN bottle accessories 7.0 NaNaxis=1 betekent "kijk over elke rij heen" in plaats van omlaag langs elke kolom. En let op wie er niet in de lijst staat: bestelling 1006, waarvan de prijs - is. Voor zover pandas weet, is die rij compleet. Onthoud dat even. De daadwerkelijke rijen bekijken is die extra regel meer dan waard: zo merk je bijvoorbeeld op dat elk ontbrekend aantal van dezelfde vestiging komt, wat een vraag is voor de mensen van die vestiging en niet voor pandas.
Als een aandeel, niet als een aantal
Bij acht rijen zijn aantallen prima. Bij tachtigduizend zegt "312 ontbrekend" weinig totdat je weet dat het 0,4% is. .mean() van een True/False-kolom is de fractie van True:
print(orders.isna().mean())order_id 0.000
date 0.000
branch 0.125
product 0.000
category 0.000
quantity 0.125
price 0.125
dtype: float64Eén op de acht cellen van branch, quantity en price ontbreekt — 12,5% per stuk. Een kolom die voor 90% leeg is en een kolom die voor 0,1% leeg is vragen om totaal verschillende beslissingen, en dit is hoe je het onderscheid maakt.
Verborgen ontbrekende waarden: wanneer het gat als tekst is geschreven
isna() telde één ontbrekende prijs, maar twee prijzen zijn onbruikbaar. De tweede is de - in bestelling 1006. Voor pandas is - niet ontbrekend — het is een volkomen geldige tekstwaarde. En één stuk tekst in een kolom met getallen is genoeg om van de hele kolom str te maken, precies wat info() liet zien.
Zo achterhaal je wat er werkelijk in een kolom staat:
print(orders["price"].unique())<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: strElke waarde staat tussen aanhalingstekens — het is allemaal tekst, inclusief '15.0'. En er staan twee soorten gaten naast elkaar: nan, dat pandas herkende, en '-', dat het niet herkende.
Waarom werd N/A wel nan maar - niet? Omdat read_csv een ingebouwde lijst heeft van strings die het als ontbrekend beschouwt: een lege cel, NA, N/A, n/a, NaN, null, None en nog een paar andere. -, ?, missing, 0 en unknown staan niet op die lijst. Wat degene die de data invoerde ook heeft gebruikt voor "onbekend", bepaalt welk type gat je krijgt.
Er zijn twee manieren om de verborgen markeringen om te zetten in echte NaN.
Oplossing 1: geef de markering door aan read_csv — na_values=
Als je de markering weet, is de schoonste oplossing die op het moment van inlezen. na_values= voegt jouw strings toe aan de ingebouwde lijst:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
print(orders["price"].dtype)
print(orders.isna().sum())float64
order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 2
dtype: int64Vergelijk dit met de telling van daarnet. branch en quantity zijn ongewijzigd — na_values= voegt toe aan de lijst, het vervangt hem niet, dus lege cellen en N/A worden nog steeds herkend. Wat veranderde is price: dat is nu float64, en er worden 2 ontbrekende waarden gerapporteerd — de N/A en de - zijn nu beide echte NaN. De kolom is weer numeriek, dus berekeningen erop betekenen weer echte berekeningen.
Merk op dat na_values=["-"] geldt voor elke kolom. Hier is dat wat we willen. Als een - ergens anders een echte waarde zou kunnen zijn — bijvoorbeeld een productcode — geef dan een dict mee, zoals na_values={"price": ["-"]}, zodat alleen de kolom price wordt beïnvloed.
Oplossing 2: converteer achteraf — pd.to_numeric(errors="coerce")
Soms weet je niet van tevoren welke vreemde strings er in een kolom staan, of is het bestand ingelezen door de code van iemand anders. Dan converteer je de kolom na het inlezen. Bekijk eerst wat een gewone conversie doet:
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5Die foutmelding is nuttig: hij noemt de problematische waarde en de positie ervan. Het toevoegen van errors="coerce" zegt: "alles wat je niet in een getal kunt omzetten, maak je NaN":
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)0 15.0
1 60.0
2 850.0
3 NaN
4 8.0
5 NaN
6 15.0
7 120.0
Name: price, dtype: float64Om de tabel daadwerkelijk te wijzigen, wijs je het resultaat weer toe aan de kolom — to_numeric levert een nieuwe Series op en laat raw met rust:
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())float64 2Welke oplossing kies je?
errors="coerce" is krachtig en bot. Het zet alles wat niet-numeriek is om in NaN — inclusief een echte prijs waarin een typefout staat zoals 1O0 (met de letter O) of 1,200 met een duizendtallen-komma. Dat zijn geen ontbrekende waarden; het zijn waarden met een typefout, en door ze te forceren (coercen) gooi je stilletjes informatie weg.
De vaste gewoonte is daarom: tel vooraf, tel achteraf, en verklaar het verschil.
- Je kent de markering (
-,?,missing): gebruikna_values=inread_csv. Dit is precies — alleen die specifieke string wordt ontbrekend. - Onbekende rommel, en de kolom moet numeriek zijn: gebruik
pd.to_numeric(errors="coerce"). Dit vangt alles op, dus controleer wat het heeft opgevangen. - Sommige waarden zijn typefouten die je kunt herstellen: bekijk eerst
unique(), herstel ze en converteer daarna pas. Coercen zou echte data vernietigen.
Vóór het coercen toonde raw["price"].unique() precies één niet-numerieke string ('-') plus één nan. Na het coercen zijn er precies 2 NaN. De getallen komen overeen, dus er is niets anders verloren gegaan. Die controle maakt het hele verschil tussen data opschonen en data beschadigen.
Vanaf hier gebruiken we de versie die is ingelezen met na_values=["-"].
Hoe berekeningen omgaan met NaN
Nu de gaten echte NaN zijn, moet je weten wat pandas ermee doet — want het stopt niet om het te vragen.
Kolomsamenvattingen slaan NaN over
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
q = orders["quantity"]
print("len :", len(q))
print("count:", q.count())
print("sum :", q.sum())
print("mean :", q.mean())
print("check:", q.sum() / q.count())len : 8
count: 7
sum : 76.0
mean : 10.857142857142858
check: 10.857142857142858Drie gedragingen om te onthouden:
lentelt rijen;count()telt waarden. Het verschil ertussen is het aantal gaten. Dit is hetzelfde idee als deNon-Null Countininfo().sum()slaatNaNover. Het telt de zeven bekende aantallen op. Dat is logisch — maar het betekent wel dat "76" het totaal is van de bekende aantallen, niet van alle bestellingen.mean()deelt doorcount(), niet doorlen. Het gemiddelde wordt berekend over de zeven bekende waarden. Dat is meestal wat je wilt; het gat als 0 behandelen zou het gemiddelde zonder reden omlaag trekken.
Als je liever te horen krijgt dat er iets ontbreekt, zorgt skipna=False ervoor dat het resultaat NaN wordt zodra er ook maar één waarde ontbreekt:
print(q.sum(skipna=False))nanRekenen tussen kolommen verspreidt NaN
Samenvattingen slaan gaten over; rekenkundige bewerkingen verspreiden ze. Alles wat met NaN wordt gecombineerd levert NaN op, want "7 keer een onbekende prijs" is zelf ook onbekend:
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))0 180.0
1 NaN
2 1700.0
3 NaN
4 240.0
5 NaN
6 300.0
7 480.0
dtype: float64
total: 2900.0
rows counted: 5 of 8Voeg die twee gedragingen samen en je ziet de valkuil van het begin van het hoofdstuk. revenue.sum() printte 2900.0 zonder enige waarschuwing — maar drie van de acht bestellingen droegen niets bij. Bij bestelling 1002 ontbreekt een quantity, bij 1004 en 1006 ontbreekt een price. Het totaal is alleen eerlijk als je vermeldt: "5 van de 8 bestellingen". Daarom vroeg het plan om een telling naast elk totaal.
Ontbrekende waarden verwijderen: dropna()
dropna() verwijdert rijen die NaN bevatten. Zonder argumenten verwijdert het elke rij met enig gat in welke kolom dan ook:
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)Drie van de acht rijen zijn weg — en bestelling 1004 verdween omdat zijn branch ontbrak, terwijl het voor een vraag over aantallen een volkomen bruikbare rij was. Dat is het probleem met een kale dropna(): het beantwoordt "verwijder alles wat niet perfect is", wat zelden de vraag is.
subset= — alleen de kolommen die de vraag nodig heeft
Geef aan welke kolommen aanwezig moeten zijn:
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0Alleen bestelling 1002 wordt verwijderd — de ene rij waarvan het aantal echt onbekend is. Voor de omzet zijn beide kolommen nodig:
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])order_id quantity price
0 1001 12.0 15.0
2 1003 2.0 850.0
4 1005 30.0 8.0
6 1007 20.0 15.0
7 1008 4.0 120.0Let op de index: 0, 2, 4, 6, 7. dropna() behoudt de oorspronkelijke labels, net zoals filteren dat doet, zodat je een rij altijd kunt herleiden naar het bestand. Als je weer 0, 1, 2… nodig hebt, voeg dan .reset_index(drop=True) toe.
how="all" — alleen rijen die volledig leeg zijn
Geëxporteerde spreadsheets eindigen vaak met een paar rijen die in elke kolom leeg zijn. how="all" verwijdert alleen die en laat elke rij met ten minste één waarde staan:
print(orders.dropna(how="all").shape)(8, 7)Hier wordt niets verwijderd — geen van onze rijen is geheel leeg — en dat is precies wat je wilt bevestigen.
dropna() levert een nieuwe tabel op
Zoals de meeste methoden in pandas verandert dropna() orders niet; het geeft je een nieuw DataFrame. Als je orders.dropna() op een regel op zichzelf schrijft, gebeurt er niets met orders. Wijs het resultaat toe — aan een nieuwe naam als je de volledige tabel nog nodig hebt, wat in een rapport zoals het onze het geval is (om te kunnen melden hoeveel er zijn weggelaten).
Ontbrekende waarden vervangen: fillna()
Soms heeft een gat een logische vervanging. fillna() plaatst een waarde waar NaN stond.
Eén waarde voor één kolom
De ontbrekende branch is het duidelijkste geval. De bestelling heeft plaatsgevonden en de omzet is echt; we weten alleen niet welke vestiging deze heeft aangenomen. Door het in te vullen met een label behoud je de rij en blijft het gat zichtbaar in elke latere telling per vestiging:
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']De vorm van die regel is belangrijk: neem de kolom, doe fillna, en wijs het opnieuw toe aan dezelfde kolom. fillna levert een nieuwe Series op; zonder de orders["branch"] = ervoor wordt er niets opgeslagen.
Verschillende waarden voor verschillende kolommen: een dict
Geef een dict van {kolom: waarde} mee om meerdere kolommen tegelijk in te vullen, elk met een eigen waarde. Kolommen die niet worden genoemd, blijven onaangeroerd:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())order_id 0
date 0
branch 0
product 0
category 0
quantity 0
price 2
dtype: int64price heeft nog steeds zijn twee gaten, omdat we het niet hebben genoemd. Dat is het voordeel van de dict-vorm boven orders.fillna(0), die elke kolom, inclusief prijzen, met 0 zou vullen — en een prijs van 0 betekent een gratis tas.
Invullen verandert het antwoord
Het aantal opvullen met 0 leek onschuldig. Vergelijk de gemiddelden:
print("skip the gap :", orders["quantity"].mean())
print("fill with 0 :", orders["quantity"].fillna(0).mean())
print("fill with median:", orders["quantity"].fillna(orders["quantity"].median()).mean())skip the gap : 10.857142857142858
fill with 0 : 9.5
fill with median: 10.375Drie verschillende gemiddelden uit één kolom, en het enige verschil is wat je hebt besloten dat een onbekend aantal betekent:
- 0 zegt "deze bestelling verkocht niets". Voor bestelling 1002, een bestelling van notitieboekjes die zeker iets heeft verkocht, klopt dat niet, en het trekt het gemiddelde omlaag.
- De mediaan (hier 7) zegt "ga uit van een typische bestelling". Het houdt het gemiddelde redelijk op zijn plaats, maar het totaal bevat nu 7 notitieboekjes die niemand heeft geteld.
- Overslaan zegt "we weten het niet, dus we laten het weg". Het gemiddelde wordt berekend over de zeven bestellingen die we wel kennen.
Geen van deze is in het algemeen "juist". Elk beantwoordt een net iets andere vraag. Daarom hoort de beslissing thuis in je plan, opgeschreven, en niet in de eerste de beste standaardinstelling waar je naar greep.
Een vuistregel die meestal werkt:
- Een label (branch, categorie, naam): vul in met
"Unknown"of iets vergelijkbaars. De rij blijft behouden en het gat blijft zichtbaar als een eigen groep. - Een telling waarbij leeg ook echt nul betekent (retouren, klachten): vul in met
0— maar alleen wanneer de bron met een leeg veld ook echt nul bedoelt. - Een meting of geldbedrag (verkocht aantal, prijs, een cijfer): behoud
NaN, sluit het uit van de berekening en rapporteer hoeveel er zijn uitgesloten. Verzonnen getallen worden verzonnen totalen. - Een kolom die de vraag nodig heeft, grotendeels aanwezig:
dropna(subset=[...])alleen voor die specifieke vraag. Een klein verlies en een eerlijk antwoord.
Gehele getallen terugkrijgen
Na het invullen heeft quantity geen gaten meer, maar het is nog steeds float64 — pandas zet dit niet vanzelf terug. Zodra er geen NaN meer over is, werkt astype(int):
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64Probeer je dit vóór het invullen, dan mislukt het, omdat er geen geheel getal bestaat dat "onbekend" betekent — zie "Wanneer het misgaat" hieronder.
Een compleet voorbeeld
clean_orders.py brengt het plan uit "Voordat je de code schrijft" samen in één programma: inlezen met de bekende markering, elk gat rapporteren, de branch invullen, rijen uitsluiten die niet kunnen worden meegeteld, en de totalen rapporteren samen met het aantal bestellingen dat ze dekken.
import pandas as pd
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
# 1. Look first
print("Rows, columns:", orders.shape)
missing = orders.isna().sum()
print("Missing per column:")
print(missing[missing > 0])
print()
# 2. Decide per column: branch -> label; quantity and price -> keep, exclude
orders["branch"] = orders["branch"].fillna("Unknown")
# 3. Only rows with both numbers can be counted for revenue
countable = orders.dropna(subset=["quantity", "price"])
skipped = orders[orders[["quantity", "price"]].isna().any(axis=1)]
revenue = countable["quantity"] * countable["price"]
print("Orders counted for revenue:", len(countable), "of", len(orders))
print("Total quantity (known): ", int(orders["quantity"].sum()))
print("Total revenue (counted): ", revenue.sum())
print()
print("Not counted - check these with the branch:")
print(skipped[["order_id", "branch", "quantity", "price"]])Rows, columns: (8, 7)
Missing per column:
branch 1
quantity 1
price 2
dtype: int64
Orders counted for revenue: 5 of 8
Total quantity (known): 76
Total revenue (counted): 2900.0
Not counted - check these with the branch:
order_id branch quantity price
1 1002 south NaN 60.0
3 1004 Unknown 7.0 NaN
5 1006 south 1.0 NaNWaarom het zo is geschreven:
na_values=["-"]staat op de allereerste regel. De markering werd eenmalig gevonden door naarunique()te kijken. Door dit inread_csvte zetten, is de kolom vanaf het begin numeriek, en iedereen die het programma leest ziet welke markering het bestand gebruikt.missing[missing > 0]print alleen de kolommen die gaten bevatten. Bij een bestand met veertig kolommen is dat het verschil tussen een leesbaar rapport en een muur van nullen.- De branch is ingevuld, de getallen niet. Dat is de beslissingenlijst uit het plan, omgezet in code. Een lezer kan het ermee oneens zijn, maar kan het in elk geval zien.
dropna(subset=...)noemt de twee kolommen die omzet nodig heeft, zodat bestelling 1004 wordt uitgesloten vanwege de ontbrekende prijs, niet vanwege de ontbrekende vestiging.- De overgeslagen rijen worden geprint, niet alleen geteld. "3 bestellingen niet meegeteld" roept de vraag op "welke dan?" — beantwoord die in het rapport. Die drie bestellingen vormen een lijst die iemand daadwerkelijk kan gaan controleren en oplossen.
- "Total quantity (known)" en "(counted)" geven duidelijk aan wat de getallen zijn. Beide zijn ondergrenzen; de labels voorkomen dat iemand ze als compleet beschouwt.
Wanneer het misgaat
TypeError: Cannot perform reduction 'mean' with string dtype Je vroeg om het gemiddelde van een kolom die pandas als tekst heeft ingelezen — meestal omdat één cel een markering bevat zoals -, ? of missing. Voer df["col"].unique() uit om de boosdoener te vinden, en lees het bestand daarna opnieuw in met na_values=["-"] of converteer met pd.to_numeric(df["col"], errors="coerce").
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())TypeError: Cannot perform reduction 'mean' with string dtypesum() gaf een lange reeks cijfers terug, zoals 15.060.0850.0... Dezelfde oorzaak, maar een erger symptoom: bij een tekstkolom voegt sum() de strings samen in plaats van getallen op te tellen, zonder enige foutmelding te geven. Elk totaal dat er zo uitziet, is afkomstig uit een str-kolom. Controleer df.dtypes voordat je iets optelt.
ValueError: Unable to parse string "-" at position 5 pd.to_numeric kwam tekst tegen die het niet kon converteren, en meldde precies wat en waar. Voeg errors="coerce" toe (nadat je met unique() hebt gecontroleerd of de vreemde waarden echt markeringen voor "onbekend" zijn en geen typefouten), of herstel de typefouten eerst.
(df["col"] == np.nan).sum() geeft 0 aan, maar er zijn gaten NaN is nooit aan iets gelijk, inclusief zichzelf, dus == np.nan komt met geen enkele cel overeen. Gebruik df["col"].isna().
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer Je riep astype(int) aan op een kolom die nog NaN bevat. Er bestaat geen geheel getal dat "onbekend" betekent. Vul de gaten eerst in of verwijder ze, en converteer daarna pas:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')(De ontbrekende spaties in "integer.Replace" en "typethat" zitten in de melding van pandas zelf.) De melding noemt ook 'Int64' met een hoofdletter I: een apart integer-type van pandas dat wel ontbrekende waarden kan bevatten. Dat is nuttig, maar eerst invullen of verwijderen is de eenvoudigere oplossing en degene waar je als eerste naar grijpt.
ChainedAssignmentError — en het gat is er nog steeds Je schreef de vorm met inplace=True die je in veel oudere tutorials tegenkomt:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())1
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method 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).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.htmlDe telling is nog steeds 1: er is niets ingevuld. In pandas 3 gedraagt orders["quantity"] zich altijd als een eigen kopie (Copy-on-Write), dus het "in place" invullen ervan vult die kopie in, die vervolgens wordt weggegooid. Oudere pandas-versies lieten dit soms werken, en daarom zie je dit patroon overal online. Schrijf de toewijzing in plaats daarvan volledig uit — dit werkt in elke versie en maakt direct duidelijk wat er verandert:
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0dropna() verwijderde veel meer rijen dan je had verwacht Een kale dropna() verwijdert een rij bij een gat in welke kolom dan ook — inclusief een grotendeels lege kolom notes of discount die je niet eens gebruikte. Print df.isna().sum() om de kolom met veel gaten te vinden, en gebruik daarna dropna(subset=[...]) met alleen de kolommen die je vraag nodig heeft.
TypeError: can't multiply sequence by non-int of type 'float' Je vermenigvuldigde een getallenkolom met een tekstkolom (quantity * price terwijl price str is). De oplossing is hetzelfde als bij de eerste fout: maak price eerst numeriek.
Stap 4 van 6 — Voorspellen
Test je begrip
Eén aantal is leeg. Wat wordt er geprint?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.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,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
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["quantity"].sum(), orders["quantity"].count(), len(orders))- A76.0 7 8
- B76.0 8 8
- C76 8 8
- Dnan 7 8
Het doel is om de bestellingen zonder prijs te vinden. Twee prijzen ontbreken. Wat wordt er geprint?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.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,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
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), na_values=["-"])
missing_price = orders[orders["price"] == float("nan")]
print(len(missing_price))- A0 — `NaN` is nergens aan gelijk, zelfs niet aan `NaN`, dus `==` vindt het nooit
- B1
- CEen `TypeError`: je kunt niet vergelijken met `nan`
- D2
De kolom price is ingelezen als str omdat één cel - bevat en een andere N/A. Welke regel zet dit om in getallen, waarbij beide gaten NaN worden?
- Aorders["price"] = pd.to_numeric(orders["price"], errors="coerce")
- Borders["price"] = orders["price"].fillna(0)
- Corders["price"] = orders["price"].astype(float)
- Dorders["price"] = orders["price"].dropna()
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 winkel is begonnen met thuisbezorging, en het rittenlogboek van de bezorgers voor de eerste week arriveert als deliveries.csv. Maak het precies zo aan:
order_id,branch,distance_km,fee,rider
2001,north,3.5,60,R1
2002,north,,60,R2
2003,south,8.0,?,R1
2004,east,12.0,120,
2005,north,2.0,40,R2
2006,south,6.5,90,?
2007,east,,110,R3
2008,north,4.0,60,R3De eigenaar vraagt: hoeveel is er aan bezorgkosten opgehaald, bij hoeveel bezorgingen ontbreken de bezorgkosten, en wat zijn de gemiddelde bezorgkosten per kilometer?
Bekijk het bestand vóórdat je code schrijft met unique() en bepaal wat de verborgen markering is. Schrijf vervolgens deliveries_report.py dat:
- Het bestand zo inleest dat de verborgen markering een echte ontbrekende waarde wordt in elke kolom waarin deze voorkomt
- Het aantal ontbrekende waarden per kolom print, evenals de onvolledige rijen
- De ontbrekende
riderinvult met"unassigned" - Het bekende totaal aan bezorgkosten print, het aantal bezorgingen zonder bezorgkosten, en de kosten per kilometer — totale bezorgkosten gedeeld door totale afstand — uitsluitend berekend op basis van bezorgingen waarbij zowel de kosten als de afstand bekend zijn
De laatste drie regels die je programma print, moeten exact luiden:
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesSchrijf vóór de code ook een beslissingenlijst op zoals die in "Voordat je de code schrijft": wat betekent een gat voor elk van distance_km, fee en rider, en ga je invullen, verwijderen of behouden?
Probeer daarna één experiment: bereken de bezorgkosten per kilometer met een kale dropna() in plaats van dropna(subset=...). Verandert het getal? Hoeveel rijen zijn er gebruikt, en waarom?
Oplossing
Eerst het plan. De vraag bestaat uit drie delen: een totaal, een telling van gaten en een verhouding. Het inlezen met info() zou fee tonen als str — een kolom met bezorgkosten zou getallen moeten bevatten, dus er zit tekst in verborgen. Beslissingen per kolom:
rider— de bezorging heeft plaatsgevonden, de bezorger is niet geregistreerd. Invullen met"unassigned".fee— we weten niet wat er in rekening is gebracht. Behouden alsNaN; tel de bekende bezorgkosten op en tel de onbekende.distance_km— we weten niet hoe ver het was. Behouden alsNaN; sluit die rijen uit van de verhouding per km.
Voor de verhouding moeten beide getallen bekend zijn voor dezelfde bezorging — anders zou je de kosten van zeven bezorgingen delen door de afstanden van zes bezorgingen.
Stap 1 — gewoon inlezen en kijken, om de markering te vinden:
import pandas as pd
raw = pd.read_csv("deliveries.csv")
print(raw.shape)
print(raw.dtypes)
print(raw["fee"].unique())
print(raw["rider"].unique())(8, 5)
order_id int64
branch str
distance_km float64
fee str
rider str
dtype: object
<StringArray>
['60', '?', '120', '40', '90', '110']
Length: 6, dtype: str
<StringArray>
['R1', 'R2', nan, '?', 'R3']
Length: 5, dtype: strfee is str vanwege de ?. En ? verschijnt ook als rider — dezelfde markering, gebruikt in twee kolommen. distance_km is al float64: de gaten daarin zijn lege cellen, die pandas vanzelf heeft herkend. Omdat de markering overal hetzelfde is, lost na_values=["?"] tijdens het inlezen beide kolommen in één keer op.
Het volledige programma, deliveries_report.py:
import pandas as pd
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
print("Types after reading:")
print(deliveries.dtypes)
print()
print("Missing per column:")
print(deliveries.isna().sum())
print()
print("Incomplete rows:")
print(deliveries[deliveries.isna().any(axis=1)])
print()
deliveries["rider"] = deliveries["rider"].fillna("unassigned")
fee_total = deliveries["fee"].sum()
fee_missing = deliveries["fee"].isna().sum()
print("Fees collected (known):", fee_total)
print("Deliveries with no fee:", fee_missing)
both = deliveries.dropna(subset=["fee", "distance_km"])
per_km = both["fee"].sum() / both["distance_km"].sum()
print("Fee per km:", round(per_km, 2), "from", len(both), "of", len(deliveries), "deliveries")Types after reading:
order_id int64
branch str
distance_km float64
fee float64
rider str
dtype: object
Missing per column:
order_id 0
branch 0
distance_km 2
fee 1
rider 2
dtype: int64
Incomplete rows:
order_id branch distance_km fee rider
1 2002 north NaN 60.0 R2
2 2003 south 8.0 NaN R1
3 2004 east 12.0 120.0 NaN
5 2006 south 6.5 90.0 NaN
6 2007 east NaN 110.0 R3
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesControleer de getallen een keer met de hand, want zo leer je een opgeschoond totaal te vertrouwen. Bekende bezorgkosten: 60 + 60 + 120 + 40 + 90 + 110 + 60 = 540, van zeven bezorgingen — 2003 heeft er geen. Voor de verhouding laat dropna(subset=["fee", "distance_km"]) 2003 vallen (geen kosten) en 2002 en 2007 (geen afstand). Vraag aan pandas welke rijen er overbleven en waar ze toe optellen, in plaats van te vertrouwen op hoofdrekenen:
print(both["order_id"].tolist())
print(both["fee"].sum(), both["distance_km"].sum())[2001, 2004, 2005, 2006, 2008]
370.0 28.0370 / 28.0 = 13.21, wat precies het getal is dat het programma printte. Let op wat de verhouding niet deed: het deelde de 540 van zeven bezorgingen niet door de afstanden van zes bezorgingen. Beide getallen zijn afkomstig uit dezelfde vijf rijen, wat de enige manier is waarop een getal "per kilometer" enige betekenis heeft.
Nog twee beslissingen die de moeite waard zijn om op te merken. rider werd vóór de berekening ingevuld, maar heeft er nooit invloed op gehad — subset= keek alleen naar fee en distance_km. En het totaal aan bezorgkosten is gelabeld met "(known)", waarbij het aantal ontbrekende bezorgkosten er direct onder is geprint, zodat niemand 540 aanziet voor alles wat er is opgehaald.
Het experiment. Een kale dropna() verwijdert ook 2004 en 2006 — waarvan zowel de kosten als de afstand in orde waren — omdat hun rider ontbrak. Als je dit uitvoert op de tabel voordat je rider invult:
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
strict = deliveries.dropna()
print(strict["order_id"].tolist())
print(round(strict["fee"].sum() / strict["distance_km"].sum(), 2))[2001, 2005, 2008]
16.84Slechts drie bezorgingen overleven dit, en de verhouding springt van 13.21 naar 16.84 — niet omdat er kosten zijn veranderd, maar omdat de twee langste ritten (12 en 6,5 km), die toevallig per kilometer goedkoper zijn, zijn weggegooid vanwege een ontbrekende naam. De kolom rider heeft niets te maken met de vraag en zou dus niet moeten bepalen welke rijen meetellen. Dat is waar subset= voor bedoeld is.
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