01 · Punto de partida
El bug nº 1 de la simulación
Los Temas 22 y 34 dieron la teoría (floor, rechazo, firmas). Este tema es el taller mecánico: cada error con su código roto, su síntoma medible y su arreglo de una línea. Si solo recordás una tabla de este curso, que sea la de abajo: cubre el 90 % de los sorteos viciados que verás en código real.
Los tres pozos: módulo con resto, redondeo en vez de piso, y bordes (uno de más, uno de menos, el signo).
- ¿Qué rompe cada pozo y con qué síntoma?
- ¿Cómo queda la línea arreglada en cada caso?
- ¿Qué pasa con negativos y con rangos gigantes?
- ¿Cómo audito mis enteros en 30 segundos?
02 · Definición
Los tres pozos
Módulo
x % k con resto r = m % k favorece r valores. Solo vale si r = 0; si no, rechazo (Tema 22).
Redondeo
round da medios a los bordes; int() trunca hacia cero y rompe negativos. Solo floor parte parejo.
Bordes
randint incluye ambos; randrange excluye el tope; int(u·k) jamás da k. Un ±1 cambia el sorteo.
| Roto | Síntoma | Arreglo |
|---|---|---|
rand() % 6 + 1 (resto) | 4 caras en 0,17 | randint(1,6) o rechazo |
round(u*6)+1 | 1 y 6 en 0,083 + sietes | floor(u*6)+1 |
randint(0, len) | IndexError 1/(n+1) | randrange(len) / choice |
int(u*k) con u<0 | Hacia cero, asimétrico | floor (hacia −∞) |
03 · Casos especiales
Negativos, gigantes y lenguajes
Los pozos se hunden más con rangos negativos (int(-2,5) = -2, no −3: el −3 sale la mitad) y con k gigante (k > 2³²: un uint32 no cubre; hacen falta 64 bits o rechazo sobre fuente amplia).
- Negativos: floor, no int
math.floor(-0,5) = -1peroint(-0,5) = 0:int()trunca hacia cero y desbalancea. Con u ≥ 0 da igual; con desplazamientos negativos, solo floor.- k gigante
- Sortear en
0 … 10¹⁸con fuente de 32 bits deja 99,9 % del rango inalcanzable: usargetrandbits(60)+ rechazo, o la API del lenguaje (randrangeya lo hace). - Cada lenguaje, su convención
- Python
randintincluye; Crand() % nsesga y encima los bits bajos ciclan corto; JSMath.floor(Math.random()*n)es la forma (no crypto, Tema 18).
Bien programado
API + auditoría
randint/randrange/choice, conteo de min/max/distintos y χ² rápido. El bug no llega a producción.
Mal programado
Aritmética a mano
%, round, +1 de más o de menos: cada línea manual es un pozo potencial sin auditoría.
04 · Ejemplos
Tres caídas al pozo
- 1Dado con
%.randrange(100) % 6 + 1: resto 4, χ² ≈ 25. Arreglo:randint(1, 6). - 2Índice con
len.randint(0, len(xs)):IndexError1/7. Arreglo:choice(xs). - 3Negativo con
int.int(u·k)tras desplazar mal: el negativo extremo sale la mitad. Arreglo:floor.
05 · Implementación en Python
Roto y arreglado lado a lado
Cinco dados con N = 12000: el sano y cuatro pozos. El χ² (crítico 11,07) y los bordes los ordenan de un vistazo.
Python en tu navegador. Cambiá la semilla: los sanos pasan siempre, los rotos fallan siempre.
import math
import random
def chi6(obs):
e = sum(obs) / 6
return round(sum((o - e) ** 2 / e for o in obs), 2)
rng = random.Random(35)
N = 12000
us = [rng.random() for _ in range(N)]
xs = [rng.randrange(100) for _ in range(N)]
dados = {
"sano": [rng.randint(1, 6) for _ in range(N)],
"mod": [(x % 6) + 1 for x in xs],
"round": [1 + round(u * 6) for u in us],
}
for nombre, d in dados.items():
obs = [d.count(c) for c in range(1, 7)]
print(nombre, "min/max:", min(d), max(d), "chi2:", chi6(obs))
sano χ² ≈ 5 con 1…6; mod ≈ 12+ con resto; round con sietes (max 7) y χ² de cientos.
Negativos: int frente a floor
import math
vals = [-2.7, -2.5, -2.2, 2.2, 2.5, 2.7]
print("int: ", [int(v) for v in vals])
print("floor:", [math.floor(v) for v in vals])
06 · Exploración
Laboratorio: los pozos en barras
Cinco dados con N hasta 12000: sano, % con resto, round (con sietes fuera de rango), off-by-one que nunca da 6, y truncado con negativos que hunde una cara. El panel trae χ², rango y veredicto.
Cinco pozos, un sano
min · max · χ²
Sano con N = 6000: 1…6 parejos.
Barras en 0 donde debía haber 1/6 = borde perdido. Barras naranjas = fuera de banda E ± 2√E.
Preguntas para explorar
- Con off-by-one: ¿qué cara falta siempre? ¿Qué χ² sale con N = 12000?
- Con round: ¿dónde van los sietes? ¿El χ² sobre 1…6 los ve?
- Con N = 1200 en el pozo %: ¿siempre rechaza? ¿Qué enseña sobre potencia?
Ver respuestas sugeridas
- Falta el 6 (rango 1…5): χ² ≈ miles. Ni el mejor generador salva un rango mal pedido.
- Fuera del dado (cara 7 fantasma ≈ 8 %): el χ² sobre 1…6 ya explota por los bordes hundidos; el min/max los delata directo.
- No siempre: con N chico el resto se esconde en el ruido. Los pozos se juzgan con N grande (Tema 34).
07 · Comprensión
Confusiones frecuentes
«% es inocuo con números grandes»
El resto manda, no el tamaño: m = 2³² con k = 10⁶ deja resto 967296 (0,02 % de ventaja sistemática). Inocuo solo si r = 0 o hay rechazo.
«int() es floor para enteros»
Solo con argumentos ≥ 0. Con negativos trunca hacia cero: int(-1,5) = -1 ≠ floor = -2. En rangos con negativos, floor.
«randint y randrange son lo mismo»
randint(1,6) da 1…6; randrange(1,6) da 1…5. Un carácter de diferencia, un 6 ausente, χ² ≈ miles.
«Si el rango es grande no necesito 64 bits»
Sortear en 0…10¹⁸ con 32 bits deja el 99,9 % inalcanzable aunque el χ² sobre una muestra no lo note. Cobertura ≠ uniformidad parcial.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: cazar el ausente
Generá 6000 con randrange(1, 6) por error y demostrá con min/max que el 6 falta.
import random
rng = random.Random(8)
dados = [rng.randrange(1, 6) for _ in range(6000)]
print(min(dados), max(dados), sorted(set(dados)))
Ver solución razonada
1 5 [1, 2, 3, 4, 5]: el 6 jamás sale. El auditor de bordes lo caza en 1 segundo; el χ² sobre 1…5 encima “pasaría”.
Ejercicio 2: int contra floor en negativos
Generá enteros en −3…3 de dos formas y compará la frecuencia del −3.
import math
import random
rng = random.Random(8)
N = 12000
us = [rng.random() for _ in range(N)]
con_int = [int(u * 7) - 3 for u in us]
con_floor = [math.floor(u * 7) - 3 for u in us]
print("int: ", round(con_int.count(-3) / N, 4))
print("floor:", round(con_floor.count(-3) / N, 4))
Ver solución
Ambos dan −3…3 aquí (u ≥ 0 los iguala): la diferencia aparece al desplazar antes de truncar. Moraleja: floor siempre, int nunca para este uso.
Ejercicio 3: auditor de 30 segundos
Escribí auditar_enteros(dados, low, high) con min/max/distintos/χ² y probalo con un dado sano y uno roto.
Ver una posible respuesta
import random
def auditar_enteros(dados, low, high):
k = high - low + 1
e = len(dados) / k
obs = [dados.count(c) for c in range(low, high + 1)]
chi = sum((o - e) ** 2 / e for o in obs)
return min(dados), max(dados), len(set(dados)), round(chi, 2)
rng = random.Random(8)
sano = [rng.randint(1, 6) for _ in range(6000)]
roto = [rng.randrange(1, 6) for _ in range(6000)]
print("sano:", auditar_enteros(sano, 1, 6))
print("roto:", auditar_enteros(roto, 1, 6))
Sano ≈ (1, 6, 6, χ² chico); roto con max 5 y χ² gigante. Pegalo en cada proyecto que sortee enteros.
09 · Síntesis
Ideas para recordar
- Módulo: solo con resto 0; si no, rechazo.
- Redondeo: floor parte parejo; round e int truncan mal.
- Bordes: k = high−low+1; randint incluye, randrange excluye.
- Negativos → floor; gigantes → 64 bits o API.
- Auditoría: min, max, distintos y χ² en 30 segundos.
En el próximo tema aseguramos la ciencia: reproducibilidad de experimentos con semillas y versiones.