01 · Punto de partida
El algoritmo que sí reparte 1/n!
El Tema 32 definió la mezcla justa; este tema la construye: Fisher-Yates recorre el arreglo de atrás hacia adelante, sortea j ∈ {0,…,i} e intercambia. Con n−1 sorteos produce las n! permutaciones equiprobables en O(n) y O(1) extra.
Todo el tema cabe en una línea —el rango—: achicarlo da perfección, congelarlo da el sesgo más famoso del muestreo.
- ¿Por qué j ∈ {0…i} y no en todo el arreglo?
- ¿Cómo prueba la inducción que cada π sale con 1/n!?
- ¿Qué es la variante inside-out y cuándo sirve?
- ¿Cómo se ve el sesgo del rango fijo en la matriz?
02 · Definición
Rango menguante e inducción
En el paso i hay i+1 candidatos para la posición i, cada uno con 1/(i+1). Fijada esa posición, el resto es una mezcla justa de i elementos por hipótesis inductiva: 1/(i+1)·1/i! = 1/(i+1)!. Base: con i = 0 no hay nada que sortear.
Sufijo blindado
Lo ya fijado (posiciones > i) no se toca más: cada posición se decide una sola vez, con el rango exacto de candidatos restantes.
Inside-out
Construye la mezcla hacia adelante copiando la fuente: ideal en streaming, cuando el arreglo destino nace mezclado sin existir antes.
Lineal y en sitio
n−1 uniformes discretos, cero estructuras auxiliares. Nadie mezcla más rápido sin resignar uniformidad.
| Paso i | Rango de j | Fija la posición |
|---|---|---|
| 3 | {0,1,2,3} | 3 con 1/4 cada candidato |
| 2 | {0,1,2} | 2 con 1/3 cada candidato |
| 1 | {0,1} | 1 con 1/2 (la 0 queda sola) |
03 · Anatomía
Variantes, enteros y base uniforme
- Inside-out O(n)
- a[0] = fuente[0]; para i ≥ 1: j ∈ {0,…,i}; a[i] = a[j]; a[j] = fuente[i]. Misma ley 1/n!, útil si la mezcla se consume en orden o la fuente es un iterador.
- Entero sin módulo
j = int((i+1)·U)es exacto en la práctica (Tema 17);randbits % (i+1)sesga salvo que el rango base sea múltiplo. Con i grande, rechazo de Lemire.- Parcial gratis
- Frenar en i = n−m da el subconjunto ordenado del Tema 31: Fisher-Yates contiene al sin reemplazo como prefijo.
- Fuente base honesta
- El algoritmo no crea azar: con PRNG pobre o semilla repetida, la mezcla “perfecta” repite el mismo π. Semilla fresca por experimento.
Rango {0…i}
Las n! equiprobables
Matriz de posiciones plana y fijos Poisson(1): pasa toda la auditoría del Tema 32.
Rango {0…n−1} fijo
n^n secuencias torcidas
n^n no es múltiplo de n!: algunas π salen de más. Con n = 4 la (0,0) trepa a ≈ 0,42.
04 · Dónde mezcla Yates
Del algoritmo al ecosistema
- 1shuffle de librería.
random.shufflees Fisher-Yates: este tema explica lo que ya usás. - 2Subconjuntos.
Frenar antes de tiempo da el Tema 31 sin código extra.
- 3Streaming.
Inside-out mezcla lo que llega sin guardarlo todo primero.
- 4Tests por permutación.
Miles de mezclas justas por segundo para la nula empírica.
05 · Representación en Python
Clásico e inside-out frente a frente
Ambas variantes con la misma semilla conceptual: idéntica ley, distinto recorrido. Auditoría por matriz plana.
Python en tu navegador. Activá el modo ingenuo mentalmente: cambiá el rango y mirá la casilla (0,0).
import random
random.seed(290)
n, B = 8, 6000
def fisher_yates(xs):
xs = list(xs)
for i in range(len(xs)-1, 0, -1):
j = int((i+1)*random.random())
xs[i], xs[j] = xs[j], xs[i]
return xs
def inside_out(fuente):
a = [fuente[0]]
for i in range(1, len(fuente)):
j = int((i+1)*random.random())
a.append(fuente[i])
a[i], a[j] = a[j], a[i]
return a
c00 = sum(1 for _ in range(B) if fisher_yates(range(n))[0] == 0)
random.seed(290)
c00b = sum(1 for _ in range(B) if inside_out(list(range(n)))[0] == 0)
print("clásico (0,0):", round(c00/B, 3), "vs", round(1/n, 3))
print("inside-out (0,0):", round(c00b/B, 3), "vs", round(1/n, 3))
Ambas ≈ 0,125. Si alguna casilla se despega con B = 6000, el rango está mal puesto.
06 · Exploración
Laboratorio: el rango fijo deja manchas
La matriz n×n resume B mezclas: cada casilla debe rondar 1/n. Con el rango correcto la peor mancha es ruido; activá el modo ingenuo y mirá cómo el chi-cuadrado explota.
Matriz de la mezcla
j ∈ {0…i} vs j ∈ {0…n−1}
Con n 8 y B 6000, matriz plana en 0,125.
Barras = peores 25 casillas vs línea 1/n; el número es el chi total. Probá el ingenuo con n = 8.
Preguntas para explorar
- Con n = 8 correcto, ¿chi ≈ n²−1 = 63? ¿Mancha pequeña?
- Activá el ingenuo. ¿El chi se multiplica? ¿La (0,0) se dispara hacia 0,39?
- Con B = 500 correcto, ¿el chi baila aunque el método sea justo?
Ver respuestas sugeridas
- Sí: chi ≈ 63 ± 16 con matriz justa; la mancha es solo ruido.
- Sí: el chi trepa al doble o más y la (0,0) sube hacia ≈ 0,39 con n = 8.
- Sí: con B chico el chi fluctúa ±√(2·gl); la justicia se juzga con miles.
07 · Comprensión
Confusiones frecuentes
«j en todo el arreglo es lo mismo»
No: n^n secuencias sobre n! salidas no reparte parejo. Solo el rango {0…i} da 1/n! exacto.
«Recorro de adelante igual»
Hacia adelante también vale si el rango es {i…n−1} (espejo del algoritmo). Hacia adelante con {0…i} es otro algoritmo y hay que re-probarlo.
«% (i+1) es uniforme»
Solo si el rango base es múltiplo de i+1 (Tema 17). Si no, descarte del excedente o sesgo silencioso.
«Mezclo dos veces para más azar»
Doble mezcla justa sigue justa: no suma nada y duplica el costo. El azar lo pone la base, no las pasadas.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: chi de la matriz n = 6
n = 6, B = 12000 (semilla 291): el chi debe rondar 35 ± 12. El ingenuo da > 200.
import random
random.seed(291)
n, B = 6, 12000
mat = [[0]*n for _ in range(n)]
for _ in range(B):
xs = list(range(n))
for i in range(n-1, 0, -1):
j = int((i+1)*random.random())
xs[i], xs[j] = xs[j], xs[i]
for j, v in enumerate(xs):
mat[v][j] += 1
e = B/n
print("chi:", round(sum((c-e)**2/e for f in mat for c in f), 1), "vs ~35")
Ver solución razonada
≈ 35 (gl = 35, banda típica 23–47). Si da cientos, el rango es fijo o el entero va con módulo.
Ejercicio 2: inside-out en streaming
Mezclá un iterador que no existe como lista (semilla 292) y verificá casilla (0,0) ≈ 1/8.
import random
random.seed(292)
n, B = 8, 8000
c00 = 0
for _ in range(B):
a = [0]
for i in range(1, n):
j = int((i+1)*random.random())
a.append(i)
a[i], a[j] = a[j], a[i]
c00 += (a[0] == 0)
print(round(c00/B, 3), "vs 0.125")
Ver solución
≈ 0,125. La fuente nunca existió como arreglo: la mezcla nació ordenada al azar.
Ejercicio 3: el módulo que mancha
Generá j con int(100·U) % (i+1) vs int((i+1)·U) con n = 7, B = 15000: compará el chi.
Ver una posible respuesta
import random
def chi(mal, seed):
random.seed(seed)
n, B = 7, 15000
mat = [[0]*n for _ in range(n)]
for _ in range(B):
xs = list(range(n))
for i in range(n-1, 0, -1):
j = int(100*random.random()) % (i+1) if mal else int((i+1)*random.random())
xs[i], xs[j] = xs[j], xs[i]
for j, v in enumerate(xs):
mat[v][j] += 1
e = B/n
return sum((c-e)**2/e for f in mat for c in f)
print("bien:", round(chi(False, 293), 1), "vs ~48")
print("mal: ", round(chi(True, 294), 1))
Bien ≈ 48 (gl = 48); mal trepa a ≈ 90, fuera de banda: el módulo ingenuo mancha.
09 · Síntesis
Ideas para recordar
- Fisher-Yates: j ∈ {0…i}, O(n), 1/n! exacto.
- Inside-out: misma ley en streaming.
- Rango fijo o módulo ⇒ sesgo medible en el chi.
- Parcial gratis ⇒ subconjuntos del Tema 31.
- Auditar con matriz plana + chi ≈ n²−1.
En el próximo tema los datos mandan: distribuciones empíricas.