Random, but repeatable
Monte Carlo simulation, bootstrapping and many tests need random numbers, yet a result nobody can reproduce is worthless: you cannot debug it, compare two versions of a model, or show a colleague. The numbers a computer produces are not truly random. A pseudo-random number generator is a deterministic algorithm that, from an initial value called the seed, produces a long sequence that behaves like random draws. Same seed, same sequence, every time and on every machine.
import numpy as np
rng = np.random.default_rng(42) # a Generator created from the seed 42
print(rng.random(3).round(4).tolist()) # [0.774, 0.4389, 0.8586]
rng = np.random.default_rng(42) # the same seed again: the same numbers
print(rng.random(3).round(4).tolist()) # [0.774, 0.4389, 0.8586]
rng = np.random.default_rng(43) # another seed: other numbers
print(rng.random(3).round(4).tolist()) # [0.6523, 0.0438, 0.02]Use a Generator, not global state
Older code calls np.random.seed(42) once and then np.random.normal(...) anywhere: all the code shares one hidden global generator. Any library, or any new line that draws a number, shifts everyone else's sequence, so results change for reasons that have nothing to do with your model. The modern way is to create a Generator object with np.random.default_rng(seed) and pass it around explicitly. Its methods random, normal, choice, integers do all the drawing, and two generators never interfere.
def simulate_returns(seed, n, mu, sigma):
rng = np.random.default_rng(seed) # a private generator, not the global one
return rng.normal(mu, sigma, size=n)
first = simulate_returns(7, 5, 0.0005, 0.01)
second = simulate_returns(7, 5, 0.0005, 0.01)
print(first.round(5).tolist()) # [0.00051, 0.00349, -0.00224, -0.00841, -0.00405]
print(np.array_equal(first, second)) # True repeatableResampling and independent streams
A bootstrap resamples the history to see how much a statistic could have varied: draw as many returns as you have, with replacement, using rng.choice(returns, size=len(returns), replace=True). Some days appear twice, others not at all. When many simulations run in parallel, giving each process the seeds 1, 2, 3 is risky because nearby seeds are not guaranteed to be independent. np.random.SeedSequence(seed).spawn(k) derives k child sequences that are designed not to overlap; build one default_rng(child) from each.
data = np.array([1, 2, 3, 4, 5])
rng = np.random.default_rng(1)
print(rng.choice(data, size=len(data), replace=True).tolist()) # [3, 3, 4, 5, 1] 3 appears twice, 2 not at all
# Three independent streams from one seed, for three parallel simulations
children = np.random.SeedSequence(2026).spawn(3)
streams = [np.random.default_rng(child) for child in children]
print([stream.random(2).round(4).tolist() for stream in streams])
# [[0.3827, 0.0204], [0.6711, 0.158], [0.5508, 0.2339]]rng = np.random.default_rng(42)
print(rng.integers(1, 7, size=8).tolist()) # [1, 5, 4, 3, 3, 6, 1, 5] eight dice rolls, repeatable
rng = np.random.default_rng(42)
print(rng.uniform(-0.01, 0.01, 3).round(5).tolist()) # [0.00548, -0.00122, 0.00717]rng = np.random.default_rng(seed): the same seed always gives the same numbers; use its methodsrandom,normal,choice,integers.- Pass the generator around instead of using the hidden global state
np.random.seed. choice(..., replace=True)is a bootstrap;SeedSequence(seed).spawn(k)giveskindependent streams; log the seed you used.
Next up: sampling from distributions, turning uniform random numbers into normal, exponential and discrete draws.
Lab · what's graded
- Create a repeatable generator with
make_rng25 pts - Draw reproducible returns with
simulate_returns25 pts - Resample history with
bootstrap_returns25 pts - Split one seed into independent streams with
independent_streams25 pts