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)+1vale yround(u·6)+1no? - ¿Cuándo
x % kfavorece a los primeros valores? - ¿Cómo elimino el sesgo descartando pocos valores?
- ¿
randintincluye 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.
| Partís de | Receta pareja | Notas |
|---|---|---|
u en [0,1) | low + floor(u*k) | Canónica; int() vale porque u ≥ 0 |
Entero 0 … m−1, m % k == 0 | low + (x % k) | Solo si divide exacto |
Entero 0 … m−1, m % k != 0 | Rechazar 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ásx < limite, dasx % k; si no, pedís otro entero. Nunca “re-mapeés” el descartado con otro módulo.- Costo esperado
- Intentos por valor =
m / limite ≤ 2sik ≤ m/2(siempre en la práctica). El bucle termina casi siempre al primer intento. - Python ya lo hace
randint / randrange / choiceusan rechazo interno (_randbelow);secrets.randbelowtambié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
- 1Dado 1…6.
1 + floor(u·6). Cada cara 1/6; el 6 sale, el 7 jamás. - 2Índice 0…n−1.
floor(u·n). Base dechoice(lista)y del Tema 23. - 3Sorteo 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.
Barras parejas o viciadas
floor(u·k) vs x % k
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
- Con k = 10 (100 % 10 = 0): ¿el
%crudo sesga? ¿Y con k = 6? - Con k = 6 y rechazo: ¿cuántos valores se descartan en %? ¿Coincide con 4/100?
- Subí N a 12000 con
%y k = 6: ¿el desvío se achica o persiste?
Ver respuestas sugeridas
- Con k = 10 no hay resto:
%es parejo. Con k = 6 el resto 4 vicia 4 caras (las 4 primeras). - ≈ 4 % descartados: el precio de la justicia. Con m = 2³² sería ≈ 0 %.
- 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. roundcarga los bordes a la mitad;inttrunca como floor solo si u ≥ 0.% ksolo vale sim % k == 0; si no, rechazo conlim = m − (m % k).randintincluye b,randrangelo 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.