Generación de Variables Aleatorias · Tema 41

Biblioteca de generación

El proyecto final: una librería completa con API clara, semillas reproducibles y batería en verde.

01 · Punto de partida

Del cuaderno suelto al módulo que se importa

Tenés 40 temas de recetas en bloques sueltos. Este proyecto las convierte en biblioteca: un módulo azar con una función por distribución, parámetros con nombre, semilla explícita y una batería de tests que corre antes de cada entrega.

El contrato es simple: misma semilla ⇒ misma muestra, bit a bit; y cada generador pasa chi, KS y QQ antes de mergearse.

  • ¿Qué API cubre todo el curso sin crecer sin control?
  • ¿Cómo se aíslan las semillas por experimento?
  • ¿Qué tests corren en segundos y cuáles en la noche?
  • ¿Cómo se documenta cada decisión del Tema 40?

02 · Definición

Contrato de la librería

Cada generador recibe rng primero y parámetros con nombre después; devuelve un flotante o entero, nunca imprime. muestrear(f, n, rng) produce vectores. bateria() corre chi/KS/QQ sobre el catálogo y reporta pasa/falla por función.

rng→x

Funciones puras

Sin estado global: el azar entra por rng inyectado. Dos experimentos no se roban la secuencia.

seed#

Semilla por experimento

Un entero por corrida, guardado junto a (método, N, versión). Reproducir = reinyectar el trío.

✓/✗

Batería que bloquea

Chi, KS y rachas del Tema 38 sobre cada generador: un rojo frena el release, como un test unitario.

El curso, convertido en API.
FunciónMétodo (Tema)Test que la cuida
bernoulli/exponencialUmbral / inversa (16, 22)Chi + KS
binomial/poisson/gammaSuma, Knuth, MT (19, 20, 26)Chi + media/var
normal/fisher_yatesPolar, FY (25, 33)QQ + matriz chi

03 · Anatomía

Módulos, versiones y costo

Un módulo, cuatro zonas
base (RNG + utilidades), discretas, continuas, muestreo (con/sin reemplazo, permutaciones) y validación. Cada zona, un archivo; cada función, su test.
Versionar el azar
Si cambia un algoritmo, cambia la versión mayor: la misma semilla puede dar otra muestra y los experimentos viejos deben advertirlo.
Batería en dos velocidades
Rápida (N = 2000, chi+KS) en cada commit; completa (N = 10⁵, QQ, rachas, eficiencia) de noche. Lo lento no frena, lo rápido no perdona.
Performance con techo
Medir ms/1000 por generador (Tema 39) y fijar presupuestos: si polar supera su techo, el CI avisa antes que el usuario.

Biblioteca con contrato

Se importa y se confía

API estable, semillas explícitas y tests en verde: otro equipo la usa sin leer el código.

Carpeta de scripts

Se copia y se reza

Semilla global, parámetros mágicos y “a mí me anduvo”: cada reuse es un experimento nuevo sin control.

04 · El mapa de los 41 temas

De U(0,1) a la librería validada

uniforme→métodos→catálogo→biblioteca
  1. 1
    Fundamentos (1–7).

    U → F → F⁻¹ analítica y numérica: la base que todo lo genera.

  2. 2
    Métodos (8–15).

    Rechazo, composición, alias y urnas para lo que no se invierte.

  3. 3
    Catálogo (16–27).

    Doce distribuciones con receta, costo y trampa documentadas.

  4. 4
    Calidad (28–41).

    Correlación, muestreo, validación, eficiencia, selección y esta biblioteca.

05 · Representación en Python

El núcleo en 40 líneas

Seis generadores puros con rng inyectado más mini-batería de medias: el esqueleto sobre el que crece todo.

Python en tu navegador. Cambiá la semilla: cambian los valores, no las medias.

import random
import math

def bernoulli(rng, p):
    return 1 if rng.random() < p else 0

def binomial(rng, n, p):
    return sum(1 for _ in range(n) if rng.random() < p)

def geometrica(rng, p):
    return int(math.log(1 - rng.random()) / math.log(1 - p))

def poisson(rng, lam):
    L, k, q = math.exp(-lam), 0, 1.0
    while True:
        q *= rng.random()
        if q <= L:
            return k
        k += 1

def exponencial(rng, lam):
    return -math.log(1 - rng.random()) / lam

def normal(rng):
    u1 = max(rng.random(), 1e-12)
    u2 = rng.random()
    return math.sqrt(-2*math.log(u1)) * math.cos(2*math.pi*u2)

rng = random.Random(410)
N = 6000
print("bern:", round(sum(bernoulli(rng, 0.3) for _ in range(N))/N, 3))
print("geom:", round(sum(geometrica(rng, 0.3) for _ in range(N))/N, 3), "vs 2.333")
print("pois:", round(sum(poisson(rng, 4.0) for _ in range(N))/N, 3))
print("exp:", round(sum(exponencial(rng, 0.5) for _ in range(N))/N, 3), "vs 2.0")

0,3 · 2,33 · 4,0 · 2,0: el núcleo respira. Cada línea nueva entra con su test.

06 · Exploración

Laboratorio: tablero de aceptación

Seis generadores de la biblioteca contra su F teórica: cada barra es D/D_crít (bajo 1, verde). Movete N y semilla: con N grande hasta el sesgo chico enciende alguna.

EXPERIMENTO 41

Batería del catálogo

D/D_crít < 1 en los 6

Los resultados numéricos aparecen debajo.
Tests pasados6 / 6
Peor ratio0,50
N3 000
Semilla1

Batería en verde: 6 de 6.

Bernoulli, binomial, geométrica, Poisson, exponencial y normal. Probá las 5 semillas y N = 12000.

Preguntas para explorar

  1. Con N = 3000, ¿los 6 en verde en las 5 semillas?
  2. Subí a N = 12000. ¿Alguna barra roza el 1? ¿Falla o avisa?
  3. ¿Qué test agregarías para rachas si la batería corriera de noche?
Ver respuestas sugeridas
  1. Sí: implementaciones exactas con D ≈ 0,3–0,7 del umbral.
  2. Rozar no es fallar: con α implícito, 1 de cada 20 corridas toca el borde por azar.
  3. Rachas sobre la mediana y chi de la matriz de posiciones (Temas 38 y 32).

07 · Comprensión

Confusiones frecuentes

«La batería pasa una vez y listo»

Corre en cada commit: un cambio inocente (orden de uniformes) altera secuencias y puede romper un test. Verde una vez ≠ verde siempre.

«Semilla fija global alcanza»

Un solo stream global entrelaza experimentos: agregar una corrida cambia todas las demás. Un rng por corrida.

«Documento después»

Método sin (supuestos, costo, tests) es código muerto al mes: documentar es parte del merge, no del después.

«41 temas = 41 funciones»

Los temas de criterio (36–40) no generan nada: viven como batería y guía, no como código. La API cubre el catálogo, no el índice.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: sumar la uniforme discreta

Agregá uniforme_discreta(rng, a, b) con piso y validala: media ≈ (a+b)/2 con N = 8000.

import random

def uniforme_discreta(rng, a, b):
    return a + int((b - a + 1) * rng.random())

rng = random.Random(415)
N = 8000
m = [uniforme_discreta(rng, 1, 6) for _ in range(N)]
print(round(sum(m)/N, 3), "vs 3.5")
Ver solución razonada

≈ 3,5. El test de verdad agrega chi contra 1/6 por cara (Tema 38).

Ejercicio 2: gamma por suma entera

Agregá gamma_erlang(rng, k, lam) y validala con α = 4, λ = 2: media ≈ 2, var ≈ 1.

import random
import math

def gamma_erlang(rng, k, lam):
    return -sum(math.log(1 - rng.random()) for _ in range(k)) / lam

rng = random.Random(412)
N = 6000
m = [gamma_erlang(rng, 4, 2.0) for _ in range(N)]
media = sum(m)/N
print(round(media, 3), round(sum((x-media)**2 for x in m)/N, 3))
Ver solución

≈ (2,0; 1,0). La general con Marsaglia-Tsang queda como extensión con su propio test.

Ejercicio 3: harness que condena al ingenuo

Escribí el test de matriz chi que el shuffle ingenuo no pasa y Fisher-Yates sí (n = 6).

Ver una posible respuesta
import random

def chi_matriz(mezcla, n, B, seed):
    rng = random.Random(seed)
    mat = [[0]*n for _ in range(n)]
    for _ in range(B):
        for j, v in enumerate(mezcla(rng, list(range(n)))):
            mat[v][j] += 1
    e = B/n
    return sum((c-e)**2/e for f in mat for c in f)

def fy(rng, xs):
    for i in range(len(xs)-1, 0, -1):
        j = int((i+1)*rng.random())
        xs[i], xs[j] = xs[j], xs[i]
    return xs

print("FY chi:", round(chi_matriz(fy, 6, 12000, 413), 1), "vs gl 35")

≈ 35. El harness queda en la batería: toda mezcla futura debe pasarlo.

09 · Síntesis

Ideas para recordar

  • API pura con rng primero; semilla por experimento.
  • Batería rápida siempre, completa de noche.
  • Versionar el azar: algoritmo nuevo, versión nueva.
  • Decisiones del Tema 40 escritas junto al código.
  • 41 temas, una librería: fin del curso, inicio del uso.

Volvé al índice, recorré los 41 y que tus simulaciones sean reproducibles.