Generación de Variables Aleatorias · Tema 02

Generación de variables discretas

Cómo convertir un U ~ U(0,1) en cualquier soporte finito o numerable con la regla X = mín{x : F(x) ≥ U} y búsqueda sobre acumuladas.

01 · Punto de partida

Del umbral a la ruleta

En el Tema 1 generamos Bernoulli(p) con un umbral: 1 si U < p. Era una discreta con dos valores. Pero una demanda diaria puede valer {0,1,2,3,4}, un dado cargado {1,…,6} con probabilidades desiguales, una llegada Poisson {0,1,2,…} sin tope.

Todas se generan con la misma idea generalizada: partir el intervalo [0,1) en subintervalos de largo pk y devolver el valor cuyo intervalo atrapó a U. Es la inversión discreta, la ruleta del casino llevada a código.

  • ¿Cómo se traduce una tabla de probabilidades en intervalos?
  • ¿Qué fórmula única sirve para finitas e infinitas numerables?
  • ¿Búsqueda lineal o binaria sobre acumuladas?
  • ¿Cuándo conviene preprocesar con alias (Tema 13)?

02 · Definición

Masa, acumulada e inversa generalizada

Una discreta se define por su masa pk ≥ 0 con suma 1 y su acumulada escalonada F(x) = Σxk ≤ x pk. La inversa no existe como función lisa, pero sí como cuantil:

▦

Intervalos

U cae en [Fk-1, Fk) con probabilidad Fk − Fk-1 = pk. El largo del intervalo es la probabilidad.

◈

Exactitud

P(devolver xk) = P(Fk-1 ≤ U < Fk) = pk. No es aproximado: es exacto salvo error de punto flotante.

⚙

Costo

Buscar k cuesta O(n) lineal u O(log n) binaria. Con n grande y millones de muestras, ese costo manda (Temas 13–14 y 39).

Ejemplo concreto. Dado cargado con masa [0,10 · 0,20 · 0,30 · 0,25 · 0,10 · 0,05] para {1,…,6}. Acumuladas: 0,10 · 0,30 · 0,60 · 0,85 · 0,95 · 1,00. Si sale U = 0,73, recorremos: 0,73 > 0,60 pero ≤ 0,85 → devolvemos 4. Si U = 0,05 → 1.

Tabla de trabajo para invertir una discreta finita.
kValor xkpkFkIntervalo de U
110,100,10[0,00 · 0,10)
220,200,30[0,10 · 0,30)
330,300,60[0,30 · 0,60)
440,250,85[0,60 · 0,85)
550,100,95[0,85 · 0,95)
660,051,00[0,95 · 1,00)

03 · Anatomía

Algoritmo general y sus variantes

Preproceso (una vez)
Validar que pk ≥ 0 y suman 1 (con tolerancia), ordenar valores si conviene y construir acumuladas Fk. Costo O(n).
Muestreo (cada X)
Generar U, buscar el primer k con Fk ≥ U, devolver xk. Lineal O(n), binaria O(log n) con bisect.
Soporte infinito
Geométrica, Poisson, binomial grande: no hay tabla finita. Se acumula sobre la marcha hasta superar U, o se usa fórmula cerrada / rechazo (Temas 18–20).
Aceleración
Si n es grande y N es enorme: alias O(1) tras O(n) de tablas Prob/Alias (Temas 13–14). Si p cambia en cada llamada, el preproceso no se amortiza.

Búsqueda lineal

Simple y rápida en n chico

Recorre F hasta superar U. Ideal si n ≤ 10 o si los valores más probables están primero (ponelos al inicio).

Búsqueda binaria

Escala a n grande

Usa bisect_left(F, U) sobre acumuladas ordenadas. Imprescindible con cientos de categorías y millones de réplicas.

04 · Casos típicos

Finitas, numerables y qué viene después

tabla pₖ→acumuladas Fₖ→U → k → xₖ
  1. 1
    Finitas directas.

    Bernoulli, uniforme discreta, dado cargado, demanda empírica chica. Tabla completa + bisect. (Temas 16–17, 34–35)

  2. 2
    Numerables con fórmula.

    Geométrica(p): k = 1 + piso(ln(1−U)/ln(1−p)). Evita acumular infinito con una inversión cerrada. (Tema 18)

  3. 3
    Numerables por acumulación.

    Binomial(n,p) chico y Poisson(λ) chico: sumar pk hasta superar U. Simple pero O(media). (Temas 19–20)

  4. 4
    Masivas.

    Miles de categorías o N millonario: alias O(1). El preproceso se paga una vez. (Temas 13–14)

05 · Representación en Python

Un generador discreto genérico

La misma función sirve para cualquier finita: recibe valores y probabilidades, valida, precalcula F y muestrea con bisect.

Python en tu navegador. Editá las probabilidades y reejecutá: la proporción debe seguir a la tabla cuando N es grande.

import random
import bisect

random.seed(5)
valores = [1, 2, 3, 4, 5, 6]
probs = [0.10, 0.20, 0.30, 0.25, 0.10, 0.05]

# Preproceso: acumuladas (forzar último = 1.0).
acum = []
s = 0.0
for p in probs:
    s += p
    acum.append(s)
acum[-1] = 1.0

def discreta():
    u = random.random()
    k = bisect.bisect_left(acum, u)
    return valores[k]

muestra = [discreta() for _ in range(6000)]
for v in valores:
    print(v, round(muestra.count(v) / len(muestra), 3))

Probá cambiar probs a seis veces 1/6: todas las frecuencias deben rondar 0,167. Volvé a la cargada y verificá que el 3 domine (~0,30).

Versión lineal didáctica (equivale, más lenta)

Útil para entender el recorrido intervalo por intervalo antes de usar bisect:

import random

random.seed(5)
valores = [1, 2, 3, 4, 5, 6]
probs = [0.10, 0.20, 0.30, 0.25, 0.10, 0.05]

def discreta_lineal():
    u = random.random()
    acc = 0.0
    for v, p in zip(valores, probs):
        acc += p
        if u < acc:
            return v
    return valores[-1]  # resguardo ante redondeo

print([discreta_lineal() for _ in range(10)])

06 · Exploración

Laboratorio: dado justo frente a dado cargado

Elegí la distribución y el N. Las barras muestran la frecuencia observada; los rombos amarillos, la probabilidad teórica. Con N grande las barras se pegan a los rombos; con N chico, el ruido domina.

EXPERIMENTO 02

Inversión discreta

X = mín{x : F(x) ≥ U}

Los resultados numéricos aparecen debajo.
Valor más frecuente3
Error máx. |obs − teo|0,000
Chi-cuadrado aprox.0,00
N2 000

Con N 2000, el histograma sigue a la tabla teórica.

Seis barras para {1,…,6}. El rombo marca la probabilidad teórica de cada cara. Probá N = 100 y luego N = 6000 para ver la convergencia.

Preguntas para explorar

  1. Con distribución cargada y N = 2000, ¿qué cara domina y a qué probabilidad teórica corresponde?
  2. Cambiá a justa con N = 100. ¿Alguna cara se desvía más de 0,05? ¿Es sesgo del método o ruido?
  3. Si U = 0,73 con la tabla cargada, ¿qué valor devuelve el algoritmo? Recorre las acumuladas a mano.
Ver respuestas sugeridas
  1. El 3, con p = 0,30 (acumulada 0,60). La barra del 3 debe ser la más alta, cerca de 0,30.
  2. Es ruido muestral: con N = 100 el error típico es del orden 1/√N ≈ 0,10. Al subir a 6000 se achica.
  3. 4, porque 0,60 < 0,73 ≤ 0,85 (intervalo del 4).

07 · Comprensión

Confusiones frecuentes

«Uso round() para llevar el uniforme al entero»

round(k·U) da n+1 valores con los extremos a mitad de probabilidad. Para {a,…,b} usá a + int((b−a+1)·U) con piso, no redondeo.

«Normalizo a ojo, total suma casi 1»

“Casi 1” deja un resto sin asignar o con doble asignación en el borde. Normalizá dividiendo por la suma y forzá la última acumulada a 1,0.

«Lineal siempre alcanza»

Con 6 caras sí; con 10 000 categorías y 10 millones de muestras, O(n) es prohibitivo. Ahí van binaria o alias (Temas 13–14, 39).

«Si el histograma se parece, ya está validado»

El ojo engaña con N chico. Complementá con error máximo y chi-cuadrado (Tema 38): el laboratorio ya los calcula.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: demanda con tabla dada

Demanda en {0,1,2,3} con probs [0,2 · 0,5 · 0,2 · 0,1]. Generá 5000 valores con semilla 9 y estimá P(demanda ≥ 2).

import random
import bisect

random.seed(9)
valores = [0, 1, 2, 3]
probs = [0.2, 0.5, 0.2, 0.1]
acum = []
s = 0.0
for p in probs:
    s += p
    acum.append(s)
acum[-1] = 1.0

muestra = [valores[bisect.bisect_left(acum, random.random())] for _ in range(5000)]
print(sum(1 for x in muestra if x >= 2) / len(muestra))
Ver solución razonada

La teórica es 0,2 + 0,1 = 0,30. La muestral debe caer en 0,28–0,32. Si da 0,5, probablemente contaste ≥ 1 en vez de ≥ 2.

Ejercicio 2: predecir antes de ejecutar

Con la tabla del dado cargado del laboratorio: si U = 0,05 · 0,30 · 0,60 · 0,97, ¿qué valores salen? Después verificá con código.

import bisect

acum = [0.10, 0.30, 0.60, 0.85, 0.95, 1.0]
valores = [1, 2, 3, 4, 5, 6]
for u in [0.05, 0.30, 0.60, 0.97]:
    print(u, valores[bisect.bisect_left(acum, u)])
Ver solución

1, 2, 3 y 6. Notá los bordes: con bisect_left, U = 0,30 cae en el 2 (intervalo [0,10·0,30)) y U = 0,60 en el 3. La convención de borde debe ser consistente.

Ejercicio 3: detectar el sesgo del redondeo

Compará int(6U)+1 (correcto) contra round(5U)+1 o int(round(6U)) ingenuo con 20000 réplicas. ¿Qué caras se deforman?

Ver una posible respuesta
import random

random.seed(4)
N = 20000
bien = [1 + int(6 * random.random()) for _ in range(N)]
random.seed(4)
mal = [1 + round(5 * random.random()) for _ in range(N)]

def frec(m):
    return [round(m.count(v) / len(m), 3) for v in range(1, 7)]

print("bien:", frec(bien))
print("mal: ", frec(mal))

El método bien da seis ≈ 0,167. El ingenuo con round(5U) concentra masa en el centro y afina los extremos (o los deforma según la variante): el histograma lo delata y el chi-cuadrado se dispara.

09 · Síntesis

Ideas para recordar

  • Toda discreta se invierte con X = mín{x : F(x) ≥ U} sobre intervalos de largo pk.
  • El preproceso construye Fk una vez; el muestreo busca k con lineal o binaria.
  • Los bordes y la normalización exacta (Fn = 1) evitan valores perdidos.
  • Soportes infinitos se acumulan sobre la marcha o usan fórmula cerrada / rechazo.
  • Con n grande o N enorme, alias O(1) amortiza el preproceso.

En el próximo tema pasaremos al continuo: densidades, acumuladas lisas y la transformada inversa continua.