01 · Punto de partida
El giro sin trigonometría
Box-Muller es exacto pero paga un coseno y un seno por par. El método polar (Marsaglia) logra lo mismo sin trigonometría: tira puntos en el cuadrado [−1,1]², se queda con los que caen en el disco S < 1 y los estira con un solo logaritmo.
Es el mismo par gaussiano del Tema 24 por otro camino: un 21 % de los intentos se rechaza, pero cada intento aceptado evita la parte más cara del cálculo.
- ¿Por qué V₁,V₂ ~ U(−1,1) con S = V₁²+V₂² < 1?
- ¿De dónde sale el factor √(−2 ln S / S)?
- ¿Qué tasa de aceptación esperar y cómo auditarla?
- ¿Cuándo conviene polar sobre Box-Muller?
02 · Definición
Disco uniforme y estiramiento radial
Condicionar (V₁,V₂) a S < 1 da un punto uniforme en el disco: su dirección es el ángulo Θ parejo y su radio al cuadrado es uniforme, el sustituto exacto de U₂ y U₁ de Box-Muller. El factor f convierte ese radio en el R gaussiano.
Rechazo π/4
El disco ocupa π/4 del cuadrado: ~78,5 % de los pares se acepta. La tasa observada es el primer test del generador.
Un log por par
Un logaritmo y una raíz por dos normales, cero trigonometría. El punto (V₁,V₂) ya trae el “coseno y seno” gratis.
Par con sobrante
Como Box-Muller: cada aceptación rinde dos Z independientes; cachear la segunda duplica el rendimiento.
| Método | Uniformes / par | Trigonometría | Rechazo |
|---|---|---|---|
| Box-Muller | 2 exactos | cos + sin | 0 % |
| Polar | ≈ 2,55 en promedio | ninguna | ≈ 21,5 % |
03 · Anatomía
Aceptar, estirar y cachear
- Bucle de aceptación
- Tirar V₁ = 2U−1, V₂ = 2U−1 hasta 0 < S < 1. S = 0 se rechaza (ln 0 indefinido); S ≥ 1 cae fuera del disco. Media de intentos: 4/π ≈ 1,27.
- Factor único
f = sqrt(-2 * log(s) / s)una sola vez por par aceptado; Z₀ y Z₁ comparten el factor. No recalcular el log por coordenada.- Sobrante como Box-Muller
- Devolver Z₀ y cachear Z₁. Sin caché se tira la mitad del trabajo aceptado, que ya costó rechazos.
- Costo real
- Más uniformes por par (2,55 vs 2) pero sin senos: en CPUs sin trigonometría rápida el polar gana; con
sincosvectorizado la brecha se achica (Tema 39).
Método polar
Rápido y exacto
La opción por defecto cuando la trigonometría duele. Misma exactitud y colas que Box-Muller.
Box-Muller directo
Simple y sin rechazo
Dos uniformes, cero rechazos, costo fijo. Mejor si el costo trigonométrico no es problema.
04 · Dónde rinde el disco
Del cuadrado a la librería
- 1Mismo ruido que Box-Muller.
N(μ,σ) vía μ + σZ, pares correlacionados y procesos gaussianos.
- 2CPUs sin trigonometría.
Microcontroladores y GPUs donde cos/sin son caros: el disco los reemplaza.
- 3Clase de rechazo.
Primer rechazo continuo que acepta sin deformar: antesala del Tema 8 aplicado.
- 4Puente a ziggurat.
La idea “capas + rechazo barato” escala al método más rápido de todos. (Tema 39)
05 · Representación en Python
Disco, factor y tasa π/4
Implementación con caché más auditoría triple: tasa de aceptación, par incorrelado y banda 68 %.
Python en tu navegador. Subí N a 20000: la tasa debe seguir ≈ 0,785.
import random
import math
random.seed(210)
N = 8000
_spare = []
intentos = 0
aceptados = 0
def polar():
global intentos, aceptados
if _spare:
return _spare.pop()
while True:
intentos += 1
v1 = 2 * random.random() - 1
v2 = 2 * random.random() - 1
s = v1 * v1 + v2 * v2
if 0 < s < 1:
aceptados += 1
f = math.sqrt(-2 * math.log(s) / s)
_spare.append(v2 * f)
return v1 * f
m = [polar() for _ in range(N)]
media = sum(m) / N
var = sum((x - media) ** 2 for x in m) / N
print("tasa:", round(aceptados / intentos, 3), "vs 0.785")
print("media:", round(media, 3), "var:", round(var, 3))
print("banda |Z|<=1:", round(sum(1 for x in m if abs(x) <= 1) / N, 3), "vs 0.683")
Tasa ≈ 0,785, media ≈ 0, var ≈ 1 y banda ≈ 0,683. Si la tasa da ≈ 1, el rechazo no está filtrando.
06 · Exploración
Laboratorio: el disco que acepta π/4
Cada punto es un intento (V₁,V₂): los celestes caen en el disco y se aceptan; los naranjas caen fuera y se rechazan. La fracción celeste debe rondar π/4 ≈ 0,785 y los Z resultantes, la banda 0,683.
Disco polar
Acepta si 0 < S < 1 · Z = V·√(−2 ln S / S)
Con 3000 pares, tasa ≈ 0,785 y banda ≈ 0,683.
Cuadrado [−1,1]² con disco inscripto. Probá N = 500 (tasa ruidosa) y N = 12000 (π/4 clavado).
Preguntas para explorar
- Con N = 3000, ¿tasa ≈ 0,785? ¿Banda ≈ 0,683?
- ¿Los puntos naranjas se concentran en las esquinas? ¿Por qué ahí?
- Bajá a N = 500. ¿La tasa se aleja de π/4? ¿Sesgo o ruido?
Ver respuestas sugeridas
- Sí: tasa 0,785 ± 0,015 y banda 0,683 ± 0,02 con N = 3000.
- Sí: las esquinas quedan fuera del disco (S > 1). El rechazo vive ahí por geometría.
- Ruido: el error típico de la tasa es ≈ √[0,785·0,215/500] ≈ 0,018; solo desvíos mayores delatan bug.
07 · Comprensión
Confusiones frecuentes
«Acepto S ≤ 1 o S = 0 da igual»
S = 0 rompe el factor (división por cero y log de cero). Rechazar 0 < S < 1 estricto.
«V₁,V₂ uniformes en (0,1)»
Deben ser U(−1,1): V = 2U − 1. Con (0,1) el cuadrado no centra el disco y la tasa da otro valor.
«Un log por coordenada»
El factor f se calcula una vez por par aceptado y se comparte. Recalcularlo duplica el costo logarítmico.
«La tasa π/4 no necesita validarse»
Es el test más barato: si da 1,0 no hay rechazo; si da 0,5, el cuadrado o el disco están mal definidos.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: tasa π/4 auditada
N = 10000 (semilla 211): verificá tasa ≈ 0,785, medias ≈ 0 y |corr| < 0,03.
import random
import math
random.seed(211)
N = 10000
intentos = aceptados = 0
pares = []
while len(pares) < N // 2:
intentos += 1
v1 = 2 * random.random() - 1
v2 = 2 * random.random() - 1
s = v1 * v1 + v2 * v2
if 0 < s < 1:
aceptados += 1
f = math.sqrt(-2 * math.log(s) / s)
pares.append((v1 * f, v2 * f))
z0 = [a for a, _ in pares]
m0 = sum(z0) / len(z0)
print("tasa:", round(aceptados / intentos, 3), "vs 0.785")
print("media Z0:", round(m0, 3))
Ver solución razonada
Tasa ≈ 0,785 (±0,01 con N = 5000 pares) y media ≈ 0. Si la tasa da ≈ 0,5, generaste V en (0,1) en vez de (−1,1).
Ejercicio 2: de Z a N(μ,σ)
Con μ = 5, σ = 3 y N = 8000, transformá el par polar y verificá media ≈ 5 y banda [2,8] ≈ 0,683.
import random
import math
random.seed(212)
mu, sigma, N = 5.0, 3.0, 8000
m = []
while len(m) < N:
v1 = 2 * random.random() - 1
v2 = 2 * random.random() - 1
s = v1 * v1 + v2 * v2
if 0 < s < 1:
f = math.sqrt(-2 * math.log(s) / s)
m += [mu + sigma * v1 * f, mu + sigma * v2 * f]
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 el polar hereda exactitud y colas.
Ejercicio 3: polar vs Box-Muller en uniformes
Contá uniformes por Z de cada método con N = 20000: Box-Muller 1,0 contra polar ≈ 1,27.
Ver una posible respuesta
import random
import math
random.seed(213)
N = 20000
# Box-Muller: 2 U por 2 Z
print("Box-Muller U/Z:", 2 * (N // 2) * 2 / N / 2, "= 1.0")
# Polar: 2 U por intento, 1.27 intentos por par
intentos = aceptados = 0
while aceptados < N // 2:
intentos += 1
v1 = 2 * random.random() - 1
v2 = 2 * random.random() - 1
if 0 < v1*v1 + v2*v2 < 1:
aceptados += 1
print("Polar U/Z:", round(2 * intentos / N, 3), "vs 1.27")
Polar gasta ~27 % más uniformes pero cero trigonometría: ahí está el trueque.
09 · Síntesis
Ideas para recordar
- Polar: V₁,V₂ ~ U(−1,1), acepta 0 < S < 1, f = √(−2 ln S/S).
- Exacto como Box-Muller, sin senos ni cosenos.
- Tasa π/4 ≈ 0,785: el test más barato del método.
- Un log por par, sobrante cacheada, S = 0 siempre rechazado.
- Trueque: +27 % uniformes a cambio de −100 % trigonometría.
En el próximo tema sumamos exponenciales: la gamma con forma α.