Números Pseudoaleatorios · Tema 19

Generación de números uniformes

Del entero del generador al decimal U(0,1): dividir por m, respetar el [0,1) y entender la resolución 1/m.

01 · Punto de partida

El generador escupe enteros, vos querés decimales

Todo generador de los Temas 12 a 18 entrega enteros: un LCG da 0 … m−1, un xorshift32 da 0 … 2³²−1. Pero la simulación habla en probabilidades, proporciones y si u < p: necesita decimales en [0,1) con densidad pareja. El puente es una división, y si la hacés mal introducís el primer sesgo de tu simulación.

Este tema cierra esa brecha: la receta canónica u = x / m, qué valores puede tomar realmente y por qué el 1 queda afuera.

  • ¿Por qué se divide por m y no por m−1?
  • ¿Cuántos decimales distintos puede dar un generador con módulo m?
  • ¿Puede salir exactamente 0 o exactamente 1?
  • ¿Por qué un double necesita 53 bits y no 32?

02 · Definición

U(0,1): pareja y sin bordes tramposos

◈

Normalizar

u = x / m en flotante. Cada entero tiene chance 1/m: la uniformidad discreta se hereda al decimal.

⇄

Resolución

Con módulo m solo existen m decimales, separados 1/m. Con m=16 hay “escalones” de 0,0625; con 2³² son invisibles.

▣

Bordes [0,1)

Cerrado en 0, abierto en 1: x=0 → u=0 posible, u=1 imposible. Así int(u*k) da 0 … k−1 parejo.

Qué sale según el divisor.
RecetaRango realVeredicto
u = x / m[0,1): 0 sí, 1 nuncaCanónica: usar siempre
u = x / (m−1)[0,1]: el 1 sale con prob. 1/mRota int(u*k) y log(u)
u = (x+0.5) / mNunca da 0 ni se pega al bordeSolo si un método exige (0,1) abierto

03 · Precisión real

32 bits no llenan un double

Un float64 guarda 53 bits de mantisa: puede distinguir múltiplos de 2⁻⁵³ ≈ 1,1e−16. Un solo uint32 solo distingue múltiplos de 2⁻³² ≈ 2,3e−10: deja 2²¹ “huecos” vacíos entre cada par de valores. Por eso las librerías serias combinan dos enteros de 32 bits en uno de 53.

53 bits (CPython / NumPy)
u = (a·2²⁷ + b·2⁻²⁶…) / 2⁵³: con a de 27 bits y b de 26 bits se cubre toda la mantisa del double.
División entera accidental
En Python 2 o en C con enteros, x / m trunca a 0. Forzar flotante: x / float(m) o x / m en Python 3.
El 0 exacto
u = 0 sale con prob. 1/m: métodos como -log(u) deben mapearlo a (0,1] con 1−u (Tema 20).

Bien normalizado

u = x / m en float

m valores parejos en [0,1), media ≈ 0,5 y P(u < p) = p salvo resolución. Base de todo lo que sigue.

Mal normalizado

Dividir por m−1

El 1 aparece 1/m de las veces: int(u*6) favorece al 6 y u==1 rompe logaritmos y divisiones.

04 · Ejemplos

Del módulo al decimal

Entero 0…m−1→÷ m en float→u en [0,1)
  1. 1
    LCG m = 16.

    Solo 16 decimales: 0, 0,0625, 0,125, …, 0,9375. Ideal para ver los “escalones” en el laboratorio.

  2. 2
    xorshift32 / MT.

    u = x / 2**32: 4294967296 puntos, escalón 2,3e−10. A ojo, continuo.

  3. 3
    Double de 53 bits.

    CPython combina 27 + 26 bits: escalón 1,1e−16, toda la mantisa cubierta sin huecos.

05 · Implementación en Python

Normalizar sin sesgo

Tomamos el LCG didáctico (a=5, c=3, m=16) y lo convertimos: mirá cómo los 16 valores se repiten cíclicamente y nunca tocan el 1.

Python en tu navegador. Cambiá el divisor a m−1 y observá cómo aparece el 1 y se rompe el rango.

def lcg_uniformes(semilla, n=16, a=5, c=3, m=16):
    x = semilla % m
    sal = []
    for _ in range(n):
        x = (a * x + c) % m
        sal.append(x / m)
    return sal


us = lcg_uniformes(7)
print([round(u, 4) for u in us])
print("min:", min(us), "max:", max(us), "distintos:", len(set(us)))

16 valores distintos como máximo, entre 0 y 0,9375. El 1 no aparece jamás: eso es [0,1).

53 bits como CPython

import random

# un uint32 solo llena 32 de los 53 bits del double
x32 = random.getrandbits(32) / 2**32
# receta real de CPython: 27 + 26 bits
a = random.getrandbits(27)
b = random.getrandbits(26)
u53 = (a * 67108864 + b) / 9007199254740992
print(round(x32, 12), round(u53, 12))
print("escalon 32 bits:", 2**-32, "escalon 53 bits:", 2**-53)

06 · Exploración

Laboratorio: la resolución se ve

Elegí el módulo del LCG testigo y la cantidad de valores. La curva es u(n); con m chico verás bandas discretas (solo m alturas posibles). El panel mide resolución 1/m, distintos y si aparece el 1.

EXPERIMENTO 19

Escalones de 1/m

u = x / m en [0,1)

Los resultados numéricos aparecen debajo.
Primer u—
Distintos—
Resolución 1/m—
¿Llega a 1?—

Con m = 16 solo hay 16 alturas posibles.

Con m = 16 la cuadrícula es evidente; con m = 65536 parece continua, pero sigue siendo discreta con paso 1/65536.

Preguntas para explorar

  1. Con m = 16 y N = 240: ¿cuántos distintos hay como máximo? ¿Qué pasa al pedir más valores que el módulo?
  2. ¿Alguna configuración llega a u = 1? ¿Y a u = 0?
  3. Si tu regla es “aprobar con u < 0,001”, ¿qué módulos la hacen imposible o dentada?
Ver respuestas sugeridas
  1. 16 como máximo: pasado el período se repiten (Tema 10). Pedir 240 es mirar el mismo ciclo 15 veces.
  2. El 1 nunca; el 0 sí cuando x = 0. Dividir por m−1 “crearía” un 1 falso con prob. 1/m.
  3. Con m = 16 el umbral 0,001 cae dentro del primer escalón: o nunca ocurre o sale con prob. 1/16, nunca 1/1000. Necesitás m » 1000.

07 · Comprensión

Confusiones frecuentes

«Divido por m−1 para aprovechar hasta el 1»

El 1 no debe salir: rompe int(u*k), u**p con p chico y todo log(u). El rango correcto es [0,1), no [0,1].

«En C me da siempre 0»

División entera: x / m con dos enteros trunca. Convertí primero: (double)x / m; en Python 3 ya es flotante.

«Imprimo 16 decimales, tengo precisión 1e−16»

Imprimir no crea resolución: con m = 2³² el paso real es 2,3e−10 aunque imprimas 16 cifras. Los ceros del final son relleno.

«El 0 nunca sale, lo ignoro»

Sale con prob. 1/m y rompe -log(u)/λ con división por cero o infinito. Se trata en el Tema 20 con 1−u.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: el rango prometido

Generá 2000 uniformes del LCG m = 16 normalizado y verificá que están en [0,1): mínimo ≥ 0, máximo < 1.

def lcg_u(semilla, n, a=5, c=3, m=16):
    x = semilla % m
    for _ in range(n):
        x = (a * x + c) % m
        yield x / m


us = list(lcg_u(7, 2000))
print(min(us), max(us), all(0 <= u < 1 for u in us))
Ver solución razonada

Mínimo 0,0 y máximo 0,9375 con True: el máximo teórico es (m−1)/m. Si dividieras por m−1 verías un 1,0 intruso.

Ejercicio 2: contar la resolución

¿Cuántos valores distintos puede dar x / 256? Comprobalo generando 5000 y contando el conjunto.

def lcg_u256(semilla, n):
    x = semilla % 256
    for _ in range(n):
        x = (1103515245 * x + 12345) % 256
        yield x / 256


print(len(set(lcg_u256(1, 5000))))
Ver solución

256 como máximo (menos si el período es menor). La resolución es 1/256 ≈ 0,0039: dos umbrales más cercanos que eso son indistinguibles.

Ejercicio 3: el 0 que rompe el log

Forzá u = 0 con semilla adecuada y mostrá que 1−u lo rescata para -log(u).

Ver una posible respuesta
import math

# con m=16, semilla 5 da x=0 en el primer paso: u=0
x = (5 * 5 + 3) % 16
u = x / 16
print("u:", u)
print("1-u:", 1 - u, "-log(1-u):", round(-math.log(1 - u), 6))

u = 0 existe y log(0) explota; 1−u = 1 sigue siendo uniforme en (0,1] y mantiene la distribución. Detalle completo en el Tema 20.

09 · Síntesis

Ideas para recordar

  • Normalizar es u = x / m en flotante: m valores en [0,1).
  • Resolución 1/m: el 0 sale, el 1 nunca; no imprimir crea precisión.
  • Un uint32 no llena un double: hacen falta 53 bits (27 + 26).
  • Dividir por m−1 o truncar con enteros son los dos sesgos clásicos.
  • Todo lo que sigue (intervalos, enteros, sorteos) cuelga de este u.

En el próximo tema haremos zoom en el borde: la transformación al intervalo [0,1), el 0 exacto y el mapeo a (0,1].