In eerdere tutorials heb je geleerd hoe je gegevens kunt inlezen, selecteren en groeperen met pandas. In de praktijk zijn gegevens echter zelden helemaal goed gestructureerd, compleet en correct. Je zult vaak te maken hebben met:
- Missende waarden (lege cellen)
- Duplicaten in je dataset
- Verkeerde data types (bijvoorbeeld tekst waar getallen zouden moeten staan)
In deze tutorial leer je hoe je met pandas de meest voorkomende problemen kunt oplossen. Dit is in de praktijk vaak 80% van het werk bij data-analyse! Je focust op drie belangrijke technieken:
- Missende waarden behandelen met
dropna() en fillna()
- Duplicaten detecteren met
value_counts()
- Data types controleren met
dtypes en info() en converteren
Voordat je begint
Project setup
Maak een nieuw project aan, met een nieuwe virtual environment. Installeer daarin ook pandas.
shell
cd projects
mkdir pandas_data_opschonen && cd pandas_data_opschonen
python -m venv .venv
source .venv/bin/activate # .venv\Scripts\activate.bat voor Windows
python -m pip install pandas requests
python -m pip freeze > requirements.txt
Of met uv:
shell
cd projects
mkdir pandas_data_opschonen && cd pandas_data_opschonen
uv init
uv add pandas requests
Vervuilde KNMI dataset maken
Voor deze tutorial werk je met een aangepaste versie van de KNMI-temperatuurdata die verschillende problemen bevat. Maak een bestand create_messy_data.py:
create_messy_data.py
import pandas as pd
import numpy as np
def create_messy_knmi_data():
"""Maak een vervuilde versie van KNMI data voor de tutorial."""
# Basis data (simulatie van KNMI temperatuurdata)
years = list(range(2020, 2025))
months = ['Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni',
'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December']
data = []
for year in years:
for month in months:
# Simuleer temperatuurdata met problemen
temp = np.random.normal(10, 8) # Willekeurige temperatuur
# Voeg verschillende problemen toe
if np.random.random() < 0.15: # 15% kans op missende waarde
temp = np.nan
elif np.random.random() < 0.1: # 10% kans op string waarde
temp = f"{temp:.1f}°C"
elif np.random.random() < 0.05: # 5% kans op foutieve waarde
temp = "onbekend"
data.append({
'Station': 'De Bilt',
'Jaar': year,
'Maand': month,
'Temperatuur': temp,
'Neerslag': np.random.normal(60, 30) if np.random.random() > 0.1 else np.nan
})
# Voeg duplicaten toe
df = pd.DataFrame(data)
duplicate_rows = df.sample(n=5) # Neem 5 willekeurige rijen
df = pd.concat([df, duplicate_rows], ignore_index=True)
# Shuffle de data
df = df.sample(frac=1).reset_index(drop=True)
# Sla op als CSV
df.to_csv('knmi_messy.csv', index=False)
print("Vervuilde KNMI dataset aangemaakt: knmi_messy.csv")
return df
if __name__ == "__main__":
create_messy_knmi_data()
Voer dit script uit om je testdata te maken:
shell
python create_messy_data.py
Alle code voorbeelden van dit project zijn ook op Github te bekijken.
Data inspecteren
Voordat je begint met opschonen, is het belangrijk om te begrijpen wat voor problemen je hebt. Maak een bestand main.py met daarin een functie load_and_inspect_data():
main.py
import pandas as pd
def load_and_inspect_data():
"""Laad de data en inspecteer de problemen."""
# Laad de vervuilde data
df = pd.read_csv('knmi_messy.csv')
print("=== EERSTE INSPECTIE ===")
print(f"Dataset vorm: {df.shape}")
print(f"Kolommen: {list(df.columns)}")
print()
print("=== EERSTE 10 RIJEN ===")
print(df.head(10))
print()
print("=== DATASET INFO ===")
print(df.info())
print()
print("=== DATA TYPES ===")
print(df.dtypes)
print()
return df
if __name__ == "__main__":
df = load_and_inspect_data()
Deze functie voert een eerste inspectie uit van de data. Dit is altijd de eerste stap bij data cleaning. Met df.shape krijg je een tuple met het aantal rijen en kolommen, wat helpt om snel in te schatten hoe groot je dataset is. De df.columns eigenschap toont alle kolomnamen, handig om te zien of de kolommen logische namen hebben en of alles correct is ingelezen.
De methode df.head(10) toont de eerste 10 rijen, waarmee je een visueel beeld krijgt van je data. Vaak spot je hiermee al snel problemen zoals rare waarden of verkeerde types. De methode df.info() geeft een overzicht van het aantal niet-null waarden per kolom, de data types, en het geheugengebruik. Dit is een van de nuttigste methodes voor een eerste inspectie. Ten slotte toont df.dtypes specifiek de data types van elke kolom, wat belangrijk is omdat verkeerde types (bijvoorbeeld getallen als tekst) veel analyses onmogelijk maken.
Als je dit uitvoert, zie je verschillende problemen:
- Sommige temperatuurwaarden zijn
NaN (missende waarden)
- De kolom 'Temperatuur' heeft dtype 'object' in plaats van numeriek
- Er zijn duplicaten in de data
Missende waarden behandelen
Missende waarden detecteren
Met isna() kun je missende waarden detecteren. Voeg de volgende functie toe aan main.py:
main.py
def analyze_missing_values(df):
"""Analyseer missende waarden in de dataset."""
print("=== MISSENDE WAARDEN ANALYSE ===")
# Aantal missende waarden per kolom
missing_count = df.isna().sum()
print("Aantal missende waarden per kolom:")
print(missing_count)
print()
# Percentage missende waarden per kolom
missing_percentage = (missing_count / len(df)) * 100
print("Percentage missende waarden per kolom:")
print(missing_percentage.round(2))
print()
# Welke rijen hebben missende waarden?
rows_with_missing = df[df.isna().any(axis=1)]
print(f"Aantal rijen met missende waarden: {len(rows_with_missing)}")
print("Voorbeelden van rijen met missende waarden:")
print(rows_with_missing.head())
print()
return missing_count, missing_percentage
Deze functie geeft je een volledig overzicht van missende waarden in je dataset. De methode df.isna().sum() telt voor elke kolom hoeveel NaN waarden er zijn, wat je snel inzicht geeft in welke kolommen problemen hebben. Door het aantal missende waarden te delen door de totale lengte van de dataset, zie je de relatieve impact. Een kolom met 5 missende waarden is bijvoorbeeld een groter probleem in een dataset van 50 rijen dan in een dataset van 5000 rijen.
Met df.isna().any(axis=1) selecteer je alle rijen die minimaal één missende waarde bevatten. De parameter axis=1 zorgt ervoor dat pandas per rij (horizontaal) controleert of er een NaN voorkomt. Dit helpt je inschatten hoeveel rijen je zou verliezen als je besluit rijen met missende waarden te verwijderen.
Voeg deze functie toe aan je main.py en roep hem aan na load_and_inspect_data():
if __name__ == "__main__":
df = load_and_inspect_data()
analyze_missing_values(df)
Missende waarden verwijderen met dropna()
Je kunt missende waarden verwijderen met dropna(). Er zijn verschillende manieren:
main.py
def remove_missing_values(df):
"""Verschillende manieren om missende waarden te verwijderen."""
print("=== MISSENDE WAARDEN VERWIJDEREN ===")
print(f"Originele dataset: {df.shape}")
# Verwijder alle rijen met missende waarden
df_no_na = df.dropna()
print(f"Na verwijderen alle rijen met NaN: {df_no_na.shape}")
# Verwijder alleen rijen waar alle waarden missend zijn
df_no_all_na = df.dropna(how='all')
print(f"Na verwijderen rijen waar alles NaN is: {df_no_all_na.shape}")
# Verwijder alleen rijen met missende waarden in specifieke kolommen
df_no_temp_na = df.dropna(subset=['Temperatuur'])
print(f"Na verwijderen rijen met NaN in Temperatuur: {df_no_temp_na.shape}")
print()
return df_no_na, df_no_temp_na
De dropna() methode biedt verschillende opties om missende waarden te verwijderen. Zonder parameters verwijdert df.dropna() elke rij die minimaal één NaN bevat. Dit is de meest agressieve aanpak en kan leiden tot veel dataverlies als je veel kolommen hebt met her en der een missende waarde.
Met de parameter how='all' verwijder je alleen rijen waar alle waarden NaN zijn. Dit is veel conservatiever dan de standaard (how='any') en voorkomt onnodig dataverlies. De subset parameter laat je focussen op specifieke kolommen. Als je bijvoorbeeld dropna(subset=['Temperatuur']) gebruikt, worden alleen rijen verwijderd waar de temperatuur ontbreekt, maar blijven rijen behouden waar alleen de neerslag mist. Dit is handig als bepaalde kolommen belangrijker zijn voor je analyse dan andere.
Missende waarden vervangen met fillna()
Soms wil je missende waarden niet verwijderen, maar vervangen. Dit doe je met fillna():
main.py
def fill_missing_values(df):
"""Verschillende manieren om missende waarden te vervangen."""
print("=== MISSENDE WAARDEN VERVANGEN ===")
# Optie 1: Vervang missende waarden met een vaste waarde
df_option1 = df.copy()
df_option1['Neerslag'] = df_option1['Neerslag'].fillna(0)
print(f"Optie 1 - Vaste waarde (0): {df_option1['Neerslag'].isna().sum()} missende waarden over")
# Optie 2: Vervang missende waarden met het gemiddelde
# (Let op: dit kan alleen als de kolom numeriek is)
df_option2 = df.copy()
neerslag_mean = df_option2['Neerslag'].mean()
df_option2['Neerslag'] = df_option2['Neerslag'].fillna(neerslag_mean)
print(f"Optie 2 - Gemiddelde ({neerslag_mean:.2f}): {df_option2['Neerslag'].isna().sum()} missende waarden over")
# Optie 3: Vervang missende waarden met forward fill (gebruik vorige geldige waarde)
df_option3 = df.copy()
df_option3['Neerslag'] = df_option3['Neerslag'].ffill()
print(f"Optie 3 - Forward fill: {df_option3['Neerslag'].isna().sum()} missende waarden over")
# Optie 4: Vervang missende waarden met backward fill (gebruik volgende geldige waarde)
df_option4 = df.copy()
df_option4['Neerslag'] = df_option4['Neerslag'].bfill()
print(f"Optie 4 - Backward fill: {df_option4['Neerslag'].isna().sum()} missende waarden over")
print()
return df_option2 # Return de versie met gemiddelde voor verder gebruik
Met fillna() vervang je missende waarden in plaats van ze te verwijderen. Dit is nuttig als het verwijderen van rijen te veel dataverlies zou opleveren. De methode fillna(0) vervangt bijvoorbeeld alle NaN waarden door 0. Dit is logisch voor neerslag waar een ontbrekende waarde waarschijnlijk betekent dat er geen neerslag was gemeten.
Je kunt missende waarden ook vervangen door het gemiddelde (mean()), de mediaan (median()) of een andere statistische maat. Dit werkt alleen bij numerieke kolommen. Met ffill()' krijgt elke missende waarde de laatst bekende waarde, wat handig is bij tijdreeksen waar je verwacht dat waarden geleidelijk veranderen. De methode bfill()' gebruikt juist de eerstvolgende bekende waarde om de lege plek op te vullen.
In het voorbeeld maak je voor elke optie een kopie van de originele DataFrame met df.copy(). Dit is belangrijk omdat je zo elke methode onafhankelijk kunt oefenen zonder dat ze elkaar beïnvloeden.
Duplicaten detecteren
value_counts() voor duplicaten
Met value_counts() kun je de frequentie van waarden bekijken en duplicaten detecteren:
main.py
def analyze_duplicates(df):
"""Analyseer duplicaten in de dataset."""
print("=== DUPLICATEN ANALYSE ===")
# Check voor complete duplicaten (alle kolommen hetzelfde)
duplicate_rows = df.duplicated()
print(f"Aantal complete duplicaten: {duplicate_rows.sum()}")
if duplicate_rows.sum() > 0:
print("Voorbeelden van gedupliceerde rijen:")
print(df[duplicate_rows].head())
print()
# Check voor duplicaten op basis van specifieke kolommen
duplicate_measurements = df.duplicated(subset=['Jaar', 'Maand'])
print(f"Aantal duplicaten op Jaar+Maand: {duplicate_measurements.sum()}")
# Gebruik group_by om de frequentie te bekijken
print("\nFrequentie van Jaar+Maand combinaties:")
year_month_counts = df.groupby(['Jaar', 'Maand']).size()
duplicates_only = year_month_counts[year_month_counts > 1]
print(duplicates_only)
return duplicate_rows, duplicate_measurements
Duplicaten in je data kunnen ontstaan door import fouten, systeem bugs of menselijke fouten bij data invoer. Deze functie helpt je duplicaten te identificeren op verschillende manieren. De methode df.duplicated() geeft een boolean Series terug die aangeeft welke rijen exact identiek zijn aan een eerdere rij. Standaard wordt de eerste voorkomende rij gemarkeerd als False (geen duplicaat) en alle volgende identieke rijen als True (duplicaat). Met .sum() tel je hoeveel duplicaten er zijn.
Je kunt dit gedrag aanpassen met de keep parameter. Met keep='first' (de standaard) wordt de eerste rij behouden en latere als duplicaat gemarkeerd. Met keep='last' gebeurt het omgekeerde: de laatste voorkomende rij wordt als origineel beschouwd. Met keep=False worden alle voorkomende rijen inclusief de eerste als duplicaat gemarkeerd, wat handig is als je álle betrokken rijen wilt zien.
Met de parameter subset=['Jaar', 'Maand'] focus je alleen op specifieke kolommen. Dit is handig bij tijdreeksen waar je per jaar en maand maar één meting zou moeten hebben. De methode markeert dan rijen als duplicaat als jaar en maand identiek zijn, ongeacht de waarden in andere kolommen. Door groupby() te combineren met .size() zie je hoe vaak elke combinatie voorkomt. Dit geeft meer inzicht dan alleen het aantal duplicaten - je ziet bijvoorbeeld dat bepaalde jaar-maand combinaties 3x voorkomen terwijl andere maar 2x voorkomen.
Duplicaten verwijderen
Voeg de volgende functie toe om duplicaten te verwijderen:
main.py
def remove_duplicates(df):
"""Verwijder duplicaten uit de dataset."""
print("=== DUPLICATEN VERWIJDEREN ===")
print(f"Originele dataset: {df.shape}")
# Verwijder complete duplicaten
df_no_duplicates = df.drop_duplicates()
print(f"Na verwijderen complete duplicaten: {df_no_duplicates.shape}")
# Verwijder duplicaten op basis van specifieke kolommen
df_unique_measurements = df.drop_duplicates(subset=['Jaar', 'Maand'])
print(f"Na verwijderen duplicaten op Jaar+Maand: {df_unique_measurements.shape}")
# Behoud alleen de eerste of laatste duplicate
df_keep_first = df.drop_duplicates(subset=['Jaar', 'Maand'], keep='first')
df_keep_last = df.drop_duplicates(subset=['Jaar', 'Maand'], keep='last')
print(f"Behoud eerste duplicate: {df_keep_first.shape}")
print(f"Behoud laatste duplicate: {df_keep_last.shape}")
return df_no_duplicates
Met drop_duplicates() verwijder je dubbele rijen uit je dataset. Zonder parameters verwijdert de methode rijen die volledig identiek zijn aan een eerdere rij. Standaard wordt de eerste voorkomende rij behouden.
Met subset=['Jaar', 'Maand'] bepaal je welke kolommen uniek moeten zijn. Andere kolommen mogen verschillen. Dit is handig als je weet welke kolommen samen een unieke sleutel vormen. De keep parameter bepaalt welke van de duplicaten je behoudt. Met keep='first' (de standaard) behoud je de eerste voorkomende rij, met keep='last' behoud je de laatste voorkomende rij, en met keep=False verwijder je alle duplicaten inclusief het origineel. Bij tijdreeksdata gebruik je vaak keep='last' omdat de meest recente meting waarschijnlijk de meest accurate is.
Data types controleren en converteren
Verkeerde data types zijn een veelvoorkomend probleem. Een kolom met getallen kan als tekst (object) worden opgeslagen, wat berekeningen onmogelijk maakt. Deze functie helpt je data types te inspecteren.
dtypes en info() voor type inspectie
main.py
def analyze_data_types(df):
"""Analyseer de data types in de dataset."""
print("=== DATA TYPES ANALYSE ===")
# Bekijk alle data types
print("Data types per kolom:")
print(df.dtypes)
print()
# Bekijk unieke waarden in problematische kolommen
print("Unieke waarden in Temperatuur kolom:")
print(df['Temperatuur'].value_counts())
print()
# Identificeer problematische waarden
temp_series = df['Temperatuur']
# Welke waarden zijn geen getallen?
non_numeric = temp_series[pd.to_numeric(temp_series, errors='coerce').isna()]
print("Niet-numerieke waarden in Temperatuur:")
print(non_numeric.value_counts())
print()
return df.dtypes
De eigenschap df.dtypes toont het datatype van elke kolom. Veelvoorkomende types zijn int64, float64, object (tekst), en datetime64. Als een numerieke kolom object is, is er een probleem.
Bij twijfel over een kolom helpt value_counts() om de unieke waarden en hun frequentie te zien. Zo ontdek je bijvoorbeeld dat een temperatuurkolom waarden zoals "15.2°C" of "onbekend" bevat.
De combinatie pd.to_numeric(errors='coerce') probeert waarden naar getallen te converteren. Waarden die niet converteren worden NaN. Door deze te filteren zie je precies welke waarden problemen veroorzaken.
Type conversies
Het converteren van data types naar het juiste formaat is vaak een proces in meerdere stappen. Deze functie laat een typische workflow zien.
main.py
import numpy as np
def convert_data_types(df):
"""Converteer data types naar de juiste formaten."""
print("=== DATA TYPES CONVERTEREN ===")
# Maak een kopie
df_converted = df.copy()
# Probleem: Temperatuur kolom bevat strings zoals "15.2°C" en "onbekend"
print("Originele Temperatuur dtype:", df_converted['Temperatuur'].dtype)
# Stap 1: Vervang problematische waarden
df_converted['Temperatuur'] = df_converted['Temperatuur'].replace('onbekend', np.nan)
# Stap 2: Verwijder °C uit temperatuur strings
df_converted['Temperatuur'] = df_converted['Temperatuur'].astype(str).str.replace('°C', '')
# Stap 3: Converteer naar numeriek met error handling
df_converted['Temperatuur'] = pd.to_numeric(df_converted['Temperatuur'], errors='coerce')
print("Nieuwe Temperatuur dtype:", df_converted['Temperatuur'].dtype)
print("Missende waarden na conversie:", df_converted['Temperatuur'].isna().sum())
# Converteer Jaar naar integer (als het nog geen integer is)
df_converted['Jaar'] = df_converted['Jaar'].astype(int)
# Bekijk het resultaat
print("\nNieuwe data types:")
print(df_converted.dtypes)
print()
print("Voorbeeld van geconverteerde data:")
print(df_converted.head())
return df_converted
Waarden zoals "onbekend" kunnen niet naar een getal worden geconverteerd. Met replace() vervang je deze eerst door NaN, wat pandas wel begrijpt als missende numerieke waarde.
Als getallen eenheden bevatten ("15.2°C"), moet je deze eerst verwijderen. De combinatie .astype(str).str.replace('°C', '') zorgt ervoor dat alle waarden eerst strings zijn, waarna je het graden-symbool kunt strippen. De methode pd.to_numeric() met errors='coerce' is veiliger dan .astype(float). Als een waarde niet converteert, krijg je NaN in plaats van een foutmelding. Dit voorkomt dat je hele conversie mislukt door één rare waarde.
Voor simpele conversies waar je zeker weet dat de data klopt, gebruik je .astype(). Een jaar kan veilig naar int worden geconverteerd als je zeker weet dat er alleen getallen in staan. Controleer na conversie altijd het aantal missende waarden - dit vertelt je hoeveel waarden niet konden worden geconverteerd.
Complete opschoon-workflow
Nu kun je alles combineren in één complete workflow:
main.py
def complete_cleaning_workflow():
"""Complete workflow voor het opschonen van de data."""
print("=== COMPLETE DATA CLEANING WORKFLOW ===")
# Stap 1: Laad data
df = pd.read_csv('knmi_messy.csv')
print(f"1. Data geladen: {df.shape}")
# Stap 2: Converteer data types
df = convert_data_types(df)
print(f"2. Types geconverteerd: {df.shape}")
# Stap 3: Verwijder duplicaten
df = df.drop_duplicates(subset=['Jaar', 'Maand'])
print(f"3. Duplicaten verwijderd: {df.shape}")
# Stap 4: Behandel missende waarden
# Voor dit voorbeeld: verwijder rijen met missende temperatuur
df = df.dropna(subset=['Temperatuur'])
print(f"4. Missende temperaturen verwijderd: {df.shape}")
# Stap 5: Vul overige missende waarden
df['Neerslag'] = df['Neerslag'].fillna(df['Neerslag'].mean())
print(f"5. Missende neerslag gevuld: {df.shape}")
# Stap 6: Sla schone data op
df.to_csv('knmi_clean.csv', index=False)
print("6. Schone data opgeslagen als knmi_clean.csv")
# Stap 7: Valideer resultaat
print("\n=== VALIDATIE ===")
print("Finale data types:")
print(df.dtypes)
print()
print("Missende waarden:")
print(df.isna().sum())
print()
print("Duplicaten:")
print(f"Complete duplicaten: {df.duplicated().sum()}")
print(f"Jaar+Maand duplicaten: {df.duplicated(subset=['Jaar', 'Maand']).sum()}")
return df
if __name__ == "__main__":
# Voer de complete workflow uit
cleaned_df = complete_cleaning_workflow()
Deze workflow combineert alle technieken in de juiste volgorde. De volgorde is belangrijk. Begin met het converteren van data types, anders kun je geen berekeningen doen zoals het gemiddelde voor fillna(). Verwijder daarna duplicaten voordat je missende waarden behandelt, zodat je niet onnodig duplicaten opvult met gemiddelden. Behandel als laatste missende waarden.
Voor temperatuur verwijder je rijen met missende waarden omdat temperatuur belangrijk is voor de analyse. Voor neerslag vul je missende waarden op met het gemiddelde omdat deze kolom minder kritiek is. Na het opschonen controleer je altijd of het resultaat klopt. Check de data types, tel missende waarden, en verifieer dat er geen duplicaten meer zijn. Dit voorkomt dat je analyses uitvoert op data die nog steeds problemen heeft.
Sla de opgeschoonde data op in een nieuw bestand. Bewaar altijd je originele data - zo kun je altijd terug als je andere keuzes wilt maken bij het opschonen.
Conclusie
Je hebt nu geleerd hoe je de drie meest voorkomende problemen bij data cleaning kunt aanpakken:
- Missende waarden met
dropna() en fillna()
- Duplicaten met
value_counts() en drop_duplicates()
- Data types met
dtypes, info() en to_numeric()
Deze technieken vormen de basis van data cleaning en zul je bij vrijwel elk data-analyse project gebruiken. Met opgeschoonde data kun je vervolgens betrouwbare analyses uitvoeren.
Belangrijke tips:
- Inspecteer je data altijd eerst met
info() en head()
- Maak kopieën van je data voordat je grote wijzigingen doorvoert
- Valideer je resultaten na elke cleaning stap
- Documenteer welke keuzes je maakt bij het opschonen
Voor meer geavanceerde technieken zoals string operaties en categorische data, zie de pandas documentatie.
Alle code voorbeelden van dit project zijn ook op Github te bekijken.