Números Pseudoaleatorios · Tema 35

Errores comunes con enteros

Módulo, redondeo y bordes: los tres pozos donde cae todo programador, con ejemplo roto y arreglo al lado.

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 frente a arreglado.
RotoSíntomaArreglo
rand() % 6 + 1 (resto)4 caras en 0,17randint(1,6) o rechazo
round(u*6)+11 y 6 en 0,083 + sietesfloor(u*6)+1
randint(0, len)IndexError 1/(n+1)randrange(len) / choice
int(u*k) con u<0Hacia cero, asimétricofloor (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) = -1 pero int(-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: usar getrandbits(60) + rechazo, o la API del lenguaje (randrange ya lo hace).
Cada lenguaje, su convención
Python randint incluye; C rand() % n sesga y encima los bits bajos ciclan corto; JS Math.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

API del lenguaje→Auditar bordes→χ² rápido
  1. 1
    Dado con %.

    randrange(100) % 6 + 1: resto 4, χ² ≈ 25. Arreglo: randint(1, 6).

  2. 2
    Índice con len.

    randint(0, len(xs)): IndexError 1/7. Arreglo: choice(xs).

  3. 3
    Negativo 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.

EXPERIMENTO 35

Cinco pozos, un sano

min · max · χ²

Los resultados numéricos aparecen debajo.
χ² (crit 11,07)—
Rango—
Ausentes (1…6)—
Veredicto—

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

  1. Con off-by-one: ¿qué cara falta siempre? ¿Qué χ² sale con N = 12000?
  2. Con round: ¿dónde van los sietes? ¿El χ² sobre 1…6 los ve?
  3. Con N = 1200 en el pozo %: ¿siempre rechaza? ¿Qué enseña sobre potencia?
Ver respuestas sugeridas
  1. Falta el 6 (rango 1…5): χ² ≈ miles. Ni el mejor generador salva un rango mal pedido.
  2. Fuera del dado (cara 7 fantasma ≈ 8 %): el χ² sobre 1…6 ya explota por los bordes hundidos; el min/max los delata directo.
  3. 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.