Generación de Variables Aleatorias · Tema 14

Construcción de tablas de alias

Cómo se llenan Prob y Alias con pilas chicas y grandes: traza completa, invariante y verificación por columnas.

01 · Punto de partida

Del algoritmo a las manos

El Tema 13 mostró el muestreo O(1); este tema abre el preproceso. Construir bien es entender el invariante: en todo momento, masa en pilas + masa en columnas cerradas = n. Cada paso cierra una columna de 1 exacto emparejando un chico con un grande.

Lo haremos con el dado cargado [0,10 · 0,20 · 0,30 · 0,25 · 0,10 · 0,05], q = 6·p, paso a paso y verificable.

  • ¿Cómo se clasifican qᵢ en chicos (<1) y grandes (≥1)?
  • ¿Qué se escribe en Prob/Alias en cada emparejamiento?
  • ¿Cómo se actualiza el donante y se reclasifica?
  • ¿Cómo se verifica que cada columna mide 1/n?

02 · Definición

Traza completa del ejemplo guía

q inicial: [0,60 · 1,20 · 1,80 · 1,50 · 0,60 · 0,30]. Chicos {1,5,6}, grandes {2,3,4}.

Cada fila cierra una columna de 1,0 exacto.
Pasos (chico)g (grande)Se fijaq_g restante
11 (0,60)2 (1,20)Prob[1]=0,60 Alias[1]=20,80 → chico
25 (0,60)3 (1,80)Prob[5]=0,60 Alias[5]=31,40 → grande
36 (0,30)4 (1,50)Prob[6]=0,30 Alias[6]=40,80 → chico
42 (0,80)3 (1,40)Prob[2]=0,80 Alias[2]=31,20 → grande
54 (0,80)3 (1,20)Prob[4]=0,80 Alias[4]=31,00 → cerrar
6—3 (1,00)Prob[3]=1,00—
◫

Columna cerrada

Prob[s] propio + (1−Prob[s]) del donante = 1. Ya no se toca: mide 1/n exacto.

⇄

Donante actualizado

q_g pierde lo cedido. Si baja de 1 pasa a chicos; si no, sigue donando. Ninguna masa se crea ni destruye.

✓

Resto final

Lo que queda en pilas al final son unos exactos (Prob = 1). Forzarlos absorbe el redondeo flotante.

03 · Anatomía

Bordes que rompen implementaciones ingenuas

Tolerancia 1e−12
Clasificar con q < 1 − tol como chico. Sin tol, un 0,9999999999 por redondeo oscila de pila y el bucle no termina.
Probabilidades cero
pᵢ = 0 ⇒ qᵢ = 0 ⇒ columna vacía que solo aporta alias. Válido: Prob[i] = 0, Alias[i] = donante. Nunca sale i propio.
Normalización previa
Si Σp ≠ 1 por redondeo, dividir por la suma antes de escalar. Escalar sin normalizar reparte el error en todas las columnas.
Verificación por columnas
Para cada k: prob[k]/n + Σ_{j:alias[j]=k}(1−prob[j])/n debe dar pₖ. Test exacto, más fuerte que el histograma.

Construcción sana

Pilas que se vacían

Cada paso cierra una columna. n pasos como máximo; colas con deque O(1) por operación.

Construcción rota

Bucle o columnas ≠ 1

Síntomas: pilas que no se vacían, Prob > 1 o verificación por columnas que falla. Causa: tolerancia, normalización o alias sobrescrito.

04 · Implementación que escala

De 6 a 10⁶ categorías

normalizar→pilas deque→verificar
  1. 1
    Estructuras.

    deque para small/large (popleft O(1)). Arreglos float[n] y int[n] contiguos para caché en muestreo.

  2. 2
    Complejidad.

    O(n) tiempo, O(n) memoria. Para n = 10⁶, segundos de preproceso contra horas de bisect ahorradas.

  3. 3
    Empíricas grandes.

    Histogramas de datos con miles de bines fijos: se construye una vez y se muestrea en cada réplica. (Temas 34–35)

  4. 4
    Paralelismo.

    La construcción es secuencial; el muestreo es trivialmente paralelo (una semilla por hilo). No paralelizar pilas.

05 · Representación en Python

Constructor que imprime su traza

La misma construcción del Tema 13, ahora con registro de pasos y verificación por columnas.

Python en tu navegador. Ejecutá y seguí cada emparejamiento con el laboratorio.

from collections import deque

probs = [0.10, 0.20, 0.30, 0.25, 0.10, 0.05]
n = len(probs)
q = [p * n for p in probs]
prob = [0.0] * n
alias = [0] * n
small = deque([i for i, v in enumerate(q) if v < 1 - 1e-12])
large = deque([i for i, v in enumerate(q) if v >= 1 - 1e-12])
print("q0:", [round(v, 2) for v in q])

paso = 1
while small and large:
    s, g = small.popleft(), large.popleft()
    prob[s], alias[s] = q[s], g
    print(f"paso {paso}: s={s+1} g={g+1} Prob[{s+1}]={q[s]:.2f} Alias[{s+1}]={g+1}", end=" ")
    q[g] -= (1 - q[s])
    print(f"q[{g+1}] queda {q[g]:.2f}")
    (small if q[g] < 1 - 1e-12 else large).append(g)
    paso += 1
for i in list(small) + list(large):
    prob[i] = 1.0
    alias[i] = i
print("Prob:", [round(v, 2) for v in prob], "Alias:", [a + 1 for a in alias])

for k in range(n):
    pk = prob[k] / n + sum((1 - prob[j]) / n for j in range(n)
                           if alias[j] == k and prob[j] < 1 - 1e-12)
    print(k + 1, round(pk, 3), "ok" if abs(pk - probs[k]) < 1e-9 else "FALLA")

La verificación debe dar “ok” en las 6. Si alguna falla, el alias de esa columna apunta mal.

06 · Exploración

Laboratorio: ver llenarse las columnas

Avanzá el paso 0 → 6. Las barras son q (saldo vivo); la línea es 1. Al cerrar una columna su barra se congela en Prob y el donante baja. Leé la acción debajo.

EXPERIMENTO 14

Walker paso a paso

s chico + g grande → columna = 1

Los resultados numéricos aparecen debajo.
AcciónInicial
Chicos1·5·6
Grandes2·3·4
Cerradas0 / 6

q inicial: chicos {1,5,6}, grandes {2,3,4}.

Barras por categoría 1–6. Las cerradas se muestran en verde con su Prob; las vivas en celeste con su saldo q.

Preguntas para explorar

  1. En el paso 1, ¿qué Prob/Alias se fija y cuánto le queda al donante 2?
  2. ¿En qué paso el donante 2 pasa de grande a chico? ¿Qué lo provoca?
  3. Al final, ¿qué columnas quedan puras (Prob = 1)?
Ver respuestas sugeridas
  1. Prob[1] = 0,60, Alias[1] = 2; q₂ queda 0,80.
  2. En el paso 1 mismo: de 1,20 a 0,80 tras ceder 0,40.
  3. La 3 (y las donantes que terminan en 1 tras ceder todo: según orden, 2/3/4 quedan puras al cerrar).

07 · Comprensión

Confusiones frecuentes

«El orden de emparejar no importa, pero el resultado es único»

El orden no afecta la correctitud pero sí qué alias toca a cada columna: hay muchas tablas válidas para la misma p. No comparar tablas por igualdad, sino por verificación por columnas.

«Prob puede pasar de 1»

Nunca: Prob[s] = qₛ < 1 por definición de chico. Si ves Prob > 1, clasificaste mal o no normalizaste.

«Con pᵢ = 0 la columna se salta»

No se salta: queda Prob[i] = 0 con alias donante. Saltarla rompería el índice “columna = 1 + piso(n·U)”.

«El invariante es decorativo»

Es el test: suma(q en pilas) + cerradas debe dar n − cerradas… en cada paso. Si no, hay pérdida de masa por redondeo o bug.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: traza a mano y a código

Sin ejecutar: con q = [0,6 · 1,2 · 1,8 · 1,5 · 0,6 · 0,3], ¿qué se fija en el paso 1? Verificá con el bloque del Tema 13.

# Paso 1: s=1 (0.6), g=2 (1.2) -> Prob[1]=0.6 Alias[1]=2, q2=0.8
print("Prob[1]=0.6 Alias[1]=2 q2=0.8")
Ver solución razonada

Exacto. El donante cede 1 − 0,6 = 0,4 y pasa de 1,20 a 0,80 (de grande a chico).

Ejercicio 2: columna con cero

Construí alias para p = [0,5 · 0,0 · 0,5] y verificá que la columna 2 queda Prob = 0 con alias válido y nunca sale como propia.

from collections import deque

probs = [0.5, 0.0, 0.5]
n = 3
q = [p * n for p in probs]
print("q:", q)
# q = [1.5, 0.0, 1.5]: el 0.0 es chico, se cierra con Prob=0 de inmediato.
Ver solución

Prob[2] = 0, Alias[2] = donante (1 o 3). P(salir 2 propio) = (1/3)·0 = 0: correcto, solo puede salir como alias… y con estas p tampoco, porque nadie necesita donar a una columna vacía salvo cierre.

Ejercicio 3: invariante auditado

Instrumentá el constructor para afirmar en cada paso que suma(q en pilas) + cerradas = n − cerradas·0… (formulá el invariante exacto) o al menos que toda columna cerrada mide 1.

Ver una posible respuesta
from collections import deque

probs = [0.10, 0.20, 0.30, 0.25, 0.10, 0.05]
n = len(probs)
q = [p * n for p in probs]
prob = [None] * n
alias = [None] * n
small = deque([i for i, v in enumerate(q) if v < 1 - 1e-12])
large = deque([i for i, v in enumerate(q) if v >= 1 - 1e-12])
cerradas = 0
while small and large:
    s, g = small.popleft(), large.popleft()
    assert 0 <= q[s] < 1 + 1e-9, "s debe ser chico"
    prob[s], alias[s] = q[s], g
    q[g] -= (1 - q[s])
    cerradas += 1
    assert abs((prob[s] + (1 - prob[s])) - 1) < 1e-9
    (small if q[g] < 1 - 1e-12 else large).append(g)
print("cerradas:", cerradas, "OK")

Cada columna cerrada suma 1 por construcción. El assert caza clasificación rota en el acto.

09 · Síntesis

Ideas para recordar

  • Cerrar columna = fijar Prob[s], Alias[s] y descontar del donante.
  • Invariante masa en pilas + cerradas = n guía y audita.
  • Tolerancia, normalización y q separada de Prob evitan el 90 % de bugs.
  • pᵢ = 0 ⇒ Prob = 0 con alias, sin saltar índices.
  • Verificar por columnas es exacto; el histograma es solo Monte Carlo.

En el próximo tema veremos otra familia discreta secuencial: los métodos de urna.