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 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.