Últimos Cambios |
||
Blog personal: El hilo del laberinto |
Última Actualización: 2 de noviembre de 2011
A veces tenemos que elegir N líneas al azar de un fichero. Por ejemplo, queremos elegir al azar la frase del día, o elegir una palabra de un diccionario de forma aleatoria.
La elección habitual sería leer todo el fichero en memoria y elegir una línea al azar. Pero si el fichero es muy grande es un desperdicio de memoria o, incluso, una imposibilidad si supera nuestra memoria RAM.
Trabajando con ficheros físicos, y suponiendo que estadísticamente todas las líneas tienen un tamaño comparable, podemos hacer un "seek" aleatorio en el fichero y retroceder luego carácter a carácter hasta llegar al inicio de la línea o al principio del fichero. Pero esta operación requiere conocer el tamaño del fichero y tiene problemas con codificaciones de longitud variable tipo UTF-8, ficheros comprimidos, etc. Tampoco se puede usar si no conocemos el tamaño de los datos, si el tamaño de las líneas es muy dispar o si no podemos hacer "seek" (por ejemplo, porque los datos nos están llegando por una conexión TCP). Puede ser que nos estén llegando líneas y no sepamos dónde está el final... hasta que justo llegamos a él.
Lo que queremos es un algoritmo genérico que lea el fichero una sola vez, y que nos proporcione N líneas aleatorias del mismo, sin consumir apenas memoria.
Por sencillez, supongamos que N=1. Es decir, queremos una línea al azar del fichero. El algoritmo es el siguiente:
Este algoritmo cumple lo que queremos: lee el fichero una sola vez y solo necesita recordar una línea. Falta demostrar que es correcto. Hagámoslo por inducción:
El programa en sí es trivial: (código Python compatible con Python 3 y versiones recientes de Python 2)
import random, sys for i, l in enumerate(sys.stdin, start=1) : if random.randrange(0, i) == 0 : linea = l print(linea.strip())
La demostración por inducción es fácil:
El programa literal queda un poco feucho, requiere dos números aleatorios. Podemos solucionar este hecho generando un número al azar "p" entre 0 y L, y utilizarlo tanto para decidir si quedarnos con la línea nueva (la probabilidad N/L sería la probabilidad de que p<N) como para elegir el elemento a reemplazar (directamente el elemento "p", ya que en este caso sabemos que 0<=p<N).
La cosa queda algo así: (código compatible con Python 3 y Python 2 recientes)
import random, sys NUM_ELEMENTOS = 10 lineas = [sys.stdin.readline() for i in xrange(NUM_ELEMENTOS)] for i, l in enumerate(sys.stdin, start=NUM_ELEMENTOS+1) : r = random.randrange(0, i) if r < NUM_ELEMENTOS : lineas[r] = l for i in lineas : print(i.strip())
Analizando el problema y buscando información por Internet sobre el mismo, me encuentro que se llama "reservoir sampling". El código de ejemplo en la Wikipedia es clavado al mío.
Si realmente estamos trabajando con ficheros físicos en ASCII o LATIN-1 o similares, podemos usar la sugerencia inicial de "seek". La operación es instantánea, comparado con leerse el fichero entero. Las líneas tienen que tener un tamaño comparable, porque sino las más largas tendrán una probabilidad mayor de aparecer. Una posibilidad estadística es hacer "seek"s al azar en el fichero, hasta que caemos sobre un LineFeed (en Unix) o en el offset 0 del fichero, y leemos la línea siguiente.
Aquí todas las líneas serían equiprobables, independientemente de su longitud, a costa de hacer una media de "X" seeks, donde "X" es el tamaño medio de una línea. Hacer 50 "seek"s es costoso, pero menos que leer un fichero de 10 gigabytes :). Por supuesto. esto no nos sirve si los datos no están en un fichero, sino que nos van llegando y no sabemos dónde está el final hasta que llegamos a él.
Más información sobre los OpenBadges
Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS