Números Pseudoaleatorios · Tema 22

Generación de enteros aleatorios

Rangos discretos sin trampas: floor parejo, el sesgo de % k y el rechazo que lo elimina.

01 · Punto de partida

El continuo se pisa, el entero se cuenta

El Tema 21 te dio continuos en [a,b). Pero un dado no da 3,7: da 1 … 6 exactos, cada uno con prob. 1/6. Pasar del decimal al entero con int() o % a ojo rompe esa equiprobabilidad: un valor queda con el doble de chance o un borde desaparece, y el sorteo queda viciado.

Este tema fija la receta discreta: low + floor(u·k) con k valores, y el muestreo con rechazo cuando el módulo no divide parejo.

  • ¿Por qué int(u·6)+1 vale y round(u·6)+1 no?
  • ¿Cuándo x % k favorece a los primeros valores?
  • ¿Cómo elimino el sesgo descartando pocos valores?
  • ¿randint incluye ambos extremos?

02 · Definición

Uniforme discreta: k valores, prob. 1/k

◈

Floor parejo

floor(u·k) parte [0,1) en k tercios exactos de ancho 1/k: cada entero hereda prob. 1/k. round daría medios tercios en bordes.

⇄

k valores

En [low,high] ambos incluidos hay k = high−low+1 valores. El +1 es el off-by-one más caro del curso.

▣

Sesgo módulo

Si m = q·k + r, los primeros r valores tienen q+1 preimágenes y el resto q: salen (q+1)/m frente a q/m.

Receta según tu punto de partida.
Partís deReceta parejaNotas
u en [0,1)low + floor(u*k)Canónica; int() vale porque u ≥ 0
Entero 0 … m−1, m % k == 0low + (x % k)Solo si divide exacto
Entero 0 … m−1, m % k != 0Rechazar x ≥ m − (m % k)Descarta r valores; resto parejo

03 · Muestreo con rechazo

Descartar poco para repartir parejo

Ejemplo mínimo: fuente 0 … 9 (m = 10) a dado k = 6. 10 = 1·6 + 4: con % 6 los valores 0–3 salen 2/10 y el 4–5 salen 1/10. Si descartás x ≥ 6 (los r = 4 sobrantes) y repetís, los 6 restantes quedan con 1/6 exacto. Costo: descartás r/m = 40 % aquí; con m = 2³² y k = 6 descartás 4/2³² ≈ 0 %.

Umbral
limite = m − (m % k): aceptás x < limite, das x % k; si no, pedís otro entero. Nunca “re-mapeés” el descartado con otro módulo.
Costo esperado
Intentos por valor = m / limite ≤ 2 si k ≤ m/2 (siempre en la práctica). El bucle termina casi siempre al primer intento.
Python ya lo hace
randint / randrange / choice usan rechazo interno (_randbelow); secrets.randbelow también. El % a mano es el que sesga.

Bien discreto

floor o rechazo

Cada cara 1/k exacto, bordes incluidos, auditor chi-cuadrado (Tema 29) en verde.

Mal discreto

round o % crudo

round da medios a los bordes; % con resto favorece a los primeros. Sorteo viciado y detectable.

04 · Ejemplos

Tres discretos de todos los días

u o x→floor / rechazo→low…high
  1. 1
    Dado 1…6.

    1 + floor(u·6). Cada cara 1/6; el 6 sale, el 7 jamás.

  2. 2
    Índice 0…n−1.

    floor(u·n). Base de choice(lista) y del Tema 23.

  3. 3
    Sorteo 0…999 con fuente m = 65536.

    65536 % 1000 = 536: con % crudo 536 valores salen de más; con rechazo se descartan 536/65536 ≈ 0,8 %.

05 · Implementación en Python

Contar para creer

Generamos 6000 dados de tres formas y contamos: randint y floor empatan; el % sobre fuente chica se delata solo.

Python en tu navegador. Subí N a 60000 y el sesgo del % se vuelve sistemático, no “mala suerte”.

import math
import random

rng = random.Random(7)
N = 6000
dados_floor = [1 + math.floor(rng.random() * 6) for _ in range(N)]
rng = random.Random(7)
dados_int = [rng.randint(1, 6) for _ in range(N)]
for nombre, dados in [("floor", dados_floor), ("randint", dados_int)]:
    frec = [dados.count(c) / N for c in range(1, 7)]
    print(nombre, [round(f, 4) for f in frec])

Seis frecuencias ≈ 0,1667 en ambas: dos caminos parejos al mismo dado.

El % crudo con fuente 0…9

import random

rng = random.Random(7)
N = 6000
fuente = [rng.randrange(10) for _ in range(N)]   # 0 ... 9
crudo = [1 + (x % 6) for x in fuente]


def con_rechazo(xs, m=10, k=6):
    lim = m - (m % k)
    sal = []
    it = iter(xs)
    while len(sal) < N:
        try:
            x = next(it)
        except StopIteration:
            xs.extend(rng.randrange(10) for _ in range(N))
            it = iter(xs[-N:])
            continue
        if x < lim:
            sal.append(1 + (x % k))
    return sal


fijo = con_rechazo(fuente)
for nombre, dados in [("mod-crudo", crudo), ("rechazo", fijo)]:
    print(nombre, [round(dados.count(c) / len(dados), 4) for c in range(1, 7)])

06 · Exploración

Laboratorio: el sesgo se ve en barras

Fuente fija 0 … 99 (m = 100) a un dado de k caras. Con k = 6, 100 % 6 = 4: el % crudo favorece a 4 caras. Cambiá de método y de k y mirá cómo el rechazo aplana las barras a la línea esperada 1/k.

EXPERIMENTO 22

Barras parejas o viciadas

floor(u·k) vs x % k

Los resultados numéricos aparecen debajo.
Esperada 1/k—
Máx desvío—
Descartados—
Veredicto—

Con k = 6 y % crudo, 4 caras salen de más.

La línea punteada es 1/k. Barras que la superan sistemáticamente delatan sesgo, no azar.

Preguntas para explorar

  1. Con k = 10 (100 % 10 = 0): ¿el % crudo sesga? ¿Y con k = 6?
  2. Con k = 6 y rechazo: ¿cuántos valores se descartan en %? ¿Coincide con 4/100?
  3. Subí N a 12000 con % y k = 6: ¿el desvío se achica o persiste?
Ver respuestas sugeridas
  1. Con k = 10 no hay resto: % es parejo. Con k = 6 el resto 4 vicia 4 caras (las 4 primeras).
  2. ≈ 4 % descartados: el precio de la justicia. Con m = 2³² sería ≈ 0 %.
  3. Persiste: el sesgo es sistemático (2/100 vs 1/100 por cara extra), no se diluye con N como el ruido (Tema 1).

07 · Comprensión

Confusiones frecuentes

«Uso round(u·6)+1 para el dado»

round da medios intervalos al 1 y al 6 (≈ 1/12) y enteros al resto (≈ 1/6): dado cargado contra los bordes.

«int(u·6) da 0…6»

Da 0…5 (el 6 exigiría u = 1, imposible). Para 1…6 hay que sumar 1; para 0…6 harían falta 7 valores (k = 7).

«% vale siempre, el resto es despreciable»

Con m = 2³² y k chico sí es despreciable; con m = 100 y k = 6 es 4 % de ventaja. La regla es m % k, no la intuición.

«randint(1,6) es como randrange(1,6)»

No: randint incluye el 6 (k = 6), randrange lo excluye (k = 5). Un sorteo “1…6” con randrange jamás da 6.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: dado a mano

Implementá dado(rng) con floor y verificá que solo da 1…6 en 1000 tiros.

import math
import random

def dado(rng):
    return 1 + math.floor(rng.random() * 6)


rng = random.Random(5)
tiros = [dado(rng) for _ in range(1000)]
print(min(tiros), max(tiros), len(set(tiros)))
Ver solución razonada

1 6 6: mínimo 1, máximo 6 y 6 valores distintos. Si sumaras mal el 1 verías 0 5.

Ejercicio 2: medir el sesgo módulo

Con fuente 0 … 99 y k = 6, contá frecuencias con % crudo y comprobá que 4 caras salen ≈ 0,17 y 2 salen ≈ 0,16.

import random

rng = random.Random(5)
N = 12000
xs = [rng.randrange(100) for _ in range(N)]
dados = [(x % 6) + 1 for x in xs]
print([round(dados.count(c) / N, 4) for c in range(1, 7)])
Ver solución

Cuatro caras ≈ 0,17 (17/100) y dos ≈ 0,16 (16/100): el resto 100 % 6 = 4 reparte una preimagen extra a las 4 primeras.

Ejercicio 3: rechazo correcto

Escribí entero_par(k, m, rng) con rechazo y usalo para sortear 0…999 con fuente amplia. Verificá rango y cantidad de intentos.

Ver una posible respuesta
import random

def entero_par(k, m, rng):
    lim = m - (m % k)
    intentos = 0
    while True:
        intentos += 1
        x = rng.randrange(m)
        if x < lim:
            return (x % k), intentos


rng = random.Random(5)
vals = [entero_par(1000, 65536, rng)[0] for _ in range(2000)]
print(min(vals), max(vals), len(set(vals)) > 900)

Rango 0…999 con cientos de valores distintos y ≈ 1,008 intentos por valor (536/65536 descartados). Justicia barata.

09 · Síntesis

Ideas para recordar

  • Discreto en k valores: low + floor(u·k); k = high−low+1 con ambos incluidos.
  • round carga los bordes a la mitad; int trunca como floor solo si u ≥ 0.
  • % k solo vale si m % k == 0; si no, rechazo con lim = m − (m % k).
  • randint incluye b, randrange lo excluye: leer el +1.
  • El sesgo es sistemático: no se diluye con N, se detecta contando (Tema 29).

En el próximo tema elegiremos dentro de listas: selección aleatoria de elementos con y sin sesgo.