Números Pseudoaleatorios · Tema 20

Transformación al intervalo [0,1)

Normalizar sin fugas: mapear cualquier convención entera a [0,1), tratar el 0 y el 1, y usar 1−u cuando el método lo exige.

01 · Punto de partida

Por qué [0,1) y no otro

El Tema 19 te dio la receta u = x / m. Ahora la pregunta es de contrato: toda librería (random.random(), numpy, el laboratorio del curso) promete [0,1): el 0 entra, el 1 no. Con ese contrato, int(u*k) reparte parejo, u < p ocurre con prob. p y ningún log(u) recibe un 1 fantasma por redondeo.

El problema es que los generadores reales no hablan todos el mismo idioma: C da 0 … RAND_MAX (el 1 incluido), otros dan 1 … m, y el redondeo float puede empujar un 0,9999… a 1,0. Este tema es el adaptador universal.

  • ¿Cómo convierto 1 … m o 0 … m a [0,1) sin sesgo?
  • ¿Qué hago si mi método no tolera el 0 (log, división)?
  • ¿Por qué rand() / RAND_MAX en C está mal?
  • ¿Cómo evito que el redondeo fabrique un 1,0?

02 · Definición

Cuatro intervalos, un elegido

◈

[0,1)

El canónico: 0 sí, 1 no. Lo prometen Python, NumPy y este curso. Compatible con int(u*k) y u < p.

⇄

(0,1]

Su espejo: 1−u. Para métodos con log o 1/u que no toleran el 0 pero sí el 1.

▣

(0,1) y [0,1]

El abierto excluye ambos bordes; el cerrado los incluye a los dos. Ninguno es canónico: solo aparecen como paso intermedio o bug.

Adaptador según lo que entrega tu generador.
El generador daMapeo a [0,1)Notas
0 … m−1u = x / mCaso canónico (LCG, MT, PCG)
1 … mu = (x−1) / mDesplazar antes de dividir
0 … RAND_MAX (C)u = x / (RAND_MAX + 1.0)x / RAND_MAX incluye el 1: bug clásico
Método exige (0,1]v = 1 − uMisma distribución, borde cambiado

03 · Bordes y flotante

El 0 se trata, el 1 se prohíbe

Con m finito el 0 sale con prob. 1/m y el 1 con prob. 0. Dos operaciones los ponen en riesgo: el redondeo (un valor a 1 − 2⁻⁵⁴ puede redondearse a 1,0 en double) y tus propias fórmulas (u*(m)/(m−1), min(1, …) “por seguridad”).

1−u
Si u es U[0,1), v = 1−u es U(0,1]: misma uniforme, bordes permutados. Es la forma correcta de alimentar -log (el 0 se vuelve 1, inofensivo).
RAND_MAX + 1.0
En C, rand() llega a RAND_MAX: dividir por RAND_MAX fabrica un 1 con prob. 1/(RAND_MAX+1). El divisor correcto es RAND_MAX + 1.0 en double.
Clamp correcto
Si temés el redondeo a 1,0, recortá con u = min(u, nextafter(1.0, 0.0)), nunca con min(u, 0.9999) (eso crea una masa puntual sesgada).

Bien adaptado

Desplazar y 1−u

Cada entero conserva prob. 1/m, el rango queda [0,1) exacto y el log recibe (0,1] sin cambiar la distribución.

Mal adaptado

Dividir por m−1 o sumar épsilon

Crea un 1 falso o desplaza toda la grilla: P(u < p) deja de ser p y los tests de uniformidad (Tema 27) lo detectan.

04 · Ejemplos

Tres adaptaciones reales

Entero fuente→Desplazar / reescalar→[0,1)
  1. 1
    C rand().

    Da 0 … RAND_MAX: u = rand() / (RAND_MAX + 1.0). Con / RAND_MAX el 1 sale y int(u*6) favorece al 6.

  2. 2
    Dado 1 … 6 a [0,1).

    No es uniforme fino, pero ilustra: u = (d−1) / 6 da 6 puntos parejos sin fugas.

  3. 3
    Exponencial con log.

    u en [0,1) incluye 0 que rompe -log(u): se alimenta 1−u en (0,1] y la exponencial sale intacta.

05 · Implementación en Python

Normalizar y verificar bordes

Convertimos las tres convenciones y auditamos: ningún valor puede ser 1, el mínimo puede ser 0, y 1−u nunca es 0.

Python en tu navegador. Cambiá el mapeo a x/(m−1) y mirá cómo se fuga el 1.

def a_intervalo(xs, modo="cero_m_1", m=16):
    if modo == "cero_m_1":
        return [x / m for x in xs]
    if modo == "uno_m":
        return [(x - 1) / m for x in xs]
    if modo == "cero_m":
        return [x / (m + 1) for x in xs]
    raise ValueError(modo)


base_a = list(range(16))       # 0 ... 15  (canonico)
base_b = list(range(1, 17))    # 1 ... 16
base_c = list(range(17))       # 0 ... 16  (tipo C con RAND_MAX=16)
for nombre, xs, modo, m in [("0..15", base_a, "cero_m_1", 16),
                            ("1..16", base_b, "uno_m", 16),
                            ("0..16", base_c, "cero_m", 16)]:
    us = a_intervalo(xs, modo, m)
    print(nombre, "max:", max(us), "min:", min(us), "hay 1:", any(u == 1 for u in us))

Tres convenciones, tres mapeos, cero fugas al 1. Ese es el contrato [0,1).

1−u para el logaritmo

import math

us = [0.0, 0.25, 0.5, 0.9375]   # incluye el 0 exacto
vs = [1 - u for u in us]
print("u :", us)
print("1-u:", vs, "min(1-u):", min(vs))
print([round(-math.log(v), 4) for v in vs])

06 · Exploración

Laboratorio: cazar la fuga al 1

Elegí la convención entera de origen y el mapeo. La curva es u(n) con la línea del 1 prohibido marcada en naranja. El panel cuenta cuántos 0 y cuántos 1 salen: el mapeo correcto da algún 0 y ningún 1.

EXPERIMENTO 20

Mapeos a [0,1)

origen × mapeo → bordes

Los resultados numéricos aparecen debajo.
Máximo—
Ceros exactos—
Unos (fugas)—
Veredicto—

Origen 0…15 con mapeo correcto: sin fugas.

La línea naranja es el 1 prohibido. Un solo punto tocándola invalida el mapeo para int(u*k) y logaritmos.

Preguntas para explorar

  1. Con origen 0…16 y mapeo correcto: ¿cuál es el máximo? ¿Y con el bug /(m−1)?
  2. Activá el espejo 1−u: ¿cuántos ceros quedan? ¿Por qué es el preferido para -log?
  3. Con origen 1…16 y mapeo correcto: ¿sale el 0? ¿Sigue siendo [0,1)?
Ver respuestas sugeridas
  1. Correcto: 16/17 ≈ 0,941, sin fugas. Bug: varios valores ≥ 1 (hasta 1,066): totalmente fuera de contrato.
  2. Cero ceros: el 0 se vuelve 1 y el resto queda en (0,1]. Por eso -log(1−u) nunca divide por cero.
  3. Sí: (1−1)/16 = 0. Desplazar conserva los 16 puntos parejos dentro de [0,1).

07 · Comprensión

Confusiones frecuentes

«rand() / RAND_MAX es uniforme en [0,1)»

No: es [0,1] con el 1 incluido. En simulaciones chicas el sesgo se esconde; en int(u*k) con millones de sorteos el último valor sale de más.

«Sumo 0,5/m para evitar el 0»

Desplazás toda la grilla a {0,5/m, …, 1−0,5/m}: ya no es [0,1) y P(u < p) se tuerce cerca de 0. Usá 1−u o rechazo.

«Recorto con min(u, 1)»

Apila masa en el 1 exacto: creás un átomo donde debía haber densidad cero. Si el redondeo preocupa, recortá a nextafter(1,0), no a 1.

«[0,1) y (0,1] son lo mismo en la práctica»

En media y varianza sí; en bordes no: uno tolera int(u*k) y el otro log. Elegí por método y convertí con 1−u.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: auditar un mapeo

Escribí un auditor que verifique el contrato [0,1) sobre 5000 valores: ningún 1, algún 0 posible, media ≈ 0,5.

import random

us = [random.random() for _ in range(5000)]
print("max:", max(us), "min:", min(us))
print("fugas al 1:", sum(1 for u in us if u >= 1))
print("media:", round(sum(us) / len(us), 4))
Ver solución razonada

Máximo < 1, cero fugas y media ≈ 0,5. Si tu generador propio falla el max < 1, el mapeo está mal aunque la media dé bien.

Ejercicio 2: el bug de C

Simulá rand() / RAND_MAX con RAND_MAX = 32767 y contá cuántas veces sale el 1 en 200000 sorteos. Luego corregilo con +1.0.

import random

R = 32767
mal = [random.randint(0, R) / R for _ in range(200000)]
bien = [random.randint(0, R) / (R + 1.0) for _ in range(200000)]
print("unos (mal):", sum(1 for u in mal if u == 1))
print("max (bien):", max(bien))
Ver solución

Unos (mal) ≈ 6 (1/32768 de las veces); max (bien) ≈ 0,99997 sin ningún 1. El bug es raro pero sistemático: justo el que arruina colas y sorteos.

Ejercicio 3: espejo para exponencial

Generá 5 exponenciales con -log(1−u) incluyendo el caso u = 0. ¿Qué pasaría con -log(u)?

Ver una posible respuesta
import math

us = [0.0, 0.2, 0.5, 0.7, 0.9375]
print([round(-math.log(1 - u), 4) for u in us])

Sale [0, 0,2231, 0,6931, 1,204, 2,7726] sin errores. Con -log(u) el primer valor sería infinito: el espejo es obligatorio.

09 · Síntesis

Ideas para recordar

  • [0,1) es el contrato: 0 sí, 1 nunca; habilita int(u*k) y u < p.
  • Cada convención entera tiene su mapeo: desplazar o reescalar antes de dividir.
  • En C, el divisor es RAND_MAX + 1.0, nunca RAND_MAX.
  • Si el método no tolera el 0, alimentar 1−u en (0,1].
  • Auditar bordes (max < 1, conteo de 0/1) es tan importante como la media.

En el próximo tema escalaremos ese [0,1) a cualquier rango: generación en intervalos arbitrarios [a,b) sin sesgos.