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 … mo0 … ma [0,1) sin sesgo? - ¿Qué hago si mi método no tolera el 0 (log, división)?
- ¿Por qué
rand() / RAND_MAXen 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.
| El generador da | Mapeo a [0,1) | Notas |
|---|---|---|
0 … m−1 | u = x / m | Caso canónico (LCG, MT, PCG) |
1 … m | u = (x−1) / m | Desplazar 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 − u | Misma 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
ues U[0,1),v = 1−ues 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 aRAND_MAX: dividir porRAND_MAXfabrica un 1 con prob. 1/(RAND_MAX+1). El divisor correcto esRAND_MAX + 1.0en double. - Clamp correcto
- Si temés el redondeo a 1,0, recortá con
u = min(u, nextafter(1.0, 0.0)), nunca conmin(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
- 1C
rand().Da
0 … RAND_MAX:u = rand() / (RAND_MAX + 1.0). Con/ RAND_MAXel 1 sale yint(u*6)favorece al 6. - 2Dado
1 … 6a [0,1).No es uniforme fino, pero ilustra:
u = (d−1) / 6da 6 puntos parejos sin fugas. - 3Exponencial con log.
uen [0,1) incluye 0 que rompe-log(u): se alimenta1−uen (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.
Mapeos a [0,1)
origen × mapeo → bordes
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
- Con origen 0…16 y mapeo correcto: ¿cuál es el máximo? ¿Y con el bug
/(m−1)? - Activá el espejo 1−u: ¿cuántos ceros quedan? ¿Por qué es el preferido para
-log? - Con origen 1…16 y mapeo correcto: ¿sale el 0? ¿Sigue siendo [0,1)?
Ver respuestas sugeridas
- Correcto:
16/17 ≈ 0,941, sin fugas. Bug: varios valores ≥ 1 (hasta 1,066): totalmente fuera de contrato. - Cero ceros: el 0 se vuelve 1 y el resto queda en (0,1]. Por eso
-log(1−u)nunca divide por cero. - 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)yu < p. - Cada convención entera tiene su mapeo: desplazar o reescalar antes de dividir.
- En C, el divisor es
RAND_MAX + 1.0, nuncaRAND_MAX. - Si el método no tolera el 0, alimentar
1−uen (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.