01 · Punto de partida
De la planicie al giro gaussiano
La normal no tiene inversa cerrada, pero dos uniformes sí alcanzan si se mira en polares: un radio exponencial y un ángulo uniforme, girados con coseno y seno, dan dos N(0,1) independientes de un tiro.
Es el método exacto por defecto del Tema 23 y la referencia contra la que se mide el polar del Tema 25: misma exactitud, distinto costo trigonométrico.
- ¿Por qué R = √(−2 ln U₁) y Θ = 2πU₂?
- ¿Por qué Z₀ y Z₁ salen independientes?
- ¿Qué le cuesta al método cada par (log, raíz, senos)?
- ¿Qué pasa si U₁ = 0 o si se reutiliza mal el par?
02 · Definición
Radio exponencial y ángulo uniforme
R² = −2 ln U₁ ~ Exp(1/2): el radio al cuadrado es exponencial. Θ ~ U(0,2π) reparte la dirección pareja. El giro (cos, sin) proyecta el punto en dos coordenadas gaussianas independientes.
Radio con cola real
−2 ln U₁ hereda la cola exponencial: por eso Box-Muller sí llega a ±4σ y más, a diferencia de la suma-12.
Ángulo parejo
Θ uniforme en el círculo: la nube (Z₀,Z₁) queda con simetría radial, firma visual de independencia.
Par inseparable
Cada dos uniformes rinden dos normales. Guardar la sobrante (“spare”) duplica el rendimiento efectivo.
| Entrada | Transformación | Aporte |
|---|---|---|
| U₁ | √(−2 ln U₁) | Distancia al centro con cola exponencial |
| U₂ | 2πU₂ | Dirección equiprobable en el círculo |
| Ambas | R·cos Θ, R·sin Θ | Dos N(0,1) independientes |
03 · Anatomía
Costo, bordes y sobrante
- Costo por par
- Un log, una raíz, un cos y un sin para dos normales (≈ 0,5 de cada uno por Z). El seno/coseno domina: por eso existe el polar (Tema 25).
- Borde U₁ = 0
- ln 0 = −∞ y R explota. Rechazar U₁ = 0 exacto o clampear a
1e−12; U₂ = 0 es inofensivo (ángulo 0). - Sobrante (“spare”)
- Generar de a pares y devolver una por vez: la segunda queda cacheada. Sin caché se desperdicia la mitad del trabajo.
- Independencia frágil
- Z₀ y Z₁ del mismo par son independientes entre sí; mezclar U₁,U₂ entre pares o reutilizar Θ rompe la independencia aunque cada marginal parezca normal.
Box-Muller con caché
Exacto y rendidor
Dos normales por ciclo trigonométrico. La referencia exacta para validar métodos más rápidos.
Suma-12 / atajos
Baratos pero truncos
Sin trigonometría pero con cola cortada en ±6 y 12 U por muestra. Solo para enseñar el TCL.
04 · Dónde gira Box-Muller
Del par a la librería
- 1Ruido gaussiano.
Sensores, errores y simulación física con N(μ,σ) vía μ + σZ.
- 2Pares correlacionados.
Combinar Z₀,Z₁ independientes para lograr correlación objetivo. (Temas 28–29)
- 3Referencia exacta.
Patrón para validar el polar, ziggurat y aproximaciones rápidas. (Temas 25, 39)
- 4Colas y riesgo.
VaR y eventos 3σ+ donde la suma-12 no llega.
05 · Representación en Python
Par exacto con sobrante
Implementación con caché: un llamado produce dos normales y guarda la segunda. Auditoría de par completo.
Python en tu navegador. Subí N a 20000: la correlación debe seguir ≈ 0.
import random
import math
random.seed(200)
N = 8000
_spare = []
def box_muller():
if _spare:
return _spare.pop()
while True:
u1 = random.random()
if u1 > 1e-12:
break
u2 = random.random()
r = math.sqrt(-2.0 * math.log(u1))
t = 2.0 * math.pi * u2
_spare.append(r * math.sin(t))
return r * math.cos(t)
m = [box_muller() for _ in range(N)]
media = sum(m) / N
var = sum((x - media) ** 2 for x in m) / N
pares = [(m[i], m[i + 1]) for i in range(0, N - 1, 2)]
pm0 = sum(a for a, _ in pares) / len(pares)
pm1 = sum(b for _, b in pares) / len(pares)
cov = sum((a - pm0) * (b - pm1) for a, b in pares) / len(pares)
print("media:", round(media, 3), "var:", round(var, 3))
print("corr aprox:", round(cov, 3), "vs 0.0")
print("banda |Z|<=1:", round(sum(1 for x in m if abs(x) <= 1) / N, 3), "vs 0.683")
Media ≈ 0, var ≈ 1, corr ≈ 0 y banda ≈ 0,683. Si la corr se despega, revisá el caché del par.
06 · Exploración
Laboratorio: nube redonda e incorrelada
Cada punto es un par (Z₀,Z₁) Box-Muller: la nube debe ser redonda (simetría radial) y la correlación ≈ 0. Los anillos marcan 1σ, 2σ y 3σ. Subí N para estabilizar las estadísticas.
Pares Box-Muller
Z₀ = R·cosΘ, Z₁ = R·sinΘ
Con 3000 pares, nube redonda y corr ≈ 0.
Dispersión (Z₀,Z₁) en [−4,4]² con anillos 1σ–3σ. Probá N = 500 (nube rala) y N = 12000 (redonda y densa).
Preguntas para explorar
- Con N = 3000, ¿medias ≈ 0 y corr ≈ 0? Leelas en el panel.
- ¿La nube se alarga en diagonal o es redonda? ¿Qué dice eso de la independencia?
- Bajá a N = 500. ¿La corr se aleja de 0? ¿Es sesgo o ruido?
Ver respuestas sugeridas
- Sí: medias ≈ 0 (±0,05) y corr ≈ 0 (±0,04 con N = 3000).
- Redonda: sin dirección privilegiada, firma de independencia. Una elipse diagonal delataría correlación.
- Ruido: con N = 500 el error típico de la corr es ≈ 1/√500 ≈ 0,045; desvíos mayores sí serían bug.
07 · Comprensión
Confusiones frecuentes
«Descarto Z₁ y genero otro par»
Funciona pero rinde la mitad: cada par ya trae dos normales independientes. Guardar la sobrante es gratis.
«Uso grados en vez de radianes»
Θ = 2πU₂ exige radianes. Con grados el giro da vueltas de más y la nube deja de ser redonda.
«Reutilizo U₁ para el próximo par»
Rompe la independencia entre pares aunque cada Z parezca N(0,1). Cada par pide dos uniformes frescos.
«Con medias ≈ 0 ya valido»
Falta la correlación ≈ 0 y la banda 0,683. Dos marginales normales y correlacionadas no pasan el test de par.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: par auditado
N = 10000 (semilla 201): verificá medias ≈ 0, varianzas ≈ 1 y |corr| < 0,03.
import random
import math
random.seed(201)
N = 10000
pares = []
for _ in range(N // 2):
u1 = max(random.random(), 1e-12)
u2 = random.random()
r = math.sqrt(-2 * math.log(u1))
t = 2 * math.pi * u2
pares.append((r * math.cos(t), r * math.sin(t)))
z0 = [a for a, _ in pares]
z1 = [b for _, b in pares]
m0, m1 = sum(z0)/len(z0), sum(z1)/len(z1)
cov = sum((a-m0)*(b-m1) for a, b in pares) / len(pares)
print(round(m0, 3), round(m1, 3), round(cov, 3))
Ver solución razonada
Medias ≈ 0 (±0,03) y cov ≈ 0 (±0,02 con N = 5000 pares). Si la cov da ≈ ±1, mezclaste coseno y seno del mismo ángulo en una sola Z.
Ejercicio 2: de Z a N(μ,σ)
Con μ = 5, σ = 3 y N = 8000, transformá el par y verificá media ≈ 5 y banda [2,8] ≈ 0,683.
import random
import math
random.seed(202)
mu, sigma, N = 5.0, 3.0, 8000
m = []
for _ in range(N // 2):
u1 = max(random.random(), 1e-12)
u2 = random.random()
r = math.sqrt(-2 * math.log(u1))
t = 2 * math.pi * u2
m += [mu + sigma * r * math.cos(t), mu + sigma * r * math.sin(t)]
media = sum(m) / N
print(round(media, 3), "vs 5.0")
print(round(sum(1 for x in m if abs(x-mu) <= sigma) / N, 3), "vs 0.683")
Ver solución
≈ 5,0 y ≈ 0,68. La ubicación-escala sobre Box-Muller hereda exactitud y colas.
Ejercicio 3: el costo del seno
Generá 20000 normales por Box-Muller y por suma-12 y compará uniformes gastados: 1 por Z contra 12.
Ver una posible respuesta
import random
import math
import time
random.seed(203)
N = 20000
t0 = time.perf_counter()
for _ in range(N // 2):
u1 = max(random.random(), 1e-12)
u2 = random.random()
r = math.sqrt(-2 * math.log(u1))
t = 2 * math.pi * u2
_ = (r * math.cos(t), r * math.sin(t))
t1 = time.perf_counter()
random.seed(203)
for _ in range(N):
_ = sum(random.random() for _ in range(12)) - 6.0
t2 = time.perf_counter()
print("U por Z: Box-Muller 1.0 vs suma-12 12.0")
print("tiempos:", round(t1-t0, 3), "vs", round(t2-t1, 3))
Box-Muller gasta 12× menos uniformes; el tiempo depende de la trigonometría (ahí gana el polar del Tema 25).
09 · Síntesis
Ideas para recordar
- Box-Muller: R = √(−2 ln U₁), Θ = 2πU₂, Z₀/Z₁ = R·cos/sin Θ.
- Exacto con colas reales, 1 uniforme por normal con caché.
- Cuidar U₁ = 0 y usar radianes.
- Validar par: medias, varianzas y correlación ≈ 0.
- Referencia para validar el polar y otros métodos rápidos.
En el próximo tema evitamos la trigonometría: el método polar con rechazo en el disco.