01 · Punto de partida
Un solo stream acopla lo que toca
Si tu banco simulado saca llegadas y tiempos de servicio del mismo stream, agregar un cliente de más corre toda la secuencia de servicio: dos escenarios dejan de ser comparables y un cambio inocente altera todo. La cura es asignar un stream por componente: el de llegadas solo da llegadas, el de servicio solo servicio, cada uno con vida propia.
Y hay un reverso poderoso: para comparar dos escenarios conviene lo contrario a propósito —los mismos streams en ambos (common random numbers)— para que la diferencia mida el cambio y no el azar.
- ¿Por qué un stream único acopla componentes?
- ¿Cómo creo k streams garantizados sin solape?
- ¿Cuándo conviene compartir streams a propósito (CRN)?
- ¿Por qué las semillas “1, 2, 3…” son una trampa?
02 · Definición
Streams, substreams y madre
Stream
Secuencia larga independiente: la de llegadas, la de servicio. No comparten estado ni posición.
Semilla madre
Un solo número maestro del que derivan todos (SeedSequence(2026).spawn(3)). Trazable y sin colisiones.
CRN
Common Random Numbers: mismos streams en escenarios A y B para comparar con menos varianza. Excepción deliberada.
| Generador | Mecanismo | Notas |
|---|---|---|
| NumPy (PCG64) | SeedSequence(m).spawn(k) | Garantía matemática, lo recomendado |
| PCG32 | Incremento impar por stream | 2⁶³ streams disjuntos |
| Philox | Llave por stream | Ideal paralelo (Tema 17) |
random clásico | Random(hash(madre, i)) | Práctico; sin garantía formal |
03 · Técnicas de partición
Espaciar, incrementar o derivar
Hay tres formas honestas de partir: bloques (stream i arranca en i·L: exige salto rápido), incrementos/llaves (cada stream vive en su carril: PCG/Philox) y derivación por hash (semilla i = hash(madre, i): práctica universal sin garantías formales).
- Semillas 1, 2, 3…
- Trampa clásica: con LCG chicos los estados iniciales están pegados y los streams se solapan o correlacionan. Parecen “distintas” y son vecinas del mismo ciclo.
- Reloj por stream
- Dos streams nacidos en el mismo milisegundo son gemelos (Tema 33). El tiempo no es esquema de partición.
- CRN bien usado
- Escenarios A/B con idénticos streams: la diferencia A−B tiene menos varianza porque el azar común se cancela. Solo vale si el consumo por componente es idéntico en ambos.
Bien partido
Madre + spawn
Streams disjuntos garantizados, réplica 137 re-corrible sola, comparación CRN válida.
Mal partido
Semillas a mano
Solapes silenciosos y acoples entre componentes: cambiar un parámetro altera streams ajenos.
04 · Ejemplos
Tres repartos típicos
- 1Banco: llegadas vs servicio.
Agregar una llegada no mueve el servicio: los escenarios son comparables y el debug es por componente.
- 2A/B con CRN.
Misma madre en cola con 1 y 2 cajeros: la diferencia de espera mide los cajeros, no la suerte.
- 3100 réplicas.
spawn(100): la réplica 57 se re-corre sola con su hijo 57. Sin spawn, repetir una es repetir todas.
05 · Implementación en Python
Derivar streams de una madre
Con la librería estándar: semilla hija = hash(madre, índice), un Random por componente. Mismo esquema mental que spawn, sin NumPy.
Python en tu navegador. Cambiá la madre y todo cambia; cambiá un componente y los demás ni se enteran.
import hashlib
import random
def hijo(madre, i):
h = hashlib.sha256(f"{madre}:{i}".encode()).digest()
return int.from_bytes(h[:8], "big")
def streams(madre, n=3):
return [random.Random(hijo(madre, i)) for i in range(n)]
lleg, serv, ruta = streams(2026)
print([round(lleg.random(), 4) for _ in range(3)])
print([round(serv.random(), 4) for _ in range(3)])
Tres generadores independientes desde la madre 2026: tocar llegadas no mueve servicio.
CRN: mismos streams, dos escenarios
import hashlib
import random
def hijo(madre, i):
h = hashlib.sha256(f"{madre}:{i}".encode()).digest()
return int.from_bytes(h[:8], "big")
def espera(cajeros, madre=2026, n=2000):
lleg = random.Random(hijo(madre, 0))
serv = random.Random(hijo(madre, 1))
t, libres = 0.0, [0.0] * cajeros
suma = 0.0
for _ in range(n):
t += lleg.expovariate(1.0)
s = serv.expovariate(1.5)
c = min(range(cajeros), key=lambda i: libres[i])
ini = max(t, libres[c])
suma += ini - t
libres[c] = ini + s
return suma / n
print("1 cajero:", round(espera(1), 4))
print("2 cajeros:", round(espera(2), 4))
06 · Exploración
Laboratorio: solape a la vista
LCG de juguete m = 256 con dos streams de largo L: con semillas vecinas (1, 2) se solapan; con partición por bloques (0, 128) no. El panel cuenta valores compartidos y correlación entre streams.
Vecinas vs bloques
solape = valores en común
Vecinas con L = 60: solape visible.
Puntos violeta = valores que aparecen en ambos streams. Con bloques bien separados no hay ninguno.
Preguntas para explorar
- Con vecinas y L = 200: ¿cuántos comparten? ¿Por qué tantos si “son distintas”?
- Con bloques y L = 120: ¿hay solape? ¿Y con L = 200?
- ¿Qué enseña esto sobre sembrar réplicas con 1, 2, 3… en un LCG real?
Ver respuestas sugeridas
- Casi todos: la semilla 2 está a un paso de la 1 en el mismo ciclo de 256. Vecinas = casi el mismo stream corrido.
- Con L = 120 no (bloques 0–119 y 128–247 disjuntos); con L = 200 sí en el borde: el bloque debe ser ≥ L con margen.
- Que “distinto número” ≠ “distinto tramo”: sin esquema (spawn/incremento/llave) las réplicas se pisan.
07 · Comprensión
Confusiones frecuentes
«Una semilla por corrida alcanza»
Alcanza para repetir, no para independizar: todos los componentes comparten el ciclo y se acoplan. Hace falta una semilla por componente derivada de la madre.
«Semillas al azar son independientes»
Dos puntos al azar en un ciclo de p pueden caer cerca y solaparse (Tema 33): el azar no garantiza disjunción, el esquema sí.
«CRN es hacer trampa»
No: es reducción de varianza legítima y documentada. Trampa sería usar CRN y no declararlo, o usarlo con consumos desalineados.
«Más streams = más lento»
Crear 100 Random cuesta microsegundos; el consumo total es el mismo. El costo es cero, el beneficio es independencia.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: vecinas que se pisan
Con el juguete m = 256 mostrá que las semillas 1 y 2 comparten casi todo en L = 200.
def lcg(semilla, n, a=13, c=7, m=256):
x = semilla % m
sal = []
for _ in range(n):
x = (a * x + c) % m
sal.append(x)
return sal
a, b = lcg(1, 200), lcg(2, 200)
print("compartidos:", len(set(a) & set(b)))
Ver solución razonada
≈ 169 de 200 valores en común: la semilla 2 vive en el mismo ciclo de 256, a pocos pasos de la 1. Vecinas ≠ independientes.
Ejercicio 2: bloques que separan
Tomá un solo stream madre y partilo en bloques [0:60] y [128:188]: verificá disjunción.
def lcg(semilla, n, a=13, c=7, m=256):
x = semilla % m
sal = []
for _ in range(n):
x = (a * x + c) % m
sal.append(x)
return sal
todo = lcg(0, 256)
print("solape:", len(set(todo[0:60]) & set(todo[128:188])))
Ver solución
0 compartidos: bloques disjuntos del mismo ciclo. Así se reparte un LCG cuando no hay spawn: con regla y margen.
Ejercicio 3: CRN que reduce varianza
Compará 1 vs 2 cajeros con mismos streams (CRN) y con streams distintos: ¿cuál diferencia es más estable entre madres?
Ver una posible respuesta
import hashlib
import random
def hijo(madre, i):
h = hashlib.sha256(f"{madre}:{i}".encode()).digest()
return int.from_bytes(h[:8], "big")
def espera(cajeros, s1, s2, n=500):
lleg = random.Random(s1)
serv = random.Random(s2)
t, libres, suma = 0.0, [0.0] * cajeros, 0.0
for _ in range(n):
t += lleg.expovariate(1.0)
s = serv.expovariate(1.5)
c = min(range(cajeros), key=lambda i: libres[i])
ini = max(t, libres[c])
suma += ini - t
libres[c] = ini + s
return suma / n
for madre in [11, 22, 33]:
d_crn = espera(1, hijo(madre, 0), hijo(madre, 1)) - espera(2, hijo(madre, 0), hijo(madre, 1))
d_ind = espera(1, hijo(madre, 0), hijo(madre, 1)) - espera(2, hijo(900 + madre, 0), hijo(900 + madre, 1))
print(madre, "CRN:", round(d_crn, 4), "indep:", round(d_ind, 4))
Ambas vías estiman la misma brecha (≈ 1,0 a favor de 2 cajeros). La ventaja de CRN —menos varianza del estimador de la diferencia porque el azar común se cancela— se aprecia al promediar decenas de madres: extendé el bucle a 30 y compará el desvío de cada columna.
09 · Síntesis
Ideas para recordar
- Un stream por componente desde una madre; nunca un único stream global.
- Partir con esquema: spawn / incremento / llave / bloques con margen.
- Semillas vecinas o por reloj no independizan: solapan.
- CRN: mismos streams en A y B para comparar, declarado y con igual consumo.
- Réplica re-corrible sola = spawn por índice.
En el próximo tema escalamos a paralelo real: simulaciones paralelas y semillas sin solapamientos.