Aléatoire, mais reproductible
La simulation de Monte Carlo, le bootstrap et de nombreux tests ont besoin de nombres aléatoires, mais un résultat que personne ne peut reproduire ne vaut rien : on ne peut ni le déboguer, ni comparer deux versions d'un modèle, ni le montrer à un collègue. Les nombres produits par un ordinateur ne sont pas vraiment aléatoires. Un générateur de nombres pseudo-aléatoires est un algorithme déterministe qui, à partir d'une valeur initiale appelée graine (seed), produit une longue suite qui se comporte comme des tirages aléatoires. Même graine, même suite, à chaque fois et sur chaque 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]Utilisez un Generator, pas un état global
Le code ancien appelle une fois np.random.seed(42) puis np.random.normal(...) n'importe où : tout le code partage un seul générateur global caché. Toute bibliothèque, ou toute nouvelle ligne qui tire un nombre, décale la suite de tous les autres, si bien que les résultats changent pour des raisons sans rapport avec votre modèle. La méthode moderne consiste à créer un objet Generator avec np.random.default_rng(seed) et à le transmettre explicitement. Ses méthodes random, normal, choice, integers font tous les tirages, et deux générateurs n'interfèrent jamais.
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 repeatableRééchantillonnage et flux indépendants
Un bootstrap rééchantillonne l'historique pour voir de combien une statistique aurait pu varier : on tire autant de rendements qu'on en a, avec remise, avec rng.choice(returns, size=len(returns), replace=True). Certains jours apparaissent deux fois, d'autres pas du tout. Quand de nombreuses simulations tournent en parallèle, donner à chaque processus les graines 1, 2, 3 est risqué car des graines voisines ne sont pas garanties indépendantes. np.random.SeedSequence(seed).spawn(k) dérive k séquences filles conçues pour ne pas se recouvrir ; construisez un default_rng(child) pour chacune.
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): la même graine donne toujours les mêmes nombres ; utilisez ses méthodesrandom,normal,choice,integers.- Transmettez le générateur plutôt que d'utiliser l'état global caché
np.random.seed. choice(..., replace=True)est un bootstrap ;SeedSequence(seed).spawn(k)donnekflux indépendants ; notez la graine utilisée.
À suivre : l'échantillonnage de lois, pour transformer des nombres aléatoires uniformes en tirages normaux, exponentiels et discrets.
Lab : ce qui est noté
- Créer un générateur reproductible avec
make_rng25 pts - Tirer des rendements reproductibles avec
simulate_returns25 pts - Rééchantillonner l'historique avec
bootstrap_returns25 pts - Séparer une graine en flux indépendants avec
independent_streams25 pts