来自python中非常长的迭代的随机样本

时间:2016-02-26 17:31:11

标签: python python-3.x random

我有一个很长的python生成器,我想通过随机选择一个值的子集来“稀释”。不幸的是,random.sample()不适用于任意迭代。显然,它需要支持len()操作的东西(也许是对序列的非顺序访问,但这一点并不清楚)。而且我不想建立一个庞大的列表,所以我可以把它缩小。

事实上,可以在一次通过中从序列统一进行采样,而不知道它的长度 - Programming perl中有一个很好的算法可以做到这一点(编辑:“水库采样”,谢谢@ user2357112!)。但有没有人知道提供此功能的标准python模块?

演示问题(Python 3)

>>> import itertools, random
>>> random.sample(iter("abcd"), 2)
...
TypeError: Population must be a sequence or set.  For dicts, use list(d).

在Python 2上,错误更透明:

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()

如果除了random.sample()之外别无选择,我会试着将生成器包装成一个提供__len__方法的对象(我可以提前找出长度)。所以我会接受一个答案,说明如何干净利落地做到这一点。

5 个答案:

答案 0 :(得分:8)

由于您知道iterable返回的数据的长度,因此您可以使用xrange()在您的iterable中快速生成索引。然后你就可以运行iterable,直到你抓住了所有的数据:

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)

在替代方案中,这里是使用&#34;算法R&#34;的实施例:

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)

请注意,算法R不会为结果提供随机顺序。在给出的示例中,'b'永远不会在结果中位于'a'之前。

答案 1 :(得分:2)

使用O(n)算法R https://en.wikipedia.org/wiki/Reservoir_sampling,从k中选择iterable个随机元素:

import itertools
import random

def reservoir_sample(iterable, k):
    it = iter(iterable)
    if not (k > 0):
        raise ValueError("sample size must be positive")

    sample = list(itertools.islice(it, k)) # fill the reservoir
    random.shuffle(sample) # if number of items less then *k* then
                           #   return all items in random order.
    for i, item in enumerate(it, start=k+1):
        j = random.randrange(i) # random [0..i)
        if j < k:
            sample[j] = item # replace item with gradually decreasing probability
    return sample

示例:

>>> reservoir_sample(iter('abcdefghijklmnopqrstuvwxyz'), 5)
['w', 'i', 't', 'b', 'e']

reservoir_sample()代码来自this answer

答案 2 :(得分:1)

如果您需要具有固定频率的原始迭代器的子集(即,如果生成器生成10000个数字,您需要“统计”其中100个,如果它生成1000000个数字,则需要10000个 - 始终为1% ),你会将迭代器包装在一个构造中,产生内循环的结果,概率为1%。

所以我想你想要来自未知基数来源固定数量样本,就像你提到的Perl算法一样。

你可以将迭代器包装在一个拥有自己的小内存的构造中,以便跟踪库,并以降低的概率循环它。

import random

def reservoir(iterator, size):
    n = size
    R = iterator[0:n]
    for e in iterator:
        j = random.randint(0, n-1)
        n = n + 1
        if (j < size):
                R[j] = e
    return R

所以

print reservoir(range(1, 1000), 3)

可能会打印出来

[656, 774, 828]

我已尝试如上所述生成一百万轮,并将三列的分布与此滤波器进行比较(我期望高斯分布)。

#                get first column and clean it
python file.py | cut -f 1 -d " " | tr -cd "0-9\n" \
    | sort | uniq -c | cut -b1-8 | tr -cd "0-9\n" | sort | uniq -c

虽然不是(还)真正的高斯,但对我来说看起来还不错。

答案 3 :(得分:0)

一种可能的方法是在迭代器周围构建一个生成器来选择随机元素:

def random_wrap(iterator, threshold):
    for item in iterator:
        if random.random() < threshold:
            yield item

当您不知道长度并且迭代器的可能大小将是令人望而却步时,此方法将非常有用。请注意,保证最终列表的大小是有问题的。

一些示例运行:

>>> list(random_wrap(iter('abcdefghijklmnopqrstuvwxyz'), 0.25))
['f', 'h', 'i', 'r', 'w', 'x']

>>> list(random_wrap(iter('abcdefghijklmnopqrstuvwxyz'), 0.25))
['j', 'r', 's', 'u', 'x']

>>> list(random_wrap(iter('abcdefghijklmnopqrstuvwxyz'), 0.25))
['c', 'e', 'h', 'n', 'o', 'r', 'z']

>>> list(random_wrap(iter('abcdefghijklmnopqrstuvwxyz'), 0.25))
['b', 'c', 'e', 'h', 'j', 'p', 'r', 's', 'u', 'v', 'x']

答案 4 :(得分:0)

使用带有随机选择器功能的itertools.compress()功能:

itertools.compress(long_sequence, (random.randint(0, 100) < 10 for x in itertools.repeat(1)))