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

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.