Data Cleaning & Preprocessing
Data cleaning en preprocessing vormen een groot deel van elk AI-project. Ruwe data is zelden direct bruikbaar — je moet ontbrekende waarden oplossen, features transformeren en de data in het juiste formaat krijgen voor je model.
Werken met Pandas
Pandas is de standaard Python-library voor datamanipulatie. Hieronder de meest gebruikte operaties.
Data inladen en verkennen
| import pandas as pd
# Inladen
df = pd.read_csv("data.csv")
# Eerste indruk
df.head() # Eerste 5 rijen
df.shape # (rijen, kolommen)
df.info() # Datatypes en missing values
df.describe() # Statistische samenvatting
df.columns # Kolomnamen
|
Selecteren en filteren
| # Kolom selecteren
df["kolom_naam"]
df[["kolom1", "kolom2"]]
# Rijen filteren
df[df["leeftijd"] > 30]
df[(df["leeftijd"] > 30) & (df["stad"] == "Amsterdam")]
# Specifieke cel
df.loc[5, "kolom_naam"] # Op label
df.iloc[5, 2] # Op positie
|
Aanpassen en toevoegen
| # Nieuwe kolom
df["nieuwe_kolom"] = df["kolom1"] + df["kolom2"]
# Kolom verwijderen
df = df.drop(columns=["ongewenste_kolom"])
# Waarden vervangen
df["status"] = df["status"].replace({"oud": "nieuw"})
# Datatype wijzigen
df["kolom"] = df["kolom"].astype(float)
|
Oplossen van missing values
Ontbrekende waarden (NaN) komen in bijna elke dataset voor. De aanpak hangt af van hoeveel data ontbreekt en waarom.
Detecteren
| # Hoeveel missing per kolom
df.isnull().sum()
# Percentage missing
df.isnull().sum() / len(df) * 100
# Rijen met missing values
df[df.isnull().any(axis=1)]
|
Strategieën
| Strategie |
Wanneer toepassen |
Code |
| Verwijderen (rijen) |
Weinig missing, willekeurig verdeeld |
df.dropna() |
| Verwijderen (kolommen) |
Kolom heeft >50% missing |
df.drop(columns=["kolom"]) |
| Vullen met gemiddelde |
Numeriek, willekeurig missing |
df["kolom"].fillna(df["kolom"].mean()) |
| Vullen met mediaan |
Numeriek met uitschieters |
df["kolom"].fillna(df["kolom"].median()) |
| Vullen met modus |
Categorisch |
df["kolom"].fillna(df["kolom"].mode()[0]) |
| Forward/backward fill |
Tijdreeksen |
df["kolom"].fillna(method="ffill") |
| Voorspellende imputatie |
Patronen in data nuttig |
Gebruik KNNImputer of IterativeImputer |
| from sklearn.impute import SimpleImputer, KNNImputer
# Simpele imputatie
imputer = SimpleImputer(strategy="mean")
df[["kolom"]] = imputer.fit_transform(df[["kolom"]])
# KNN imputatie
knn_imputer = KNNImputer(n_neighbors=5)
df_imputed = knn_imputer.fit_transform(df)
|
!!! warning "Let op"
Analyseer altijd waarom waarden ontbreken. Als data niet willekeurig ontbreekt (bijv. mensen met laag inkomen vullen inkomensvraag niet in), kan imputatie je analyse vertekenen.
One-Hot Encoding
Veel ML-modellen werken alleen met numerieke data. One-hot encoding zet categorische variabelen om naar binaire kolommen.
Voorbeeld
| Kleur |
→ |
Kleur_Rood |
Kleur_Blauw |
Kleur_Groen |
| Rood |
|
1 |
0 |
0 |
| Blauw |
|
0 |
1 |
0 |
| Groen |
|
0 |
0 |
1 |
Implementatie
| # Met Pandas
df_encoded = pd.get_dummies(df, columns=["kleur"], prefix="kleur")
# Met scikit-learn
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse=False, handle_unknown="ignore")
encoded = encoder.fit_transform(df[["kleur"]])
|
Wanneer wel/niet gebruiken
| Situatie |
Aanpak |
| Categorieën zonder rangorde (nominaal) |
One-hot encoding |
| Categorieën met rangorde (ordinaal) |
Label encoding (0, 1, 2, ...) |
| Zeer veel categorieën (>20) |
Target encoding of embeddings |
| Tree-based modellen |
Label encoding vaak voldoende |
!!! tip "Drop first"
Bij one-hot encoding kun je één kolom weglaten om multicollineariteit te voorkomen: pd.get_dummies(df, drop_first=True)
Normalisatie en Standaardisatie
Veel modellen presteren beter als features op dezelfde schaal liggen.
Methoden
| Methode |
Formule |
Bereik |
Wanneer gebruiken |
| Min-Max Normalisatie |
$(x - min) / (max - min)$ |
[0, 1] |
Geen uitschieters, bounded output nodig |
| Standaardisatie (Z-score) |
$(x - \mu) / \sigma$ |
Geen vast bereik |
Normaal verdeelde data, uitschieters aanwezig |
| Robust Scaling |
$(x - mediaan) / IQR$ |
Geen vast bereik |
Veel uitschieters |
Implementatie
1
2
3
4
5
6
7
8
9
10
11
12
13 | from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler
# Min-Max
scaler = MinMaxScaler()
df_scaled = scaler.fit_transform(df[["kolom1", "kolom2"]])
# Standaardisatie
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df[["kolom1", "kolom2"]])
# Robust
scaler = RobustScaler()
df_scaled = scaler.fit_transform(df[["kolom1", "kolom2"]])
|
!!! warning "Fit op training, transform op test"
Fit de scaler alleen op trainingsdata om data leakage te voorkomen:
| scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
|
Train, Validatie en Test sets
Je data moet worden opgesplitst om je model eerlijk te evalueren.
De drie sets
| Set |
Doel |
Typisch percentage |
| Training |
Model trainen |
60-80% |
| Validatie |
Hyperparameters tunen, modelkeuze |
10-20% |
| Test |
Finale, onpartijdige evaluatie |
10-20% |
Implementatie
| from sklearn.model_selection import train_test_split
# Eerst train+val vs test
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Dan train vs val
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.25, random_state=42 # 0.25 van 0.8 = 0.2
)
|
Cross-validation
Bij beperkte data kun je k-fold cross-validation gebruiken:
| from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X, y, cv=5) # 5-fold
print(f"Gemiddelde score: {scores.mean():.3f} (+/- {scores.std():.3f})")
|
!!! note "Stratified split"
Bij ongebalanceerde classificatie, gebruik stratify=y om de klasseverdeling te behouden:
| X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
|
Feature Creation
Feature creation (of feature engineering) is het maken van nieuwe features uit bestaande data om het model te helpen patronen te vinden.
Technieken
| Techniek |
Voorbeeld |
| Combineren |
df["bmi"] = df["gewicht"] / (df["lengte"] ** 2) |
| Datum extractie |
Dag van de week, maand, seizoen uit datetime |
| Binning |
Leeftijd → leeftijdsgroepen (18-25, 26-35, ...) |
| Aggregatie |
Gemiddelde aankoop per klant over laatste 30 dagen |
| Text features |
Woordenfrequentie, tekstlengte, sentiment |
| Interacties |
df["feature1_x_feature2"] = df["f1"] * df["f2"] |
| # Datum features
df["dag_van_week"] = df["datum"].dt.dayofweek
df["maand"] = df["datum"].dt.month
df["is_weekend"] = df["dag_van_week"].isin([5, 6]).astype(int)
# Binning
df["leeftijdsgroep"] = pd.cut(
df["leeftijd"],
bins=[0, 18, 30, 50, 100],
labels=["kind", "jong", "midden", "senior"]
)
|
PCA (Principal Component Analysis)
PCA is een techniek voor dimensiereductie: het reduceert het aantal features terwijl zoveel mogelijk informatie behouden blijft.
Wanneer gebruiken?
- Veel features (hoge dimensionaliteit)
- Features zijn gecorreleerd
- Visualisatie van hoog-dimensionale data
- Snellere training door minder features
Hoe werkt het (simpel)?
- PCA vindt de richtingen (componenten) waarin de data het meeste varieert
- De eerste component vangt de meeste variantie, de tweede de op-één-na meeste, etc.
- Je behoudt alleen de eerste N componenten
Implementatie
1
2
3
4
5
6
7
8
9
10
11
12
13 | from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# Eerst standaardiseren (belangrijk voor PCA)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# PCA toepassen
pca = PCA(n_components=0.95) # Behoud 95% van de variantie
X_pca = pca.fit_transform(X_scaled)
print(f"Origineel: {X.shape[1]} features")
print(f"Na PCA: {X_pca.shape[1]} componenten")
|
!!! tip "Hoeveel componenten?"
- Gebruik n_components=0.95 om 95% van de variantie te behouden
- Of plot de "explained variance ratio" om een knik te vinden
Under- en Oversampling
Bij ongebalanceerde datasets (bijv. 95% negatief, 5% positief) kan je model de minderheidsklasse negeren.
Technieken
| Techniek |
Beschrijving |
Risico |
| Undersampling |
Verwijder voorbeelden uit de meerderheidsklasse |
Verlies van informatie |
| Oversampling |
Dupliceer voorbeelden uit de minderheidsklasse |
Overfitting |
| SMOTE |
Genereer synthetische voorbeelden voor minderheidsklasse |
Kan ruis introduceren |
Implementatie
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | from imblearn.over_sampling import SMOTE, RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler
# Random oversampling
ros = RandomOverSampler(random_state=42)
X_resampled, y_resampled = ros.fit_resample(X_train, y_train)
# SMOTE
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
# Random undersampling
rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)
|
!!! warning "Alleen op trainingsdata"
Pas sampling alleen toe op de trainingsdata, nooit op validatie of test. Anders krijg je een vertekend beeld van de werkelijke performance.
Data Augmentatie
Data augmentatie vergroot je dataset door variaties van bestaande data te maken. Dit is vooral populair bij beelddata.
Beeldaugmentatie
| Techniek |
Beschrijving |
| Rotatie |
Beeld draaien (bijv. -15° tot +15°) |
| Flip |
Horizontaal of verticaal spiegelen |
| Crop |
Willekeurig deel van het beeld uitsnijden |
| Zoom |
In- of uitzoomen |
| Brightness/Contrast |
Helderheid en contrast aanpassen |
| Noise |
Ruis toevoegen |
1
2
3
4
5
6
7
8
9
10
11
12 | from tensorflow.keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True,
zoom_range=0.2
)
# Augmented batches genereren
augmented_images = datagen.flow(X_train, y_train, batch_size=32)
|
Tekstaugmentatie
| Techniek |
Beschrijving |
| Synonym replacement |
Woorden vervangen door synoniemen |
| Random insertion |
Willekeurige woorden invoegen |
| Random deletion |
Willekeurige woorden verwijderen |
| Back-translation |
Vertalen naar andere taal en terug |
Synthetische Data
Synthetische data is kunstmatig gegenereerde data die de eigenschappen van echte data nabootst.
Toepassingen
| Toepassing |
Reden |
| Privacy |
Echte data bevat gevoelige informatie |
| Schaarste |
Te weinig echte data beschikbaar |
| Balancering |
Minderheidsklasse aanvullen |
| Testing |
Edge cases simuleren |
Generatiemethoden
| Methode |
Beschrijving |
| Statistische methoden |
Sampling uit geschatte verdelingen |
| SMOTE |
Interpolatie tussen bestaande datapunten |
| GANs |
Generative Adversarial Networks voor realistische data |
| VAEs |
Variational Autoencoders |
| LLMs |
Taalmodellen voor tekstgeneratie |
| # Simpele synthetische data met numpy
import numpy as np
# Genereer data die lijkt op originele verdeling
synthetic_data = np.random.normal(
loc=original_data.mean(),
scale=original_data.std(),
size=(1000, original_data.shape[1])
)
|
Aandachtspunten
| Aandachtspunt |
Toelichting |
| Kwaliteitscontrole |
Check of synthetische data realistische patronen heeft |
| Geen nieuwe informatie |
Synthetische data kan geen patronen bevatten die niet in originele data zitten |
| Privacy niet gegarandeerd |
Sommige methoden kunnen originele datapunten "onthouden" |
| Validatie apart |
Test altijd op echte data, niet op synthetische |