Números Pseudoaleatorios · Tema 23

Selección aleatoria de elementos

Elegir al azar en listas y arreglos: índice parejo con floor, choice sin sesgo y cuándo pedir ayuda a secrets.

01 · Punto de partida

Sortear no es señalar con el dedo

El Tema 22 te dio enteros parejos. Ahora esos enteros son posiciones: el ganador de una rifa, la carta que sale, la canción que suena. La regla no cambió (prob. 1/n por elemento), pero aparecen trampas nuevas: listas que se modifican entre sorteos, índices con % viciado y “el primero nunca sale” por off-by-one.

La receta es una sola línea —lista[floor(u·n)]— y dos decisiones: con o sin reemplazo (este tema es con reemplazo: el elegido puede volver a salir) y simulación o secreto.

  • ¿Por qué lista[int(u·n)] es parejo y lista[x % n] a veces no?
  • ¿Qué significa elegir con reemplazo?
  • ¿Cuándo choice y cuándo secrets.choice?
  • ¿Por qué no conviene mezclar para sacar un solo elemento?

02 · Definición

Índice parejo, elemento parejo

◈

Índice parejo

i = floor(u·n) en 0 … n−1: hereda el 1/n del Tema 22. El elemento es lista[i], sin tocar la lista.

⇄

Con reemplazo

Cada sorteo parte de la lista completa: el mismo puede salir dos veces seguidas. Sin reemplazo es muestreo (Tema 25).

▣

Ponderada ≠ pareja

Si unos pesan más (loot raro), ya no es 1/n: es choices(pesos). Este tema es equiprobable; la ponderada se menciona y se deriva.

Qué usar según el caso.
CasoRecetaNotas
Un elemento, simulaciónrandom.choice(lista)Índice floor interno, con reemplazo
Un elemento, con premiosecrets.choice(lista)Sin semilla, sin sesgo (Tema 18)
Varios con reemplazochoices(lista, k=r)Pueden repetirse; no es muestra (Tema 25)

03 · Técnica correcta

Floor sí, módulo a veces, mezcla no

choice hace floor(u·n) por dentro: nunca toca la lista y cuesta O(1). Las alternativas tienen costo o sesgo escondido.

Floor manual
lista[math.floor(rng.random()*len(lista))]: idéntico a choice. Útil para entender y para fuentes propias con rechazo (Tema 22).
Módulo heredado
lista[x % n] con fuente 0 … m−1 sesga si m % n ≠ 0: los primeros m % n elementos salen de más. Con m = 100 y n = 6, cuatro elementos tienen ventaja.
Mezclar para uno
shuffle + tomar el primero cuesta O(n) y muta la lista: despilfarro para un solo sorteo. La mezcla es del Tema 24.

Bien sorteado

choice / floor

O(1), sin mutar, parejo 1/n. Repetible con semilla para depurar, auditable contando frecuencias.

Mal sorteado

% sobre resto o shuffle

Sesgo sistemático o costo O(n) con efectos laterales. El sorteo “sale” pero viciado o lento.

04 · Ejemplos

Tres sorteos típicos

Lista de n→Índice 0…n−1→Elemento
  1. 1
    Rifa entre 200.

    choice(participantes) con semilla registrada y auditoría pública. Si hay premio grande: secrets.choice.

  2. 2
    Carta de mazo.

    Con reemplazo (reponer): puede salir dos veces el as. Sin reposición es otro problema (Temas 24–25).

  3. 3
    Canción / anuncio rotativo.

    Millones de sorteos O(1) sin mutar la lista: choices(lista, k=miles) de una vez.

05 · Implementación en Python

Sortear y contar

Sorteamos 6000 veces entre 6 nombres con choice y con floor manual: ambos dan ≈ 1/6 por nombre. Contar es auditar.

Python en tu navegador. Cambiá la lista a 7 elementos y el % del laboratorio se comporta distinto: el resto manda.

import math
import random
from collections import Counter

nombres = ["Ana", "Bruno", "Carla", "Diego", "Elena", "Franco"]
rng = random.Random(21)
s1 = [rng.choice(nombres) for _ in range(6000)]
rng = random.Random(21)
s2 = [nombres[math.floor(rng.random() * len(nombres))] for _ in range(6000)]
for nombre, s in [("choice", s1), ("floor", s2)]:
    c = Counter(s)
    print(nombre, [round(c[n] / len(s), 4) for n in nombres])

Seis frecuencias ≈ 0,1667 en ambas vías: el índice floor es el motor de choice.

Sorteo con premio

import secrets

participantes = ["Ana", "Bruno", "Carla", "Diego", "Elena", "Franco"]
print(secrets.choice(participantes))
print(secrets.choice(participantes))

06 · Exploración

Laboratorio: ¿salen todos parejo?

Lista de n elementos (A, B, C…) sorteados N veces desde fuente 0 … 99. Compará floor (parejo) con % crudo: cuando 100 % n ≠ 0, los primeros elementos se disparan en naranja.

EXPERIMENTO 23

Sorteo entre n

choice vs % crudo

Los resultados numéricos aparecen debajo.
Esperada 1/n—
Máx desvío—
Menos elegido—
Veredicto—

Con n = 6 y % crudo, 4 elementos salen de más.

La línea punteada es 1/n. Con floor todas las barras la rondan; con % y resto, un grupo queda sistemáticamente arriba.

Preguntas para explorar

  1. Con n = 5 (100 % 5 = 0) y %: ¿hay sesgo? ¿Y con n = 6?
  2. Con n = 6, floor y 12000 sorteos: ¿el desvío se achica? ¿Pasa lo mismo con %?
  3. Si sorteás de a un ganador por día durante un año, ¿cuánto premio de más se llevan los favorecidos por el %?
Ver respuestas sugeridas
  1. Con n = 5 no hay resto: parejo. Con n = 6 el resto 4 vicia los 4 primeros (17/100 frente a 16/100).
  2. Floor sí se achica (ruido que se diluye); % no: el sesgo es estructural y persiste con N.
  3. ≈ 6 % de sorteos de más por favorecido (17 vs 16): en 365 días son ~22 premios de ventaja. Auditable y denunciable.

07 · Comprensión

Confusiones frecuentes

«Para uno solo, mezclo y tomo el primero»

Funciona pero cuesta O(n) y muta la lista: para un sorteo es 1000× más trabajo que choice O(1) sin efectos laterales.

«Con reemplazo y sin reemplazo dan lo mismo si saco uno»

Con uno solo coinciden; con varios difieren: con reemplazo hay repetidos, sin reemplazo no (Tema 25). Nombrarlo evita bugs de diseño.

«randint(0, len(lista)) elige bien»

Off-by-one: el índice válido llega a len−1. randint(0, len) incluye len → IndexError 1/(n+1) de las veces.

«Si salió 3 veces seguidas, está cargado»

Con reemplazo las repeticiones son normales (prob. 1/n² para un par): solo frecuencias sistemáticas en miles de sorteos denuncian sesgo.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: rifa entre 200

Sorteá 3 ganadores con reemplazo entre 200 participantes y verificá que pertenecen a la lista.

import random

parts = [f"P{i:03d}" for i in range(200)]
rng = random.Random(4)
gan = [rng.choice(parts) for _ in range(3)]
print(gan, all(g in parts for g in gan))
Ver solución razonada

Tres códigos válidos con True. Con reemplazo podrían repetirse: si el reglamento lo prohíbe, es muestreo sin reemplazo (Tema 25), no triple choice.

Ejercicio 2: cazar el off-by-one

Provocá el IndexError de randint(0, len) y corregilo con randrange.

import random

xs = ["a", "b", "c", "d", "e", "f"]
rng = random.Random(1)
mal = 0
for _ in range(6000):
    try:
        xs[rng.randint(0, len(xs))]
    except IndexError:
        mal += 1
print("errores:", mal)
print(xs[rng.randrange(len(xs))])
Ver solución

≈ 857 errores (≈ 1/7): el índice len es inválido. randrange(len) o choice lo eliminan de raíz.

Ejercicio 3: auditor de sorteo

Escribí auditar(sorteos, elementos) que devuelva la frecuencia mínima y máxima, y probalo con 6000 sorteos.

Ver una posible respuesta
import random
from collections import Counter

def auditar(sorteos, elementos):
    c = Counter(sorteos)
    fs = [c[e] / len(sorteos) for e in elementos]
    return min(fs), max(fs)


els = ["A", "B", "C", "D", "E", "F"]
rng = random.Random(8)
s = [rng.choice(els) for _ in range(6000)]
print([round(v, 4) for v in auditar(s, els)])

Mín y máx ≈ 0,1667 ± 0,015. Brechas mayores y estables entre elementos indican % viciado o lista mutada.

09 · Síntesis

Ideas para recordar

  • Un elemento parejo: lista[floor(u·n)] = choice, prob. 1/n, con reemplazo.
  • % sobre fuente con resto favorece a los primeros; floor/rechazo no.
  • choice es O(1) sin mutar; mezclar para uno es O(n) con efectos laterales.
  • Premio o ventaja → secrets.choice; simulación → rng.choice con semilla.
  • Índice válido 0 … n−1: randint(0, len) es off-by-one.

En el próximo tema mezclaremos todo: permutaciones aleatorias con Fisher-Yates.