Random sample from a very long iterable, in python

python, python-3.x, random

Solution

Since you know the length the data returned by your iterable, you can use `xrange()` to quickly generate indices into your iterable. Then you can just run the iterable until you've grabbed all of the data:

import random

def sample(it, length, k):
    indices = random.sample(xrange(length), k)
    result = [None]*k
    for index, datum in enumerate(it):
        if index in indices:
            result[indices.index(index)] = datum
    return result

print sample(iter("abcd"), 4, 2)

In the alternative, here is an implementation of resevior sampleing using "Algorithm R":

import random

def R(it, k):
    '''https://en.wikipedia.org/wiki/Reservoir_sampling#Algorithm_R'''
    it = iter(it)
    result = []
    for i, datum in enumerate(it):
        if i < k:
            result.append(datum)
        else:
            j = random.randint(0, i-1)
            if j < k:
                result[j] = datum
    return result

print R(iter("abcd"), 2)

Note that algorithm R doesn't provide a random order for the results. In the example given, `'b'` will never precede `'a'` in the results.

Problem

I have a long python generator that I want to "thin out" by randomly selecting a subset of values. Unfortunately, `random.sample()` will not work with arbitrary iterables. Apparently, it needs something that supports the `len()` operation (and perhaps non-sequential access to the sequence, but that's not clear). And I don't want to build an enormous list just so I can thin it out. As a matter of fact, it is possible to sample from a sequence uniformly in one pass, without knowing its length-- there's a nice algorithm in `Programming perl` that does just that (edit: "reservoir sampling", thanks @user2357112!). But does anyone know of a standard python module that provides this functionality? Demo of the problem (Python 3) ``` >>> import itertools, random >>> random.sample(iter("abcd"), 2) ... TypeError: Population must be a sequence or set. For dicts, use list(d). ``` On Python 2, the error is more transparent: ``` Traceback (most recent call last): File "<pyshell#12>", line 1, in <module> random.sample(iter("abcd"), 2) File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/random.py", line 321, in sample n = len(population) TypeError: object of type 'iterator' has no len() ``` If there's no alternative to `random.sample()`, I'd try my luck with wrapping the generator into an object that provides a `__len__` method (I can find out the length in advance). So I'll accept an answer that shows how to do that cleanly.

Original source

Related problems