【问题标题】:Random sample from a very long iterable, in python来自非常长的可迭代的随机样本,在 python 中
【发布时间】:2016-06-10 01:06:14
【问题描述】:

我有一个很长的 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__ 方法的对象中(我可以提前找出长度)。所以我会接受一个显示如何干净地做到这一点的答案。

【问题讨论】:

  • 您在寻找水库采样吗?这不是 Python 自带的,可能是因为它只对疯狂的大流有意义。此外,__len__ 还不够; random.sample 需要随机访问。
  • 需要明确的是,random.sample 确实 可用于任意序列,但不能用于任意可迭代对象。见docs.python.org/2/glossary.html
  • 如您所述,您可以将生成器包装在提供 len 方法的对象中。但至于其实现细节,这将有助于了解您的生成器到底在做什么/它是如何实现的。
  • 如果你知道len()先验,那么你可以这样做:indices = random.sample(xrange(len),k),然后运行你的生成器,直到你提取了每个索引数据。跨度>

标签: python python-3.x random


【解决方案1】:

由于您知道可迭代对象返回的数据长度,您可以使用xrange() 快速生成可迭代对象的索引。然后你可以运行迭代器,直到你获取所有数据:

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)

或者,这里是使用“算法 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)

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

【讨论】:

  • 这是个好主意,谢谢!不知何故,我确定random.sample(x)range 一起工作,但我没有想到要对索引进行抽样。但是 indices 在您的解决方案中应该是 dict - 否则它会非常缓慢。然后,由于我不关心订单,你可以直接返回[datum for n, datum in enumerate(it) if n in indices]
  • 我还考虑过对索引进行排序并简单地将迭代器推进到下一个选定的索引(因此避免了大量的 dict 查找),但我怀疑除非输入真的很大,否则编写代码是否值得。
  • 1- 您的第一个代码示例不必要地使用了O(n*k)(二次)算法(index in indicesindices.index(index)O(k) 操作)。 2-你可以使用randrange(i)而不是randint(0, i-1)(相同的结果)3-如果输入的项目少于k,那么你的代码产生的顺序不是随机的(虽然,也许这并不重要案子)。你可能想shuffle the input in this case
  • @J.F.Sebastian(和 Rob),O(n*k) 的问题是为什么我说indices 应该是dict,而不是列表。这样就可以解决了。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    如果您需要具有固定频率的原始迭代器的子集(即,如果生成器生成 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
    

    虽然(还)不是真正的高斯分布,但它对我来说已经足够好了。

    【讨论】:

    • 好点:在许多情况下,基于频率的样本就足够了。我什至可以按频率过采样,然后使用random.sample() 将其修剪到所需的大小。
    • 您能否解释一下为什么会出现正态分布?我可以看到你在计算频率,但统计数据不是我的强项......另外,如果你期待一个高斯,你为什么不得到一个(或者为什么“还没有”)?
    • 高斯是“共同”随机分布“瞄准”某个点的极限。我希望所有数字平均以相同的频率出现,但这不会实现,除非是随机机会。将会发生的是值将围绕预期结果分组;如果我有无限数量的样本,这种“意外”的分布将描述一个高斯分布。如果我不能更好地解释这一点,我很抱歉。
    • 你的解释很清楚,虽然你没有说你如何解释你看到的偏差。但无论如何,我不确定你的假设是否正确:频率是离散的,不是连续的,并且不能低于零,所以不可能有真正的高斯。对于绘制的任何特定值,其预期频率具有泊松分布,并且由于泊松的总和也是泊松,我认为这就是对所有输入值求和时会得到的结果。但是统计数据不是我的强项,所以谁知道...
    【解决方案4】:

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

    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']
    

    【讨论】:

    • 请注意,这总是按排序顺序返回结果。它永远不会返回,例如,['b', 'a']
    • 这可能适用于某些应用程序,但它不能解决提供统一的固定大小样本的问题。
    • 我其实很喜欢保序抽样,但确实没办法保证结果的大小。我可以按频率过采样并使用 random.sample() 将其修剪到所需的大小,但原始样本小于所需大小的可能性总是非零。
    【解决方案5】:

    使用itertools.compress() 函数,带有随机选择器函数:

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

    【讨论】:

    • 感谢您的建议,但这不会生成特定尺寸的样本。在许多情况下,这将是一个不错的选择,但这不是问题所在。
    猜你喜欢
    • 2015-02-10
    • 1970-01-01
    • 1970-01-01
    • 2020-09-12
    • 2020-09-18
    • 2012-03-04
    • 2018-04-05
    相关资源
    最近更新 更多