Generación de Variables Aleatorias · Tema 33

Fisher-Yates

Mezcla perfecta en O(n): rango menguante {0…i}, prueba por inducción y variante inside-out.

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.

i↓

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.

O(n)

Lineal y en sitio

n−1 uniformes discretos, cero estructuras auxiliares. Nadie mezcla más rápido sin resignar uniformidad.

Paso a paso con n = 4: los rangos mandan.
Paso iRango de jFija 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

arreglo→i↓, j≤i→π justa
  1. 1
    shuffle de librería.

    random.shuffle es Fisher-Yates: este tema explica lo que ya usás.

  2. 2
    Subconjuntos.

    Frenar antes de tiempo da el Tema 31 sin código extra.

  3. 3
    Streaming.

    Inside-out mezcla lo que llega sin guardarlo todo primero.

  4. 4
    Tests 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.

EXPERIMENTO 33

Matriz de la mezcla

j ∈ {0…i} vs j ∈ {0…n−1}

Los resultados numéricos aparecen debajo.
Mancha máx.0,000
Chi-cuadrado0,0
Casilla (0,0)0,125
Teórica 1/n0,125

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

  1. Con n = 8 correcto, ¿chi ≈ n²−1 = 63? ¿Mancha pequeña?
  2. Activá el ingenuo. ¿El chi se multiplica? ¿La (0,0) se dispara hacia 0,39?
  3. Con B = 500 correcto, ¿el chi baila aunque el método sea justo?
Ver respuestas sugeridas
  1. Sí: chi ≈ 63 ± 16 con matriz justa; la mancha es solo ruido.
  2. Sí: el chi trepa al doble o más y la (0,0) sube hacia ≈ 0,39 con n = 8.
  3. 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.