Feature Engineering voor Machine Learning met Python: De Complete Gids

Ontdek hoe je met Python, pandas en scikit-learn krachtige features bouwt voor ML-modellen. Van ontbrekende waarden en categorische variabelen tot tijdreeksfeatures en geautomatiseerde pipelines — met praktische codevoorbeelden.

Feature Engineering voor Machine Learning met Python: De Complete Gids

Als je ooit een machine learning model hebt gebouwd, dan weet je: de keuze van het algoritme is maar een klein stukje van de puzzel. Het echte verschil? Dat zit 'm in de voorbereiding van je data — en dan specifiek in het creëren van goede features. Feature engineering is het proces van het selecteren, transformeren en aanmaken van inputvariabelen zodat je model er optimaal van kan leren.

Eerlijk gezegd, feature engineering kan tot 80% van de tijd van een data science project opslokken. En dat is niet voor niks. Een slimme set features kan het verschil maken tussen een model dat 'gewoon okee' is en eentje dat écht waarde toevoegt. Zelfs het meest geavanceerde deep learning model gaat onderuit als de input rommelig is.

In deze handleiding loop ik stap voor stap met je door de belangrijkste feature engineering technieken in Python. We gebruiken pandas, NumPy en scikit-learn — drie bibliotheken die je als data scientist eigenlijk altijd nodig hebt. Elk concept komt met praktische codevoorbeelden die je meteen kunt toepassen.

Wat is Feature Engineering en Waarom is het Belangrijk?

Feature engineering is de kunst (en ja, het ís een kunst) van het omzetten van ruwe data naar bruikbare inputvariabelen voor ML-modellen. Het gaat verder dan kolommen selecteren — het omvat het creëren van nieuwe variabelen die verborgen patronen in je data blootleggen.

Stel je voor dat je een model bouwt om huisprijzen te voorspellen. Je hebt de oppervlakte in vierkante meters en het aantal kamers. Maar wat als je ook de prijs per vierkante meter berekent? Of de verhouding tussen kamers en oppervlakte? Zulke afgeleide features kunnen patronen zichtbaar maken die de oorspronkelijke variabelen afzonderlijk verborgen houden.

De Impact op Modelprestaties

Het verschil dat goede feature engineering maakt is niet marginaal — het is soms ronduit verbluffend. In de praktijk zie je regelmatig dat:

  • Betere features overtreffen betere algoritmen: Een lineair model met zorgvuldig ontworpen features presteert vaak beter dan een complex neuraal netwerk met ruwe data.
  • Domeinkennis is onvervangbaar: Geen enkel geautomatiseerd systeem kan de inzichten van een domeinexpert volledig vervangen. Weten dat de ratio schuld/inkomen belangrijk is voor kredietrisico? Dat vereist financiële kennis.
  • Snellere training en betere interpretatie: Goede features maken modellen niet alleen nauwkeuriger, maar ook sneller om te trainen en makkelijker te begrijpen.

Goed, laten we beginnen met de fundamentele technieken. Eerst: ontbrekende waarden.

Ontbrekende Waarden Behandelen

De eerste stap in vrijwel elk feature engineering proces is het afhandelen van ontbrekende waarden. Ruwe datasets bevatten bijna altijd missende data — door onvolledige invoer, systeemfouten of bewuste keuzes. Hoe je hiermee omgaat heeft directe invloed op je model.

Ontbrekende Waarden Detecteren

Pandas biedt handige methoden om ontbrekende waarden op te sporen:

import pandas as pd
import numpy as np

# Voorbeelddataset laden
df = pd.DataFrame({
    'leeftijd': [25, 30, np.nan, 45, 50, np.nan, 35],
    'salaris': [30000, 45000, 50000, np.nan, 80000, 55000, np.nan],
    'stad': ['Amsterdam', 'Rotterdam', None, 'Utrecht', 'Amsterdam', 'Den Haag', 'Rotterdam'],
    'opleiding': ['HBO', 'WO', 'WO', 'MBO', None, 'HBO', 'WO']
})

# Overzicht van ontbrekende waarden
print(df.isnull().sum())
print(f"\nPercentage ontbrekend per kolom:")
print((df.isnull().sum() / len(df) * 100).round(1))

Strategie 1: Verwijderen

De meest simpele aanpak is het verwijderen van rijen of kolommen met ontbrekende waarden. Dit werkt alleen als het percentage missende data klein is en de ontbrekende waarden willekeurig verdeeld zijn:

# Rijen verwijderen met ontbrekende waarden
df_clean = df.dropna()

# Alleen rijen verwijderen waar specifieke kolommen missen
df_clean = df.dropna(subset=['leeftijd', 'salaris'])

# Kolommen verwijderen met meer dan 50% ontbrekende waarden
drempel = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=drempel)

Strategie 2: Imputatie

Vaak is het beter om ontbrekende waarden te vervangen dan ze te verwijderen. Welke imputatiemethode je kiest hangt af van het type variabele en de verdeling van je data:

from sklearn.impute import SimpleImputer

# Numerieke kolommen: mediaan-imputatie (robuust tegen uitschieters)
num_imputer = SimpleImputer(strategy='median')
df[['leeftijd', 'salaris']] = num_imputer.fit_transform(df[['leeftijd', 'salaris']])

# Categorische kolommen: modus-imputatie (meest voorkomende waarde)
cat_imputer = SimpleImputer(strategy='most_frequent')
df[['stad', 'opleiding']] = cat_imputer.fit_transform(df[['stad', 'opleiding']])

print(df)

Strategie 3: Indicator Toevoegen

Soms is het feit dat een waarde ontbreekt op zichzelf informatief. Dan kun je een binaire indicator toevoegen:

# Indicator-kolom voor ontbrekende waarden
df['salaris_ontbreekt'] = df['salaris'].isnull().astype(int)

# Dit behoudt de informatie dat de waarde oorspronkelijk ontbrak,
# zelfs nadat je de NaN hebt geimputeerd

Dit patroon is bijzonder nuttig wanneer het ontbreken van data niet willekeurig is. Denk bijvoorbeeld aan mensen met een hoog inkomen die vaker hun salaris niet invullen — die indicator draagt dan waardevolle informatie.

Categorische Variabelen Coderen

Machine learning algoritmen werken met getallen, niet met tekst. Dus categorische variabelen (zoals stad, kleur of productcategorie) moeten worden omgezet naar numerieke waarden. De coderingsmethode die je kiest kan een flink verschil maken.

One-Hot Encoding

One-hot encoding maakt een aparte binaire kolom voor elke unieke categorie. Dit is dé standaardmethode voor nominale variabelen — variabelen zonder natuurlijke volgorde:

# One-hot encoding met pandas
df_encoded = pd.get_dummies(df, columns=['stad'], prefix='stad', dtype=int)
print(df_encoded.head())

# One-hot encoding met scikit-learn (aanbevolen voor pipelines)
from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
stad_encoded = encoder.fit_transform(df[['stad']])
print(f"Categorieen: {encoder.categories_}")
print(stad_encoded)

Let op: One-hot encoding kan leiden tot een explosie van features bij variabelen met veel unieke waarden (hoge cardinaliteit). Bij meer dan 20-30 categorieën is het slim om alternatieven te overwegen.

Label Encoding

Label encoding kent een uniek geheel getal toe aan elke categorie. Dit is geschikt voor ordinale variabelen — variabelen met een natuurlijke volgorde:

from sklearn.preprocessing import LabelEncoder, OrdinalEncoder

# LabelEncoder voor een enkele kolom
le = LabelEncoder()
df['opleiding_encoded'] = le.fit_transform(df['opleiding'])
print(dict(zip(le.classes_, le.transform(le.classes_))))

# OrdinalEncoder met expliciete volgorde (aanbevolen)
ordinal_encoder = OrdinalEncoder(
    categories=[['MBO', 'HBO', 'WO']]  # van laag naar hoog
)
df['opleiding_ordinaal'] = ordinal_encoder.fit_transform(df[['opleiding']])
print(df[['opleiding', 'opleiding_ordinaal']])

Target Encoding

Target encoding vervangt elke categorie door het gemiddelde van de doelvariabele voor die categorie. Krachtig bij hoge cardinaliteit, maar je moet wel opletten voor data-lekkage:

from sklearn.preprocessing import TargetEncoder

# Voorbeelddata met doelvariabele
df_target = pd.DataFrame({
    'stad': ['Amsterdam', 'Rotterdam', 'Amsterdam', 'Utrecht',
             'Rotterdam', 'Amsterdam', 'Utrecht', 'Rotterdam'],
    'prijs': [350000, 250000, 400000, 280000,
              230000, 380000, 300000, 260000]
})

# Target encoding (scikit-learn 1.3+)
te = TargetEncoder(smooth='auto')
df_target['stad_encoded'] = te.fit_transform(
    df_target[['stad']], df_target['prijs']
)
print(df_target)

De smooth-parameter voorkomt overfitting bij categorieën met weinig observaties door het categoriegemiddelde te mengen met het globale gemiddelde.

Numerieke Features Schalen en Transformeren

Veel ML-algoritmen — met name die gebaseerd zijn op afstanden (KNN, SVM) of gradiënten (neurale netwerken, logistische regressie) — presteren beter wanneer numerieke features op dezelfde schaal staan. Dit is een van die dingen die je in het begin misschien over het hoofd ziet, maar het maakt echt verschil.

Standaardisatie (Z-score Normalisatie)

Standaardisatie transformeert features zodat ze een gemiddelde van 0 en een standaarddeviatie van 1 hebben:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
numerieke_kolommen = ['leeftijd', 'salaris']

df[numerieke_kolommen] = scaler.fit_transform(df[numerieke_kolommen])
print(f"Gemiddelde na scaling: {df[numerieke_kolommen].mean().values}")
print(f"Std na scaling: {df[numerieke_kolommen].std().values}")

Min-Max Normalisatie

Min-max scaling brengt waarden naar een vast bereik, meestal [0, 1]:

from sklearn.preprocessing import MinMaxScaler

mm_scaler = MinMaxScaler(feature_range=(0, 1))
df[numerieke_kolommen] = mm_scaler.fit_transform(df[numerieke_kolommen])

Robuuste Scaling

Heb je last van uitschieters in je dataset? Dan is RobustScaler je vriend. Het gebruikt de mediaan en het interkwartielafstand (IQR) in plaats van gemiddelde en standaarddeviatie:

from sklearn.preprocessing import RobustScaler

robust_scaler = RobustScaler()
df[numerieke_kolommen] = robust_scaler.fit_transform(df[numerieke_kolommen])

Logaritmische en Power Transformaties

Scheve verdelingen kunnen je modelprestaties flink ondermijnen. Logaritmische transformaties helpen om scheve data meer normaal verdeeld te maken:

from sklearn.preprocessing import PowerTransformer

# Yeo-Johnson transformatie (werkt ook met negatieve waarden)
pt = PowerTransformer(method='yeo-johnson')
df['salaris_transformed'] = pt.fit_transform(df[['salaris']])

# Eenvoudige log-transformatie (alleen voor positieve waarden)
df['salaris_log'] = np.log1p(df['salaris'])  # log1p = log(1+x), veilig voor 0

Nieuwe Features Creëren

Het creëren van nieuwe features vanuit bestaande variabelen — dáár wordt feature engineering echt leuk. Dit is vaak het verschil tussen een goed model en een uitstekend model.

Interactiefeatures

Interactiefeatures combineren twee of meer variabelen om nieuwe patronen vast te leggen:

# Voorbeeld: vastgoeddata
vastgoed = pd.DataFrame({
    'oppervlakte': [80, 120, 65, 200, 150],
    'kamers': [3, 4, 2, 6, 5],
    'bouwjaar': [1990, 2005, 1975, 2020, 2015],
    'prijs': [280000, 450000, 195000, 680000, 520000]
})

# Interactiefeatures berekenen
vastgoed['prijs_per_m2'] = vastgoed['prijs'] / vastgoed['oppervlakte']
vastgoed['m2_per_kamer'] = vastgoed['oppervlakte'] / vastgoed['kamers']
vastgoed['leeftijd_woning'] = 2026 - vastgoed['bouwjaar']

print(vastgoed)

Polynomiale Features

Polynomiale features helpen je om niet-lineaire relaties te modelleren met lineaire algoritmen:

from sklearn.preprocessing import PolynomialFeatures

# Polynomiale features genereren (graad 2)
poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False)
X = vastgoed[['oppervlakte', 'kamers']].values
X_poly = poly.fit_transform(X)

# Bekijk de gegenereerde feature-namen
print(f"Features: {poly.get_feature_names_out()}")
print(f"Oorspronkelijk: {X.shape[1]} features -> Na transformatie: {X_poly.shape[1]} features")

Waarschuwing: Polynomiale features groeien exponentieel. Bij 10 features en graad 3 krijg je al 285 nieuwe features! Gebruik dit spaarzaam en combineer het altijd met feature selectie of regularisatie.

Binning en Discretisatie

Soms is het handig om continue variabelen om te zetten naar categorieën. Dit kan helpen bij het vastleggen van niet-lineaire effecten:

# Leeftijdsgroepen maken met pd.cut
df['leeftijdsgroep'] = pd.cut(
    df['leeftijd'],
    bins=[0, 25, 35, 45, 55, 100],
    labels=['18-25', '26-35', '36-45', '46-55', '55+']
)

# Kwantielen gebruiken voor gelijke groepen
df['salaris_kwartiel'] = pd.qcut(
    df['salaris'],
    q=4,
    labels=['Q1', 'Q2', 'Q3', 'Q4']
)

Tijdreeksfeatures: Temporele Patronen Vastleggen

Bij tijdreeksdata zijn er specifieke technieken die temporele afhankelijkheden vastleggen. Dit is essentieel voor voorspellende modellen in domeinen als financiën, energieverbruik en weervoorspelling.

Datum- en Tijdfeatures Extraheren

Uit één datumkolom kun je tientallen informatieve features halen:

# Datumfeatures genereren
verkopen = pd.DataFrame({
    'datum': pd.date_range('2025-01-01', periods=365, freq='D'),
    'omzet': np.random.randint(1000, 5000, 365)
})

# Temporele features extraheren
verkopen['jaar'] = verkopen['datum'].dt.year
verkopen['maand'] = verkopen['datum'].dt.month
verkopen['dag_van_week'] = verkopen['datum'].dt.dayofweek  # 0=maandag
verkopen['dag_van_jaar'] = verkopen['datum'].dt.dayofyear
verkopen['weeknummer'] = verkopen['datum'].dt.isocalendar().week.astype(int)
verkopen['is_weekend'] = (verkopen['dag_van_week'] >= 5).astype(int)
verkopen['kwartaal'] = verkopen['datum'].dt.quarter

print(verkopen.head(10))

Cyclische Codering van Tijdfeatures

Hier zit een subtiel probleem. Maanden en dagen zijn cyclisch: december (12) ligt dicht bij januari (1), maar als getal liggen ze ver uit elkaar. Cyclische codering lost dit slim op met sinus- en cosinustransformaties:

# Cyclische codering voor maanden
verkopen['maand_sin'] = np.sin(2 * np.pi * verkopen['maand'] / 12)
verkopen['maand_cos'] = np.cos(2 * np.pi * verkopen['maand'] / 12)

# Cyclische codering voor dag van de week
verkopen['dag_sin'] = np.sin(2 * np.pi * verkopen['dag_van_week'] / 7)
verkopen['dag_cos'] = np.cos(2 * np.pi * verkopen['dag_van_week'] / 7)

# Nu liggen december en januari numeriek dicht bij elkaar!

Lag Features

Lag features gebruiken waarden uit eerdere tijdstappen als voorspellers. De shift()-methode van pandas maakt dit simpel:

# Lag features creeren
verkopen['omzet_lag1'] = verkopen['omzet'].shift(1)   # gisteren
verkopen['omzet_lag7'] = verkopen['omzet'].shift(7)   # vorige week, zelfde dag
verkopen['omzet_lag30'] = verkopen['omzet'].shift(30) # vorige maand

# Verschil ten opzichte van vorige periode (differencing)
verkopen['omzet_verschil'] = verkopen['omzet'].diff()
verkopen['omzet_pct_change'] = verkopen['omzet'].pct_change()

print(verkopen[['datum', 'omzet', 'omzet_lag1', 'omzet_lag7', 'omzet_verschil']].head(10))

Belangrijk: Lag features introduceren NaN-waarden aan het begin van je dataset. Vergeet niet deze rijen te verwijderen voordat je gaat modelleren, of gebruik een imputatiemethode.

Rolling Window Features

Rolling window features berekenen statistieken over een voortschrijdend venster. Heel handig om trends en seizoenspatronen mee te vangen:

# Voortschrijdend gemiddelde en standaarddeviatie
verkopen['omzet_rolling_mean_7'] = verkopen['omzet'].rolling(window=7).mean()
verkopen['omzet_rolling_std_7'] = verkopen['omzet'].rolling(window=7).std()
verkopen['omzet_rolling_mean_30'] = verkopen['omzet'].rolling(window=30).mean()

# Voortschrijdend minimum en maximum
verkopen['omzet_rolling_min_7'] = verkopen['omzet'].rolling(window=7).min()
verkopen['omzet_rolling_max_7'] = verkopen['omzet'].rolling(window=7).max()

# Expanding window (cumulatief gemiddelde)
verkopen['omzet_expanding_mean'] = verkopen['omzet'].expanding(min_periods=1).mean()

print(verkopen[['datum', 'omzet', 'omzet_rolling_mean_7', 'omzet_rolling_mean_30']].tail(10))

Feature Engineering Pipelines met scikit-learn

In productieomgevingen is het cruciaal om je feature engineering stappen reproduceerbaar en lekvrij te maken. scikit-learn's Pipeline en ColumnTransformer zijn hiervoor onmisbaar. Ik kan dit niet genoeg benadrukken.

Waarom Pipelines Gebruiken?

Zonder pipeline loop je het risico op data-lekkage: informatie uit de testset die doorsijpelt naar je training. Als je bijvoorbeeld de mediaan van de volledige dataset gebruikt voor imputatie, bevat je trainingsset indirect info over de testset. Pipelines voorkomen dit automatisch doordat ze fit alleen op de trainingsdata uitvoeren.

De ColumnTransformer: Verschillende Bewerkingen per Kolomtype

De ColumnTransformer is het sleutelstuk. Hiermee pas je verschillende transformaties toe op verschillende subsets van kolommen, die vervolgens worden samengevoegd tot één featureset:

from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# Voorbeelddataset
data = pd.DataFrame({
    'leeftijd': [25, 30, np.nan, 45, 50, 35, 28, 55, np.nan, 40],
    'salaris': [30000, 45000, 50000, np.nan, 80000, 42000, 35000, np.nan, 60000, 52000],
    'stad': ['Amsterdam', 'Rotterdam', 'Amsterdam', 'Utrecht', 'Amsterdam',
             'Den Haag', 'Rotterdam', 'Utrecht', 'Amsterdam', 'Den Haag'],
    'opleiding': ['HBO', 'WO', 'WO', 'MBO', 'WO', 'HBO', 'MBO', 'WO', 'HBO', 'WO'],
    'target': [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
})

# Kolommen definieren
numerieke_features = ['leeftijd', 'salaris']
categorische_features = ['stad', 'opleiding']

# Numerieke pipeline: imputatie + standaardisatie
numerieke_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# Categorische pipeline: imputatie + one-hot encoding
categorische_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

# ColumnTransformer combineert beide pipelines
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numerieke_pipeline, numerieke_features),
        ('cat', categorische_pipeline, categorische_features)
    ],
    remainder='drop'
)

# Volledige pipeline: preprocessing + model
volledige_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# Train-test split en training
X = data.drop('target', axis=1)
y = data['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

volledige_pipeline.fit(X_train, y_train)
score = volledige_pipeline.score(X_test, y_test)
print(f"Nauwkeurigheid: {score:.2f}")

Dynamische Kolomselectie met make_column_selector

In plaats van kolomnamen handmatig op te geven, kun je make_column_selector gebruiken om kolommen automatisch te selecteren op basis van hun datatype:

preprocessor_auto = ColumnTransformer(
    transformers=[
        ('num', numerieke_pipeline, make_column_selector(dtype_include='number')),
        ('cat', categorische_pipeline, make_column_selector(dtype_include='object'))
    ]
)

# Dit is robuuster: als je later kolommen toevoegt,
# worden ze automatisch door de juiste pipeline verwerkt

Custom Transformers Bouwen

Voor domeinspecifieke features kun je eigen transformers bouwen die naadloos integreren in scikit-learn pipelines. Persoonlijk vind ik dit een van de krachtigste features van scikit-learn:

from sklearn.base import BaseEstimator, TransformerMixin

class VastgoedFeatureCreator(BaseEstimator, TransformerMixin):
    """Custom transformer voor vastgoed-specifieke features."""

    def fit(self, X, y=None):
        return self

    def transform(self, X):
        X = X.copy()
        X['prijs_per_m2'] = X['prijs'] / X['oppervlakte']
        X['m2_per_kamer'] = X['oppervlakte'] / X['kamers']
        X['leeftijd_woning'] = 2026 - X['bouwjaar']
        return X

# Gebruik in een pipeline
vastgoed_pipeline = Pipeline([
    ('feature_creator', VastgoedFeatureCreator()),
    ('preprocessor', preprocessor),
    ('model', RandomForestClassifier())
])

Hyperparameter Tuning van de Hele Pipeline

Een groot voordeel van pipelines is dat je de hyperparameters van preprocessing én model tegelijk kunt optimaliseren met GridSearchCV:

from sklearn.model_selection import GridSearchCV

# Parameters benoemen met dubbele underscores
param_grid = {
    'preprocessor__num__imputer__strategy': ['mean', 'median'],
    'classifier__n_estimators': [50, 100, 200],
    'classifier__max_depth': [3, 5, 10, None]
}

grid_search = GridSearchCV(
    volledige_pipeline,
    param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1
)
grid_search.fit(X_train, y_train)

print(f"Beste parameters: {grid_search.best_params_}")
print(f"Beste score: {grid_search.best_score_:.3f}")

Geautomatiseerde Feature Engineering

Handmatig features ontwerpen is krachtig, maar het kost tijd. Gelukkig zijn er steeds meer tools die dit proces deels kunnen automatiseren.

Feature-engine: Gespecialiseerde Transformers

De Feature-engine bibliotheek biedt kant-en-klare transformers die volledig compatibel zijn met scikit-learn pipelines:

# pip install feature-engine
from feature_engine.encoding import MeanEncoder
from feature_engine.imputation import MeanMedianImputer
from feature_engine.outliers import Winsorizer
from feature_engine.creation import CyclicalFeatures

# Automatische mediaan-imputatie voor numerieke kolommen
imputer = MeanMedianImputer(imputation_method='median')

# Target encoding met ingebouwde kruisvalidatie
mean_encoder = MeanEncoder(smoothing='auto')

# Uitschieters aftoppen op het 5e en 95e percentiel
winsorizer = Winsorizer(capping_method='quantiles', tail='both', fold=0.05)

# Cyclische features voor maand en dag
cyclical = CyclicalFeatures(variables=['maand', 'dag_van_week'])

SHAP: Features Begrijpen en Selecteren

Na het creëren van features wil je natuurlijk weten welke daadwerkelijk bijdragen. SHAP (SHapley Additive exPlanations) geeft inzicht in feature-belang op zowel globaal als lokaal niveau:

import shap
from sklearn.ensemble import GradientBoostingClassifier

# Model trainen
model = GradientBoostingClassifier(n_estimators=100, random_state=42)
model.fit(X_train_processed, y_train)

# SHAP-waarden berekenen
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test_processed)

# Feature importance plot
shap.summary_plot(shap_values, X_test_processed,
                  feature_names=feature_namen, show=False)

SHAP helpt je niet alleen te zien welke features belangrijk zijn, maar ook hoe ze het model beïnvloeden — positief of negatief, lineair of niet-lineair. Onmisbaar voor het iteratief verbeteren van je featureset.

Feature Selectie: Minder is Meer

Niet alle features die je aanmaakt zijn nuttig. Te veel features kunnen zelfs leiden tot overfitting, langere trainingstijden en slechtere prestaties. Feature selectie helpt je de meest informatieve subset te vinden.

Variantiedrempel

Features met weinig of geen variatie dragen simpelweg niet bij aan het model:

from sklearn.feature_selection import VarianceThreshold

# Verwijder features met bijna geen variatie
selector = VarianceThreshold(threshold=0.01)
X_selected = selector.fit_transform(X_processed)
print(f"Features voor selectie: {X_processed.shape[1]}")
print(f"Features na selectie: {X_selected.shape[1]}")

Selectie op Basis van Modelprestaties

scikit-learn biedt methoden om features te selecteren op basis van hun bijdrage aan het model:

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import GradientBoostingClassifier

# Gebruik feature importances van een boommodel
selector = SelectFromModel(
    GradientBoostingClassifier(n_estimators=100, random_state=42),
    threshold='median'
)
X_selected = selector.fit_transform(X_train_processed, y_train)

# Welke features zijn geselecteerd?
geselecteerde_mask = selector.get_support()
geselecteerde_features = [f for f, s in zip(feature_namen, geselecteerde_mask) if s]
print(f"Geselecteerde features: {geselecteerde_features}")

Recursieve Feature Eliminatie

Recursieve Feature Eliminatie (RFE) verwijdert stap voor stap de minst belangrijke features:

from sklearn.feature_selection import RFECV

# RFE met kruisvalidatie om het optimale aantal features te vinden
rfecv = RFECV(
    estimator=GradientBoostingClassifier(n_estimators=50, random_state=42),
    step=1,
    cv=5,
    scoring='accuracy',
    min_features_to_select=3
)
rfecv.fit(X_train_processed, y_train)

print(f"Optimaal aantal features: {rfecv.n_features_}")
print(f"Ranking: {rfecv.ranking_}")

Praktisch Voorbeeld: Een Complete Feature Engineering Workflow

Oké, laten we alles samenbrengen in een realistisch voorbeeld. We bouwen een complete feature engineering pipeline voor een klantverloop-voorspelmodel (churn prediction):

import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.base import BaseEstimator, TransformerMixin

# Simuleer een klantdataset
np.random.seed(42)
n = 1000

klanten = pd.DataFrame({
    'leeftijd': np.random.randint(18, 70, n),
    'maandelijks_bedrag': np.random.uniform(20, 200, n).round(2),
    'contract_duur_maanden': np.random.randint(1, 60, n),
    'aantal_tickets': np.random.poisson(2, n),
    'contract_type': np.random.choice(
        ['maandelijks', 'jaarlijks', 'twee_jaar'], n),
    'betaalmethode': np.random.choice(
        ['creditcard', 'iDEAL', 'automatisch', 'factuur'], n),
    'registratiedatum': pd.date_range('2020-01-01', periods=n, freq='D'),
})

# Doelvariabele simuleren
klanten['churned'] = (
    (klanten['contract_duur_maanden'] < 12).astype(int) * 0.3 +
    (klanten['aantal_tickets'] > 3).astype(int) * 0.3 +
    np.random.uniform(0, 0.4, n)
).round().astype(int)

# Custom transformer voor domeinspecifieke features
class ChurnFeatureCreator(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None):
        return self

    def transform(self, X):
        X = X.copy()
        X['totale_uitgaven'] = (
            X['maandelijks_bedrag'] * X['contract_duur_maanden']
        )
        X['tickets_per_maand'] = (
            X['aantal_tickets'] / X['contract_duur_maanden'].clip(lower=1)
        )
        X['registratie_maand'] = (
            pd.to_datetime(X['registratiedatum']).dt.month
        )
        X['registratie_dag_van_week'] = (
            pd.to_datetime(X['registratiedatum']).dt.dayofweek
        )
        X = X.drop(columns=['registratiedatum'])
        return X

# Kolommen na feature creation
numerieke_features_na = [
    'leeftijd', 'maandelijks_bedrag', 'contract_duur_maanden',
    'aantal_tickets', 'totale_uitgaven', 'tickets_per_maand',
    'registratie_maand', 'registratie_dag_van_week'
]
categorische_features_na = ['contract_type', 'betaalmethode']

# Pipelines per kolomtype
num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

cat_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

preprocessor = ColumnTransformer([
    ('num', num_pipeline, numerieke_features_na),
    ('cat', cat_pipeline, categorische_features_na)
])

# Complete pipeline
pipeline = Pipeline([
    ('feature_creator', ChurnFeatureCreator()),
    ('preprocessor', preprocessor),
    ('model', GradientBoostingClassifier(n_estimators=100, random_state=42))
])

# Evaluatie met kruisvalidatie
X = klanten.drop('churned', axis=1)
y = klanten['churned']

scores = cross_val_score(pipeline, X, y, cv=5, scoring='accuracy')
print(f"Kruisvalidatie scores: {scores.round(3)}")
print(f"Gemiddelde nauwkeurigheid: {scores.mean():.3f} (+/- {scores.std():.3f})")

Dit voorbeeld laat zien hoe je een complete, reproduceerbare workflow bouwt die:

  • Domeinspecifieke features creëert via een custom transformer
  • Numerieke en categorische kolommen apart verwerkt
  • Alles samenvoegt in een pipeline die veilig is voor kruisvalidatie
  • Makkelijk uit te breiden is met nieuwe features of stappen

Best Practices en Veelgemaakte Fouten

Na jarenlang werken met feature engineering in productieomgevingen, zijn er een paar lessen die ik steeds weer tegenkom:

Doe Dit Wel

  1. Begin eenvoudig: Start met basale features en voeg geleidelijk complexiteit toe. Meet de impact van elke toevoeging.
  2. Gebruik altijd pipelines: Handmatige preprocessing buiten een pipeline is een recept voor data-lekkage. Geloof me.
  3. Documenteer je features: Leg vast wat elke feature betekent, hoe die is berekend en waarom je denkt dat die relevant is.
  4. Valideer met kruisvalidatie: Gebruik altijd kruisvalidatie om de echte bijdrage van nieuwe features te meten. Een verbetering op de trainingsset alleen zegt niks.
  5. Combineer domeinkennis met data: De beste features komen voort uit een combinatie van begrip van het probleem en statistische analyse.

Vermijd Dit

  1. Feature-lekkage: Gebruik nooit informatie die op het moment van voorspelling niet beschikbaar zou zijn. Als je omzet voorspelt, mag je niet de omzet van dezelfde dag als feature gebruiken.
  2. Te veel features: Meer is niet altijd beter. De 'vloek van dimensionaliteit' zorgt ervoor dat modellen slechter presteren naarmate de featureruimte groeit ten opzichte van het aantal observaties.
  3. Fitten op testdata: Pas transformaties altijd eerst aan op trainingsdata en transformeer daarna de testdata met dezelfde parameters. Dit geldt voor scaling, imputatie én encoding.
  4. Multicollineariteit negeren: Sterk gecorreleerde features (correlatie > 0.9) voegen weinig toe maar kunnen modellen destabiliseren. Check je correlatiematrices.

Conclusie

Feature engineering is misschien wel de belangrijkste vaardigheid voor elke data scientist. Het is het snijvlak van domeinkennis, creativiteit en technische expertise. In dit artikel hebben we de volledige toolkit doorgenomen:

  • Ontbrekende waarden: verwijderen, imputeren of als feature markeren
  • Categorische variabelen: one-hot, label en target encoding
  • Numerieke transformaties: standaardisatie, normalisatie en power transformaties
  • Feature creatie: interacties, polynomiaal en binning
  • Tijdreeksfeatures: datum-extractie, cyclische codering, lag features en rolling windows
  • Pipelines: ColumnTransformer, custom transformers en GridSearchCV
  • Feature selectie: variantiedrempel, modelselectie en RFE

De sleutel tot succes? Niet alles blindelings toepassen, maar weloverwogen kiezen wat bij jouw probleem en dataset past. Begin simpel, meet alles, en bouw stap voor stap verder. Met de technieken uit dit artikel ben je goed uitgerust om features te bouwen die je ML-modellen écht naar een hoger niveau tillen.

Over de Auteur Editorial Team

Our team of expert writers and editors.