Aleatorio, pero repetible
La simulación Monte Carlo, el bootstrap y muchas pruebas necesitan números aleatorios, pero un resultado que nadie puede reproducir no vale nada: no se puede depurar, ni comparar dos versiones de un modelo, ni enseñárselo a un colega. Los números que produce un ordenador no son realmente aleatorios. Un generador de números pseudoaleatorios es un algoritmo determinista que, a partir de un valor inicial llamado semilla (seed), produce una secuencia larga que se comporta como extracciones aleatorias. La misma semilla da la misma secuencia, siempre y en cualquier máquina.
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]Usa un Generator, no estado global
El código antiguo llama una vez a np.random.seed(42) y luego a np.random.normal(...) en cualquier sitio: todo el código comparte un único generador global oculto. Cualquier biblioteca, o cualquier línea nueva que saque un número, desplaza la secuencia de todos los demás, de modo que los resultados cambian por razones ajenas a tu modelo. La forma moderna es crear un objeto Generator con np.random.default_rng(seed) y pasarlo explícitamente. Sus métodos random, normal, choice, integers hacen todas las extracciones, y dos generadores nunca interfieren.
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 repeatableRemuestreo y flujos independientes
Un bootstrap remuestrea el historial para ver cuánto podría haber variado un estadístico: se extraen tantos rendimientos como se tienen, con reemplazo, con rng.choice(returns, size=len(returns), replace=True). Algunos días aparecen dos veces, otros ninguna. Cuando muchas simulaciones corren en paralelo, dar a cada proceso las semillas 1, 2, 3 es arriesgado porque las semillas cercanas no tienen garantizada la independencia. np.random.SeedSequence(seed).spawn(k) deriva k secuencias hijas diseñadas para no solaparse; construye un default_rng(child) con cada una.
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 misma semilla da siempre los mismos números; usa sus métodosrandom,normal,choice,integers.- Pasa el generador en lugar de usar el estado global oculto
np.random.seed. choice(..., replace=True)es un bootstrap;SeedSequence(seed).spawn(k)dakflujos independientes; registra la semilla usada.
A continuación: el muestreo de distribuciones, para convertir números aleatorios uniformes en extracciones normales, exponenciales y discretas.
Lab: qué se califica
- Crear un generador repetible con
make_rng25 pts - Generar rendimientos reproducibles con
simulate_returns25 pts - Remuestrear el historial con
bootstrap_returns25 pts - Dividir una semilla en flujos independientes con
independent_streams25 pts