Python voor Data Science: NumPy, Pandas en Scikit-Learn in 2026

Een praktijkgerichte handleiding voor Python data science met NumPy 2.1, Pandas 2.2 en Scikit-Learn 1.6. Van ruwe data tot een volledig getraind machine learning-model met productie-ready code.

Python data science tutorial met NumPy Pandas en Scikit-Learn code en dashboard-illustratie

Python blijft in 2026 de onbetwiste standaard voor data science. Het ecosysteem rondom NumPy, Pandas en Scikit-Learn heeft zich de afgelopen jaren verder geconsolideerd en biedt ontwikkelaars en datawetenschappers een stabiele, performante basis voor het hele traject van ruwe data tot productiewaardige modellen. Deze handleiding doorloopt dat volledige traject stap voor stap: van array-bewerkingen en dataopschoning tot het trainen, valideren en exporteren van een machine learning-pipeline. Alle codevoorbeelden zijn direct uitvoerbaar met Python 3.12 en de nieuwste bibliotheekversies.

Gebruikte versies in deze tutorial

De voorbeelden in dit artikel zijn geschreven en getest met Python 3.12, NumPy 2.1, Pandas 2.2 en Scikit-Learn 1.6. Oudere versies kunnen afwijkend gedrag vertonen, met name rond Copy-on-Write in Pandas en de vernieuwde array-API in NumPy.

NumPy Array-bewerkingen voor Efficiente Berekeningen

NumPy vormt het rekenkundige fundament van vrijwel elke data science-stack in Python. Het kernprincipe is eenvoudig maar krachtig: bewerkingen worden niet element voor element in een Python-loop uitgevoerd, maar gevectoriseerd op C-niveau. Dat levert bij grote datasets snelheidswinsten op van factor 10 tot 100 ten opzichte van standaard Python-lijsten.

python
# numpy_basics.py
import numpy as np

# Create arrays from different sources
prices = np.array([29.99, 49.99, 19.99, 99.99, 39.99])
quantities = np.arange(1, 6)  # [1, 2, 3, 4, 5]

# Vectorized arithmetic — no loops needed
revenue = prices * quantities
print(revenue)  # [29.99, 99.98, 59.97, 399.96, 199.95]

# Statistical aggregations
print(f"Total revenue: ${revenue.sum():.2f}")   # $789.85
print(f"Mean price: ${prices.mean():.2f}")       # $47.99
print(f"Std deviation: ${prices.std():.2f}")     # $27.64

# Boolean indexing — filter without loops
premium_mask = prices > 40
premium_items = prices[premium_mask]  # [49.99, 99.99]

Boolean indexing is een van de meest onderschatte features van NumPy. In plaats van een for-loop met een if-conditie maakt het een vergelijkingsoperatie op de gehele array in een keer, wat resulteert in een boolean-masker. Dat masker dient vervolgens als index om alleen de elementen te selecteren die aan de voorwaarde voldoen. Dit patroon komt overal terug in data science: bij het filteren van uitschieters, het selecteren van rijen op basis van drempelwaarden, of het toepassen van conditionele transformaties. Het is niet alleen sneller dan een loop, maar ook compacter en minder foutgevoelig.

Reshaping en Broadcasting in NumPy

Bij het werken met multidimensionale data is het essentieel om te begrijpen hoe NumPy de assen van een array interpreteert. De parameter axis bepaalt langs welke dimensie een operatie wordt uitgevoerd: axis=0 werkt kolomsgewijs (over de rijen heen) en axis=1 rijsgewijs (over de kolommen heen). Broadcasting maakt het bovendien mogelijk om arrays van verschillende vormen met elkaar te combineren zonder expliciete loops.

python
# numpy_reshape.py
import numpy as np

# Monthly sales data: 4 products x 3 months
sales = np.array([
    [120, 150, 130],  # Product A
    [200, 180, 220],  # Product B
    [90, 110, 95],    # Product C
    [300, 280, 310],  # Product D
])

# Column-wise mean (average per month)
monthly_avg = sales.mean(axis=0)  # [177.5, 180.0, 188.75]

# Row-wise sum (total per product)
product_totals = sales.sum(axis=1)  # [400, 600, 295, 890]

# Normalize each product relative to its own max
normalized = sales / sales.max(axis=1, keepdims=True)
# keepdims=True preserves the shape for broadcasting
print(normalized[0])  # [0.8, 1.0, 0.867] — Product A relative to its peak

# Reshape for Scikit-Learn (requires 2D input)
flat_sales = sales.flatten()  # 1D array of 12 values
reshaped = flat_sales.reshape(-1, 1)  # 12x1 column vector

De axis-parameter is een veelvoorkomende bron van verwarring bij beginners. Een handige ezelsbruggetje: de waarde van axis geeft aan welke dimensie verdwijnt na de operatie. Bij een matrix van 4x3 levert axis=0 een resultaat van vorm (3,) op, omdat de rij-dimensie wordt samengevoegd. Omgekeerd geeft axis=1 een resultaat van vorm (4,), omdat de kolom-dimensie verdwijnt. Het argument keepdims=True voorkomt dit vormverlies, wat onmisbaar is wanneer het resultaat later via broadcasting met de oorspronkelijke array moet worden gecombineerd, zoals bij normalisatie per rij.

Pandas DataFrame-manipulatie en Dataopschoning

In de praktijk begint elk data science-project met rommelige data. Kolommen bevatten ontbrekende waarden, datatypes kloppen niet, en er zitten duplicaten of onmogelijke waarden in de dataset. Pandas biedt een uitgebreide gereedschapskist om deze opschoningsstappen reproduceerbaar en transparant uit te voeren. Het gebruik van method chaining maakt het mogelijk om meerdere transformaties in een leesbare keten te schrijven, zonder tussentijdse variabelen die de namespace vervuilen.

python
# pandas_cleaning.py
import pandas as pd
import numpy as np

# Load and inspect raw data
df = pd.read_csv("candidates.csv")
print(df.shape)       # (1500, 8)
print(df.dtypes)      # Check column types
print(df.isna().sum()) # Count missing values per column

# Clean in a reproducible chain
df_clean = (
    df
    .dropna(subset=["salary", "experience_years"])      # Drop rows missing critical fields
    .assign(
        salary=lambda x: x["salary"].clip(lower=20000, upper=500000),  # Cap outliers
        experience_years=lambda x: x["experience_years"].astype(int),
        hired_date=lambda x: pd.to_datetime(x["hired_date"], errors="coerce"),
    )
    .drop_duplicates(subset=["email"])                  # Remove duplicate candidates
    .query("experience_years >= 0")                     # Filter invalid entries
    .reset_index(drop=True)
)

print(f"Cleaned: {len(df)} -> {len(df_clean)} rows")

Method chaining is meer dan een stilistische voorkeur — het dwingt een functionele, stapsgewijze denkwijze af die het opschoningsproces documenteert in de code zelf. Elke stap in de keten ontvangt het DataFrame van de vorige stap en retourneert een nieuw (of gewijzigd) DataFrame. De .assign()-methode is bijzonder krachtig omdat lambda-functies toegang hebben tot het DataFrame zoals het er op dat punt in de keten uitziet, niet zoals het er oorspronkelijk uitzag. Dit voorkomt subtiele bugs die ontstaan wanneer transformaties van elkaar afhankelijk zijn. Voor productieomgevingen verdient het aanbeveling om de keten te voorzien van commentaar per stap, zodat collega's de redenering achter elke transformatie direct kunnen volgen.

Pandas 2.2 Copy-on-Write

Vanaf Pandas 2.2 is Copy-on-Write (CoW) het standaardgedrag. Dit betekent dat elke indexeringsoperatie een lazy copy retourneert: de data wordt pas echt gekopieerd op het moment dat een van de twee objecten wordt gewijzigd. In de praktijk elimineert dit de beruchte SettingWithCopyWarning volledig en voorkomt het onbedoelde mutaties van het oorspronkelijke DataFrame. Code die eerder expliciet .copy() gebruikte, werkt nog steeds correct maar is niet langer noodzakelijk. Het is wel raadzaam om bestaande code te testen met pd.options.mode.copy_on_write = True voordat naar Pandas 2.2 wordt geupgraded.

GroupBy-aggregaties en Feature Engineering met Pandas

Nadat de data is opgeschoond, is de volgende stap het extraheren van betekenisvolle patronen en het construeren van features die een machine learning-model kan gebruiken. De groupby()-methode van Pandas is hiervoor het centrale werkpaard: het groepeert rijen op basis van een of meerdere kolommen en past vervolgens een aggregatiefunctie toe per groep. Voor feature engineering in de context van data science-sollicitaties is het cruciaal om zowel geaggregeerde statistieken als relatieve maatstaven te beheersen.

python
# pandas_groupby.py
import pandas as pd

# Aggregate candidate stats by department
dept_stats = (
    df_clean
    .groupby("department")
    .agg(
        avg_salary=("salary", "mean"),
        median_experience=("experience_years", "median"),
        headcount=("email", "count"),
        max_salary=("salary", "max"),
    )
    .sort_values("avg_salary", ascending=False)
)
print(dept_stats.head())

# Create features for ML: encode categorical + add aggregated stats
df_features = (
    df_clean
    .assign(
        # Ratio of individual salary to department average
        salary_ratio=lambda x: x["salary"] / x.groupby("department")["salary"].transform("mean"),
        # Time since hire in days
        tenure_days=lambda x: (pd.Timestamp.now() - x["hired_date"]).dt.days,
        # Binary encoding
        is_senior=lambda x: (x["experience_years"] >= 5).astype(int),
    )
)

De .transform()-methode verdient bijzondere aandacht. Waar .agg() het aantal rijen reduceert tot een per groep, behoudt .transform() de oorspronkelijke index en voegt het geaggregeerde resultaat terug aan elke rij van de groep. Dit is precies wat nodig is bij feature engineering: het salaris van een individuele kandidaat delen door het gemiddelde van diens afdeling vereist dat het afdelingsgemiddelde beschikbaar is op rij-niveau. Zonder .transform() zou dit een aparte groupby() plus een merge() vergen, wat zowel trager als minder leesbaar is. Het patroon x.groupby(...)[kolom].transform(functie) binnen .assign() is daarmee een van de meest waardevolle idiomen in Pandas voor data science-toepassingen.

Klaar om je Data Science & ML gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Een Scikit-Learn Pipeline vanaf Nul Opbouwen

Met opgeschoonde data en geengineerde features is het tijd om het machine learning-model op te zetten. Scikit-Learn biedt met de Pipeline- en ColumnTransformer-klassen een elegant mechanisme om preprocessing en modellering samen te voegen in een enkel object. Dit is niet alleen handig — het is essentieel om data leakage te voorkomen en om het gehele model reproduceerbaar op te slaan.

python
# sklearn_pipeline.py
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import classification_report
import pandas as pd

# Define column groups
numeric_features = ["salary", "experience_years", "salary_ratio", "tenure_days"]
categorical_features = ["department", "role_level"]
target = "promoted"

# Split before any preprocessing
X = df_features[numeric_features + categorical_features]
y = df_features[target]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Build the preprocessing + model pipeline
preprocessor = ColumnTransformer(
    transformers=[
        ("num", StandardScaler(), numeric_features),           # Scale numeric columns
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),  # Encode categories
    ]
)

pipeline = Pipeline([
    ("preprocessor", preprocessor),
    ("classifier", GradientBoostingClassifier(
        n_estimators=200,
        learning_rate=0.1,
        max_depth=4,
        random_state=42,
    )),
])

# Train and evaluate
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred))

De ColumnTransformer is het scharnierpunt van de pipeline. In vrijwel elke dataset bestaan numerieke en categorische kolommen naast elkaar, en elke groep vereist een andere voorbewerking. De ColumnTransformer past de juiste transformer toe op de juiste subset van kolommen en voegt de resultaten horizontaal samen tot een enkele featurematrix. Het argument handle_unknown="ignore" bij OneHotEncoder zorgt ervoor dat categoriewaarden die wel in de testset maar niet in de trainingsset voorkomen, worden genegeerd in plaats van een fout te veroorzaken. Dit is een kleine maar cruciale instelling voor productietoepassingen waar de invoerdata niet altijd voorspelbaar is.

Kruisvalidatie en Hyperparameter-optimalisatie

Een model trainen op een enkele train/test-split geeft een beperkt beeld van de werkelijke prestaties. Kruisvalidatie verdeelt de trainingsdata in meerdere folds en traint het model herhaaldelijk, telkens met een andere fold als validatieset. Dit levert een robuustere schatting van de modelprestaties op en legt eventuele overfitting bloot die bij een enkele split verborgen zou blijven.

python
# sklearn_tuning.py
from sklearn.model_selection import cross_val_score, GridSearchCV
import numpy as np

# 5-fold cross-validation on the full pipeline
scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring="f1")
print(f"F1 scores: {scores}")
print(f"Mean F1: {scores.mean():.3f} (+/- {scores.std() * 2:.3f})")

# Grid search over hyperparameters
param_grid = {
    "classifier__n_estimators": [100, 200, 300],
    "classifier__max_depth": [3, 4, 5],
    "classifier__learning_rate": [0.05, 0.1, 0.2],
}

grid_search = GridSearchCV(
    pipeline,
    param_grid,
    cv=5,
    scoring="f1",
    n_jobs=-1,      # Use all CPU cores
    verbose=1,
)

grid_search.fit(X_train, y_train)
print(f"Best params: {grid_search.best_params_}")
print(f"Best F1: {grid_search.best_score_:.3f}")

# Evaluate the best model on held-out test set
best_model = grid_search.best_estimator_
print(classification_report(y_test, best_model.predict(X_test)))

De dubbele-underscore-syntax (classifier__n_estimators) is Scikit-Learns manier om parameters van geneste objecten binnen een pipeline aan te spreken. Het deel voor de dubbele underscore verwijst naar de naam van de stap in de pipeline (in dit geval "classifier"), en het deel erna naar de parameter van dat specifieke object. Dit mechanisme werkt recursief: bij een geneste ColumnTransformer binnen een pipeline kan een parameternaam er uitzien als preprocessor__num__with_mean. GridSearchCV combineert deze syntax met kruisvalidatie om systematisch elke combinatie van hyperparameters te evalueren. Met n_jobs=-1 worden alle beschikbare CPU-kernen ingezet, wat bij 27 combinaties en 5 folds het verschil kan zijn tussen minuten en uren rekentijd.

Pas op voor data leakage

Voer preprocessing altijd uit binnen de pipeline, niet ervoor. Wanneer scaling of encoding wordt toegepast op de volledige dataset voordat de train/test-split plaatsvindt, lekt informatie uit de testset naar het trainingsproces. De pipeline garandeert dat elke fold in de kruisvalidatie onafhankelijk wordt getransformeerd op basis van uitsluitend de trainingsdata van die fold. Dit geldt ook voor feature engineering-stappen zoals het berekenen van gemiddelden per groep.

Modellen Opslaan en Laden voor Productie

Nadat het beste model is geselecteerd via kruisvalidatie en hyperparameter-tuning, moet het worden opgeslagen zodat het in een productieomgeving kan worden ingezet zonder het trainingsproces te herhalen. De joblib-bibliotheek serialiseert het complete pipeline-object, inclusief de preprocessing-stappen en het getrainde model, naar een enkel bestand.

python
# sklearn_export.py
import joblib
from pathlib import Path

# Save the complete pipeline (preprocessor + model)
model_dir = Path("models")
model_dir.mkdir(exist_ok=True)
joblib.dump(best_model, model_dir / "promotion_model_v1.joblib")

# Load and predict in a different process
loaded_model = joblib.load(model_dir / "promotion_model_v1.joblib")
new_data = pd.DataFrame({
    "salary": [75000],
    "experience_years": [4],
    "salary_ratio": [1.05],
    "tenure_days": [730],
    "department": ["Engineering"],
    "role_level": ["Mid"],
})

prediction = loaded_model.predict(new_data)
probability = loaded_model.predict_proba(new_data)[:, 1]
print(f"Promoted: {bool(prediction[0])}, Confidence: {probability[0]:.2%}")

Het opslaan van de volledige pipeline in plaats van alleen het model is een bewuste keuze. Wanneer uitsluitend het getrainde model wordt geexporteerd, moet de preprocessing-logica apart worden gerepliceerd in de productieomgeving, wat een veelvoorkomende bron van fouten is. Door de Pipeline als geheel te serialiseren, accepteert loaded_model.predict() dezelfde ruwe invoerdata als tijdens het trainen: de StandardScaler en OneHotEncoder worden automatisch toegepast met de parameters die tijdens het fitten zijn geleerd. Bij het versiebeheer van modellen is het raadzaam om zowel het .joblib-bestand als de bijbehorende Python-omgevingspecificaties (via requirements.txt of pyproject.toml) op te slaan, zodat compatibiliteitsproblemen bij het laden kunnen worden voorkomen.

Conclusie

Deze handleiding heeft het volledige traject doorlopen van ruwe data tot een productiewaardige machine learning-pipeline. De kernpunten samengevat:

  • NumPy biedt gevectoriseerde bewerkingen die Python-loops overbodig maken. Boolean indexing, broadcasting en de axis-parameter vormen de basis voor efficiente numerieke berekeningen.
  • Pandas transformeert rommelige datasets via method chaining en .assign() tot schone, gestructureerde DataFrames. De groupby()/.transform()-combinatie is onmisbaar voor feature engineering op rij-niveau.
  • Scikit-Learn Pipelines bundelen preprocessing en modellering in een enkel object, waardoor data leakage wordt voorkomen en het model direct inzetbaar is in productie.
  • Kruisvalidatie met GridSearchCV geeft een betrouwbare schatting van modelprestaties en optimaliseert hyperparameters systematisch.
  • Serialisatie via joblib slaat de complete pipeline op, zodat preprocessing en model altijd synchroon blijven.

De combinatie van deze drie bibliotheken dekt het overgrote deel van de data science-werkzaamheden in de praktijk. Voor verdere verdieping is het aan te raden om te experimenteren met alternatieve modellen binnen dezelfde pipeline-structuur — de modulaire opzet van Scikit-Learn maakt het verwisselen van een GradientBoostingClassifier voor een RandomForestClassifier of LogisticRegression tot een eenregelige aanpassing.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#data-science
#python
#numpy
#pandas
#scikit-learn
#machine-learning
#tutorial

Delen

Gerelateerde artikelen