Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

#Inhoud

Introductie en workflow

  1. Waarom Data Science met Python?
  2. Installatie en omgeving
  3. Jupyter Notebooks
  4. Versiebeheer en samenwerking
  5. Projecttemplates

Numpy

  1. NumPy‑arrays
  2. Indexeren, reshape
  3. Universele functies
  4. Lineaire algebra
  5. Prestaties
  6. Interoperabiliteit

Pandas

  1. Pandas: Series en DataFrame
  2. Data inlezen en schrijven
  3. Data schoonmaken
  4. Groeperen
  5. Tijdsreeksen
  6. Samenvoegen
  7. Prestatie,
  8. Geavanceerde indexen

Matplotlib

  1. Matplotlib basis
  2. Veelgebruikte grafieken
  3. Aangepaste visualisaties
  4. Best practices
  5. Integratie met pandas en NumPy
  6. Exporteren

Scikit-learn

  1. machine learning met scikit‑learn
  2. preprocessing
  3. Regressie
  4. Classificatie
  5. Modelselectie
  6. Clustering
  7. Feature engineering

Hoofdstuk 1 — Waarom Data Science met Python? Overzicht, use‑cases en ecosysteem

Let op: De tekst is in het Nederlands; alle codeblokken gebruiken Engels waar van toepassing.

Doel van dit hoofdstuk:

  • Begrijpen waarom Python een toonaangevende taal is voor data‑analyse, ‑engineering en ‑science.
  • Een overzicht krijgen van het ecosysteem (NumPy, pandas, Matplotlib, Jupyter, scikit‑learn, enz.).
  • Voorbeelden van use‑cases in business, research en productie (ETL, analyse, ML, dashboards).

Aanbevolen voorkennis: Basiscomputervaardigheden; geen programmeerervaring vereist.

1.1 Waarom Python voor data?

Python biedt een sterke combinatie van leesbaarheid, een grote community en krachtige bibliotheken. Het is:

  • Vrij en open‑source met een actief ecosysteem.
  • Breed inzetbaar: van data‑analyse en machine learning tot web‑apps en automatisering.
  • Interactief (Jupyter), maar ook geschikt voor scripts en services.

Use‑cases:

  • Exploratieve data‑analyse, rapportage en dashboards.
  • ETL/ELT‑pijplijnen en data‑engineering workflows.
  • Machine learning: van prototyping tot uitrol.
  • Wetenschappelijk onderzoek en reproducible computing.

1.2 Het wetenschappelijke Python‑ecosysteem

Belangrijke bouwstenen en hun rol:

  • NumPy: N‑dimensionale arrays, vectorisatie en numerieke basis.
  • pandas: tabulaire data (Series/DataFrame), I/O, wrangling, aggregaties.
  • Matplotlib: kernvisualisatie (basis), met uitbreidingen (Seaborn, Plotly) buiten scope.
  • Jupyter: interactieve notebooks, combineert code, tekst en figuren.
  • scikit‑learn: machine‑learning‑API met consistente fit/transform/predict‑filosofie.
  • SciPy: wetenschappelijke routines (optimalisatie, signaalverwerking, statistiek).
  • SQLAlchemy: databaseconnecties voor SQL‑bronnen.
  • PyArrow: kolomdata en Parquet/Arrow‑integratie.

1.3 Workflow: van notebook naar product

  • Start in Jupyter voor verkenning; migreer naar modules/pakketten voor hergebruik.
  • Gebruik versiebeheer (Git) voor geschiedenis en samenwerking.
  • Automatiseer omgeving (requirements/environment) voor reproduceerbaarheid.
  • Exporteer figuren en rapporten; publiceer dashboards of endpoints waar nodig.

1.4 Interoperabiliteit en performance in vogelvlucht

  • Vectorisatie (NumPy/pandas) is meestal sneller dan Python‑loops.
  • Gebruik kolomformaat (Parquet) voor efficiënte I/O.
  • Profiteer van ecosysteemintegratie: pandas ↔ NumPy ↔ Matplotlib ↔ scikit‑learn.

1.5 Voorbeelden van domeinen en patronen

  • Retail: vraagvoorspelling, mandanalyse, A/B‑tests.
  • Finance: risicomodellen, anomaliedetectie, tijdreeksen.
  • Industrie: predictief onderhoud, sensordata, kwaliteitscontrole.
  • Publieke sector/onderzoek: open data, reproducible science.

1.6 Tips

  • Leer de kernpakketten eerst (NumPy, pandas, Matplotlib), breid daarna uit.
  • Werk met kleine, herhaalbare stappen; commit vaak.
  • Documenteer aannames en pin versies voor reproduceerbaarheid.

1.7 Samenvatting

Python is krachtig door zijn ecosysteem en community. Je gebruikt het voor verkenning, analyse, visualisatie en ML. In de volgende hoofdstukken leer je de praktische workflow inrichten (installatie, Jupyter, Git) en zet je de stap naar robuuste projecten.

Hoofdstuk 2 — Installatie en omgeving: Python, pip, virtualenv en conda

Let op: De tekst is in het Nederlands; shell‑commando’s en codeblokken zijn in het Engels.

Doel van dit hoofdstuk:

  • Een werkende Python‑installatie opzetten op Windows/macOS/Linux.
  • Pakketbeheer begrijpen met pip/venv en een alternatief met conda/mamba.
  • Beste praktijken voor geïsoleerde omgevingen en reproduceerbare dependencies.

Aanbevolen voorkennis: Hoofdstuk 1.

2.1 Python installeren

Aanbevolen opties:

  • Gebruik de officiële installer (python.org) of je OS‑pakketbeheerder (Homebrew, apt, winget).
  • Alternatief: conda/mamba‑distributie (Anaconda/Miniforge/Mambaforge) met gebundelde wetenschappelijke pakketten.

Controleer versie:

python --version
python3 --version

2.2 pip en virtualenv/venv

Gebruik per project een geïsoleerde omgeving.

# Maak een venv (Python 3.8+)
python -m venv .venv
# Activeer
# Windows (PowerShell):
.\.venv\Scripts\Activate.ps1
# macOS/Linux (bash/zsh):
source .venv/bin/activate

# Upgrade pip en installeer pakketten
python -m pip install --upgrade pip
pip install numpy pandas matplotlib jupyterlab scikit-learn

# Exporteer requirements
pip freeze > requirements.txt

Deactiveer met deactivate.

2.3 conda/mamba omgevingen (alternatief)

Conda biedt binair beheerde pakketten, handig voor complexe dependencies.

# Installeer mamba (snel) via Mambaforge/Miniforge
mamba --version

# Maak en activeer een omgeving
mamba create -n datasci python=3.11 numpy pandas matplotlib jupyterlab scikit-learn
mamba activate datasci

# Exporteer omgeving
mamba env export --no-builds > environment.yml

Tips:

  • Gebruik bij voorkeur één systeem (pip/venv of conda/mamba) per project om mixen te vermijden.
  • pin versies in requirements/environment voor reproduceerbaarheid.

2.4 JupyterLab en kernels

Installeer JupyterLab en koppel je omgeving als kernel.

pip install jupyterlab ipykernel
python -m ipykernel install --user --name datasci-venv --display-name "Python (datasci-venv)"
# Start JupyterLab
jupyter lab

2.5 Mapstructuur van een data‑project (vooruitblik)

Een pragmatische structuur (voorbeeld):

project/
  data/           # ruwe/brondata (niet committen als groot/gevoelig)
  notebooks/      # verkenning en rapportage
  src/            # herbruikbare functies/modules
  reports/        # figuren en resultaten
  tests/          # (optioneel) unit tests
  requirements.txt of environment.yml
  README.md

2.6 Troubleshooting

  • Meerdere Python’s op systeem? Controleer which python / where python.
  • Proxy/firewall‑issues met pip? Stel PIP_INDEX_URL en PIP_TRUSTED_HOST in indien nodig.
  • Compileerproblemen? Overweeg conda/mamba voor geprecompileerde wheels.

2.7 Samenvatting

Je hebt nu opties om Python te installeren, geïsoleerde omgevingen te maken met pip/venv of conda/mamba, en JupyterLab te starten. In Hoofdstuk 3 richten we de dagelijkse workflow in met Jupyter en reproducibility‑patronen.

Hoofdstuk 3 — Werkwijze met Jupyter Notebooks, reproducibility en projectstructuur

Let op: De tekst is in het Nederlands; codevoorbeelden en shell‑commando’s zijn in het Engels.

Doel van dit hoofdstuk:

  • Efficiënt werken met Jupyter (Lab/Notebook): cellen, kernels, magics, export.
  • Reproduceerbare analyses: vaste omgeving, vaste random seeds, datamanagement.
  • Praktische projectstructuur voor data‑werk en samenwerking.

Aanbevolen voorkennis: Hoofdstuk 1–2.

3.1 Jupyter: basis en best practices

Start JupyterLab:

jupyter lab

Belangrijke concepten:

  • Kernel: de Python‑runtime; herstart bij foutieve staat of geheugenlekken.
  • Cellen: mix van code en Markdown; gebruik koppen en tekst voor context.
  • Magics: %timeit, %matplotlib inline, %%bash, etc.

Tips:

  • Houd notebooks klein en doelgericht; verplaats herhaalbare code naar src/ modules.
  • Gebruik Restart & Run All voor een schone run.
  • Exporteer naar HTML/PDF voor rapportage; controleer output.

3.2 Reproduceerbaarheid: omgeving en seeds

  • Pin pakketversies in requirements.txt of environment.yml.
  • Leg hardware/OS/versies vast in een REPORT.md of in de notebook header.
  • Stel random seeds waar relevant:
import numpy as np
rng = np.random.default_rng(seed=42)
  • Noteer datums van data‑extracts en I/O‑paden.

3.3 Data‑beheer in projecten

  • Bewaar ruwe data onveranderd in data/raw/; schrijf tussenresultaten naar data/processed/.
  • Gebruik Parquet voor kolom‑georiënteerde opslag en snellere herlaad.
  • Houd gevoelige data buiten Git (git‑ignore); gebruik secrets managers i.p.v. hard‑coded wachtwoorden.

3.4 Projectstructuur (voorbeeld)

project/
  data/
    raw/
    processed/
  notebooks/
    01-exploratie.ipynb
    02-rapport.ipynb
  src/
    __init__.py
    features.py
    viz.py
  reports/
    figures/
  requirements.txt  # of environment.yml
  README.md

3.5 Notebook → module: code hergebruik

Verplaats functies naar src/ en importeer vanuit notebooks:

# src/features.py
import pandas as pd

def add_amount(df: pd.DataFrame) -> pd.DataFrame:
    return df.assign(amount=df['qty'] * df['price'])

# In notebook
from src.features import add_amount

3.6 Valstrikken en performance

  • Notebooks kunnen “stateful drift” veroorzaken; herstart regelmatig.
  • Grote tabellen/figuren maken notebooks zwaar; cache tussenresultaten in Parquet.
  • Gebruik versiebeheer (Git) voor geschiedenis; commit notebooks met nbdime diff (optioneel).

3.7 Samenvatting

Je kunt nu productief werken in Jupyter met aandacht voor reproduceerbaarheid en structuur. In Hoofdstuk 4 leer je samenwerken met Git/GitHub en review‑workflows.

Hoofdstuk 4 — Versiebeheer en samenwerking: Git, GitHub en code review

Let op: De tekst is in het Nederlands; shell‑commando’s zijn in het Engels.

Doel van dit hoofdstuk:

  • Basis Git‑workflow: init, commit, branch, merge, pull/push.
  • Werken met GitHub (of GitLab): pull requests, issues, CI‑indicaties.
  • Code review‑principes voor data‑projecten (notebooks, scripts, data‑artefacten).

Aanbevolen voorkennis: Hoofdstuk 1–3.

4.1 Git initialiseren en eerste commit

# In een nieuwe map
git init
git status

echo "# Project" > README.md
git add README.md
git commit -m "init"

4.2 Branches en merges

git switch -c feature/eda
# werk... voeg bestanden toe
git add notebooks/01-exploratie.ipynb
git commit -m "EDA eerste versie"

# terug naar main en merge
git switch main
git merge feature/eda

4.3 Remote en samenwerken

git remote add origin https://github.com/<org>/<repo>.git
git push -u origin main
  • Gebruik pull requests (PR) voor review.
  • Koppel issues aan PR’s voor traceerbaarheid.

4.4 Code review voor data‑science

Checklist:

  • Is de analyse reproduceerbaar? (requirements/environment aanwezig)
  • Zijn notebooks schoon gerund (Restart & Run All) en niet te groot?
  • Is data‑in/‑uit duidelijk gedocumenteerd (paden, formaten, privacy)?
  • Zijn resultaten/figuren interpreteerbaar en correct gelabeld?
  • Zijn functies/herbruikbare logica in src/ ondergebracht met tests (indien toepasselijk)?

4.5 Diffen van notebooks

  • Gebruik nbdime of GitHub’s notebook viewer om verschillen te bekijken.
  • Overweeg export naar script (jupyter nbconvert --to script) voor review van logica.

4.6 Basis CI‑signalen (kort)

  • Voeg een eenvoudige CI toe die pip install -r requirements.txt uitvoert en basislinting doet (ruff/flake8).
  • Optioneel: run snelle tests (pytest -q).

4.7 Samenvatting

Git en GitHub ondersteunen samenwerking en review. Gebruik branches en PR’s, en hanteer een reviewchecklist die focust op reproduceerbaarheid, duidelijkheid en datakwaliteit. In Hoofdstuk 5 kijken we naar projecttemplates, cookiecutter en packaging basics.

Hoofdstuk 5 — Projecttemplates, cookiecutter en packaging basics

Let op: De tekst is in het Nederlands; shell‑commando’s en bestandsstructuren zijn in het Engels.

Doel van dit hoofdstuk:

  • Snel starten met consistente projecttemplates (Cookiecutter).
  • Basis van Python‑packages (src‑layout, pyproject.toml, installable packages).
  • Distributie en versiebeheer op hoofdlijnen.

Aanbevolen voorkennis: Hoofdstuk 1–4.

5.1 Waarom templates?

  • Consistentie: dezelfde structuur en tooling voor alle projecten.
  • Snelheid: minder opzetwerk, meer focus op inhoud.
  • Kwaliteit: ingebouwde linters, tests, CI‑skelet.

5.2 Cookiecutter gebruiken

Installeer Cookiecutter en genereer een project:

pip install cookiecutter
cookiecutter https://github.com/cookiecutter/cookiecutter-pypackage
# of een data‑science template, bijv. (voorbeeld):
# cookiecutter https://github.com/drivendata/cookiecutter-data-science

Je beantwoordt prompts; daarna krijg je een projectmap met best practices.

5.3 src‑layout en pyproject.toml

Moderne packaging gebruikt pyproject.toml en een src/‑layout.

project/
  src/
    mypkg/
      __init__.py
      core.py
  tests/
  pyproject.toml

Voorbeeld pyproject.toml (poetry/pep517‑achtig):

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "mypkg"
version = "0.1.0"
authors = [{name = "Jij", email = "jij@example.com"}]
dependencies = [
  "numpy",
  "pandas",
]

Installeer lokaal in editable mode:

pip install -e .

5.4 Versiebeheer en releases (kort)

  • Houd een CHANGELOG.md bij met semantische versies (MAJOR.MINOR.PATCH).
  • Automatiseer releases (GitHub Releases/Actions) en tags (git tag v0.1.0).
  • Publiceren naar PyPI kan met twine (buiten scope voor volledige flow).

5.5 Data‑science templates: extra overwegingen

  • Mappen voor data/ (raw/processed), notebooks/, reports/, src/.
  • Voorgeconfigureerde linting (ruff/flake8), formatting (black), en pre‑commit hooks.
  • CI‑workflow voor basischecks en (optioneel) tests.

5.6 Samenvatting

Templates en packaging geven je een vliegende start en betere onderhoudbaarheid. Combineer dit met Git‑workflows (Hoofdstuk 4) en de tools uit Deel III–V voor robuuste, herhaalbare data‑projecten.

Hoofdstuk 6 — NumPy‑arrays en vectorisatie: denken in arrays

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen wat een ndarray is en waarom het de kern vormt van wetenschappelijk rekenen in Python.
  • Leren hoe je arrays maakt, inspecteert en transformeert.
  • Inzicht krijgen in vectorisatie en waarom dit meestal sneller en leesbaarder is dan Python‑loops.
  • Basisprincipes van dtypes, geheugenlayout, views vs. copies en eenvoudige broadcasting.

Aanbevolen voorkennis: Python‑basis (variabelen, functies, lists), elementaire wiskunde.

6.1 Wat is een NumPy‑array (ndarray)?

Een NumPy‑array (numpy.ndarray) is een homogene, N‑dimensionale container voor numerieke data. "Homogeen" betekent dat alle elementen hetzelfde datatypedelen (bijv. float64 of int32). Arrays ondersteunen snelle vector‑ en matrixoperaties dankzij continu geheugen en C/Fortran‑geoptimaliseerde implementaties.

Voorbeeld van een 1D‑array (vector) en 2D‑array (matrix):

import numpy as np

# 1D array
a = np.array([1, 2, 3, 4])
# 2D array (matrix)
B = np.array([[1, 2, 3],
              [4, 5, 6]])

print(a.ndim)   # 1
print(B.shape)  # (2, 3)
print(B.dtype)  # e.g., int64 (platform dependent)

Belangrijke attributen:

  • ndim: aantal dimensies (assen)
  • shape: tuple met de lengte per as
  • size: totaal aantal elementen (np.prod(shape))
  • dtype: data type van de elementen
  • itemsize: bytes per element

6.2 Arrays aanmaken

Er zijn veel manieren om arrays te maken.

6.2.1 Vanuit Python‑iterables

import numpy as np

np.array([1, 2, 3])                 # from list
np.array((1.0, 2.5, 3.14))          # from tuple (dtype inferred as float)

# specify dtype
np.array([1, 2, 3], dtype=np.float32)

6.2.2 Reeks‑generatoren

np.arange(0, 10, 2)          # [0, 2, 4, 6, 8]
np.linspace(0, 1, 5)         # 5 points between 0 and 1 inclusive
np.geomspace(1, 1000, 4)     # [1., 10., 100., 1000.]

6.2.3 Vul‑ en identiteitsfuncties

np.zeros((2, 3))             # 2x3 zeros (float64 by default)
np.ones((3, 3), dtype=int)
np.full((2, 2), fill_value=7)
np.empty((1000,))            # uninitialized memory (fast, contents arbitrary)
np.eye(3)                    # 3x3 identity matrix

6.2.4 Willekeurige waarden

Let op: API’s verschillen tussen np.random (legacy) en np.random.default_rng (aanbevolen).

# Recommended modern RNG
rng = np.random.default_rng(seed=42)
rng.random((2, 3))       # uniform [0,1)
rng.normal(loc=0, scale=1, size=(2, 2))
rng.integers(low=0, high=10, size=5)

6.3 Dtypes en conversies

Het dtype bepaalt precisie, bereik, en geheugenverbruik.

Veelgebruikte dtypes: np.int32, np.int64, np.float32, np.float64, np.bool_, np.complex128.

x = np.array([1, 2, 3], dtype=np.int32)
x_float = x.astype(np.float64)   # copy with cast

# upcasting rules in mixed operations
print((x + 0.5).dtype)  # float64

Kies het kleinste dtype dat voldoet aan je nauwkeurigheidseisen om geheugen en tijd te besparen (bijv. float32 i.p.v. float64 voor grote modellen als de precisie voldoende is).

6.4 Basisbewerkingen en vectorisatie

Vectorisatie betekent opereren op hele arrays zonder expliciete Python‑loops. Dit is vaak sneller en duidelijker.

# Elementwise operations
x = np.array([1, 2, 3])
y = np.array([10, 20, 30])

print(x + y)      # [11 22 33]
print(x * 10)     # [10 20 30]
print(np.sqrt(x)) # [1.         1.41421356 1.73205081]

Vergelijk met een Python‑loop:

# Pure Python (slower and more verbose)
xs = [1, 2, 3]
ys = [10, 20, 30]
res = [xi + yi for xi, yi in zip(xs, ys)]  # vectorized feel but still Python loop

Veel NumPy‑functies zijn ufuncs (universele functies) die elementgewijs werken en vaak C‑geoptimaliseerd zijn. In Hoofdstuk 8 bespreken we ufuncs en aggregaties uitgebreider.

6.5 Vorm (shape), herstructureren en dimensies

Basisinspectie en reshaping:

A = np.arange(12)         # shape (12,)
A2 = A.reshape(3, 4)      # shape (3, 4)
A3 = A.reshape(2, 2, 3)   # shape (2, 2, 3)

print(A2.ndim)  # 2
print(A3.shape) # (2, 2, 3)

Gebruik -1 als flexibele dimensie:

B = np.arange(24).reshape(2, -1, 3)  # infer the middle dimension -> (2, 4, 3)

Rijen‑ versus kolom‑major opslaan:

  • C‑order (row‑major, default): laatste index verandert het snelst.
  • Fortran‑order (column‑major): eerste index verandert het snelst.
C = np.asfortranarray(np.arange(6).reshape(2, 3))
print(C.flags['F_CONTIGUOUS'])  # True

6.6 Broadcasting (basis)

Broadcasting maakt bewerkingen tussen arrays met verschillende shapes mogelijk door impliciete uitlijning van dimensies.

Regels (intuïtief):

  • Vergelijk shapes van rechts naar links.
  • Dimensies zijn compatibel als ze gelijk zijn of als één van beide 1 is.
  • Het resultaat heeft de "uitgebreide" shape.

Voorbeelden:

x = np.array([1, 2, 3])           # shape (3,)
M = np.array([[10], [20], [30]])  # shape (3,1)

print(M + x)  # result shape (3,3): rows [10,20,30] plus x

# Add a scalar to all elements
print(M + 5)

Meer geavanceerde cases en expliciete newaxes komen in Hoofdstuk 7 aan bod.

6.7 Views vs. copies en geheugen

Veel operaties in NumPy maken een view: een nieuwe "kijk" op hetzelfde blok geheugen zonder data te kopiëren. Kopieën kosten tijd en geheugen.

A = np.arange(6)
B = A.reshape(2, 3)  # view (no copy)
B[0, 0] = 99
print(A)  # [99  1  2  3  4  5]  <- changed!

# Force a real copy
C = A.copy()
C[0] = -1
print(A[0], C[0])  # 99 -1

Niet alle bewerkingen leveren views; sommige slicings wel, fancy indexing niet. In Hoofdstuk 7 werken we dit uit.

6.8 Boolean masks (kort intro)

Boolean arrays kunnen gebruikt worden om te filteren of te selecteren.

x = np.array([3, -1, 0, 5, 2])
mask = x > 0
print(mask)     # [ True False False  True  True]
print(x[mask])  # [3 5 2]

Diepere behandeling van masking en geavanceerde indexing volgt in Hoofdstuk 12.

6.9 Prestaties: waarom vectorisatie werkt

  • Minder Python‑overhead: ufuncs en vectoroperaties zijn in C geschreven.
  • Betere cache‑lokaliteit door contigu geheugen.
  • SIMD‑instructies en BLAS‑aanroepen onder water.

Snelle tips:

  • Vermijd Python‑loops over elementen; gebruik ufuncs en aggregaties.
  • Kies een passend dtype (bijv. float32 als precisie toereikend is).
  • Gebruik in‑place operaties waar mogelijk: x *= 2 i.p.v. x = x * 2.
  • Overweeg np.ascontiguousarray of np.asfortranarray wanneer bibliotheken layout vereisen.

6.10 Veelgemaakte valkuilen

  • Onbedoelde kopieën bij fancy indexing of typecasts (astype maakt een kopie) — let op geheugen.
  • Meng geen Python lists en arrays in vectoroperaties: cast eerst naar np.array.
  • Verwarring over shapes met 1‑dimensionele arrays vs. kolom/rij‑vectoren; gebruik np.newaxis of reshape.

6.11 Korte oefeningen

  1. Maak een array van 0 t/m 99, reshape naar (20, 5) en normaliseer elke kolom naar gemiddeld 0 en std 1, zonder expliciete Python‑loops.
  2. Simuleer 1000 dobbelsteenworpen (waarden 1–6) met de moderne RNG en bereken de relatieve frequenties per uitkomst.
  3. Laat met een klein voorbeeld zien dat een view de onderliggende data deelt, en vergelijk met een echte kopie.

Hint voor 1): gebruik ufuncs en broadcasting; voor 2): rng.integers en np.bincount; voor 3): reshape, copy en toewijzing.


Werkboek en uitwerkingen:

  • Oefeningen bij dit hoofdstuk vind je in ../workbook/hoofdstuk-06/oefeningen.md.
  • Uitwerkingen staan in ../solutions/hoofdstuk-06/uitwerkingen.md.

Vervolg:

  • Hoofdstuk 7: indexeren, slicen, reshapen en broadcasting in detail.
  • Hoofdstuk 8: ufuncs, aggregaties en NaN‑behandeling.

Hoofdstuk 7 — Indexeren, slicen, reshapen en broadcasting (NumPy‑verdieping)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Krachtig en correct werken met NumPy‑indexering en slicing (1D en N‑D).
  • Begrijpen wanneer je een view of een kopie krijgt (en waarom dat uitmaakt).
  • Reshapen en vormmanipulatie (incl. ravel, flatten, resize, volgordes C/Fortran).
  • Broadcasting in detail, inclusief veelvoorkomende patronen en valkuilen.

Aanbevolen voorkennis: Hoofdstuk 6 (ndarray‑basis, shapes, eenvoudige broadcasting), basis Python.

7.1 Basisindexering en slicing (views)

Eenvoudige slicing in NumPy levert in het algemeen een view op dezelfde data (geen kopie). Aanpassingen via de slice veranderen dus het origineel.

import numpy as np

x = np.arange(10)           # [0 1 2 3 4 5 6 7 8 9]
sub = x[2:7]                # view of x (elements 2..6)
sub[:] = -1                  # in-place modify the view
print(x)                    # [ 0  1 -1 -1 -1 -1 -1  7  8  9]
  • Syntax: start:stop:step waarbij start inclusief is en stop exclusief.
  • Weglaten van grenzen betekent begin of einde van de as.
  • Een negatieve step keert de volgorde om (negatieve stride).
x[::-1]     # reversed copy view (uses a negative stride)
x[::2]      # every second element
x[1:-1:2]   # slice from index 1 to the one-before-last, step 2

Let op: hoewel slicing meestal een view is, zijn er uitzonderingen bij niet‑triviale cases met fancy indexing (zie §7.3) die altijd een kopie oplevert.

7.2 Multidimensionale slicing en as‑volgorde

Je kunt per as slicen met komma‑gescheiden indices.

A = np.arange(24).reshape(4, 6)  # shape (4, 6)
# rows 1..2 (inclusive of 1, exclusive of 3) and columns 2..4
B = A[1:3, 2:5]    # shape (2, 3), view

# select a single row keeps 1D array unless you use a slice
row1 = A[1]        # shape (6,) — 1D
row1_2D = A[1:2]   # shape (1, 6) — 2D view

# mix of slices and integers
col3 = A[:, 3]     # shape (4,) — 1D selection of the 4th column
block = A[::2, ::3]  # strides over both axes
  • Een integer‑selectie op een as verwijdert die dimensie in het resultaat.
  • Gebruik slicing met start:stop om de dimensie te behouden.

7.2.1 Negatieve indexen en grenzen

Negatieve indexen tellen vanaf het einde: -1 is de laatste, -2 de een‑na‑laatste.

A[-1, -1]     # last element in last row
A[:, -2:]     # last two columns

7.3 Fancy indexing: integer en boolean arrays (kopieën)

Fancy indexing met lijsten/arrays van indices of met boolean masks maakt ALTIJD een kopie van de geselecteerde elementen. Dit is krachtig, maar kan geheugen kosten.

7.3.1 Integer‑array indexing

x = np.arange(10)
idx = np.array([0, 3, 3, 7])
y = x[idx]        # copy with elements at positions 0, 3, 3, 7
y[0] = 999
print(x[0])       # 0 (unchanged, because y is a copy)

Je kunt multi‑axis integer arrays combineren:

M = np.arange(12).reshape(3, 4)
rows = np.array([0, 2])
cols = np.array([1, 3])
sel = M[rows[:, None], cols]   # shape (2, 2): pairs (0,1), (0,3), (2,1), (2,3)

7.3.2 Boolean‑mask indexing

x = np.array([3, -1, 0, 5, 2])
mask = x > 0
pos = x[mask]      # copy of positive elements

# Assigning via boolean mask writes in-place to original (on the left side)
x[x < 0] = 0       # negative values set to 0 in x

Let op: rechts van de toekenning (x[mask]) is het resultaat een kopie; links (x[mask] = ...) schrijft terug naar x.

7.4 Views vs copies: hoe herken je ze?

  • Basis slicing (met :) → meestal view.
  • Fancy indexing (integer/boolean) → kopie.
  • reshape → meestal view wanneer mogelijk (contigu geheugen); anders kan het een kopie maken.
  • ravel() → retourneert een 1D‑view indien mogelijk; flatten() maakt altijd een kopie.
A = np.arange(6).reshape(2, 3)
B = A.reshape(3, 2)        # view if memory layout allows
C = A.flatten()            # always a copy
D = np.ravel(A)            # view if possible

B[0, 0] = 99
print(A[0, 0])  # likely 99 if B is a view

Tip: gebruik np.shares_memory(A, B) om te controleren of arrays geheugen delen.

7.5 Vormmanipulatie: reshape, ravel, resize, squeeze, expand

  • reshape(newshape): retourneert view indien mogelijk; -1 voor inferentie.
  • ravel(): 1D weergave (view indien mogelijk), respecteert standaard C‑order.
  • flatten(order='C'|'F'): altijd kopie; kies orde.
  • resize(newshape): kan data heralloceren en aanvullen; gebruik spaarzaam.
  • np.squeeze(a, axis=None): verwijdert eenheden‑dimensies (dimensies met lengte 1).
  • np.expand_dims(a, axis): voegt een as toe; gelijkwaardig aan a[np.newaxis, ...] of a[..., np.newaxis].
x = np.arange(6)
X = x.reshape(2, 3)
col_vec = x[:, np.newaxis]     # shape (6, 1)
row_vec = x[np.newaxis, :]     # shape (1, 6)

Y = np.squeeze(np.zeros((1, 3, 1)))  # shape (3,)
Z = np.expand_dims(np.array([1, 2, 3]), axis=-1)  # shape (3, 1)

7.6 Broadcasting in detail

Broadcasting vergelijkt shapes van rechts naar links. Dimensies zijn compatibel als ze gelijk zijn of één is 1. Het resultaat neemt de uitgebreide dimensies aan.

7.6.1 Voorbeelden met expliciete as‑uitbreiding

x = np.array([1, 2, 3])        # (3,)
W = np.array([10, 20, 30, 40]) # (4,)
M = x[np.newaxis, :] + W[:, np.newaxis]  # (4, 3)

7.6.2 Schalen en centreren per kolom/rij

A = np.arange(12).reshape(3, 4)
col_mean = A.mean(axis=0)      # shape (4,)
A_centered = A - col_mean      # broadcast over rows
row_std = A.std(axis=1, keepdims=True)  # shape (3,1)
A_scaled = A / row_std

7.6.3 Geavanceerder: afstandsmatrices met broadcasting

points = np.array([[0.0, 0.0],
                   [1.0, 0.0],
                   [0.0, 1.0]])         # shape (3, 2)
# pairwise squared Euclidean distances
D = ((points[:, np.newaxis, :] - points[np.newaxis, :, :]) ** 2).sum(axis=-1)
# D.shape -> (3, 3)

7.7 Prestaties, geheugen en valkuilen

  • Fancy indexing kan onverwacht veel geheugen gebruiken (kopieën) — gebruik boolean assignment of slicing waar mogelijk.
  • Onbedoelde broadcast naar zeer grote shapes kan een MemoryError veroorzaken — check shapes expliciet (.shape).
  • Negatieve strides (zoals x[::-1]) kunnen problemen geven met sommige C‑extensies die contiguïteit vereisen — gebruik x.copy() of np.ascontiguousarray wanneer nodig.
  • In‑place bewerkingen (+=, *=) zijn sneller en alloceren minder, maar let op type‑promotie (kan kopieën veroorzaken).

7.8 Veelgemaakte patronen

  • Kolom/rij‑vectoren expliciet maken met [:, None] of None, :.
  • Batch‑dimensie toevoegen voor vectorized evaluaties: x[None, ...] of x[..., None].
  • Masked updates: A[A < 0] = 0.
  • Indexpatronen bouwen voor blokselecties met np.ix_:
M = np.arange(16).reshape(4, 4)
rows = np.array([0, 2])
cols = np.array([1, 3])
block = M[np.ix_(rows, cols)]   # shape (2, 2), copy

7.9 Korte oefeningen

  1. Geef een 2D array A (shape (5, 6)). Selecteer een 3x3 blok in het midden met slicing en zet de waarden op 0 (in‑place). Toon dat A is aangepast.
  2. Maak van een vector x (shape (N,)) een kolomvector en een rijvector en vorm daarmee de sommatrix van shape (N, N) zonder tile.
  3. Gebruik fancy indexing om uit een matrix M (shape (4, 5)) de elementen op (0,0), (1,2), (2,4), (3,1) in een nieuwe array te verzamelen.
  4. Gegeven punten in R^d in array P met shape (n, d): construeer een matrix met alle paargewijze afstanden door broadcasting te gebruiken (geen loops).

Hints: gebruik np.newaxis/None, np.ix_, sum(axis=-1) en let op shapes.


Werkboek en uitwerkingen:

  • Oefeningen bij dit hoofdstuk vind je in ../workbook/hoofdstuk-07/oefeningen.md.
  • Uitwerkingen staan in ../solutions/hoofdstuk-07/uitwerkingen.md.

Verder lezen en vervolg:

  • Hoofdstuk 12 behandelt geavanceerde indexing, masking en boolean logica diepgaander.
  • Terugkoppeling met Hoofdstuk 6 voor basisbegrippen over arrays en geheugen.

Hoofdstuk 8 — Universele functies (ufuncs), aggregaties en NaN‑behandeling

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen wat universele functies (ufuncs) zijn en waarom ze de ruggengraat vormen van vectorisatie in NumPy.
  • Leren werken met ufunc‑eigenschappen: broadcasting, type‑promotie, out‑parameter, where, en speciale methoden zoals reduce, accumulate en outer.
  • Aggregaties correct gebruiken (per as/axis, met/zonder keepdims) en de verschillen kennen tussen standaard en NaN‑bewuste varianten.
  • Robuuste NaN/Inf‑behandeling: detectie, filteren, maskeren en numerieke valkuilen.

Aanbevolen voorkennis: Hoofdstuk 6 (arrays, vectorisatie), Hoofdstuk 7 (indexeren, reshapen, broadcasting).

8.1 Wat is een ufunc?

Een universele functie (ufunc) is een C‑geoptimaliseerde, elementgewijze functie die werkt op NumPy‑arrays met ondersteuning voor broadcasting, type‑casting en meerdere invoer/uitvoer‑argumenten.

Veel voorkomende ufuncs:

  • Unair (één input): np.negative, np.abs, np.sqrt, np.exp, np.log, np.sin, np.isnan, np.isfinite.
  • Binair (twee inputs): np.add, np.subtract, np.multiply, np.divide, np.power, np.maximum, np.minimum.
import numpy as np

x = np.array([1.0, 4.0, 9.0])
print(np.sqrt(x))         # [1. 2. 3.]
print(np.add(x, 1))       # [ 2. 5. 10.]

Waarom ufuncs?

  • Snel: geschreven in C, vermijden Python‑loops.
  • Expressief: elementgewijze semantiek met broadcasting.
  • Consistent: uniforme API (o.a. out, where, dtype‑regels).

8.2 Broadcasting en ufuncs

Ufuncs ondersteunen standaard broadcasting (zie Hoofdstuk 7).

x = np.array([1, 2, 3])          # shape (3,)
M = np.array([[10], [20], [30]]) # shape (3,1)
print(np.add(M, x))              # shape (3,3)

Je kunt explieter zijn met as‑uitbreiding:

col = x[:, None]                 # (3,1)
row = x[None, :]                 # (1,3)
sum_matrix = np.add(col, row)    # (3,3)

8.3 Type‑promotie en casting

NumPy beslist het uitvoer‑dtype op basis van de input‑dtypes en de ufunc.

x = np.array([1, 2, 3], dtype=np.int32)
y = np.array([0.5, 1.5, 2.5], dtype=np.float64)
print((x + y).dtype)  # float64 (upcast)

Let op:

  • Integer deling met / produceert floats; // is floor division (let op negatieven).
  • Overflows zijn mogelijk met integer dtypes; kies int64 of floats waar nodig.

8.4 De out‑parameter en in‑place rekenen

Veel ufuncs accepteren out= om het resultaat in een bestaande array te schrijven (minder allocaties).

x = np.arange(5, dtype=np.float64)
y = np.empty_like(x)
np.sqrt(x, out=y)          # y gets sqrt(x)
np.add(x, 1, out=x)        # in-place: x <- x + 1

Dit is geheugen‑ en cache‑vriendelijk. Pas op met type‑compatibiliteit: out moet het resultaat kunnen representeren (geen stille truncatie).

8.5 Voorwaardelijk rekenen met where

where op een ufunc laat je elementgewijs bepalen waar de operatie plaatsvindt; elders wordt de bestaande out behouden.

x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
res = np.empty_like(x)
np.sqrt(np.abs(x), out=res)                 # fill res first
np.sqrt(x, out=res, where=(x >= 0))         # only where x>=0; others keep previous
print(res)  # sqrt for non-negative x, abs->sqrt for negatives via prefill

Vergelijk met de hogere‑niveau helper np.where(cond, A, B) die een nieuwe array bouwt; de ufunc‑variant kan allocaties vermijden.

8.6 Speciale ufunc‑methoden: reduce, accumulate, outer

Ufuncs bieden methoden die structurele operaties uitvoeren zonder expliciete Python‑loops.

  • reduce: herhaalde toepassing langs een as (klassieke aggregatie).
  • accumulate: tussenresultaten (prefix‑operatie).
  • outer: cartesiaanse toepassing op alle paren elementen uit twee vectoren.
x = np.array([1, 2, 3, 4])
print(np.add.reduce(x))        # 10 (equivalent aan np.sum)
print(np.multiply.accumulate(x))  # [ 1  2  6 24]

u = np.array([1, 2, 3])
v = np.array([10, 20, 30])
print(np.multiply.outer(u, v))   # 3x3 outer product

Opmerking: Voor veelgebruikte gevallen bestaan directe functies (np.sum, np.cumsum, np.outer). De ufunc‑methoden zijn expliciet en kunnen handig zijn in generieke code.

8.7 Aggregaties: som, gemiddelden, extremen, arg‑varianten

Aggregaties reduceren dimensies. Belangrijke parameters:

  • axis: over welke as(sen) te aggregeren. None (default) aggregeert over alles.
  • keepdims: behoud eenheden‑dimensies (handig voor broadcasting terug).
  • dtype: reken‑dtype (bijv. som van int32 als int64 om overflow te vermijden).
A = np.arange(12).reshape(3, 4)
print(A.sum())                 # scalar
print(A.sum(axis=0))           # shape (4,)
print(A.mean(axis=1, keepdims=True))  # shape (3,1)
print(A.argmax(axis=1))        # index van maximum per rij

Let op: argmin/argmax geven indexen terug; gebruik die voor geavanceerde selectie.

8.8 NaN‑bewuste varianten en ontbrekende waarden

Standaard aggregaties propageren NaNs: elk NaN in de reductie maakt het resultaat NaN.

x = np.array([1.0, np.nan, 3.0])
print(np.sum(x))     # nan

Gebruik de NaN‑varianten om NaNs te negeren:

print(np.nanmean(x))   # 2.0
print(np.nanstd(x))    # std over niet-NaN elementen
print(np.nanmin(x), np.nanmax(x))

Detecteren en maskeren:

A = np.array([1.0, np.nan, np.inf, -np.inf])
mask = np.isfinite(A)          # True voor eindige waarden
finite_vals = A[mask]

Veelvoorkomende strategieën:

  • Filter NaNs met boolean masks (np.isnan, np.isfinite).
  • Vervang NaNs met np.nan_to_num(A, nan=0.0, posinf=None, neginf=None) of np.where(np.isnan(A), fill, A).
  • Gebruik dtype=float wanneer NaNs verwacht worden; integers kunnen geen NaN representeren.

8.9 Logische en vergelijkings‑ufuncs

Vergelijkingsufuncs leveren boolean arrays, bruikbaar voor masking of all/any.

x = np.array([1, 2, 3])
y = np.array([2, 2, 2])
print(np.greater(x, y))   # [False False  True]
print(np.equal(x, y))     # [False  True False]
print(np.logical_and(x > 0, y == 2))  # boolean combine
print((x > 0).all(), (x > 0).any())

Let op verschil tussen logische operators en bitwise operators:

  • Voor boolean arrays: & (and), | (or), ~ (not) werken elementgewijs, maar vereisen haakjes door operator‑precedence.
  • Gebruik geen and/or met arrays (die werken niet elementgewijs).

8.10 Numerieke stabiliteit, overflow en precisie

  • Integers kunnen over‑ en onderlopen (stil). Gebruik grotere dtypes of floats bij cumulatieve sommen (dtype=np.int64 of float64).
  • np.exp kan snel naar inf gaan; combineer met np.logaddexp/np.log1p/np.expm1 voor stabiele vormen.
  • Voor sommen met floats kan np.sum afhankelijk van volgorde kleine fouten maken; np.sum(..., dtype=np.float64) of math.fsum (op Python lists) kan nauwkeuriger zijn. np.nansum heeft dezelfde overwegingen.
x = np.array([1e16, 1, -1e16])
print(np.sum(x))          # 0.0 of 1.0 afhankelijk van order; hier vaak 0.0
print(np.sum(x, dtype=np.float64))

8.11 Veel voorkomende patronen en idiomen

  • Normaliseren per kolom: Z = (A - A.mean(axis=0)) / A.std(axis=0, ddof=0).
  • Clippen van waarden: np.clip(x, a_min, a_max) (ufunc‑achtig, zeer snel).
  • Sigmoid en softmax:
z = np.linspace(-6, 6, 5)
sigmoid = 1 / (1 + np.exp(-z))
# Softmax per rij
X = np.array([[1.0, 2.0, 3.0], [1.0, 2.0, 4.0]])
Xmax = X.max(axis=1, keepdims=True)
exp = np.exp(X - Xmax)
softmax = exp / exp.sum(axis=1, keepdims=True)
  • Gebruik where om veilig te delen:
num = np.array([1.0, 2.0, 0.0])
den = np.array([0.0, 2.0, 0.0])
res = np.divide(num, den, out=np.zeros_like(num), where=den!=0)

8.12 Prestaties en geheugen

  • Vermijd np.vectorize voor performance; het is een handige wrapper rond Python‑loops, niet snel. Gebruik echte ufuncs of schrijf C/Numba/Cython indien nodig.
  • Gebruik out= en in‑place operatoren (+=, *=, etc.) om allocaties te beperken.
  • Let op broadcasting naar enorme tussenresultaten (geheugenexplosie). Controleer shapes en gebruik associativiteit/volgorde om tussenstappen te beperken.
  • Kies passende dtypes (float32 kan veel sneller/zuiniger zijn op grote arrays; test nauwkeurigheid).

8.13 Korte oefeningen

  1. Geef een vector x en gebruik np.add.reduce om de som te berekenen. Vergelijk met np.sum en verklaar de relatie.
  2. Gebruik np.multiply.accumulate om een cumulatief product van een vector te berekenen en verifieer met np.cumprod.
  3. Maak een (n, m) array met NaNs en bereken de kolomgemiddelden waarbij NaNs genegeerd worden; vergelijk mean met nanmean.
  4. Construeer een matrix van alle paren‑som combinaties van twee vectoren met np.add.outer en check de shape.
  5. Deel twee arrays veilig waarbij delen door nul voorkomt, zonder warnings en zonder NaN, met behulp van where en out.

Werkboek en uitwerkingen:

  • Oefeningen bij dit hoofdstuk vind je in ../workbook/hoofdstuk-08/oefeningen.md.
  • Uitwerkingen staan in ../solutions/hoofdstuk-08/uitwerkingen.md.

Verder lezen en vervolg:

  • Hoofdstuk 6: basis vectorisatie en ndarray‑mechanica.
  • Hoofdstuk 7: broadcasting‑regels en vormmanipulaties.
  • Hoofdstuk 12: geavanceerde masking/boolean logica en meer indexeringstrucs.

Hoofdstuk 9 — Lineaire algebra, random number generation en numerieke stabiliteit

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Krachtig werken met lineaire algebra in NumPy: vectoren, matrices, products (@, matmul, einsum).
  • Lineaire systemen oplossen, least squares, rangen, condities en de pseudo-inverse.
  • Overzicht van decomposities (SVD, eigenwaarden/-vectoren) en hun toepassingen.
  • Moderne random number generation (RNG) met numpy.random.Generator voor reproduceerbare experimenten.
  • Numerieke stabiliteit: conditionering, schaalproblemen, stabiele patronen en valkuilen.

Aanbevolen voorkennis: Hoofdstuk 6–8 (arrays, indexeren/broadcasting, ufuncs/aggregaties), basis lineaire algebra.

9.1 Vectors en matrices in NumPy

NumPy vertegenwoordigt vectoren en matrices als ndarray’s. Een 1D-array is een vector; 2D is een matrix.

import numpy as np

x = np.array([1.0, 2.0, 3.0])          # vector (3,)
A = np.array([[1.0, 2.0],
              [3.0, 4.0],
              [5.0, 6.0]])             # matrix (3, 2)

print(x.shape)  # (3,)
print(A.shape)  # (3, 2)

Kolom- en rijvectoren expliciteren met None/np.newaxis (zie H6–H7):

col = x[:, None]  # (3, 1)
row = x[None, :]  # (1, 3)

9.2 Matrix- en vectorproducten: @, matmul, einsum

  • @ en np.matmul voeren matrixvermenigvuldiging uit (geen elementgewijs product).
  • np.dot is voor 1D gelijk aan inwendig product; voor 2D komt het overeen met matmul.
  • np.einsum geeft expliciete controle over indexcontracties (leesbaar bij complexe patronen).
u = np.array([1.0, 2.0, 3.0])      # (3,)
B = np.array([[1.0, 0.0, 2.0],
              [0.0, 1.0, 3.0]])    # (2, 3)

# matrix-vector
y = B @ u            # shape (2,)

# matrix-matrix
C = np.array([[1.0, 2.0], [3.0, 4.0]])   # (2,2)
D = B @ C             # (2,3) @ (2,2) -> ValueError (incompatible)
# Correct: (3,2) @ (2,2)
E = B.T @ C           # shape (3, 2) @ (2, 2) -> (3, 2)

# einsum voorbeelden
# inwendig product van u met zichzelf
ip = np.einsum('i,i->', u, u)      # scalar
# batch matrix-vector: (n,m) x (m,) voor meerdere matrices/vektoren
M = np.arange(12.0).reshape(3, 4)
w = np.arange(4.0)
res = M @ w                         # (3,)

Tip: controleer shapes expliciet om dimensionele fouten te voorkomen.

9.3 Normen, lengtes en projecties

  • Vectornormen: np.linalg.norm(x, ord=2) (Euclidisch); ord=1, ord=np.inf voor L1/L∞.
  • Matrixnormen: ook via np.linalg.norm; let op definitie (induced norms).
  • Normaliseren (unit vector) en projecties.
x = np.array([3.0, 4.0])
length = np.linalg.norm(x)          # 5.0
unit = x / length                   # [0.6, 0.8]

# Projecteer vector v op richting u (niet-nul)
u = np.array([1.0, 2.0, 2.0])
v = np.array([2.0, 1.0, 0.0])
proj_v_on_u = (v @ u) / (u @ u) * u

Orthogonaliteit: u @ v == 0 (binnen numerieke tolerantie) betekent loodrecht.

9.4 Lineaire systemen: solve, lstsq, pinv

Los systemen Ax = b op met stabiele routines; vermijd handmatig inverteren.

A = np.array([[3.0, 2.0],
              [1.0, 2.0]])
b = np.array([5.0, 5.0])

# Exact vierkant en niet-singulier
x = np.linalg.solve(A, b)

# Over- of onderbepaald: least squares
M = np.array([[1.0, 1.0],
              [1.0, 2.0],
              [1.0, 3.0]])
y = np.array([1.0, 2.0, 2.5])
coef, resid, rank, s = np.linalg.lstsq(M, y, rcond=None)

# Pseudo-inverse (Moore-Penrose)
M_pinv = np.linalg.pinv(M)     # gebaseerd op SVD
coef2 = M_pinv @ y

Belangrijk:

  • Gebruik solve/lstsq in plaats van inv(A) @ b. Inversen zijn duurder en numeriek minder stabiel.
  • Controleer rank en s (singuliere waarden) om conditionering te beoordelen.

9.5 Rang, determinant, condities, condition number

  • Rang (np.linalg.matrix_rank(A)) geeft het aantal lineair onafhankelijke rijen/kolommen.
  • Determinant (np.linalg.det(A)) vertelt over singulariteit maar is gevoelig voor schaal.
  • Condition number (np.linalg.cond(A)) meet gevoeligheid; hogere waarden impliceren instabiliteit.
A = np.array([[1.0, 2.0], [2.0, 4.000001]])
print(np.linalg.matrix_rank(A))
print(np.linalg.det(A))
print(np.linalg.cond(A))

Regels:

  • Slecht geconditioneerde systemen vergroten afrondingsfouten; her-schaal kolommen/rijen of gebruik regularisatie.
  • Inspecteer cond(A) en pas model/algoritme aan bij grote waarden.

9.6 Eigenwaarden/-vectoren en SVD (overzicht)

  • Eigen-decompositie: A v = λ v voor vierkante matrices.
  • SVD: A = U Σ Vᵀ werkt voor rechthoekige matrices en is robuust; nuttig voor PCA, pseudo-inversen en rangen.
A = np.array([[2.0, 0.0], [0.0, 1.0]])
# Eigenwaarden/-vectoren
w, V = np.linalg.eig(A)
# SVD
U, S, VT = np.linalg.svd(A, full_matrices=False)

# Reconstructie
A_rec = (U * S) @ VT

Opmerkingen:

  • Voor symmetrische (Hermitische) matrices is np.linalg.eigh numeriek stabieler dan eig.
  • SVD levert singuliere waarden in aflopende volgorde; kleine waarden wijzen op bijna-singulariteit.

9.7 Geen inverses bouwen: solve/cho_solve

Als A symmetrisch positief-definiet (SPD) is, gebruik Cholesky-factorisatie.

# SPD voorbeeld
S = np.array([[4.0, 2.0], [2.0, 3.0]])
L = np.linalg.cholesky(S)   # S = L @ L.T
# los Sx=b op via forward/back substitution
b = np.array([1.0, 2.0])
# Gebruik solve met triangular matrices via scipy.linalg (indien beschikbaar) of terugvallen op np.linalg.solve
x = np.linalg.solve(S, b)

Richtlijnen:

  • Vermijd inv(A). Gebruik specifieke oplossers.
  • Hergebruik factorisaties waar mogelijk.

9.8 Moderne RNG: numpy.random.Generator

Gebruik de nieuwe API voor reproduceerbare en onafhankelijke streams.

rng = np.random.default_rng(seed=42)   # PCG64 als default

# Distributies
u = rng.random(5)                      # Uniform [0,1)
z = rng.standard_normal(5)             # N(0,1)
k = rng.integers(low=0, high=10, size=5)

# Multivariate normaal
mean = np.array([0.0, 0.0])
cov = np.array([[1.0, 0.8], [0.8, 2.0]])  # symmetrisch PSD
samples = rng.multivariate_normal(mean, cov, size=1000, check_valid='warn')

# Permutaties
arr = np.arange(10)
rng.shuffle(arr)        # in-place
p = rng.permutation(5)  # nieuw array

Best practices:

  • Gebruik één Generator per task/worker; vermijd globale np.random state.
  • Zaai (seed) expliciet voor reproduceerbaarheid; gebruik SeedSequence voor hiërarchische seeds in parallelle settings.

9.9 Numerieke stabiliteit in de praktijk

  • Schaal feature-matrices (kolomgewijs centreren/standaardiseren) om conditionering te verbeteren.
  • Gebruik stabiele vormen: logsumexp in plaats van naïeve np.exp + sum + log.
  • Kies geschikte dtypes (float64 standaard; float32 voor snelheid/geheugen, maar controleer fouten).
  • Vermijd subtractie van bijna gelijke getallen (catastrofale annulatie).
# Softmax (stabiel per rij)
X = np.array([[1.0, 2.0, 3.0], [1.0, 2.0, 4.0]])
Xmax = X.max(axis=1, keepdims=True)
exp = np.exp(X - Xmax)
softmax = exp / exp.sum(axis=1, keepdims=True)

# Log-sum-exp truc
logsumexp = Xmax[:, 0] + np.log(exp.sum(axis=1))

Conditionering illustratie:

A = np.array([[1.0, 1.0], [1.0, 1.000001]])
b = np.array([2.0, 2.000001])
sol = np.linalg.solve(A, b)
print('cond(A)=', np.linalg.cond(A), ' solution=', sol)

9.10 Prestaties en geheugen

  • NumPy gebruikt BLAS/LAPACK (via OpenBLAS/MKL) voor @, svd, eig, etc.
  • Zorg voor contigue arrays met juiste dtype (float64 of float32) voor maximale performance.
  • Voorkom enorme tussenresultaten: gebruik @/einsum i.p.v. broadcast+sum wanneer het eigenlijk een matrixproduct is.
  • Batched operaties: @ en matmul ondersteunen broadcasting over leading dims (3D+).
# Batched matmul: (batch,n,m) @ (batch,m,p) -> (batch,n,p)
A = np.random.default_rng(0).random((10, 3, 4))
B = np.random.default_rng(1).random((10, 4, 5))
C = A @ B
assert C.shape == (10, 3, 5)

9.11 Veelvoorkomende patronen en valkuilen

  • Z-standaardisatie: Z = (X - X.mean(axis=0)) / X.std(axis=0, ddof=0).
  • Regressie met lstsq i.p.v. inv(X.T@X) @ X.T @ y.
  • Projectiematrix met orthonormale kolommen Q: P = Q @ Q.T (symmetrisch idempotent).
  • Controleer symmetrie (np.allclose(A, A.T)) voordat je eigh gebruikt.

9.12 Korte oefeningen

  1. Geef A (shape (3,3)) en b (shape (3,)). Los Ax=b op met np.linalg.solve en verifieer door terug te vermenigvuldigen.
  2. Maak een matrix X (shape (n,d)) en bereken de covariantiematrix S = X_centered.T @ X_centered / (n-1). Gebruik np.linalg.eig of np.linalg.svd om de hoofdrichtingen te vinden.
  3. Demonstreer het verschil tussen inv(A) @ b en solve(A, b) qua snelheid/nauwkeurigheid op een willekeurige SPD matrix.
  4. Gebruik de moderne RNG om 1000 samples uit N(0, I) in 2D te trekken en schat de empirische covariantie; vergelijk met de theoretische.
  5. Laat met een slecht geconditioneerde matrix zien dat kleine verstoringen in b leiden tot grote verschillen in x.

Werkboek en uitwerkingen:

  • Oefeningen bij dit hoofdstuk vind je in ../workbook/hoofdstuk-09/oefeningen.md.
  • Uitwerkingen staan in ../solutions/hoofdstuk-09/uitwerkingen.md.

Verder lezen en vervolg:

  • Hoofdstuk 8: numerieke stabiliteit van ufuncs en aggregaties.
  • Hoofdstuk 10: prestaties en geheugenbeheer met NumPy.
  • Appendix/verdere bronnen: LAPACK/BLAS, Golub & Van Loan (Matrix Computations).

Hoofdstuk 10 — Prestaties en geheugenbeheer met NumPy

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen waar NumPy-snelheid vandaan komt en hoe je die benut.
  • Leren hoe geheugenlayout, dtypes, views/kopieën en broadcasting de performance en het geheugengebruik beïnvloeden.
  • Praktische strategieën voor minder allocaties, betere cache-lokaliteit en controle over multithreading.
  • Werken met grote data (memory mapping, chunking) en tips om out-of-memory te vermijden.

Aanbevolen voorkennis: Hoofdstuk 6–9 (arrays, indexeren/broadcasting, ufuncs/aggregaties, lineaire algebra en numerieke stabiliteit).

10.1 Waarom is NumPy snel?

  • Vectorisatie: operaties zijn geïmplementeerd in C (ufuncs/BLAS), vermijden Python-loop overhead.
  • Contigu geheugen en eenvoudige dtypes bevorderen cache-hits en SIMD-vectorisatie.
  • Aanroepen naar geoptimaliseerde bibliotheken (BLAS/LAPACK) voor lineaire algebra.

Vergelijking loop vs vectorisatie:

import numpy as np
from time import perf_counter

n = 2_000_000
x = np.random.default_rng(0).random(n)
y = np.random.default_rng(1).random(n)

# Python loop (for illustration; avoid in production)
start = perf_counter()
res_py = [xi + yi for xi, yi in zip(x, y)]
t_loop = perf_counter() - start

# Vectorized
start = perf_counter()
res_np = x + y
t_vec = perf_counter() - start

len(res_py), np.allclose(res_np, np.array(res_py)), t_loop, t_vec

10.2 Dtypes, alignement en geheugenvoetafdruk

  • Kies het kleinste dtype dat nauwkeurig genoeg is (bijv. float32 i.p.v. float64) om geheugen en bandbreedte te besparen.
  • itemsize en size bepalen de bytes: bytes = size * itemsize.
  • Integers kunnen over-/onderlopen; floats hebben precisie/NaN/Inf.
import numpy as np

n = 10_000_000
x32 = np.empty(n, dtype=np.float32)
x64 = np.empty(n, dtype=np.float64)
mb32 = x32.nbytes / (1024**2)
mb64 = x64.nbytes / (1024**2)
mb32, mb64

10.3 Views vs. kopieën: kostprijs en detectie

  • Views zijn gratis in geheugen (gedeelde data) en snel te maken; kopieën kosten O(n) tijd en geheugen.
  • Fancy indexing (integer/boolean) maakt altijd een kopie; slicing meestal een view.
  • Detecteer met np.shares_memory(a, b) en inspecteer a.flags voor contiguïteit.
import numpy as np

A = np.arange(12).reshape(3, 4)
B = A[:, 1:3]          # view
C = A[:, [1, 2]]       # copy (fancy indexing)
np.shares_memory(A, B), np.shares_memory(A, C)

10.4 Contiguïteit, strides en order (C vs Fortran)

  • C-order: laatste index varieert het snelst (standaard). Fortran-order: eerste index varieert het snelst.
  • Contigue arrays doorlopen in geheugenvolgorde bevordert cache-lokaliteit.
  • Converteer layout indien nodig voor bibliotheken: np.ascontiguousarray, np.asfortranarray.
import numpy as np

A = np.arange(12).reshape(3, 4)          # C-order by default
AF = np.asfortranarray(A)
A.flags['C_CONTIGUOUS'], AF.flags['F_CONTIGUOUS'], A.strides, AF.strides

10.5 Broadcasting: krachtig maar let op explosies

  • Broadcasting maakt tijdelijk grotere logische shapes; het resultaat wordt niet gematerialiseerd, maar sommige combinaties creëren enorme tijdelijke arrays bij vervolgbewerkingen.
  • Tip: reken in de juiste volgorde en reduceer vroeg (bijv. som over de gebroadcastte as binnen dezelfde uitdrukking).
import numpy as np

X = np.random.default_rng(0).random((10_000, 128))   # (N, d)
w = np.random.default_rng(1).random(128)             # (d,)
# Goed: reduceer binnen de uitdrukking
s = np.sum(X * w, axis=1)    # (N,)

Valkuil: onbedoelde broadcast naar (N, N) voor paargewijze afstanden; gebruik efficiëntere vormen (@, einsum) of chunking.

10.6 Minder allocaties: in-place en out=

  • Gebruik in-place operators (+=, -=, *=, np.add(..., out=...)) om tijdelijke arrays te vermijden.
  • Let op dtype-compatibiliteit; anders forceert NumPy een kopie.
import numpy as np

x = np.arange(5, dtype=np.float64)
np.add(x, 1.0, out=x)    # in-place add
# Voorwaardelijk schrijven zonder extra allocaties
y = np.empty_like(x)
np.sqrt(x, out=y)
np.sqrt(x, out=y, where=(x > 0))
y

10.7 Operationele fusie en ufunc-idiomen

  • Combineer ufuncs zodat NumPy intern minder tussenresultaten aanmaakt (niet alle ufuncs fuseren automatisch, maar out= en where= helpen).
  • Gebruik ufunc-methoden (reduce, accumulate, outer) en aggregatieparameters (axis, keepdims) om werk te beperken.
import numpy as np

A = np.random.default_rng(0).standard_normal((1000, 1000))
# Minder allocaties: centreer en schaal in twee passes maar zonder extra kopieën
col_mean = A.mean(axis=0)
A -= col_mean
col_std = A.std(axis=0, ddof=0)
A /= col_std

10.8 BLAS/LAPACK en threading

  • Matrixvermenigvuldiging en decomposities gebruiken BLAS/LAPACK (OpenBLAS, MKL, Accelerate).
  • Multithreading wordt door de backend geregeld; te veel threads kan tot oversubscripie leiden bij nested parallelisme.
  • Omgevingsvariabelen (bij processtart instellen): OPENBLAS_NUM_THREADS, MKL_NUM_THREADS, OMP_NUM_THREADS.
import numpy as np
np.show_config()   # laat zien welke BLAS/LAPACK eronder zit

Tip: in multiprocessing of binnen joblib/tqdm: beperk BLAS-threads per proces om thrash te vermijden.

10.9 NumExpr/Numba/Cython (wanneer verder gaan)

  • np.vectorize is geen performance-tool (het is syntactic sugar over Python-loops). Vermijd voor snelheid.
  • numexpr kan grote expresies sneller uitvoeren door chunking en multi-threading op cache-vriendelijke blokken.
  • Numba JIT-compileert Python naar machinecode; nuttig voor custom kernels die niet goed uit te drukken zijn met ufuncs/broadcasting.
  • Cython voor nog fijnmaziger controle; meer setup.

Doel van dit boek: eerst optimaal NumPy gebruiken; pas daarna JIT/extern als nodig.

10.10 Geheugenmapping voor grote datasets (np.memmap)

  • Memory-mapped bestanden laten je arrays gebruiken die groter dan RAM kunnen zijn door paginagebaseerd IO.
  • Werkt goed voor sequentiële/geslicede toegang; random access blijft schijfgebonden.
import numpy as np

# Create a memmap file
filename = 'data.bin'
N = 10_000_000
mm = np.memmap(filename, dtype='float32', mode='w+', shape=(N,))
mm[:100] = np.arange(100, dtype='float32')
mm.flush()
# Reopen read-only
mm_ro = np.memmap(filename, dtype='float32', mode='r', shape=(N,))
mm_ro[10:15]

Tips:

  • Gebruik eenvoudige dtypes en contigue layouts.
  • Sluit/flush (of laat GC doen) voor consistentie.

10.11 Chunking en streaming

  • Verwerk grote arrays in stukken om piekgeheugen te beperken.
  • Kies chunkgrootte die in cache/L3 past of afgestemd is op IO-doorvoer.
import numpy as np

rng = np.random.default_rng(0)
X = rng.random((1_000_000, 64), dtype=np.float64)
w = rng.random(64)

def chunked_dot(X, w, chunk=50_000):
    out = np.empty(X.shape[0], dtype=X.dtype)
    i = 0
    while i < X.shape[0]:
        j = min(i + chunk, X.shape[0])
        out[i:j] = X[i:j] @ w
        i = j
    return out

res = chunked_dot(X, w)
res.shape

10.12 Cache-lokaliteit en tiling

  • Door iteratie in geheugenvolgorde te houden en blokken (tiles) te gebruiken, verbeter je cache-hits.
  • Voor pure NumPy is dit vooral relevant bij eigen implementaties of bij het kiezen van C- vs Fortran-order.
import numpy as np

A = np.asfortranarray(np.arange(12).reshape(3, 4))
# Kolomoperaties kunnen dan cache-vriendelijker zijn
col_means = A.mean(axis=0)
A.flags['F_CONTIGUOUS'], col_means.shape

10.13 Profileren en meten

  • Micro-benchmarking: IPython/Jupyter %timeit; of time.perf_counter.
  • Inspecteer NumPy/BLAS-config: np.__version__, np.show_config().
  • Memory-profielen: tracemalloc, memory_profiler (extern), array.nbytes en gc-inzichten.
import numpy as np
import sys

A = np.empty((2000, 2000), dtype=np.float64)
A.nbytes / (1024**2), sys.getsizeof(A)

10.14 Veelvoorkomende patronen en checklist

  • Vermijd Python-elementloops; gebruik ufuncs/aggregaties en @ voor algebra.
  • Controleer shapes vroeg; voorkom onbedoelde (N, N) constructies.
  • Minimaliseer allocaties met in-place en out=; hergebruik buffers.
  • Kies passende dtypes; overweeg float32 bij bandbreedte-gebonden workloads.
  • Zorg voor contigu arrays als bibliotheken dat vereisen; converteer expliciet.
  • Beperk BLAS-threads om oversubscriptie te voorkomen in parallelle workloads.
  • Voor grote data: memmap, chunking en streaming.

10.15 Korte oefeningen

  1. Meet het verschil in tijd tussen een Python-loop en een vectorized optelling op 5 miljoen elementen. Gebruik perf_counter en rapporteer de ratio.
  2. Maak een grote array (5_000_000,) in float32 en float64. Bereken de geheugenvoetafdruk in MB en bespreek een scenario waarin float32 volstaat.
  3. Toon met een klein voorbeeld dat fancy indexing een kopie maakt en slicing een view. Gebruik np.shares_memory en verander een element.
  4. Implementeer een chunked dot-product y = X @ w voor X van shape (1_000_000, 128) en vergelijk de piekgeheugenbehoefte met de naïeve broadcast np.sum(X*w, axis=1).
  5. Gebruik np.memmap om een bestand van 1e7 float32’s aan te maken, vul de eerste 100 waarden, sluit en heropen, en verifieer de inhoud.
  6. Profileringsopdracht: gebruik %timeit of perf_counter om de impact van OPENBLAS_NUM_THREADS=1 vs standaard op A @ B (vorm (2000,2000)) te vergelijken. (Beschrijf hoe je de omgeving zou instellen.)

Werkboek en uitwerkingen:

  • Oefeningen bij dit hoofdstuk kunnen later toegevoegd worden in ../workbook/hoofdstuk-10/oefeningen.md.
  • Uitwerkingen kunnen later toegevoegd worden in ../solutions/hoofdstuk-10/uitwerkingen.md.

Verder lezen en vervolg:

  • Hoofdstuk 6: basis over arrays, dtypes en geheugen.
  • Hoofdstuk 7: broadcasting en vormmanipulatie (prestaties/vallenkuilen).
  • Hoofdstuk 8: ufuncs, out=, where en aggregaties (allocaties beperken).
  • Hoofdstuk 9: BLAS/SVD en numerieke stabiliteit.
  • Hoofdstuk 11: interoperabiliteit en memory-views voor zero-copy patronen.

Hoofdstuk 11 — Interoperabiliteit: NumPy met Python collections, buffers/memory‑views en PyTorch tensors

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen hoe NumPy samenwerkt met standaard Python‑collecties (list/tuple), array module en collections.abc sequenties.
  • Zero‑copy interoperabiliteit via buffer‑protocol en memory‑views (memoryview, np.frombuffer, ndarray.view) benutten.
  • Efficiënt data uitwisselen met PyTorch tensors in beide richtingen, inclusief device/dtype/contiguïteit en zero‑copy waar mogelijk.
  • Praktische patronen, valkuilen, performance‑implicaties en testbare code‑snippets.

Aanbevolen voorkennis: Hoofdstuk 6–10 (arrays, dtypes, views/kopieën, broadcasting, prestaties), basiskennis van PyTorch is nuttig maar niet noodzakelijk.

11.1 Python‑collecties naar en van NumPy

Standaard Python‑collecties zijn heterogeen en niet geoptimaliseerd voor vectorisatie, maar NumPy kan er goed mee werken door te converteren naar homogene arrays.

import numpy as np

# list/tuple → ndarray (copy of data)
x_list = [1, 2, 3]
x_arr = np.array(x_list)            # dtype inferred (usually int64 or int32)

y_tuple = (1.0, 2.5, 3.14)
y_arr = np.array(y_tuple, dtype=np.float32)  # explicit dtype cast

# ndarray → Python list (copy)
back_to_list = x_arr.tolist()

Opmerkingen:

  • np.array(obj) maakt doorgaans een kopie; voor nested lists zal NumPy proberen te inferren naar 2D/ND shape. Inconsistente lengtes geven dtype=object — ongunstig voor rekenen.
  • .tolist() maakt altijd een kopie en kan langzaam zijn op grote arrays; gebruik alleen voor IO of weergave.

11.1.1 De array module en collections.abc sequenties

De standaardbibliotheek heeft array('f'), array('d'), array('i'), etc. Deze ondersteunen het buffer‑protocol, waardoor zero‑copy uitwisseling met NumPy mogelijk is.

import numpy as np
from array import array

arr_mod = array('d', [1.0, 2.0, 3.0])   # double precision
# Zero-copy view of the same memory (shape (3,))
view_np = np.frombuffer(arr_mod, dtype=np.float64)
view_np[0] = 99.0
assert arr_mod[0] == 99.0               # shares memory!

# Opposite direction: create array('d') from ndarray bytes (copy)
x = np.array([10.0, 20.0], dtype=np.float64)
arr_copy = array('d', x)                # this copies the data

collections.abc beschrijft sequentie‑interfaces; NumPy accepteert elk object dat zich als een sequentie gedraagt (iterable van elementaire waarden) voor np.array of ufuncs die Python iterables nemen, maar dat leidt vrijwel altijd tot een kopie.

11.2 Buffer‑protocol en memory‑views

Voor performance‑kritische interoperabiliteit is het buffer‑protocol essentieel. Het stelt Python‑objecten in staat hun ruwe geheugen te delen zonder kopiëren.

Belangrijkste bouwstenen:

  • memoryview(obj): generieke manier om een buffer van obj te bekijken.
  • np.frombuffer(buffer, dtype, count, offset): maak een 1D‑array die het buffer deelt (zero‑copy) — let op dtype en uitlijning.
  • ndarray.tobytes(), ndarray.data, ndarray.view(new_dtype): lage‑niveau toegang/manipulatie.
import numpy as np

# bytes/bytearray → NumPy zonder kopie (view)
buf = bytearray(b"\x00\x00\x80?\x00\x00\x00@")  # two float32: 1.0, 2.0
mv = memoryview(buf)
a = np.frombuffer(mv, dtype=np.float32)
assert np.shares_memory(a, np.frombuffer(buf, dtype=np.float32))

# dtype‑re‑interpretatie met .view (geen byte‑kopie, wel herinterpretatie)
x = np.arange(4, dtype=np.uint8)
y = x.view(np.int32)  # requires correct length and alignment; platform dependent

Valkuilen en richtlijnen:

  • Zero‑copy werkt alleen bij compatibele dtypes, endianness en uitlijning; anders moet je kopiëren (.astype(new_dtype, copy=True)).
  • Veel buffer‑bronnen zijn 1D; voor multi‑dimensies moet je np.ndarray(shape, dtype, buffer=..., strides=...) of a = np.frombuffer(...).reshape(...) gebruiken.
  • Gebruik np.shares_memory(a, b) om te controleren of je echt geheugen deelt.

11.3 Memory‑views van NumPy naar ander Python‑code

Je kunt een NumPy‑array als buffer aanbieden aan andere libraries, of direct via memoryview gebruiken.

import numpy as np

A = np.arange(12, dtype=np.int16).reshape(3, 4)
# Expose a read‑only memoryview of A's data (C‑order assumed)
mv = memoryview(A)
# mv can be passed to any buffer‑consumer (e.g., file IO, struct unpacking)

Let op:

  • Niet alle consumers begrijpen striding/ND‑vorm; vaak moet je een contigue view aanleveren (np.ascontiguousarray(A)).
  • Negatieve strides (zoals slices met omkering) zijn vaak onverenigbaar met C‑extensions; maak dan een kopie of ascontiguousarray.

11.4 PyTorch tensors ↔ NumPy arrays

PyTorch en NumPy zijn nauw verweven. Je kunt vaak zero‑copy views delen tussen CPU‑tensors en NumPy arrays. GPU‑tensors (CUDA/MPS) vereisen kopiëren naar host, en terug.

Voorbeelden (CPU):

import numpy as np
import torch

# NumPy → Torch (zero‑copy view when possible)
a_np = np.arange(6, dtype=np.float32).reshape(2, 3)
t_torch = torch.from_numpy(a_np)  # shares memory if a_np is writable and contiguous

# Mutations reflect both ways
t_torch[0, 0] = -1.0
assert a_np[0, 0] == -1.0

# Torch → NumPy (CPU)
t2 = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float64)
a2 = t2.numpy()  # shares memory if t2 is a CPU tensor

# If non‑contiguous, make it contiguous first
u = torch.arange(12, dtype=torch.float32).reshape(3, 4).t()  # transpose -> non‑contiguous
u_contig = u.contiguous()
a_from_u = u_contig.numpy()

Belangrijk:

  • Zero‑copy geldt alleen voor CPU‑tensors en NumPy arrays; op GPU moet je .cpu() gebruiken (kopie): a_gpu = t_cuda.detach().cpu().numpy().
  • Dtype‑mapping: float32 ↔ torch.float32, float64 ↔ torch.float64, int64 ↔ torch.int64 (PyTorch’s default int), bool ↔ torch.bool, etc.
  • Writability/ownership: torch.from_numpy vereist dat de NumPy‑array writeable is; anders krijg je een tensor die mogelijk beperkingen heeft. Omgekeerd deelt tensor.numpy() geheugen en respecteert requires_grad=False. Voor autograd: gebruik .detach().numpy() om grad‑history te verbreken.

11.4.1 Device transfers en pinned memory

# CPU tensor to GPU, then back to NumPy
x_cpu = torch.arange(6, dtype=torch.float32).reshape(2, 3)
x_gpu = x_cpu.to('cuda')        # requires CUDA‑capable setup
x_back = x_gpu.detach().cpu().numpy()   # copy to host

Pinned memory kan host↔device transfers versnellen, maar vereist geavanceerde setup (pin_memory=True bij DataLoader, of tensor.pin_memory() op CPU‑buffers).

11.4.2 Non‑contiguïteit, strides en views

PyTorch en NumPy hebben beide strides; zero‑copy gedeelde views vereisen compatibele strides en contiguïteit. Na transposes/slices is .contiguous() of np.ascontiguousarray vaak nodig.

# Non‑contiguous example
w = torch.arange(12, dtype=torch.float32).reshape(3, 4)
wT = w.t()                         # transpose, non‑contiguous
# a direct wT.numpy() raises an error in older versions; in modern PyTorch it may copy
wT_c = wT.contiguous()
a = wT_c.numpy()

11.5 Seriële uitwisseling: DLPack en __array__ protocol

Voor breder ecosysteem‑interop bestaan standaarden:

  • __array__/__array_priority__: Python protocol waarmee objecten zich als NumPy‑array kunnen presenteren. np.asarray(obj) kan dan een view of kopie verkrijgen.
  • DLPack: een zero‑copy tensor overdrachtsformaat ondersteund door PyTorch, CuPy, JAX, TensorFlow e.a.
# PyTorch ↔ NumPy via __array__ protocol (NumPy side)
# Many torch tensors implement __array__ returning a NumPy view for CPU tensors.

# PyTorch ↔ andere frameworks via DLPack
import torch

# Torch to DLPack capsule
x = torch.arange(6, dtype=torch.float32)
capsule = torch.utils.dlpack.to_dlpack(x)
# Another framework (e.g., cupy) could import it zero‑copy on the same device
# cupy_arr = cupy.fromDlpack(capsule)

Opmerking: NumPy zelf consumeert DLPack niet standaard (v1.26); interop met GPU frameworks loopt dan via die frameworks of via kopiëren naar CPU.

11.6 Praktische patronen

  • Data inlezen als Python list/iterable → éénmalig casten naar np.array met gewenst dtype en shape; daarna overal arrays gebruiken.
  • Gebruik het buffer‑protocol voor zero‑copy tussen array module, bytearray, memoryview en NumPy.
  • Voor PyTorch training/inferentie op CPU: gebruik torch.from_numpy om datasets/tensors te maken zonder kopie; zorg dat arrays contigu en van het juiste dtype zijn.
  • Voor GPU: verzamel in NumPy, zet om naar torch CPU tensor met from_numpy, en roep .to('cuda') één keer vóór de training/inferentie.
  • Let op eigendom en levensduur: als een tensor/array een view is op andermans geheugen, houd dat bron‑object in scope.

11.7 Veelgemaakte valkuilen

  • Onbedoelde kopieën bij .astype, np.array(list) herhaald in een loop, of tensor.numpy() op GPU (maakt impliciet een CPU kopie via .cpu()).
  • Endianness/dtype mismatch bij frombuffer geeft corrupte waarden; specificeer dtype correct.
  • Niet‑contigue layouts breken zero‑copy; materialiseer contigu.
  • Schrijven naar gedeeld geheugen kan onverwachte bijwerkingen veroorzaken; gebruik .copy() als isolatie gewenst is.

11.8 Korte oefeningen

  1. Maak een array('d',[1,2,3]) en construeer er een NumPy‑view bovenop met np.frombuffer. Pas een element aan en toon dat het originele array‑object mee verandert (zero‑copy).
  2. Converteer een geneste Python‑list naar een 2D‑NumPy‑array met een expliciet dtype. Laat zien wat er gebeurt als de sub‑lijsten verschillende lengtes hebben.
  3. Maak een bytearray met twee little‑endian float32’s en lees ze in met np.frombuffer en de juiste dtype. Reshape naar (1,2) zonder kopie en controleer np.shares_memory.
  4. Zet een NumPy array float32 om naar een PyTorch CPU‑tensor met torch.from_numpy en toon bidirectionele mutaties. Maak daarna een transpose in PyTorch en converteer naar NumPy; los contiguïteit op.
  5. Demonstreer de roundtrip CPU→GPU→CPU met PyTorch (.to('cuda') en .cpu()) en converteer terug naar NumPy, met aandacht voor waar kopieën plaatsvinden.

Werkboek en uitwerkingen:

  • Oefeningen bij dit hoofdstuk kunnen later toegevoegd worden in ../workbook/hoofdstuk-11/oefeningen.md.
  • Uitwerkingen kunnen later toegevoegd worden in ../solutions/hoofdstuk-11/uitwerkingen.md.

Verder lezen en vervolg:

  • Hoofdstuk 6–7: views/kopieën, contiguïteit en broadcasting.
  • Hoofdstuk 10: prestaties en geheugenbeheer; zero‑copy patronen.
  • PyTorch docs: torch.from_numpy, Tensor.numpy, DLPack utils; Python docs: buffer protocol en memoryview.

Hoofdstuk 13 — Introductie tot pandas: Series en DataFrame

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen wat pandas is en wanneer je het gebruikt ten opzichte van puur Python of NumPy.
  • Kennismaken met de twee kernobjecten: Series en DataFrame.
  • Leren hoe je tabulaire data maakt, inspecteert, selecteert en bewerkt.
  • Inzicht krijgen in indexen, uitlijning (alignment), dtypes en omgaan met missende waarden op basisniveau.

Aanbevolen voorkennis: Python‑basis, NumPy‑arrays (Hoofdstuk 6–11).

13.1 Wat is pandas en waarom?

pandas is een bibliotheek voor data‑analyse in Python, geoptimaliseerd voor gestructureerde (tabelvormige) data. Het biedt:

  • Series (1D gelabelde array) en DataFrame (2D tabel met rijen en kolommen).
  • Krachtige indexering, selectie, groepering, reshaping en I/O (CSV, Excel, Parquet, SQL).
  • Vectorized operations met automatische uitlijning op labels (indexen).

Installatie (indien nodig):

pip install pandas

Basisimport:

import pandas as pd
import numpy as np

13.2 Series: een gelabelde 1D‑array

Een Series is vergelijkbaar met een 1D NumPy‑array, maar met labels (index) en rijkere functionaliteit.

13.2.1 Series maken

import pandas as pd

s = pd.Series([10, 20, 30], index=["a", "b", "c"], name="scores")
print(s)
# a    10
# b    20
# c    30
# Name: scores, dtype: int64

Belangrijke attributen: s.index, s.values, s.dtype, s.name.

Je kunt ook vanuit een dict maken (keys worden index):

data = {"apples": 3, "bananas": 5, "oranges": 2}
stock = pd.Series(data)

13.2.2 Selectie en slicing

s["a"]          # label-based; returns scalar 10
s[["a", "c"]]  # fancy indexing -> Series
s[0]             # position-based; discouraged, gebruik .iloc

s.loc["a"]      # label-based expliciet
s.iloc[0]        # position-based expliciet

s["a":"c"]     # label-slice inclusief eindlabel
s.iloc[0:2]      # position-slice exclusief einde (zoals Python/NumPy)

13.2.3 Vectorbewerkingen en uitlijning

Operaties zijn elementgewijs en worden uitgelijnd op labels.

s1 = pd.Series([1, 2, 3], index=["a", "b", "c"])
s2 = pd.Series([10, 20, 30], index=["b", "c", "d"])

s1 + s2
# a     NaN
# b    11.0
# c    23.0
# d     NaN
# dtype: float64

Missende combinaties geven NaN. Gebruik fill_value met method chaining via .add e.d.:

(s1.add(s2, fill_value=0))
# a     1.0
# b    12.0
# c    33.0
# d    30.0

13.2.4 Basisstatistiek en methodes

s.mean(), s.median(), s.std()
s.min(), s.max()
s.isna(), s.notna()
s.fillna(0)
s.astype("float32")

13.3 DataFrame: een 2D tabel

Een DataFrame is een verzameling Series die kolommen vormen, met een gedeelde rij‑index.

13.3.1 DataFrame maken

Constructors vanuit dict, lijst van dicts, NumPy, of CSV/Parquet/…

import numpy as np

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 32, 29],
    "score": [88.5, 92.0, 79.0],
})

# Met aangepaste index
people = pd.DataFrame(
    [[25, 88.5], [32, 92.0], [29, 79.0]],
    index=["Alice", "Bob", "Charlie"],
    columns=["age", "score"],
)

Veelgebruikte attributes en inspectie:

df.shape, df.dtypes, df.index, df.columns

df.head()   # eerste 5 rijen
df.tail(3)  # laatste 3 rijen
df.info()   # kolomtypes, non-null counts
df.describe()  # numerieke samenvatting

13.3.2 Selectie: kolommen en rijen

# Kolommen
ages = df["age"]         # Series
subset = df[["name", "score"]]  # DataFrame

# Rijen
first_row = df.iloc[0]          # Series (row)
rows_1_3 = df.iloc[1:3]         # DataFrame slice
row_bob = people.loc["Bob"]    # via indexlabel

# Rijen + kolommen (2D selectie)
val = df.loc[0, "score"]       # scalar
block = df.loc[0:1, ["name", "score"]]

Regels:

  • Gebruik .loc[rows, cols] voor labelgebaseerd, .iloc[rows, cols] voor positiegebaseerd.
  • Enkelvoudige kolomselectie df["col"] geeft een Series; dubbele haken df[["c1", "c2"]] geeft een DataFrame.

13.3.3 Filteren en booleaanse indexering

mask = (df["age"] >= 30) & (df["score"] > 90)
df[mask]

# Met loc, explicieter en uitbreidbaar
df.loc[(df["age"] >= 30) & (df["score"] > 90), ["name", "score"]]

13.3.4 Nieuwe kolommen en vectorbewerkingen

df["passed"] = df["score"] >= 85

# Vectorbewerkingen uitgelijnd op index
df["score_norm"] = (df["score"] - df["score"].mean()) / df["score"].std()

# Met NumPy ufuncs
df["age_log"] = np.log(df["age"])  # werkt elementgewijs

13.3.5 Dtypes en conversies

df.dtypes

# Conversie
df["age"] = df["age"].astype("int16")
df = df.astype({"score": "float32"})

# Categoricals
cats = pd.Series(["low", "med", "high"]).astype("category")

13.3.6 Missende waarden (NaN) basis

df2 = pd.DataFrame({"a": [1, np.nan, 3], "b": [np.nan, 5, 6]})

df2.isna()           # booleaanse DataFrame
df2.dropna()         # rijen met NaN weg

df2.fillna({"a": 0, "b": df2["b"].mean()})  # per kolom vulling

13.3.7 Sorteren en herindexeren

df.sort_values(by=["score", "age"], ascending=[False, True])
df.sort_index()  # sorteer op rijindex

# Herindexeren
df_re = df.reindex([0, 1, 2, 3])  # voegt evt. NaN toe voor ontbrekende rijen

13.4 Indexfilosofie en uitlijning

  • Indexen labelen rijen/kolommen en sturen selectie, join/merge, en alignment.
  • Operaties tussen Series/DataFrames lijnen uit op indexlabels; ontbrekende labels → NaN.
  • Reset/zet index waar nuttig:
df_reset = people.reset_index(names="person")
df_set = df_reset.set_index("person")

13.5 In‑/uitvoer (teaser)

Uitgebreide I/O volgt in Hoofdstuk 14. Kort voorbeeld:

# CSV lezen en schrijven
df = pd.read_csv("data.csv")
df.to_csv("out.csv", index=False)

# Parquet (vereist pyarrow of fastparquet)
# df.to_parquet("out.parquet")

13.6 Veelvoorkomende valkuilen en tips

  • Gebruik .loc en .iloc i.p.v. df[row][col] (dat maakt onnodige kopieën en kan SettingWithCopyWarnings geven).
  • Let op automatische uitlijning; het is vaak gewenst, maar kan tot NaN leiden als labels niet overeenkomen.
  • Werk zoveel mogelijk kolomgewijs (vectorized) in plaats van rij‑loops voor performance en duidelijkheid.
  • Gebruik df.info() en df.dtypes vroeg om types en missende waarden te begrijpen.

13.7 Samenvatting

Je leerde de basis van pandas met Series en DataFrame: maken, selecteren, filteren, berekenen, en omgaan met missende waarden. In de volgende hoofdstukken duiken we in I/O (CSV/Excel/Parquet/SQL), wrangling en groeperen/pivoteren.

Hoofdstuk 14 — Data inlezen en schrijven: CSV, Excel, Parquet en SQL

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Data inlezen uit en wegschrijven naar veelgebruikte formaten: CSV, Excel, Parquet en SQL.
  • Leren welke parameters belangrijk zijn voor correcte parsing (scheidingsteken, encoding, datums, dtypes, missende waarden).
  • Begrijpen van voor- en nadelen per formaat en performance-/geheugentips.

Aanbevolen voorkennis: Hoofdstuk 13 (pandas‑basis), Python/NumPy.

14.1 CSV: de klassieker

CSV is een tekstformaat met kolommen gescheiden door een delimiter (vaak , of ;). Het is universeel, maar mist schema en types.

14.1.1 Lezen met pd.read_csv

import pandas as pd

# Basic
df = pd.read_csv("data.csv")

# Veelgebruikte opties
# - sep: scheidingsteken (bijv. ';' in Nederlandse exporten)
# - encoding: bronencoding (bijv. 'utf-8', 'latin-1')
# - header/names: eerste rij als header of kolomnamen expliciet aangeven
# - usecols: subset van kolommen lezen
# - dtype: kolomtypes forceren
# - parse_dates: kolommen als datums parsen
# - na_values: extra strings als NaN behandelen
# - on_bad_lines: 'error' (default), 'warn' of 'skip'
# - engine: 'c' (snel), 'python' (toleranter)
# - nrows/skiprows: subset van rijen
# - compression: 'gzip', 'zip', 'bz2', ...

sales = pd.read_csv(
    "sales_2024.csv",
    sep=';',
    encoding='utf-8',
    usecols=["date", "product", "qty", "price"],
    dtype={"product": "string", "qty": "int32", "price": "float32"},
    parse_dates=["date"],
    na_values=["", "NA", "NULL", "-"],
)

Datum parsing tips:

  • parse_dates=["date"] laat pandas zelf detecteren. Voor exotische formaten kun je dayfirst=True of date_format (nieuwe pandas versies) gebruiken.
  • Grote snelheidswinst is mogelijk met date_format="%Y-%m-%d" indien constant formaat.

Grote bestanden:

# Streamen met chunksize om geheugen te sparen
it = pd.read_csv("big.csv", chunksize=100_000)

total_qty = 0
for chunk in it:
    total_qty += chunk["qty"].sum()

14.1.2 Schrijven met DataFrame.to_csv

# Schrijf zonder indexkolom, met semicolon delimiter en UTF-8
sales.to_csv("sales_out.csv", index=False, sep=';', encoding='utf-8')

# Gecomprimeerd
sales.to_csv("sales_out.csv.gz", index=False, compression='gzip')

Valkuilen:

  • Locale notaties (decimalen met , en duizendtallen met .); gebruik decimal=',' en thousands='.' bij het lezen.
  • Automatische type-inferentie kan strings/ID’s als numeriek zien; forceer dtype.
  • CSV heeft geen schema → verlies van types bij round-trips mogelijk.

14.2 Excel: werkbladen en tabbladen

Excel-bestanden zijn binair (XLSX). Je hebt meestal een extra engine nodig.

Installatie:

pip install openpyxl  # voor .xlsx lezen/schrijven

14.2.1 Lezen met pd.read_excel

# Specifieke sheet, kolommen en dtype
orders = pd.read_excel(
    "orders.xlsx",
    sheet_name="2024-Q1",
    usecols=["order_id", "date", "amount"],
    dtype={"order_id": "string", "amount": "float32"},
    parse_dates=["date"],
    engine="openpyxl",
)

# Meerdere sheets in één keer → dict van DataFrames
all_sheets = pd.read_excel("orders.xlsx", sheet_name=None, engine="openpyxl")

14.2.2 Schrijven met DataFrame.to_excel

# Eén sheet
orders.to_excel("orders_out.xlsx", index=False, sheet_name="Q1", engine="openpyxl")

# Meerdere sheets
with pd.ExcelWriter("orders_multi.xlsx", engine="openpyxl") as xw:
    orders.to_excel(xw, index=False, sheet_name="Q1")
    orders.sample(100).to_excel(xw, index=False, sheet_name="Sample")

Valkuilen:

  • Excel kan cellen als tekst/numeriek wisselend formatteren; dwing dtype in pandas.
  • Grootte en performance minder efficiënt dan kolom-georiënteerde formaten.

14.3 Parquet: kolom-georiënteerd, snel en gecomprimeerd

Parquet is een kolomarig, gecomprimeerd, schematisch formaat. Ideaal voor analytische workloads.

Installatie:

pip install pyarrow  # aanbevolen
# of
pip install fastparquet

14.3.1 Lezen met pd.read_parquet

import pandas as pd

df = pd.read_parquet("dataset.parquet")

# Alleen bepaalde kolommen
small = pd.read_parquet("dataset.parquet", columns=["date", "amount"]) 

14.3.2 Schrijven met DataFrame.to_parquet

# Compressie en engine instellen
orders.to_parquet("orders.parquet", engine="pyarrow", compression="snappy", index=False)

14.3.3 Partitionering en mappen

Veel data‑engineering workflows schrijven partities per sleutel (bijv. jaar/maand) als directorystructuur:

/transactions/
  year=2024/
    month=01/
      part-0000.parquet
    month=02/
      part-0000.parquet
  year=2025/
    month=01/

Lezen met partitiekolommen (pyarrow nodig):

# Vereist pyarrow.dataset onder water
# df = pd.read_parquet("transactions/", engine="pyarrow")

Voordelen Parquet:

  • Snellere I/O, kolomselectie, compressie (snappy/zstd/gzip), behoud van dtypes en datums. Nadelen:
  • Vereist extra dependency; minder universeel deelbaar dan CSV.

14.4 SQL: relationele databases

pandas kan lezen/schrijven via SQLAlchemy of DB-API connecties naar o.a. SQLite, PostgreSQL, MySQL.

Installatie voorbeeld (SQLite inbegrepen bij Python):

pip install sqlalchemy

14.4.1 Verbinden (SQLAlchemy engine)

from sqlalchemy import create_engine
engine = create_engine("sqlite:///example.db")

14.4.2 Lezen: pd.read_sql, read_sql_query, read_sql_table

import pandas as pd

# Volledige tabel
users = pd.read_sql_table("users", con=engine)

# Query (met parameter binding voor veiligheid)
query = "SELECT * FROM users WHERE country = :country AND age >= :min_age"
users_de = pd.read_sql_query(query, con=engine, params={"country": "DE", "min_age": 18})

14.4.3 Schrijven: DataFrame.to_sql

# Maak een DataFrame en schrijf naar SQL
import pandas as pd
new_users = pd.DataFrame({
    "user_id": [1, 2, 3],
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 32, 29],
    "country": ["NL", "DE", "BE"],
})

# if_exists: 'fail' (default), 'replace', 'append'
new_users.to_sql("users", con=engine, if_exists="append", index=False)

Prestatietips:

  • Gebruik chunksize bij to_sql en read_sql voor grote datasets.
  • Stel dtype expliciet in bij to_sql voor kolomtypes (SQLAlchemy types).

Veiligheid:

  • Gebruik nooit string‑concatenatie voor parameters in SQL; gebruik params/bound parameters.

14.5 Encodings, NA’s, en locale issues

  • Encodings: probeer altijd UTF‑8; als het niet lukt, test latin-1 of bron‑encoding van de export.
  • Missende waarden: specificeer na_values (CSV), controleer na_filter, en pas fillna/dropna toe.
  • Decimalen en duizendtallen: decimal=',', thousands='.' bij lezen.
  • Dtypes: voorkom ongewenste object‑kolommen; gebruik dtype of achteraf astype.

14.6 Performance en geheugen

  • Gebruik kolomsubset (usecols voor CSV/Excel, columns voor Parquet) om minder te lezen.
  • Voor zeer grote CSV’s: overweeg conversie naar Parquet voor sneller hergebruik.
  • Werk met chunksize en verwerk streamend.
  • Let op low_memory bij read_csv: standaard True kan type‑inconsistenties geven; zet uit en specificeer dtype als mogelijk.

14.7 Mini‑workflow: CSV → schoonmaken → Parquet → SQL

import pandas as pd
from sqlalchemy import create_engine

# 1) Lees CSV met opties
raw = pd.read_csv(
    "transactions.csv",
    sep=';',
    parse_dates=["date"],
    dtype={"product_id": "string", "qty": "int32", "price": "float32"},
    na_values=["", "NA", "NULL"]
)

# 2) Eenvoudige schoonmaak
raw = raw.dropna(subset=["product_id", "qty"])  # vereiste velden
raw["amount"] = raw["qty"] * raw["price"]

# 3) Schrijf naar Parquet voor snel hergebruik
raw.to_parquet("transactions.parquet", compression="snappy", index=False)

# 4) Laad subset kolommen en schrijf naar SQL
subset = pd.read_parquet("transactions.parquet", columns=["date", "product_id", "amount"]) 
engine = create_engine("sqlite:///analytics.db")
subset.to_sql("transactions", con=engine, if_exists="replace", index=False)

14.8 Samenvatting

Je leerde:

  • CSV/Excel lezen en schrijven met cruciale parameters voor delimiter, encoding, dtypes en datums.
  • Parquet gebruiken voor performance, compressie en schema‑behoud.
  • SQL‑bronnen aan te spreken via SQLAlchemy met veilige parameterbinding en to_sql/read_sql. Met deze I/O‑basis kun je betrouwbare data‑pijplijnen opzetten en datasets efficiënt hergebruiken.

Hoofdstuk 15 — Data schoonmaken en wrangling: missing values, typen en categorieën

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Systematisch omgaan met missende waarden: detecteren, filteren, invullen en imputeren.
  • Datatypes begrijpen en beheren: numeriek, string, boolean, datetime, timedelta en categorisch.
  • Tekstkolommen opschonen en normaliseren; duplicates en outliers aanpakken.
  • Praktische wrangling‑patronen toepassen met method‑chaining (intro) voor leesbare pipelines.

Aanbevolen voorkennis: Hoofdstuk 13–14 (pandas basis en I/O).

15.1 Missende waarden herkennen en analyseren

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'id': [1, 2, 3, 4],
    'age': [25, None, 31, 28],
    'score': [88.0, 92.5, None, 86.0],
    'city': ['Amsterdam', 'Rotterdam', None, 'Utrecht']
})

# Detectie
mask_na = df.isna()
count_na_per_col = df.isna().sum()
rows_with_any_na = df[df.isna().any(axis=1)]

# Verwijderen
drop_any = df.dropna()             # rijen met een NaN weg
subset_req = df.dropna(subset=['age', 'score'])

Wanneer verwijderen, wanneer vullen?

  • Verwijderen als de missings zeldzaam zijn en willekeurig verspreid (MCAR).
  • Vullen/imputeren als informatieverlies te groot is of de kolom essentieel is.

15.2 Missende waarden vullen (fillna) en eenvoudige imputatie

# Eenvoudig vullen per kolom
filled = df.fillna({
    'age': df['age'].median(),
    'score': df['score'].mean(),
    'city': 'Onbekend'
})

# Forward/backward fill op tijdreeksen of gesorteerde data
filled_ffill = df.sort_values('id').fillna(method='ffill')
filled_bfill = df.sort_values('id').fillna(method='bfill')

# Alleen numeriek vullen
num_cols = df.select_dtypes(include='number').columns
df[num_cols] = df[num_cols].fillna(df[num_cols].median())

Let op: Eenvoudige invulling kan statistieken vertekenen; documenteer keuzes en overweeg geavanceerde imputatie in ML‑pipelines (zie Deel VI).

15.3 Dtypes beheren en converteren

# Inspectie
print(df.dtypes)

# Numeriek
df['age'] = df['age'].astype('float32')

# String (pandas 1.0+ string dtype)
df['city'] = df['city'].astype('string')

# Boolean met NA‑ondersteuning
s = pd.Series([True, False, None], dtype='boolean')

# Datetime en timedelta
sales = pd.DataFrame({'date': ['2025-01-01','2025-02-03'], 'days': [10, 20]})
sales['date'] = pd.to_datetime(sales['date'], format='%Y-%m-%d')
sales['deadline'] = sales['date'] + pd.to_timedelta(sales['days'], unit='D')

15.4 Categorical dtypes: geheugen en performance

Categorische kolommen slaan herhalende labels efficiënt op en versnellen sommige operaties (groupby/sort).

sizes = pd.Series(['S','M','M','L','S','XL','M'])
sizes_cat = sizes.astype('category')

# Orde instellen
sizes_ord = pd.Categorical(sizes, categories=['S','M','L','XL'], ordered=True)

# Toepassing: binned levels
import numpy as np
scores = pd.Series([72, 88, 91, 67, 85])
bins = [0, 80, 90, 100]
labels = ['low', 'med', 'high']
levels = pd.cut(scores, bins=bins, labels=labels).astype('category')

15.5 Tekstopschoning en normalisatie

Veel real‑world datasets bevatten rommelige strings.

products = pd.Series(['  Apple iPhone  ', 'SAMSUNG Galaxy', 'xiaomi-REDMI', None], dtype='string')

clean = (products
         .str.strip()
         .str.replace('-', ' ', regex=False)
         .str.title()
         .str.replace('Iphone', 'iPhone', regex=False)
        )

# Splitsen en extractie met regex
emails = pd.Series(['alice@example.com','bob@test.org'], dtype='string')
usernames = emails.str.extract(r'^(?P<user>[^@]+)@(?P<domain>.+)$')

15.6 Duplicaten en outliers

# Duplicaten
unique_df = df.drop_duplicates()
# Duplicaten op subset
unique_city = df.drop_duplicates(subset=['city'])

# Eenvoudige outlier detectie met z‑score
import numpy as np
vals = pd.Series([10, 11, 10.5, 200, 9.8, 10.1])
z = (vals - vals.mean()) / vals.std()
outliers = vals[abs(z) > 3]

15.7 Wrangling‑patronen met method‑chaining

# Voorbeeld pipeline: kolommen selecteren -> missings vullen -> nieuwe variabelen -> filter -> sort
result = (
    df
    .assign(age_filled=lambda d: d['age'].fillna(d['age'].median()))
    .assign(score_z=lambda d: (d['score'] - d['score'].mean()) / d['score'].std())
    .dropna(subset=['city'])
    .query('age_filled >= 18')
    .sort_values(['score_z'], ascending=False)
)

Tip: Gebruik tussenstappen met .pipe(func, ...) om grotere ketens te structureren (zie Hoofdstuk 19 voor meer over pipe).

15.8 Samenvatting

  • Herken en behandel missende waarden met isna, dropna, fillna, en forward/backward fills.
  • Beheer dtypes expliciet: numeriek, string, boolean, datetime/timedelta, en category voor efficiënte labels.
  • Reinig tekstkolommen met .str‑methodes; verwijder duplicaten en signaleer outliers.
  • Combineer stappen in leesbare wrangling‑pipelines met method‑chaining.

Hoofdstuk 16 — Groeperen, aggregeren en pivoteren (groupby en pivot_table)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Data groeperen en samenvatten met groupby en aggregaties.
  • Meervoudige aggregaties en kolomnamen beheren.
  • Pivoteren met pivot, pivot_table en kruistabellen (crosstab).
  • Window‑functies voor rollende/expanderende berekeningen.

Aanbevolen voorkennis: Hoofdstuk 13–15.

16.1 GroupBy: basisconcepten

groupby splitst data per sleutel, voert een operatie uit en combineert het resultaat.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'region': ['EU','EU','US','US','APAC','EU'],
    'product': ['A','B','A','B','A','A'],
    'qty': [2,3,1,5,2,4],
    'price': [10.0, 12.0, 9.0, 11.0, 8.5, 10.0]
})

# Totaal per regio
sales_by_region = df.groupby('region')['qty'].sum()

# Meerdere keys
sales_by_reg_prod = df.groupby(['region','product'])['qty'].sum()

16.2 Aggregaties en meervoudige stats

# Eenvoudige aggregaties
agg1 = df.groupby('region').agg(qty_sum=('qty','sum'), price_mean=('price','mean'))

# Meervoudige aggregaties per kolom
agg2 = df.groupby('product').agg({
    'qty': ['sum','mean','count'],
    'price': ['mean','min','max']
})

# Kolomnamen flattenen
agg2.columns = ['_'.join(col).strip() for col in agg2.columns.to_flat_index()]

16.3 Transform vs. aggregate

transform retourneert een Series aligned met de oorspronkelijke index (handig voor normalisatie per groep).

# Z‑score van qty per product
df['qty_z_per_product'] = (
    df.groupby('product')['qty']
      .transform(lambda s: (s - s.mean())/s.std())
)

16.4 Filteren op groepsniveau

# Houd alleen producten met totale qty >= 5
filtered = df.groupby('product').filter(lambda g: g['qty'].sum() >= 5)

16.5 Pivot en pivot_table

# pivot: geen aggregatie; vereist unieke combinaties
no_agg = pd.DataFrame({
    'day': ['Mon','Tue','Wed'],
    'product': ['A','A','A'],
    'qty': [5, 3, 7]
})
wide = no_agg.pivot(index='day', columns='product', values='qty')

# pivot_table: met aggregatie en duplicate handling
pt = pd.pivot_table(df, index='region', columns='product', values='qty', aggfunc='sum', fill_value=0)

16.6 Crosstab (frequentietabellen)

ct = pd.crosstab(df['region'], df['product'], margins=True, normalize='index')

16.7 Window‑functies: rolling en expanding

s = pd.Series([1, 2, 3, 4, 5], dtype='float64')
rolling_mean = s.rolling(window=3, min_periods=1).mean()
expanding_sum = s.expanding(min_periods=1).sum()

16.8 Prestaties en geheugen

  • Gebruik categorische dtypes voor group keys met veel herhaling.
  • Kies specifieke kolommen vóór groupby om geheugen te besparen.
  • Voor grote datasets: overweeg observed=True bij categoricals om lege combinaties te vermijden.

16.9 Samenvatting

  • groupby voor splits‑apply‑combine; agg, transform, filter voor verschillende doelen.
  • pivot_table en crosstab voor brede tabellen en frequenties.
  • rolling en expanding voor vensterberekeningen.

Hoofdstuk 17 — Tijdsreeksen en datum/tijd in pandas

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Datums en tijden correct parsen, opslaan en formatteren.
  • Indexeren en resamplen van tijdreeksen; frequenties en offsets.
  • Rolling/expanding op tijdsgebaseerde vensters; time zone handling.
  • Use‑cases: kalenderfeatures, business‑dagen, periode‑objecten.

Aanbevolen voorkennis: Hoofdstuk 13–16.

17.1 Datums parsen en dtypes

import pandas as pd
import numpy as np

s = pd.Series(['2025-01-01', '2025-01-02', '2025-02-15'])
dates = pd.to_datetime(s, format='%Y-%m-%d')

# Datetime64[ns]
assert str(dates.dtype).startswith('datetime64')

# Tijdsdelta
delta = pd.to_timedelta(["1D", "12H", "30min"])  # dagen, uren, minuten

17.2 DatetimeIndex en frequenties

# Reeks met dagelijkse frequentie
ts = pd.Series(np.arange(5), index=pd.date_range('2025-01-01', periods=5, freq='D'))

# Subselectie op datum
jan = ts['2025-01']
specific = ts['2025-01-03']

# Herindexeren naar een andere frequentie
hourly = ts.resample('H').ffill()  # upsample met forward‑fill

17.3 Resampling en aggregaties

rng = pd.date_range('2025-01-01', periods=1000, freq='H')
vals = pd.Series(np.random.default_rng(0).standard_normal(len(rng)), index=rng)

# Dagelijkse som en gemiddelde
by_day = vals.resample('D').agg(['sum','mean'])

# Business‑day frequentie met eind van de maand
month_end = vals.resample('M').mean()

17.4 Time‑based rolling windows

# Tijdsvenster van 7 dagen, min 1 observatie
roll7 = vals.rolling('7D', min_periods=1).mean()

17.5 Time zones (tz‑aware)

utc = pd.to_datetime(['2025-01-01 12:00', '2025-06-01 12:00'], utc=True)

# Convert naar Europa/Amsterdam
ams = utc.tz_convert('Europe/Amsterdam')

# Lokale naive -> lokaliseer eerst, dan converteren
naive = pd.to_datetime(['2025-01-01 12:00'])
ams_local = naive.tz_localize('Europe/Amsterdam')

17.6 Periodes en PeriodIndex

# Maandperioden
p = pd.period_range('2025-01', '2025-06', freq='M')
series_p = pd.Series(range(len(p)), index=p)

# Conversie tussen Timestamp en Period via asfreq / to_period
monthly = ts.to_period('M')
daily_back = monthly.to_timestamp(how='end')

17.7 Kalenderfeatures en business-dagen

# Weekdag, maand, jaar als features
features = pd.DataFrame({'date': pd.date_range('2025-01-01', periods=10, freq='D')})
features['weekday'] = features['date'].dt.weekday
features['is_month_end'] = features['date'].dt.is_month_end

# Business Day offset
bdr = pd.bdate_range('2025-01-01', periods=5)

17.8 Veelvoorkomende valkuilen

  • Mixing naive (tz‑naive) en tz‑aware timestamps geeft fouten; maak alles tz‑aware of hou het consistent naive.
  • Resampling vereist een datumindex; zet eerst set_index('date').
  • Herken DST‑overgangen bij tz‑convert/localize; sommige tijden bestaan dubbel of niet.

17.9 Samenvatting

  • Gebruik to_datetime en date_range/bdate_range voor datumconstructies.
  • Resample naar gewenste frequentie en gebruik tijdsgebaseerde rolling vensters.
  • Beheer time zones expliciet; PeriodIndex voor periodieke data zoals maanden/kwartalen.

Hoofdstuk 18 — Samenvoegen en joinen: merge, concat en align

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Tabelverbindingen uitvoeren met merge (inner/left/right/outer/indicator, suffixes).
  • Verticale/horizontale concatenatie met concat en uitlijning met indexen.
  • Uitlijning en wiskunde tussen tabellen met verschillende indexen/labels (align).
  • Geavanceerde joins: many‑to‑many, validatie en performance‑tips.

Aanbevolen voorkennis: Hoofdstuk 13–17.

18.1 Merge basis

import pandas as pd

left = pd.DataFrame({'id': [1,2,3], 'name': ['Alice','Bob','Charlie']})
right = pd.DataFrame({'id': [1,2,2,4], 'score': [88, 92, 85, 77]})

inner = pd.merge(left, right, on='id', how='inner')
left_join = pd.merge(left, right, on='id', how='left')
outer = pd.merge(left, right, on='id', how='outer', indicator=True)

18.2 Merge op verschillende kolomnamen en suffixes

sales = pd.DataFrame({'prod_id':[1,2], 'qty':[5,3]})
prods = pd.DataFrame({'id':[1,2], 'name':['A','B']})

merged = sales.merge(prods, left_on='prod_id', right_on='id', how='left', suffixes=('_s','_p'))

18.3 Many‑to‑one, one‑to‑one en many‑to‑many validatie

# validatie dwingt relatie af en voorkomt onverwachte duplicatie
mto = left.merge(right, on='id', how='left', validate='one_to_many')
oto = left.merge(left, on='id', how='left', validate='one_to_one')

18.4 Concat: stapelen en naast elkaar zetten

# Verticaal stapelen (rijen)
part1 = pd.DataFrame({'id':[1,2], 'qty':[5,3]})
part2 = pd.DataFrame({'id':[3,4], 'qty':[2,7]})
stacked = pd.concat([part1, part2], axis=0, ignore_index=True)

# Horizontaal (kolommen)
feat1 = pd.DataFrame({'id':[1,2,3], 'a':[10,20,30]})
feat2 = pd.DataFrame({'id':[1,2,3], 'b':[5,6,7]})
joined = pd.concat([feat1.set_index('id'), feat2.set_index('id')], axis=1).reset_index()

18.5 Align: uitlijning voor wiskunde

s1 = pd.Series([1, 2, 3], index=['a','b','c'])
s2 = pd.Series([10, 20, 30], index=['b','c','d'])

# Align retourneert gere‑indexeerde kopieën
A, B = s1.align(s2, join='outer', fill_value=0)
result = A + B

18.6 Joins op index

left_i = left.set_index('id')
right_i = right.set_index('id')
joined_i = left_i.join(right_i, how='left', lsuffix='_l', rsuffix='_r')

18.7 Prestatie, geheugen en valkuilen

  • Zet join keys naar een compact dtype (bijv. int32, category) voor minder geheugen.
  • Sorteer op keys voor snellere merges in sommige gevallen.
  • Controleer duplicaten en cardinaliteit; gebruik validate om onverwachte cartesiaanse explosies te voorkomen.
  • Bij concat: gebruik ignore_index=True om herindexering te voorkomen.

18.8 Samenvatting

  • merge voor SQL‑achtige joins; concat voor stapelen of kolommen combineren.
  • align voor veilige wiskunde met verschillende indexen.
  • Let op relatie‑type validatie en geheugenoptimalisaties.

Hoofdstuk 19 — Prestatie, memory en method‑chaining (pipe)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen van geheugenverbruik en dtypes; kolom‑selectie en kopieën vermijden.
  • Vectorisatie, blokgewijze verwerking en efficiënte I/O.
  • Method‑chaining en functionele stijl met .pipe voor onderhoudbare pipelines.
  • Profiling en meetbare optimalisaties.

Aanbevolen voorkennis: Hoofdstuk 13–18.

19.1 Geheugeninspectie en dtypes

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'id': np.arange(1_000_000),
    'cat': np.random.choice(list('ABCDE'), size=1_000_000),
    'x': np.random.rand(1_000_000)
})

mem_before = df.memory_usage(deep=True).sum() / 1e6

# Optimaliseer dtypes
df['id'] = df['id'].astype('int32')
df['cat'] = df['cat'].astype('category')
df['x'] = df['x'].astype('float32')

mem_after = df.memory_usage(deep=True).sum() / 1e6
print(mem_before, mem_after)

19.2 Kopieën vermijden en chained assignment

  • Gebruik .loc voor veilige toewijzing: df.loc[mask, 'col'] = value.
  • Vermijd kettingindexering df[df.a>0]['b'] = ... (SettingWithCopyWarning).
  • Werk kolomgewijs en benut ufuncs; vermijd per‑rij loops.

19.3 Vectorisatie en blokgewijze verwerking

# Vectorized
df['y'] = df['x'] * 2 + 1

# Blokgewijs lezen/verwerken bij grote CSV's
it = pd.read_csv('big.csv', chunksize=100_000)
acc = 0.0
for chunk in it:
    acc += (chunk['qty'] * chunk['price']).sum()

19.4 Efficiënte I/O en opslag

  • Gebruik Parquet met compressie (snappy/zstd) i.p.v. CSV voor herhaald lezen.
  • Sla alleen benodigde kolommen op; gebruik columns= bij Parquet.
  • Partitioneer datasets op logische sleutels (jaar/maand) voor slice‑toegang.

19.5 Method‑chaining en .pipe

def add_amount(d):
    return d.assign(amount=d['qty'] * d['price'])

def filter_valid(d):
    return d.dropna(subset=['product', 'qty', 'price'])

def normalize_amount(d):
    mean = d['amount'].mean(); std = d['amount'].std()
    return d.assign(amount_z=(d['amount'] - mean)/std)

result = (
    pd.read_csv('transactions.csv', sep=';', parse_dates=['date'])
      .pipe(filter_valid)
      .pipe(add_amount)
      .query('qty > 0 and price > 0')
      .pipe(normalize_amount)
      .sort_values(['date'])
)

19.6 Profiling en timing

# Eenvoudig tijd meten
import time
start = time.perf_counter()
_ = pd.read_csv('big.csv')
elapsed = time.perf_counter() - start
print(f"read_csv: {elapsed:.3f}s")

# memory_usage en info
mu = result.memory_usage(deep=True).sum()/1e6
print(f"Result MB: {mu:.2f}")

19.7 Parallel/alternatieve engines (kort)

  • Gebruik pyarrow engine voor read_csv (indien beschikbaar in recente pandas) voor snelheidswinst.
  • Voor zeer grote data: overweeg Dask/Polars/Spark (buiten scope), maar principes blijven gelijk.

19.8 Samenvatting

  • Beperk geheugen via dtypes en kolomselectie; voorkom onnodige kopieën.
  • Vectoriseer berekeningen en verwerk streamend met chunks.
  • Structureer data pipelines met method‑chaining en .pipe voor onderhoudbaarheid.
  • Meet, vergelijk, en documenteer performance‑winst.

Hoofdstuk 20 — Geavanceerde indexen: MultiIndex en reshaping (stack/unstack/melt)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen en gebruiken van MultiIndex voor rijen en kolommen.
  • Reshaping‑technieken: stack, unstack, melt en wide ↔ long.
  • Gecombineerd gebruik met groupby, pivot_table en sorteren.
  • Praktische patronen voor analyse en rapportage.

Aanbevolen voorkennis: Hoofdstuk 13–19.

20.1 MultiIndex maken en basisoperaties

import pandas as pd
import numpy as np

arrays = [
    ['EU','EU','US','US'],
    ['A','B','A','B']
]
idx = pd.MultiIndex.from_arrays(arrays, names=['region','product'])

s = pd.Series([10, 12, 9, 11], index=idx)

# Selectie
sel_eu = s.loc['EU']
sel_us_a = s.loc[('US','A')]

# Sorteren en herindexeren
s_sorted = s.sort_index()

20.2 MultiIndex op DataFrame kolommen

data = pd.DataFrame({
    ('sales','qty'): [2,3,1,5],
    ('sales','price'): [10.0, 12.0, 9.0, 11.0],
    ('meta','region'): ['EU','EU','US','US']
})

data.columns = pd.MultiIndex.from_tuples(data.columns)

20.3 Stack en unstack

unstack verplaatst een niveau van de rijindex naar kolommen; stack doet het omgekeerde.

# Van long (MultiIndex rows) naar wide (kolommen)
df = s.reset_index(name='value').set_index(['region','product'])
unstacked = df['value'].unstack('product')  # kolommen A, B

# Terug naar long
stacked = unstacked.stack().rename('value').reset_index()

20.4 Melt: wide naar long

wide = pd.DataFrame({
    'id': [1,2,3],
    'A_qty': [5,3,1],
    'B_qty': [2,4,6]
})

long = (wide
    .melt(id_vars='id', var_name='var', value_name='qty')
    .assign(product=lambda d: d['var'].str[0])
    .drop(columns=['var'])
)

20.5 Combineren met groupby en pivot_table

raw = pd.DataFrame({
    'region': ['EU','EU','US','US','EU'],
    'product': ['A','B','A','B','A'],
    'qty': [2,3,1,5,4]
})
pt = pd.pivot_table(raw, index='region', columns='product', values='qty', aggfunc='sum', fill_value=0)

# MultiIndex kolommen flattenen
pt.columns = pt.columns.astype(str)
pt_reset = pt.reset_index()

20.6 Index‑niveau’s hernoemen en wisselen

midx = df.index  # ['region','product']
renamed = df.rename_axis(index={'region':'reg','product':'prod'})
swapped = df.reorder_levels(['product','region']).sort_index()

20.7 Veelvoorkomende valkuilen

  • unstack kan NaN introduceren voor ontbrekende combinaties; gebruik fill_value= of fillna.
  • Let op volgorde van index‑niveaus bij selectie en sorteren; gebruik swaplevel/reorder_levels.
  • Kolom‑MultiIndex maakt code cryptischer; flatten kolomnamen waar zinvol.

20.8 Samenvatting

  • MultiIndex ondersteunt hiërarchische labeling voor flexibele selectie en reshaping.
  • Gebruik stack/unstack/melt om tussen long en wide te transformeren.
  • Combineer met groupby en pivot_table voor krachtige samenvattingen.

Hoofdstuk 21 — Matplotlib basis: Figure, Axes, styles en layouts

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen van de Matplotlib‑architectuur: Figure, Axes en Artists.
  • Leren werken met de object‑georiënteerde (OO) interface i.p.v. de stateful pyplot shortcuts.
  • Styles, kleuren, markers en lijnen instellen.
  • Layouts en meerdere subplot‑rasterindelingen maken en beheren.

Aanbevolen voorkennis: Hoofdstuk 13–20 (pandas/NumPy). Installeer Matplotlib:

pip install matplotlib

21.1 Figure en Axes: de kern

Matplotlib tekent op Axes (plotvlak) binnen een Figure (canvas/figuur). Gebruik bij voorkeur de OO‑stijl.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2*np.pi, 200)
y = np.sin(x)

fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
ax.plot(x, y, label='sin(x)')
ax.set(title='Sine', xlabel='x', ylabel='y')
ax.legend()
fig.tight_layout()
plt.show()

Belangrijkste concepten:

  • Figure: het gehele canvas (afbeelding), bevat 1+ Axes.
  • Axes: de plot zelf met assen, ticks, labels, titel en artists (lijnen, scatter, patches).
  • OO‑API: ax.plot(...) i.p.v. plt.plot(...) voor expliciete controle.

21.2 Lijnen, markers, kleuren en stijlen

fig, ax = plt.subplots()
ax.plot(x, y, color='C0', linewidth=2, linestyle='-', marker='o', markersize=4)
ax.plot(x, np.cos(x), color='#d62728', lw=1.5, ls='--', label='cos(x)')
ax.set_xlabel('x'); ax.set_ylabel('f(x)'); ax.legend()

Tips:

  • Gebruik cycler‑kleuren (C0..C9) of hex‑kleuren.
  • Transparantie via alpha=.
  • Markers: 'o', '^', 's', '.', etc.

21.3 Assen, limieten, ticks en grids

fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlim(0, 2*np.pi); ax.set_ylim(-1.1, 1.1)
ax.set_xticks([0, np.pi, 2*np.pi]); ax.set_xticklabels(['0', 'π', '2π'])
ax.grid(True, axis='both', linestyle=':', alpha=0.6)

21.4 Meerdere subplots en layouts

# 1x2 raster
fig, axes = plt.subplots(1, 2, figsize=(8, 3), sharey=True)
axes[0].plot(x, np.sin(x)); axes[0].set_title('sin')
axes[1].plot(x, np.cos(x)); axes[1].set_title('cos')
fig.suptitle('Trig'); fig.tight_layout()

Geavanceerde layout hulpmiddelen:

  • fig.tight_layout() of plt.tight_layout() voor automatische marges.
  • fig.subplots_adjust(...) voor handmatige marge‑tuning.
  • matplotlib.gridspec voor ongelijke rasters.

21.5 Styles en theming

Matplotlib heeft ingebouwde styles (themes).

import matplotlib.pyplot as plt
plt.style.available[:5]

with plt.style.context('seaborn-v0_8'):
    fig, ax = plt.subplots()
    ax.plot(x, y)
    ax.set(title='Seaborn‑style')

Eigen rcParams aanpassen:

import matplotlib as mpl
mpl.rcParams['figure.dpi'] = 150
mpl.rcParams['axes.grid'] = True

21.6 Tekst, annotaties en legendes (basis)

fig, ax = plt.subplots()
ax.plot(x, y, label='sin')
ax.legend(loc='best')
ax.set_title('Titel')
ax.annotate('maximum', xy=(np.pi/2, 1), xytext=(2, 1.2),
            arrowprops=dict(arrowstyle='->'))

21.7 Opslaan van figuren

fig.savefig('plot.png', dpi=150, bbox_inches='tight')
fig.savefig('plot.pdf')  # vector

21.8 Veelvoorkomende valkuilen

  • Gemixte OO‑ en pyplot‑stijl kan verwarrend zijn. Kies consequent voor OO.
  • Vergeet tight_layout() of bbox_inches='tight' niet bij saven om afgesneden labels te voorkomen.
  • Houd rekening met DPI en figsize voor leesbaarheid.

21.9 Samenvatting

  • Gebruik plt.subplots() om Figure en Axes te krijgen.
  • Werk met de OO‑API (ax.plot, ax.set, ax.legend) voor controle en compositie.
  • Styles/rcParams bepalen de look; layouts regelen de rangschikking van subplots.

Hoofdstuk 22 — Veelgebruikte grafieken: line, bar, histogram, box en scatter

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Leren wanneer je lijn-, staaf-, histogram-, box- en scatterplots gebruikt.
  • OO‑stijl van Matplotlib toepassen voor deze grafieken.
  • Assen schalen, labels, legendes en error bars toevoegen.
  • Veelvoorkomende varianten (stacked bars, twin axes, log‑schalen) en valkuilen.

Aanbevolen voorkennis: Hoofdstuk 21 (Matplotlib basis).

import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)

22.1 Lijngrafieken (line)

Gebruik voor trends over een continue as (tijd, meting per x).

x = np.linspace(0, 10, 200)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y, label='sin(x)', color='C0')
ax.set(xlabel='x', ylabel='y', title='Line plot')
ax.legend(); fig.tight_layout()

Varianten:

  • Meerdere lijnen per Axes; zorg voor goed onderscheid in kleur/linestyle.
  • Log‑schalen: ax.set_yscale('log') of ax.semilogy(x, y).

22.2 Staafdiagrammen (bar)

Categorische vergelijkingen of geaggregeerde waarden.

cats = ['A','B','C']
vals = [5, 3, 7]
fig, ax = plt.subplots()
ax.bar(cats, vals, color='C1', alpha=0.9)
ax.set(ylabel='count', title='Bar chart')

Horizontaal en gegroepeerd/gestapeld:

# Horizontaal
fig, ax = plt.subplots()
ax.barh(cats, vals)

# Gegroepeerd
vals2 = [4, 2, 6]
idx = np.arange(len(cats))
width = 0.4
fig, ax = plt.subplots()
ax.bar(idx - width/2, vals, width, label='2024')
ax.bar(idx + width/2, vals2, width, label='2025')
ax.set_xticks(idx); ax.set_xticklabels(cats)
ax.legend()

# Gestapeld
fig, ax = plt.subplots()
ax.bar(cats, vals, label='A')
ax.bar(cats, vals2, bottom=vals, label='B')
ax.legend()

Error bars:

means = np.array([5.0, 3.2, 7.1]); errs = np.array([0.6, 0.4, 0.8])
fig, ax = plt.subplots()
ax.bar(cats, means, yerr=errs, capsize=4)

22.3 Histogrammen (hist)

Verdelingen van continue variabelen.

data = rng.normal(loc=0, scale=1, size=10_000)
fig, ax = plt.subplots()
ax.hist(data, bins=40, density=True, alpha=0.7, color='C2')
ax.set(title='Histogram', xlabel='value', ylabel='density')

Tips:

  • Kies aantal bins bewust (bins= of regels zoals Freedman–Diaconis via numpy).
  • Voor meerdere reeksen: histtype='step' of transparantie (alpha).

22.4 Boxplots en violins

Samenvatten van verdelingen en outliers.

g1 = rng.normal(0.0, 1.0, 500)
g2 = rng.normal(1.5, 0.8, 500)
fig, ax = plt.subplots()
ax.boxplot([g1, g2], labels=['G1','G2'], showmeans=True)
ax.set(title='Box plot')

# Violinplot
fig, ax = plt.subplots()
ax.violinplot([g1, g2], showmeans=True, showmedians=True)
ax.set_xticks([1,2]); ax.set_xticklabels(['G1','G2'])

22.5 Scatterplots

Relatie tussen twee continue variabelen; kleur/marker/maat voor extra dimensies.

x = rng.normal(0, 1, 1000)
y = 2*x + rng.normal(0, 1, 1000)
size = 20 + 80*rng.random(1000)
color = x

fig, ax = plt.subplots()
sc = ax.scatter(x, y, c=color, s=size, cmap='viridis', alpha=0.7)
ax.set(xlabel='x', ylabel='y', title='Scatter')
cb = fig.colorbar(sc, ax=ax, label='x value')

Regressielijn (simpel) met NumPy:

coef = np.polyfit(x, y, deg=1)
xs = np.linspace(x.min(), x.max(), 100)
ax.plot(xs, coef[0]*xs + coef[1], color='k', lw=2, label='fit')
ax.legend()

22.6 Twin axes en secundaire as

x = np.linspace(0, 10, 200)
y1 = np.sin(x); y2 = np.cos(x)
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
ax1.plot(x, y1, color='C0'); ax1.set_ylabel('sin')
ax2.plot(x, y2, color='C3'); ax2.set_ylabel('cos')

22.7 Log‑schalen en breaks

x = np.logspace(0, 4, 200)
y = 1/x
fig, ax = plt.subplots()
ax.loglog(x, y)
ax.set(title='Log–log')

22.8 Veelvoorkomende valkuilen

  • Gebruik geen 3D voor 2D‑relaties; vermijdt chartjunk.
  • Y‑as nul‑afsnede belangrijk bij staafdiagrammen (anders misleidend), tenzij goed gemotiveerd.
  • Overplotting in scatter → gebruik alpha, kleinere markers, of hexbin/density.
  • Denk aan kleurenblindvriendelijke paletten (bv. tab10, colorcet buiten scope).

22.9 Samenvatting

  • Line: trends; Bar: categorieën; Hist/Box/Violin: verdelingen; Scatter: correlaties.
  • Voeg labels, legendes, error bars en passende schalen toe.
  • OO‑stijl blijft de aanbevolen werkwijze.

Hoofdstuk 23 — Aangepaste visualisaties, annotaties en subplots

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Complexere composities maken met meerdere Axes en GridSpec.
  • Annotaties, pijlen, tekstkaders en highlight‑technieken toepassen.
  • Balken/pijlen/patches (Rectangle, Arrow), twin axes, inset axes en broken axes.
  • Reusable functies en kleine helper‑API’s bouwen voor consistente visuals.

Aanbevolen voorkennis: Hoofdstuk 21–22.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.patches import Rectangle, FancyArrow, Circle

23.1 Subplot‑composities met GridSpec

plt.subplots dekt veel gevallen. Voor asymmetrische layouts gebruik je GridSpec.

import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(8, 5))
G = gridspec.GridSpec(nrows=2, ncols=3, height_ratios=[2, 1], width_ratios=[1, 2, 1], figure=fig)
ax_main = fig.add_subplot(G[0, :])       # hele bovenste rij
ax_left = fig.add_subplot(G[1, 0])
ax_mid  = fig.add_subplot(G[1, 1])
ax_right= fig.add_subplot(G[1, 2])

x = np.linspace(0, 10, 200)
ax_main.plot(x, np.sin(x))
ax_left.bar(['A','B','C'], [3,2,5])
ax_mid.plot(x, np.cos(x), color='C3')
ax_right.hist(np.random.default_rng(0).normal(size=1000), bins=30)
fig.suptitle('GridSpec compositie'); fig.tight_layout()

23.2 Inset Axes (zoom) en inset colorbar

from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset

fig, ax = plt.subplots(figsize=(6, 4))
x = np.linspace(0, 4*np.pi, 400); y = np.sin(x) + 0.2*np.sin(10*x)
ax.plot(x, y)
ax.set(title='Inset demo')
# Inset
axins = inset_axes(ax, width="40%", height="40%", loc='upper right')
axins.plot(x, y, color='C1')
axins.set_xlim(2, 3); axins.set_ylim(-1.2, 1.2)
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")

23.3 Broken axes (gapped axis)

fig, (ax, ax2) = plt.subplots(2, 1, sharex=True, figsize=(6, 4))
y = np.concatenate([np.random.normal(10, 1, 100), np.random.normal(100, 5, 100)])
ax.boxplot(y)
ax2.boxplot(y)
ax.set_ylim(95, 110)   # bovenste bereik
ax2.set_ylim(8, 12)    # onderste bereik
# diagonale streepjes om de break te tonen
ax.spines['bottom'].set_visible(False)
ax2.spines['top'].set_visible(False)
ax.xaxis.tick_top(); ax.tick_params(labeltop=False)
ax2.xaxis.tick_bottom()
kwargs = dict(marker=[(-1, -1), (1, 1)], markersize=12, linestyle='none', color='k', mec='k', mew=1, clip_on=False)
ax.plot([0, 1], [0, 0], transform=ax.transAxes, **kwargs)
ax2.plot([0, 1], [1, 1], transform=ax2.transAxes, **kwargs)
fig.tight_layout()

23.4 Annotaties, pijlen en tekstkaders

fig, ax = plt.subplots(figsize=(6, 4))
x = np.linspace(0, 2*np.pi, 400); y = np.sin(x)
ax.plot(x, y)
ax.annotate('maximum', xy=(np.pi/2, 1.0), xytext=(2.5, 1.2),
            arrowprops=dict(arrowstyle='->', lw=1.5),
            bbox=dict(boxstyle='round,pad=0.2', fc='w', ec='0.5'))
ax.text(0.1, -1.1, 'Note: sample', fontsize=10, color='0.2')

23.5 Patches: rechthoeken, cirkels, highlighters

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(np.arange(10), np.arange(10)**0.5)
# highlight bereik x in [2,5]
ax.add_patch(Rectangle((2, ax.get_ylim()[0]), width=3, height=np.diff(ax.get_ylim())[0],
                       color='yellow', alpha=0.2, zorder=0))
# markeer punt met cirkel
ax.add_patch(Circle((6, np.sqrt(6)), radius=0.2, color='C3', alpha=0.5))

23.6 Twin axes en secundaire as met functies

fig, ax1 = plt.subplots()
x = np.linspace(0.1, 10, 200)
y = np.log(x)
ax1.plot(x, y, color='C0'); ax1.set_ylabel('log(x)')
ax2 = ax1.secondary_yaxis('right', functions=(np.exp, np.log))
ax2.set_ylabel('exp(y)')

23.7 Kleine helper‑functies voor herbruikbaarheid

def theme_axes(ax, title=None, xlabel=None, ylabel=None):
    ax.grid(True, ls=':', alpha=0.4)
    if title: ax.set_title(title)
    if xlabel: ax.set_xlabel(xlabel)
    if ylabel: ax.set_ylabel(ylabel)
    return ax

fig, ax = plt.subplots()
ax.plot(np.linspace(0,1,50), np.linspace(0,1,50)**2, label='x^2')
ax = theme_axes(ax, title='Demo', xlabel='x', ylabel='y')
ax.legend()

23.8 Valkuilen en tips

  • Te veel annotaties → visuele ruis. Kies spaarzaam en gericht.
  • Inset axes kunnen overlap veroorzaken; gebruik tight_layout() of constrained_layout=True bij figuur.
  • Gebruik z‑order om lagen te sturen (lijnen voor patches of omgekeerd).

23.9 Samenvatting

  • GridSpec en inset/broken axes geven flexibiliteit voor complexe figuren.
  • Annotaties, patches en secundaire assen maken verhalen duidelijker.
  • Bouw kleine helpers voor consistente styling en hergebruik.

Hoofdstuk 24 — Visualisatie‑best practices en data storytelling

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Leren kiezen van geschikte grafiekvormen voor verschillende vragen.
  • Best practices voor schaal, kleur, labels, ordening, en reductie van ruis.
  • Storytelling‑principes: context, focus, hiërarchie, en begeleidende annotaties.
  • Toegankelijkheid: kleurenblindvriendelijke paletten, fontgrootte, contrast.

Aanbevolen voorkennis: Hoofdstuk 21–23.

24.1 Kies de juiste grafiek voor de vraag

  • Vergelijking per categorie: staaf (bar), eventueel horizontaal (barh) voor lange labels.
  • Verdeling: histogram, KDE (via andere libs), box/violin voor meerdere groepen.
  • Relatie: scatter; met maat/kleur voor extra variabelen; hexbins/density bij overplotting.
  • Tijd: line (geen staaf), gebruik consistente tijdschaal en duidelijke ticks.

24.2 As‑schalen en baselines

  • Begin bij 0 voor staafdiagrammen (misleiding voorkomen), tenzij goed gemotiveerd met annotatie.
  • Log‑schalen voor multiplicatieve verhoudingen of grote orders van grootte.
  • Vermijd onnodige dubbele y‑assen; als gebruikt, label duidelijk en geef context.
import numpy as np
import matplotlib.pyplot as plt

x = np.logspace(0, 4, 200); y = 1/x
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8,3))
ax1.plot(x, y); ax1.set_title('Lineair')
ax2.loglog(x, y); ax2.set_title('Log–log')
fig.tight_layout()

24.3 Ordening, sortering en groepering

  • Sorteer categorieën op waarde of logische volgorde.
  • Groepeer gerelateerde elementen, gebruik tussenkopjes of subtiele grids.
  • Vermijd 3D‑staven en onnodige effecten; houd het vlak en leesbaar.
cats = ['C','A','B']; vals = [7,5,3]
order = np.argsort(vals)[::-1]
fig, ax = plt.subplots()
ax.bar(np.array(cats)[order], np.array(vals)[order])
ax.set_title('Gesorteerd')

24.4 Kleurgebruik en paletten

  • Gebruik kleur spaarzaam om nadruk te leggen; houd overige elementen grijs/blauwachtig (low‑saturation).
  • Kies kleurenblindvriendelijke paletten (bijv. Matplotlib tab10, tab20).
  • Gebruik semantische kleur (rood voor negatief, groen voor positief) consistent en met voldoende contrast.
fig, ax = plt.subplots()
vals = [5, 7, 3, 6]
colors = ['0.7','0.7','0.7','C3']  # highlight laatste balk
ax.bar(['Q1','Q2','Q3','Q4'], vals, color=colors)
ax.set_title('Highlight met kleur')

24.5 Labels, annotaties en data‑ink ratio

  • Label assen en units; voeg titel en subtitel toe voor context.
  • Direct labeling (lijnen voorzien van tekst) voorkomt legende‑sprongen.
  • Verhoog de data‑ink ratio: verwijder chartjunk, lichte grids, beperkte ticks.
x = np.linspace(0, 10, 200)
fig, ax = plt.subplots()
ax.plot(x, np.sin(x), color='C0')
ax.plot(x, np.cos(x), color='C1')
ax.text(10.1, np.sin(10), 'sin', color='C0', va='center')
ax.text(10.1, np.cos(10), 'cos', color='C1', va='center')
ax.legend().remove()
ax.grid(True, ls=':', alpha=0.4)

24.6 Onzekerheid tonen: error bars, band en bootstrap CI

  • Error bars (standaarddeviatie of standaardfout) op staaf of lijn.
  • Confidence band rond een trendlijn.
rng = np.random.default_rng(0)
x = np.linspace(0, 10, 50)
y = np.sin(x) + 0.2*rng.standard_normal(len(x))

fig, ax = plt.subplots()
ax.plot(x, y, color='C0', lw=1)
# Smooth gemiddelde en band (voorbeeld: eenvoudige rollende mean)
import pandas as pd
s = pd.Series(y).rolling(5, center=True, min_periods=1)
mean = s.mean().to_numpy(); std = s.std().fillna(0).to_numpy()
ax.fill_between(x, mean-2*std, mean+2*std, color='C0', alpha=0.2, label='±2σ')
ax.plot(x, mean, color='C0', lw=2, label='mean')
ax.legend()

24.7 Small multiples en facetting

  • Breek complexe verhalen op in kleine, vergelijkbare panelen.
  • Gebruik gedeelde assen en consistente schalen voor vergelijking.
import pandas as pd
cats = ['A','B','C','D']
fig, axes = plt.subplots(2, 2, figsize=(7,5), sharex=True, sharey=True)
for ax, c in zip(axes.ravel(), cats):
    x = np.linspace(0, 10, 100)
    ax.plot(x, np.sin(x)+(cats.index(c))*0.3)
    ax.set_title(f'Category {c}')
fig.suptitle('Small multiples'); fig.tight_layout()

24.8 Typografie, maatvoering en toegankelijkheid

  • Minimale fontgrootte 9–10 pt op scherm; groter voor projectie.
  • Gebruik dpi en figsize bewust; test export (PNG/PDF) op scherpte.
  • Gebruik markers/linestyles naast kleur om onderscheid te waarborgen.
  • Voeg alternatieve teksten/onderschriften toe buiten de figuur (documentatie).

24.9 Storytelling‑structuur

  • Begin met vraag of claim; geef context (units, periode, sampleomvang).
  • Presenteer de kernvisual met highlight/annotatie.
  • Geef nuance (onzekerheid, uitzonderingen) en next steps.

24.10 Checklist voor review

  • Is de grafiek het juiste type voor de vraag?
  • Zijn assen gelabeld met units? Zijn schalen passend (lineair/log)?
  • Is ordening logisch? Is kleurgebruik toegankelijk en betekenisvol?
  • Zijn ruis en chartjunk geminimaliseerd? Is onzekerheid duidelijk?

24.11 Samenvatting

  • Keuze van grafiektype, schaal en ordening bepaalt begrijpelijkheid.
  • Gebruik kleur en annotaties doelgericht, met aandacht voor toegankelijkheid.
  • Vertel een verhaal met context, highlight en nuance; vermijd chartjunk.

Hoofdstuk 25 — Integratie met pandas en NumPy; theming en rcParams

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Efficiënt plotten met pandas (DataFrame.plot, Series.plot) op Matplotlib‑Axes.
  • NumPy‑integratie: imshow, contour, pcolormesh, en colormaps.
  • Theming met styles, rcParams en color cyclers; consistente lay‑out voor projecten.
  • Praktische tips voor lettertypen, colormaps, en globale figuurinstellingen.

Aanbevolen voorkennis: Hoofdstuk 21–24, basis pandas/NumPy.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl

25.1 pandas plot‑API op Matplotlib‑Axes

pandas bouwt bovenop Matplotlib. Gebruik OO‑stijl door ax= te mee te geven.

# Voorbeeld‑data
rng = np.random.default_rng(0)
dates = pd.date_range('2025-01-01', periods=50, freq='D')
df = pd.DataFrame({
    'A': np.cumsum(rng.normal(0, 1, 50)),
    'B': np.cumsum(rng.normal(0, 1, 50)),
}, index=dates)

fig, ax = plt.subplots(figsize=(7, 3))
df.plot(ax=ax)
ax.set(title='pandas line plot', xlabel='date', ylabel='value')
ax.legend(title='series')
fig.tight_layout()

Veelgebruikte opties:

  • kind='bar'|'barh'|'hist'|'box'|'kde'|'area'|'scatter'|'hexbin'.
  • subplots=True om elke kolom apart te plotten (terug: array van Axes).
  • layout=(rows, cols) voor subplots raster.
  • sharex=True|False, sharey=True|False voor gedeelde assen.
fig, axes = plt.subplots(1, 2, figsize=(8, 3), sharey=True)
df[['A']].plot(kind='area', alpha=0.4, ax=axes[0])
df[['B']].plot(kind='area', alpha=0.4, ax=axes[1], color='C3')
for ax, t in zip(axes, ['A','B']):
    ax.set_title(t)
fig.suptitle('pandas area'); fig.tight_layout()

25.2 pandas scatter en hexbin

N = 2000
xy = pd.DataFrame({
    'x': rng.normal(size=N),
    'y': rng.normal(size=N)
})
fig, ax = plt.subplots(figsize=(5, 4))
xy.plot(kind='scatter', x='x', y='y', s=10, alpha=0.5, ax=ax)
ax.set_title('pandas scatter')

# Hexbin voor hoge dichtheid
fig, ax = plt.subplots(figsize=(5, 4))
xy.plot(kind='hexbin', x='x', y='y', gridsize=30, cmap='viridis', ax=ax)
ax.set_title('pandas hexbin')

25.3 NumPy‑beelden met imshow/pcolormesh/contour

# 2D rasterdata
gx, gy = np.mgrid[-3:3:0.05, -3:3:0.05]
Z = np.exp(-(gx**2 + gy**2)) * np.cos(2*gx) * np.sin(2*gy)

fig, axes = plt.subplots(1, 3, figsize=(12, 3))
im = axes[0].imshow(Z, origin='lower', cmap='viridis')
fig.colorbar(im, ax=axes[0], shrink=0.8, label='value')

pc = axes[1].pcolormesh(gx, gy, Z, shading='auto', cmap='magma')
fig.colorbar(pc, ax=axes[1], shrink=0.8)

cs = axes[2].contour(gx, gy, Z, levels=12, colors='k', linewidths=0.7)
axes[2].clabel(cs, inline=True, fontsize=8)
for ax in axes:
    ax.set(xlabel='x', ylabel='y')
fig.suptitle('NumPy rasterplots'); fig.tight_layout()

Colormap tips:

  • Gebruik perceptueel uniforme colormaps (viridis, magma, cividis).
  • Gebruik norm=mpl.colors.LogNorm() voor positieve grootheden met grote range.

25.4 Stylesheet en rcParams voor projectthema

Je kunt projectbrede stijl vastleggen via rcParams of een custom stylesheet.

# Tijdelijk een style toepassen
with plt.style.context('seaborn-v0_8'):
    fig, ax = plt.subplots()
    df.plot(ax=ax); ax.set_title('Seaborn context')

# rcParams: globaal aanpassen
mpl.rcParams.update({
    'figure.dpi': 150,
    'axes.grid': True,
    'axes.titlesize': 12,
    'axes.labelsize': 10,
    'legend.frameon': False,
})

Eigen stylebestand (optioneel): maak een .mplstyle file en laad met plt.style.use('pad/naar/custom.mplstyle').

25.5 Color cycler en consistente palettes

from cycler import cycler
cycle = cycler(color=['#1f77b4','#ff7f0e','#2ca02c','#d62728']) + cycler(linestyle=['-','--','-.',':'])
mpl.rcParams['axes.prop_cycle'] = cycle

fig, ax = plt.subplots()
for k in range(4):
    ax.plot(np.linspace(0, 1, 50), np.linspace(0, 1, 50)**(k+1), label=f'k={k+1}')
ax.legend(); ax.set_title('Custom cycler')

25.6 Lettertypen en symbolen

# Kies een font familie (let op beschikbaarheid in je systeem)
mpl.rcParams['font.family'] = 'DejaVu Sans'
# Wetenschappelijke notatie en minus teken correct
mpl.rcParams['axes.unicode_minus'] = True

Voor LaTeX‑rendering (optioneel, vereist TeX‑installatie):

# mpl.rcParams['text.usetex'] = True

25.7 Integratiepatronen met pandas → Matplotlib

  • Gebruik pandas voor aggregatie/reshaping; plot op door jou aangemaakte Axes.
  • Geef ax= door en zet titels/labels via Matplotlib, niet mixen met pandas titels.
  • Werk met constrained_layout=True bij plt.figure(..., constrained_layout=True) voor automatische layout.
agg = (df.resample('7D').mean()
         .rename(columns={'A':'A_mean','B':'B_mean'}))
fig, ax = plt.subplots(figsize=(7,3), constrained_layout=True)
agg.plot(ax=ax)
ax.set(title='7‑daags gemiddelde', xlabel='date', ylabel='value')

25.8 Valkuilen en best practices

  • pandas plot(kind=...) is handig, maar voor maatwerk: werk op Matplotlib‑Axes.
  • Mix niet te veel automatische styling van pandas met rcParams; kies één pad.
  • Standaard colormap jet vermijden; kies perceptueel uniforme opties.

25.9 Samenvatting

  • pandas integreert direct met Matplotlib via ax=; gebruik het voor snelle exploratie.
  • NumPy‑rasterdata visualiseer je met imshow/pcolormesh/contour + passende colormap/norm.
  • Stylesheets, rcParams en cyclers zorgen voor consistente theming in projecten.

Hoofdstuk 26 — Exporteren, vector vs raster en publicatie‑kwaliteit figuren

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doel van dit hoofdstuk:

  • Begrijpen wanneer je raster (PNG/JPEG) of vector (PDF/SVG) gebruikt.
  • DPI, figsize en layout correct instellen voor scherpe, consistente output.
  • savefig‑opties, transparantie, achtergronden, metadata en fonts beheersen.
  • Publicatie‑tips voor rapporten, artikelen en presentaties.

Aanbevolen voorkennis: Hoofdstuk 21–25.

26.1 Raster versus vector

  • Raster (PNG, JPEG): pixel‑gebaseerd. Voordelen: voorspelbare weergave, goed voor scherm/presentatie; Nadeel: inzoomen geeft pixelatie; tekst is raster.
  • Vector (PDF, SVG): schaalbaar zonder kwaliteitsverlies; tekst en lijnen blijven scherp. Nadeel: complexe figuren (miljoenen punten) maken zware bestanden; sommige viewers/renderers verschillen.
  • Keuze:
    • Scherm/presentatie: PNG (geen compressieartefacten), of SVG als ondersteund.
    • Drukwerk/papers: PDF (vector) is vaak vereist.

26.2 Grootte en resolutie: figsize en DPI

Matplotlib meet figuurafmetingen in inches. Pixels = inches * DPI.

import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6, 4), dpi=150)
ax.plot([0,1],[0,1])
ax.set_title('Voorbeeld')
fig.tight_layout()
fig.savefig('figure-6x4in-150dpi.png', dpi=150, bbox_inches='tight')

Richtlijnen:

  • Voor A4 rapport: vaak figuren tussen 4–6 inch breed. Voor twee‑koloms papers: 3.5 inch (enkel kolom) of ~7.2 inch (dubbel kolom).
  • DPI 150–200 voor scherm; 300+ voor print. Vectorformaten negeren DPI (behalve embedded rasters binnen de figuur).

26.3 savefig‑opties die je moet kennen

fig.savefig('plot.png', dpi=200, bbox_inches='tight', pad_inches=0.02)
fig.savefig('plot.pdf')                # vector
fig.savefig('plot.svg', facecolor='white', transparent=False)
fig.savefig('plot.tiff', dpi=300)      # voor sommige publicaties
  • bbox_inches='tight' voorkomt afgesneden labels/legendes; pad_inches voegt marge toe.
  • facecolor/edgecolor voor achtergrond en rand. transparent=True maakt achtergrond transparant (let op leesbaarheid).
  • Metadata (PDF/SVG):
fig.savefig('plot.pdf', metadata={'Title': 'Mijn figuur', 'Author': 'Team'})

26.4 Achtergrond, transparantie en anti‑aliasing

  • Transparante achtergrond (transparent=True) is handig voor slides over een gekleurde achtergrond. Controleer contrast van tekst/assen.
  • Anti‑aliasing is standaard aan; lijnen/tekst zijn vloeiend. Voor pixel‑perfecte rasterbeelden kan antialiased=False op specifieke artists.

26.5 Fonts en embedding

  • Gebruik een gangbaar font (DejaVu Sans standaard bij Matplotlib). Voor volledige controle: stel rcParams in.
import matplotlib as mpl
mpl.rcParams['font.family'] = 'DejaVu Sans'
mpl.rcParams['pdf.fonttype'] = 42   # Type 42 (TrueType) embedding voor betere compatibiliteit
mpl.rcParams['ps.fonttype'] = 42
mpl.rcParams['svg.fonttype'] = 'none'  # bewaar tekst als tekst (niet als paden)
  • Sommige tijdschriften eisen specifieke fontinstellingen; test PDF in Acrobat/Preview.

26.6 Rasterization strategie voor zware figuren

Voor figuren met heel veel punten/patches kun je specifieke artists rasterizen in een verder vector‑PDF.

import numpy as np
fig, ax = plt.subplots()
x = np.random.rand(1_000_000); y = np.random.rand(1_000_000)
sc = ax.scatter(x, y, s=1, rasterized=True, alpha=0.5)
fig.savefig('scatter.pdf')  # vector PDF met gerasterde scatter‑laag
  • rasterized=True op de artist maakt bestanden kleiner en sneller te renderen.
  • Gebruik z‑order zodat raster‑laag onder tekst/assen ligt.

26.7 Kleurbeheer en profielen (kort)

  • Matplotlib werkt in sRGB. Voor drukwerk kan CMYK vereist zijn; exporteer dan naar PDF en converteer in de publicatietool/drukproces (buiten scope van Matplotlib).
  • Zorg voor voldoende contrast; test print‑proef indien mogelijk.

26.8 Layout en marges voor publicatie

  • Gebruik constrained_layout=True of tight_layout() maar let op: niet altijd perfect met colorbars/insets. Fine‑tune met subplots_adjust of gridspec_kw.
fig, ax = plt.subplots(constrained_layout=True, figsize=(6, 4))
# ... plot ...
fig.savefig('pub.png', bbox_inches='tight')
  • Houd consistente fontgroottes (titel/labels/ticks) en lijn‑diktes aan; legende zonder rand (frameon=False) tenzij noodzakelijk.

26.9 Workflowcheck voor publicatie‑kwaliteit

  1. Bepaal doel en formaat (scherm/print, kolombreedte).
  2. Stel figsize en dpi (raster) of gebruik vectorformaat.
  3. Controleer labels, units, leesbaarheid, contrast.
  4. Minimaliseer chartjunk; zorg voor juiste kleurpalet.
  5. Exporteer met bbox_inches='tight', metadata en correcte fontinstellingen.
  6. Open het resultaat in een viewer; zoom in (vector) of bekijk op 100% (raster).

26.10 Praktische voorbeelden

# 1) Slide‑ready PNG
fig, ax = plt.subplots(figsize=(10, 5), dpi=150)
ax.plot(range(10))
fig.savefig('slide.png', dpi=150, bbox_inches='tight')

# 2) Paper‑ready enkelkoloms PDF (~3.5 inch breed)
fig, ax = plt.subplots(figsize=(3.5, 2.2))
ax.plot(range(10))
ax.set_xlabel('x'); ax.set_ylabel('y')
fig.savefig('paper.pdf')

# 3) SVG met transparante achtergrond voor web
fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(np.linspace(0,1,200), np.linspace(0,1,200)**2, s=8, alpha=0.6)
fig.savefig('web.svg', transparent=True)

26.11 Veelvoorkomende valkuilen

  • Afgesneden labels/legendes: gebruik bbox_inches='tight' en controleer tight_layout().
  • Wazige afbeeldingen in documenten: verkeerde schaal/DPI. Plaats op 100% of exporteer op doelpixeldimensies.
  • Zware vector‑PDF’s door miljoenen punten: rasterize dichte lagen.
  • Onverwachte fonts in PDF/SVG: stel fonttype rcParams juist in en test in viewer.

26.12 Samenvatting

  • Kies raster of vector passend bij het doel; stel figsize en DPI bewust in.
  • Gebruik savefig‑opties voor randen, achtergrond, metadata en fonts.
  • Voor publicatie‑kwaliteit: consistente typografie, contrast, en layout; test de export in de doelflow.

Hoofdstuk 27 — Introductie tot machine learning met scikit‑learn: API‑filosofie

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen van dit hoofdstuk:

  • Begrijpen van de uniforme API van scikit‑learn (fit/transform/predict/score).
  • Leren werken met schatters (Estimators), transformators en pipelines.
  • Vertrouwd raken met datasets en train/test‑splits.

Aanbevolen voorkennis: NumPy/pandas (Deel III–IV) en basis statistiek.

Installatie:

pip install scikit-learn numpy pandas

27.1 Kernconcepten van scikit‑learn

  • Estimator: elk object met .fit(X, y=None) dat parameters leert uit data.
  • Transformer: een Estimator met .transform(X) (of .fit_transform(X)), bv. StandardScaler.
  • Predictor: een Estimator met .predict(X) (en soms .predict_proba(X)).
  • Scorer: methode .score(X, y) of losse scoringsfunctie (bv. accuracy_score).
  • Consistente API:
    • fit(X, y) leert;
    • transform(X) zet om;
    • predict(X) voorspelt;
    • set_params/get_params voor hyperparameters;
    • werkt met arrays (NumPy), sparse matrices of pandas DataFrames.

27.2 Dataset laden en train/test‑split

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import pandas as pd

iris = load_iris(as_frame=True)
X = iris.data  # features als DataFrame
y = iris.target  # 0,1,2

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
print(X_train.shape, X_test.shape)

27.3 Eerste model: een eenvoudige classifier

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(max_iter=1000, n_jobs=None)
clf.fit(X_train, y_train)
print('Accuracy (train):', clf.score(X_train, y_train))
print('Accuracy (test): ', clf.score(X_test, y_test))

Belangrijk: gebruik een vaste random_state voor reproduceerbaarheid waar nodig.

27.4 Transformers en Pipelines

Waarom Pipelines?

  • Voorkom dat je lekken introduceert (leakage) door transformaties uitsluitend op train uit te voeren en dezelfde transformatie op test toe te passen via de pipeline.
  • Combineer preprocessing en een model in één object dat fit/predict ondersteunt.

Voorbeeld met standaardiseren van features:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('model', LogisticRegression(max_iter=1000))
])
pipe.fit(X_train, y_train)
print('Pipeline accuracy (test):', pipe.score(X_test, y_test))

27.5 Hyperparameters en set_params

pipe.set_params(model__C=0.5, model__penalty='l2')
pipe.fit(X_train, y_train)
print(pipe.get_params()['model__C'])

27.6 Evaluatie en cross‑validation (sneak peek)

from sklearn.model_selection import cross_val_score
scores = cross_val_score(pipe, X, y, cv=5, scoring='accuracy')
print('CV accuracy:', scores.mean(), '+/-', scores.std())

27.7 Veelvoorkomende valkuilen

  • Data leakage door scaler/encoder op volledige dataset te fitten buiten een pipeline.
  • Onjuiste metriek (accuracy bij scheve klassenverdeling); kies passend bij probleem.
  • Geen vaste random_state en daardoor onstabiele resultaten.

27.8 Samenvatting

  • scikit‑learn biedt een uniforme API met Estimators, Transformers, Predictors.
  • Splits data in train/test; gebruik Pipelines voor preprocessing + model.
  • Gebruik cross‑validation voor robuuste evaluatie en set_params/get_params voor tunen.

Hoofdstuk 28 — Data voorbereiden: preprocessing, Pipelines en ColumnTransformer

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen:

  • Standaardiseren, normaliseren en omgaan met missende waarden via transformers.
  • Categorische variabelen encoderen met OneHotEncoder.
  • Samengestelde preprocessing met ColumnTransformer en Pipelines.

Installatie:

pip install scikit-learn pandas numpy

28.1 Veelgebruikte transformers

  • Imputers: SimpleImputer, KNNImputer.
  • Schalen/normaliseren: StandardScaler, MinMaxScaler, RobustScaler.
  • Encoders: OneHotEncoder, OrdinalEncoder.
  • Feature selectors: SelectKBest, VarianceThreshold.

28.2 Voorbeeld dataset en missende waarden

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(42)
N = 200
X = pd.DataFrame({
    'age': rng.integers(18, 80, size=N),
    'income': rng.normal(50000, 15000, size=N),
    'city': rng.choice(['Ams', 'Rtm', 'Utr'], size=N),
    'owns_house': rng.choice([0, 1], size=N)
})
# introduceer missend
X.loc[rng.choice(X.index, size=20, replace=False), 'income'] = np.nan

y = (X['age'] + (X['income'].fillna(50000)/1000) + 10*X['owns_house'] > 90).astype(int)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y)

28.3 Kolommen scheiden in numeriek en categorisch

num_cols = ['age', 'income']
cat_cols = ['city', 'owns_house']  # owns_house als categorie (0/1)

28.4 ColumnTransformer met Pipelines

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('onehot', OneHotEncoder(handle_unknown='ignore'))
])

preprocess = ColumnTransformer([
    ('num', numeric_pipeline, num_cols),
    ('cat', categorical_pipeline, cat_cols)
])

clf = Pipeline([
    ('preprocess', preprocess),
    ('model', LogisticRegression(max_iter=1000))
])

clf.fit(X_train, y_train)
print('Accuracy (test):', clf.score(X_test, y_test))

28.5 Alternatieven en tips

  • Voor robuustheid bij outliers: RobustScaler.
  • Voor hoge cardinaliteit: overweeg target encoding (niet in scikit‑learn core) of hashing (FeatureHasher).
  • remainder='passthrough' in ColumnTransformer om niet‑gespecificeerde kolommen door te laten.

28.6 Lekken voorkomen

  • Fit alle preprocessing uitsluitend binnen de pipeline op X_train.
  • Gebruik cross_val_score op de volledige pipeline i.p.v. op voorbewerkte arrays.

28.7 Samenvatting

  • Combineer Imputer/Scaler/Encoder per kolomgroep in Pipelines.
  • Verbind deze met ColumnTransformer en sluit aan op een eindmodel.
  • Train/evalueer altijd de gehele pipeline.

Hoofdstuk 29 — Supervised learning: regressie (LinearRegression, Ridge, Lasso)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen:

  • Begrijpen van lineaire regressie en regularisatie (L2 Ridge, L1 Lasso).
  • Train/test‑split, metriekkeuze (MSE, MAE, R^2) en feature scaling.

Installatie:

pip install scikit-learn numpy pandas

29.1 Data genereren of laden

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(0)
X = pd.DataFrame({
    'x1': rng.normal(size=300),
    'x2': rng.normal(size=300),
    'x3': rng.normal(size=300)
})
# Waarheid: y = 3*x1 - 2*x2 + noise; x3 is ruis
y = 3*X['x1'] - 2*X['x2'] + rng.normal(scale=0.5, size=300)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

29.2 Lineaire regressie

from sklearn.linear_model import LinearRegression

lin = LinearRegression()
lin.fit(X_train, y_train)
print('R^2 (train):', lin.score(X_train, y_train))
print('R^2 (test): ', lin.score(X_test, y_test))
print('Coefficients:', lin.coef_, 'Intercept:', lin.intercept_)

29.3 Ridge en Lasso

Regularisatie helpt overfitting voorkomen en kan stabiliteit verhogen.

from sklearn.linear_model import Ridge, Lasso
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

ridge = Pipeline([
    ('scaler', StandardScaler()),
    ('model', Ridge(alpha=1.0, random_state=42))
]).fit(X_train, y_train)

lasso = Pipeline([
    ('scaler', StandardScaler()),
    ('model', Lasso(alpha=0.1, random_state=42, max_iter=10000))
]).fit(X_train, y_train)

print('Ridge R^2 (test):', ridge.score(X_test, y_test))
print('Lasso R^2 (test):', lasso.score(X_test, y_test))

29.4 Metrieken: MSE, MAE, R^2

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

pred = lin.predict(X_test)
print('MSE:', mean_squared_error(y_test, pred))
print('MAE:', mean_absolute_error(y_test, pred))
print('R^2:', r2_score(y_test, pred))

29.5 Feature selectie met Lasso

coef = lasso.named_steps['model'].coef_
print(coef)
# Coefs die exact 0 zijn, worden effectief verwijderd

29.6 Cross‑validation varianten

from sklearn.model_selection import cross_val_score
scores = cross_val_score(ridge, X, y, cv=5, scoring='r2')
print('CV R^2:', scores.mean())

29.7 Samenvatting

  • LinearRegression voor basismodellen; Ridge/Lasso voor regularisatie.
  • Schaal features bij Ridge/Lasso voor consistente regulering.
  • Evalueer met passende regressiemetrieken en gebruik CV voor robuustheid.

Hoofdstuk 30 — Supervised learning: classificatie (LogisticRegression, SVM, Decision Trees)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen:

  • Basis van classificatieproblemen, decision boundaries, probabilistische output.
  • Vergelijken van drie klassiekers: Logistic Regression, SVM en Decision Tree.
  • Evaluatie met accuracy, precision/recall/F1 en confusion matrix.

Installatie:

pip install scikit-learn numpy pandas matplotlib

30.1 Voorbeelddata en splitsing

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

30.2 Logistic Regression

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

logreg = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(max_iter=2000))
])
logreg.fit(X_train, y_train)
print('LogReg accuracy:', logreg.score(X_test, y_test))

30.3 Support Vector Machine (SVM)

from sklearn.svm import SVC

svm = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', SVC(kernel='rbf', probability=True, random_state=42))
]).fit(X_train, y_train)
print('SVM accuracy:', svm.score(X_test, y_test))

30.4 Decision Tree

from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(max_depth=4, random_state=42)
tree.fit(X_train, y_train)
print('Tree accuracy:', tree.score(X_test, y_test))

30.5 Evaluatiemetrieken

from sklearn.metrics import classification_report, confusion_matrix

for name, model in [('LogReg', logreg), ('SVM', svm), ('Tree', tree)]:
    y_pred = model.predict(X_test)
    print(name)
    print(confusion_matrix(y_test, y_pred))
    print(classification_report(y_test, y_pred, digits=3))

30.6 Probabilities en ROC‑AUC

from sklearn.metrics import roc_auc_score, RocCurveDisplay
import matplotlib.pyplot as plt

y_prob = logreg.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_prob)
print('ROC‑AUC:', auc)
RocCurveDisplay.from_estimator(logreg, X_test, y_test)
plt.show()

30.7 Samenvatting

  • Logistic Regression: lineair, snel, goed baseline‑model.
  • SVM: krachtig met kernels, vaak sterke performance; let op schaal en hyperparameters.
  • Decision Trees: interpreteerbaar, weinig preprocessing nodig, maar kunnen overfitten.

Hoofdstuk 31 — Modelselectie en evaluatie: cross‑validation, metrics en GridSearchCV

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen:

  • Cross‑validation begrijpen en toepassen (KFold, StratifiedKFold, GroupKFold).
  • Juiste metriek kiezen en meerdere metriekken rapporteren.
  • Hyperparameter tuning met GridSearchCV en RandomizedSearchCV.

Installatie:

pip install scikit-learn numpy pandas

31.1 Cross‑validation basics

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_breast_cancer

X, y = load_breast_cancer(return_X_y=True)
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(max_iter=2000))
])
scores = cross_val_score(pipe, X, y, cv=5, scoring='accuracy')
print('Accuracy 5‑fold:', scores.mean(), '+/-', scores.std())

31.2 StratifiedKFold en meerdere metrics

from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.metrics import make_scorer, f1_score, roc_auc_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
res = cross_validate(
    pipe, X, y, cv=cv,
    scoring={'acc':'accuracy', 'f1':make_scorer(f1_score), 'auc':'roc_auc'},
    return_train_score=False
)
print({k: res[f'test_{k}'].mean() for k in ['acc','f1','auc']})

31.3 GridSearchCV

from sklearn.model_selection import GridSearchCV

param_grid = {
    'clf__C': [0.1, 1.0, 10.0],
    'clf__penalty': ['l2'],
    'clf__solver': ['lbfgs', 'liblinear']
}

grid = GridSearchCV(pipe, param_grid=param_grid, cv=5, scoring='roc_auc', n_jobs=-1)
grid.fit(X, y)
print('Best params:', grid.best_params_)
print('Best score:', grid.best_score_)

31.4 RandomizedSearchCV en distributions

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import loguniform

param_dist = {
    'clf__C': loguniform(1e-3, 1e2)
}
rand = RandomizedSearchCV(pipe, param_distributions=param_dist, n_iter=20, cv=5, scoring='roc_auc', random_state=42)
rand.fit(X, y)
print('Best params:', rand.best_params_)

31.5 Groeps‑CV (kort)

from sklearn.model_selection import GroupKFold
import numpy as np

X_small = X[:200]
y_small = y[:200]
# Stel dat elke 20 rijen tot dezelfde groep behoren
groups = np.repeat(np.arange(10), 20)
cv = GroupKFold(n_splits=5)
for train_idx, test_idx in cv.split(X_small, y_small, groups=groups):
    pass  # normal fit/eval flow

31.6 Samenvatting

  • Gebruik CV om variantie te reduceren en robuuste schattingen te krijgen.
  • Rapporteer meerdere relevante metriekken.
  • Stem hyperparameters af met GridSearchCV of RandomizedSearchCV; houd data‑lekken tegen met pipelines.

Hoofdstuk 32 — Ongecontroleerd leren: clustering (KMeans) en dimensionality reduction (PCA)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen:

  • Begrijpen van ongecontroleerd leren: doelen en beperkingen.
  • KMeans gebruiken voor clustering en evalueren met inertia en silhouette.
  • PCA toepassen voor dimensionality reduction en visualisatie.

Installatie:

pip install scikit-learn numpy pandas matplotlib

32.1 Dataset en standaardisatie

from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

32.2 KMeans clustering

from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters=3, n_init='auto', random_state=42)
labels = kmeans.fit_predict(X_scaled)
print('Inertia:', kmeans.inertia_)

32.3 Evaluatie met silhouette score

from sklearn.metrics import silhouette_score

sil = silhouette_score(X_scaled, labels)
print('Silhouette score:', sil)

32.4 PCA voor reductie en visualisatie

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

pca = PCA(n_components=2, random_state=42)
X2 = pca.fit_transform(X_scaled)
print('Explained variance ratio:', pca.explained_variance_ratio_.round(3))

plt.scatter(X2[:, 0], X2[:, 1], c=labels, cmap='tab10', s=40)
plt.xlabel('PC1'); plt.ylabel('PC2'); plt.title('KMeans clusters (PCA 2D)')
plt.show()

32.5 Het kiezen van k (elleboog‑methode)

import numpy as np
inertias = []
for k in range(2, 8):
    km = KMeans(n_clusters=k, n_init='auto', random_state=42).fit(X_scaled)
    inertias.append(km.inertia_)
print(inertias)
# Plot inertias versus k en zoek een knikpunt (elleboog)

32.6 Samenvatting

  • KMeans groepeert data op basis van afstand; kies k zorgvuldig.
  • Silhouette score beoordeelt clusterkwaliteit (hoger is beter).
  • PCA reduceert dimensies en helpt visualiseren; verlies aan informatie is mogelijk.

Hoofdstuk 33 — Feature engineering, leakage‑valkuilen en model interpretatie (Permutation Importance, SHAP)

Let op: De tekst is in het Nederlands; alle Python codevoorbeelden zijn in het Engels.

Doelen:

  • Principes van feature engineering: interacties, niet‑lineariteit, datum/tijd features.
  • Dataleakage herkennen en voorkomen.
  • Modelinterpretatie met Permutation Importance; korte introductie tot SHAP.

Installatie:

pip install scikit-learn numpy pandas shap

Opmerking: shap kan extra afhankelijkheden vereisen en is soms traag; optioneel.

33.1 Feature engineering principes

  • Begin simpel; laat het model aangeven waar winst te behalen valt.
  • Gebruik PolynomialFeatures voor interacties/niet‑lineariteit in lineaire modellen.
  • Maak zinvolle domeinspecifieke features (ratio’s, aggregaties, datum‑extracties).
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

# Speeldata met niet‑lineariteit
df = pd.DataFrame({'x': range(100)})
df['y'] = 0.1*df['x']**2 - 2*df['x'] + 5
X = df[['x']]; y = df['y']

poly_model = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=False)),
    ('lin', LinearRegression())
])
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
poly_model.fit(X_tr, y_tr)
print('R^2 (test):', poly_model.score(X_te, y_te))

33.2 Leakage‑valkuilen

  • Imputers/scalers op de hele dataset fitten voor de split of buiten pipelines.
  • Doelfeatures die afgeleid zijn van y of informatie uit de toekomst (tijdreeksen).
  • Target encoding zonder CV‑schema.

Checklist:

  • Gebruik Pipelines en voer alle preprocessing binnen CV/splits uit.
  • Voor tijdreeksen: TimeSeriesSplit en alleen verleden gebruiken.

33.3 Permutation Importance

Permutation Importance meet de afname in performance wanneer een feature willekeurig wordt geschud.

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.inspection import permutation_importance
import pandas as pd

X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(max_iter=2000))
]).fit(X_train, y_train)

result = permutation_importance(pipe, X_test, y_test, scoring='roc_auc', n_repeats=10, random_state=42)
importances = pd.Series(result.importances_mean, index=X.columns).sort_values(ascending=False)
print(importances.head(10))

33.4 SHAP (kort)

import shap

# Voor lineaire/logistische modellen werkt KernelExplainer of LinearExplainer
explainer = shap.LinearExplainer(pipe.named_steps['clf'], shap.sample(X_train, 200))
shap_values = explainer.shap_values(shap.sample(X_test, 200))
shap.summary_plot(shap_values, shap.sample(X_test, 200), plot_type='bar')

Let op: SHAP vereist toegang tot onderliggende modelinvoer (na scaling/encoding); voor pipelines is extra werk nodig om getransformeerde features te reconstrueren.

33.5 Samenvatting

  • Feature engineering kan prestaties sterk beïnvloeden; voorkom lekken met correcte workflows.
  • Permutation Importance is model‑agnostisch en werkt goed met pipelines.
  • SHAP geeft gedetailleerde lokale en globale uitleg, maar kan zwaar zijn.