web-dev-qa-db-fra.com

Python échantillon aléatoire avec un générateur / itérable / itérateur

Savez-vous s'il existe un moyen de faire fonctionner le random.sample De python avec un objet générateur. J'essaie d'obtenir un échantillon aléatoire d'un très grand corpus de texte. Le problème est que random.sample() déclenche l'erreur suivante.

TypeError: object of type 'generator' has no len()

Je pensais qu'il y avait peut-être un moyen de le faire avec quelque chose de itertools mais je ne pouvais rien trouver avec un peu de recherche.

Un exemple quelque peu inventé:

import random
def list_item(ls):
    for item in ls:
        yield item

random.sample( list_item(range(100)), 20 )


MISE À JOUR


Conformément à la demande de MartinPieters, j'ai fait un certain timing des trois méthodes actuellement proposées. Les résultats sont les suivants.

Sampling 1000 from 10000
Using iterSample 0.0163 s
Using sample_from_iterable 0.0098 s
Using iter_sample_fast 0.0148 s

Sampling 10000 from 100000
Using iterSample 0.1786 s
Using sample_from_iterable 0.1320 s
Using iter_sample_fast 0.1576 s

Sampling 100000 from 1000000
Using iterSample 3.2740 s
Using sample_from_iterable 1.9860 s
Using iter_sample_fast 1.4586 s

Sampling 200000 from 1000000
Using iterSample 7.6115 s
Using sample_from_iterable 3.0663 s
Using iter_sample_fast 1.4101 s

Sampling 500000 from 1000000
Using iterSample 39.2595 s
Using sample_from_iterable 4.9994 s
Using iter_sample_fast 1.2178 s

Sampling 2000000 from 5000000
Using iterSample 798.8016 s
Using sample_from_iterable 28.6618 s
Using iter_sample_fast 6.6482 s

Il s'avère donc que le array.insert Présente un sérieux inconvénient en ce qui concerne les grands échantillons. Le code que j'ai utilisé pour chronométrer les méthodes

from heapq import nlargest
import random
import timeit


def iterSample(iterable, samplesize):
    results = []
    for i, v in enumerate(iterable):
        r = random.randint(0, i)
        if r < samplesize:
            if i < samplesize:
                results.insert(r, v) # add first samplesize items in random order
            else:
                results[r] = v # at a decreasing rate, replace random items

    if len(results) < samplesize:
        raise ValueError("Sample larger than population.")

    return results

def sample_from_iterable(iterable, samplesize):
    return (x for _, x in nlargest(samplesize, ((random.random(), x) for x in iterable)))

def iter_sample_fast(iterable, samplesize):
    results = []
    iterator = iter(iterable)
    # Fill in the first samplesize elements:
    for _ in xrange(samplesize):
        results.append(iterator.next())
    random.shuffle(results)  # Randomize their positions
    for i, v in enumerate(iterator, samplesize):
        r = random.randint(0, i)
        if r < samplesize:
            results[r] = v  # at a decreasing rate, replace random items

    if len(results) < samplesize:
        raise ValueError("Sample larger than population.")
    return results

if __name__ == '__main__':
    pop_sizes = [int(10e+3),int(10e+4),int(10e+5),int(10e+5),int(10e+5),int(10e+5)*5]
    k_sizes = [int(10e+2),int(10e+3),int(10e+4),int(10e+4)*2,int(10e+4)*5,int(10e+5)*2]

    for pop_size, k_size in Zip(pop_sizes, k_sizes):
        pop = xrange(pop_size)
        k = k_size
        t1 = timeit.Timer(stmt='iterSample(pop, %i)'%(k_size), setup='from __main__ import iterSample,pop')
        t2 = timeit.Timer(stmt='sample_from_iterable(pop, %i)'%(k_size), setup='from __main__ import sample_from_iterable,pop')
        t3 = timeit.Timer(stmt='iter_sample_fast(pop, %i)'%(k_size), setup='from __main__ import iter_sample_fast,pop')

        print 'Sampling', k, 'from', pop_size
        print 'Using iterSample', '%1.4f s'%(t1.timeit(number=100) / 100.0)
        print 'Using sample_from_iterable', '%1.4f s'%(t2.timeit(number=100) / 100.0)
        print 'Using iter_sample_fast', '%1.4f s'%(t3.timeit(number=100) / 100.0)
        print ''

J'ai également effectué un test pour vérifier que toutes les méthodes prennent effectivement un échantillon non biaisé du générateur. Donc, pour toutes les méthodes, j'ai échantillonné 1000 Éléments à partir de 10000100000 Fois et calculé la fréquence moyenne d'occurrence de chaque élément de la population qui se révèle être ~.1 comme on peut s'y attendre pour les trois méthodes.

38
Matti Lyra

Bien que la réponse de Martijn Pieters soit correcte, elle ralentit lorsque samplesize devient grand, car l'utilisation de list.insert dans une boucle peut avoir une complexité quadratique.

Voici une alternative qui, à mon avis, préserve l'uniformité tout en augmentant les performances:

def iter_sample_fast(iterable, samplesize):
    results = []
    iterator = iter(iterable)
    # Fill in the first samplesize elements:
    try:
        for _ in xrange(samplesize):
            results.append(iterator.next())
    except StopIteration:
        raise ValueError("Sample larger than population.")
    random.shuffle(results)  # Randomize their positions
    for i, v in enumerate(iterator, samplesize):
        r = random.randint(0, i)
        if r < samplesize:
            results[r] = v  # at a decreasing rate, replace random items
    return results

La différence commence lentement à apparaître pour les valeurs de samplesize supérieures à 10000. Heures d'appel avec (1000000, 100000):

  • iterSample: 5,05 s
  • iter_sample_fast: 2,64 s
22
DzinX

Tu ne peux pas.

Vous avez deux options: lire l'intégralité du générateur dans une liste, puis échantillonner à partir de cette liste, ou utiliser une méthode qui lit le générateur un par un et sélectionne l'échantillon à partir de cela:

import random

def iterSample(iterable, samplesize):
    results = []

    for i, v in enumerate(iterable):
        r = random.randint(0, i)
        if r < samplesize:
            if i < samplesize:
                results.insert(r, v) # add first samplesize items in random order
            else:
                results[r] = v # at a decreasing rate, replace random items

    if len(results) < samplesize:
        raise ValueError("Sample larger than population.")

    return results

Cette méthode ajuste la chance que l'élément suivant fasse partie de l'échantillon en fonction du nombre d'éléments dans l'itérable jusqu'à présent . Il n'a pas besoin de contenir plus de samplesize éléments en mémoire.

La solution n'est pas la mienne; il a été fourni dans le cadre de ne autre réponse ici sur SO .

17
Martijn Pieters

Juste pour le plaisir, voici une ligne qui échantillonne k éléments sans remplacement des n éléments générés en O (n lg - k) temps:

from heapq import nlargest

def sample_from_iterable(it, k):
    return (x for _, x in nlargest(k, ((random.random(), x) for x in it)))
7
Fred Foo

J'essaie d'obtenir un échantillon aléatoire d'un très grand corpus de texte.

Votre excellente réponse de synthèse montre actuellement la victoire de iter_sample_fast(gen, pop). Cependant, j'ai essayé la recommandation de Katriel de random.sample(list(gen), pop) - et c'est incroyablement rapide en comparaison!

def iter_sample_easy(iterable, samplesize):
    return random.sample(list(iterable), samplesize)

Sampling 1000 from 10000
Using iter_sample_fast 0.0192 s
Using iter_sample_easy 0.0009 s

Sampling 10000 from 100000
Using iter_sample_fast 0.1807 s
Using iter_sample_easy 0.0103 s

Sampling 100000 from 1000000
Using iter_sample_fast 1.8192 s
Using iter_sample_easy 0.2268 s

Sampling 200000 from 1000000
Using iter_sample_fast 1.7467 s
Using iter_sample_easy 0.3297 s

Sampling 500000 from 1000000
Using iter_sample_easy 0.5628 s

Sampling 2000000 from 5000000
Using iter_sample_easy 2.7147 s

Maintenant, à mesure que votre corpus devient très grand , matérialiser tout l'itérable en list utilisera des quantités de mémoire prohibitives. Mais nous pouvons toujours exploiter la rapidité fulgurante de Python si nous pouvons couper le problème : en gros, nous choisissons un CHUNKSIZE qui est "raisonnablement petit", faites random.sample sur des morceaux de cette taille, puis utilisez à nouveau random.sample pour les fusionner. Nous devons simplement obtenir les bonnes conditions aux limites.

Je vois comment le faire si la longueur de list(iterable) est un multiple exact de CHUNKSIZE et pas plus grand que samplesize*CHUNKSIZE:

def iter_sample_dist_naive(iterable, samplesize):
    CHUNKSIZE = 10000
    samples = []
    it = iter(iterable)
    try:
        while True:
            first = next(it)
            chunk = itertools.chain([first], itertools.islice(it, CHUNKSIZE-1))
            samples += iter_sample_easy(chunk, samplesize)
    except StopIteration:
        return random.sample(samples, samplesize)

Toutefois, le code ci-dessus produit un échantillonnage non uniforme lorsque len(list(iterable)) % CHUNKSIZE != 0, et il manque de mémoire lorsque len(list(iterable)) * samplesize / CHUNKSIZE devient "très grand". La correction de ces bogues est au-dessus de ma note de rémunération, je le crains, mais une solution est décrite dans ce billet de blog et semble assez raisonnable pour moi. (Termes de recherche: "échantillonnage aléatoire distribué", "échantillonnage de réservoir distribué".)

Sampling 1000 from 10000
Using iter_sample_fast 0.0182 s
Using iter_sample_dist_naive 0.0017 s
Using iter_sample_easy 0.0009 s

Sampling 10000 from 100000
Using iter_sample_fast 0.1830 s
Using iter_sample_dist_naive 0.0402 s
Using iter_sample_easy 0.0103 s

Sampling 100000 from 1000000
Using iter_sample_fast 1.7965 s
Using iter_sample_dist_naive 0.6726 s
Using iter_sample_easy 0.2268 s

Sampling 200000 from 1000000
Using iter_sample_fast 1.7467 s
Using iter_sample_dist_naive 0.8209 s
Using iter_sample_easy 0.3297 s

Là où nous gagnons vraiment, c'est quand samplesize est très petit par rapport à len(list(iterable)).

Sampling 20 from 10000
Using iterSample 0.0202 s
Using sample_from_iterable 0.0047 s
Using iter_sample_fast 0.0196 s
Using iter_sample_easy 0.0001 s
Using iter_sample_dist_naive 0.0004 s

Sampling 20 from 100000
Using iterSample 0.2004 s
Using sample_from_iterable 0.0522 s
Using iter_sample_fast 0.1903 s
Using iter_sample_easy 0.0016 s
Using iter_sample_dist_naive 0.0029 s

Sampling 20 from 1000000
Using iterSample 1.9343 s
Using sample_from_iterable 0.4907 s
Using iter_sample_fast 1.9533 s
Using iter_sample_easy 0.0211 s
Using iter_sample_dist_naive 0.0319 s

Sampling 20 from 10000000
Using iterSample 18.6686 s
Using sample_from_iterable 4.8120 s
Using iter_sample_fast 19.3525 s
Using iter_sample_easy 0.3162 s
Using iter_sample_dist_naive 0.3210 s

Sampling 20 from 100000000
Using iter_sample_easy 2.8248 s
Using iter_sample_dist_naive 3.3817 s
2
Quuxplusone

Si le nombre d'articles dans l'itérateur est connu (en comptant ailleurs les articles), une autre approche est:

def iter_sample(iterable, iterlen, samplesize):
    if iterlen < samplesize:
        raise ValueError("Sample larger than population.")
    indexes = set()
    while len(indexes) < samplesize:
        indexes.add(random.randint(0,iterlen))
    indexesiter = iter(sorted(indexes))
    current = indexesiter.next()
    ret = []
    for i, item in enumerate(iterable):
        if i == current:
            ret.append(item)
            try:
                current = indexesiter.next()
            except StopIteration:
                break
    random.shuffle(ret)
    return ret

Je trouve cela plus rapide, surtout quand sampsize est petit par rapport à iterlen. Cependant, lorsque l’ensemble ou presque l’échantillon est demandé, il y a des problèmes.

iter_sample (iterlen = 10000, samplesize = 100) time: (1, 'ms') iter_sample_fast (iterlen = 10000, samplesize = 100) time: (15, 'ms')

iter_sample (iterlen = 1000000, samplesize = 100) time: (65, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 100) time: (1477, 'ms')

iter_sample (iterlen = 1000000, samplesize = 1000) time: (64, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 1000) time: (1459, 'ms')

iter_sample (iterlen = 1000000, samplesize = 10000) time: (86, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 10000) time: (1480, 'ms')

iter_sample (iterlen = 1000000, samplesize = 100000) time: (388, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 100000) time: (1521, 'ms')

iter_sample (iterlen = 1000000, samplesize = 1000000) time: (25359, 'ms') iter_sample_fast (iterlen = 1000000, samplesize = 1000000) time: (2178, 'ms')

0
blitzen

Si la taille de la population n est connue, voici un code efficace en mémoire qui boucle sur un générateur, en extrayant uniquement les échantillons cibles:

from random import sample
from itertools import count, compress

targets = set(sample(range(n), k=10))
for selection in compress(pop, map(targets.__contains__, count())):
    print(selection)

Cela affiche les sélections dans l'ordre où elles sont produites par le générateur de population.

La technique consiste à utiliser la bibliothèque standard random.sample () pour sélectionner au hasard les indices cibles pour les sélections. Le second comme détermine si un indice donné est parmi les cibles et si c'est le cas donne la valeur correspondante du générateur.

Par exemple, compte tenu des cibles de {6, 2, 4}:

0  1  2  3  4  5  6  7  8  9  10   ...  output of count()
F  F  T  F  T  F  T  F  F  F  F    ...  is the count in targets?
A  B  C  D  E  F  G  H  I  J  K    ...  output of the population generator
-  -  C  -  E  -  G  -  -  -  -    ...  selections emitted by compress

Cette technique convient pour boucler sur un corpus trop grand pour tenir en mémoire (sinon, vous pouvez simplement utiliser sample () directement sur la population).

0
Raymond Hettinger

Méthode la plus rapide jusqu'à preuve du contraire lorsque vous avez une idée de la durée du générateur (et qu'il sera distribué de manière asymptotique uniforme):

def gen_sample(generator_list, sample_size, iterlen):
    num = 0
    inds = numpy.random.random(iterlen) <= (sample_size * 1.0 / iterlen)
    results = []
    iterator = iter(generator_list)
    gotten = 0
    while gotten < sample_size: 
        try:
            b = iterator.next()
            if inds[num]: 
                results.append(b)
                gotten += 1
            num += 1    
        except: 
            num = 0
            iterator = iter(generator_list)
            inds = numpy.random.random(iterlen) <= ((sample_size - gotten) * 1.0 / iterlen)
    return results

Il est à la fois le plus rapide sur le petit itérable ainsi que l'énorme itérable (et probablement tous les deux entre-temps)

# Huge
res = gen_sample(xrange(5000000), 200000, 5000000)
timing: 1.22s

# Small
z = gen_sample(xrange(10000), 1000, 10000) 
timing: 0.000441    
0
PascalVKooten