Python随机样本生成器(适用于庞大的人口规模)

时间:2015-05-05 21:33:24

标签: python random generator lazy-evaluation sample

您可能知道random.sample(population,sample_size)会快速返回一个随机样本,但如果您事先并不知道样本的大小,该怎么办?你最终会对整个人口进行抽样,或者将其改组,这是相同的。但这可能是浪费(如果大多数样本规模与人口规模相比较小)或甚至不可行(如果人口规模庞大,内存不足)。另外,如果你的代码需要在选择样本的下一个元素之前从这里跳到那里怎么办?

P.S。我在为simulated annealing处理TSP时遇到了优化随机样本的需要。在我的代码中,重新启动了数十万次,每次我都不知道是否需要选择1个元素或100%的人口元素。

5 个答案:

答案 0 :(得分:1)

首先,我会将人口分成块。块采样的功能很容易成为发生器,能够处理任意大小的样本。这也允许您将函数设置为生成器。

想象一下无限的种群,一个512的种群和8的样本大小。这意味着您可以根据需要收集尽可能多的样本,并且为了将来的减少再次采样已经采样的空间(对于1024个块,这意味着8196个样本)你可以再次采样。)

同时,这允许并行处理,这在非常大的样本的情况下是可行的。

考虑记忆中人口的例子

import random

population = [random.randint(0, 1000) for i in range(0, 150000)]

def sample_block(population, block_size, sample_size):
    block_number = 0
    while 1:
        try:
            yield random.sample(population[block_number * block_size:(block_number + 1) * block_size], sample_size)
            block_number += 1
        except ValueError:
            break

sampler = sample_block(population, 512, 8)
samples = []

try:
    while 1:
        samples.extend(sampler.next())
except StopIteration:
    pass

print random.sample(samples, 200)

如果填充是脚本(文件,块)的外部,唯一的修改是您必须将适当的块加载到内存中。无限人口抽样的概念证明如下:

import random
import time

def population():
    while 1:
        yield random.randint(0, 10000)

def reduced_population(samples):
    for sample in samples:
        yield sample

def sample_block(generator, block_size, sample_size):

    block_number = 0
    block = []
    while 1:
        block.append(generator.next())
        if len(block) == block_size:
            s = random.sample(block, sample_size)
            block_number += 1
            block = []
            print 'Sampled block {} with result {}.'.format(block_number, s)
            yield s

samples = []
result = []
reducer = sample_block(population(), 512, 12)

try:
    while 1:
        samples.append(reducer.next())
        if len(samples) == 1000:
            sampler = sample_block(reduced_population(samples), 1000, 15)
            result.append(list(sampler))
            time.sleep(5)
except StopIteration:
    pass

理想情况下,您还要收集样本并再次对其进行采样。

答案 1 :(得分:1)

我认为这就是生成器。以下是通过发电机/产量进行Fisher-Yates-Knuth采样的示例,您可以逐个获取事件并在需要时停止。

代码已更新

import random
import numpy
import array

class populationFYK(object):
    """
    Implementation of the Fisher-Yates-Knuth shuffle
    """
    def __init__(self, population):
        self._population = population      # reference to the population
        self._length     = len(population) # lengths of the sequence
        self._index      = len(population)-1 # last unsampled index
        self._popidx     = array.array('i', range(0,self._length))

        # array module vs numpy
        #self._popidx     = numpy.empty(self._length, dtype=numpy.int32)
        #for k in range(0,self._length):
        #    self._popidx[k] = k


    def swap(self, idx_a, idx_b):
        """
        Swap two elements in population
        """
        temp = self._popidx[idx_a]
        self._popidx[idx_a] = self._popidx[idx_b]
        self._popidx[idx_b] = temp

    def sample(self):
        """
        Yield one sampled case from population
        """
        while self._index >= 0:
            idx = random.randint(0, self._index) # index of the sampled event

            if idx != self._index:
                self.swap(idx, self._index)

            sampled = self._population[self._popidx[self._index]] # yielding it

            self._index -= 1 # one less to be sampled

            yield sampled

    def index(self):
        return self._index

    def restart(self):
        self._index = self._length - 1
        for k in range(0,self._length):
            self._popidx[k] = k

if __name__=="__main__":
    population = [1,3,6,8,9,3,2]

    gen = populationFYK(population)

    for k in gen.sample():
        print(k)

答案 2 :(得分:0)

我写了(在Python 2.7.9中)一个随机采样器生成器(索引),其速度仅取决于样本大小(它应该是O(ns log(ns)),其中ns是样本大小)。因此,当样本数量与人口规模相比较小时,快速,因为根本不依赖于人口规模。它不构建任何填充集合,它只选择随机索引并对采样索引使用一种bisect方法以避免重复并保持排序。给定一个可迭代population,这里是如何使用itersample生成器:

import random
sampler=itersample(len(population))
next_pick=sampler.next() # pick the next random (index of) element

import random
sampler=itersample(len(population))
sample=[]
for index in sampler:
    # do something with (index of) picked element
    sample.append(index) # build a sample
    if some_condition: # stop sampling when needed
        break

如果您需要实际元素而不仅仅是索引,只需在需要时将population iterable应用于索引(第一个和第二个示例分别为population[sampler.next()]population[index])。

一些测试的结果显示速度不依赖于人口规模,因此如果您需要从100亿人口中随机选择10个元素,您只需支付10个(请记住,我们事先并不知道我们将选择多少元素,否则您最好使用random.sample)。

Sampling 1000 from 1000000
Using itersample 0.0324 s

Sampling 1000 from 10000000
Using itersample 0.0304 s

Sampling 1000 from 100000000
Using itersample 0.0311 s

Sampling 1000 from 1000000000
Using itersample 0.0329 s

其他测试证实,样本量的运行时间略大于线性:

Sampling 100 from 1000000000
Using itersample 0.0018 s

Sampling 1000 from 1000000000
Using itersample 0.0294 s

Sampling 10000 from 1000000000
Using itersample 0.4438 s

Sampling 100000 from 1000000000
Using itersample 8.8739 s

最后,这是生成器函数itersample

import random
def itersample(c): # c: population size
    sampled=[]
    def fsb(a,b): # free spaces before middle of interval a,b
        fsb.idx=a+(b+1-a)/2
        fsb.last=sampled[fsb.idx]-fsb.idx if len(sampled)>0 else 0
        return fsb.last
    while len(sampled)<c:
        sample_index=random.randrange(c-len(sampled))
        a,b=0,len(sampled)-1
        if fsb(a,a)>sample_index:
            yielding=sample_index
            sampled.insert(0,yielding)
            yield yielding
        elif fsb(b,b)<sample_index+1:
            yielding=len(sampled)+sample_index
            sampled.insert(len(sampled),yielding)
            yield yielding
        else: # sample_index falls inside sampled list
            while a+1<b:
                if fsb(a,b)<sample_index+1:
                    a=fsb.idx
                else:
                    b=fsb.idx
            yielding=a+1+sample_index
            sampled.insert(a+1,yielding)
            yield yielding

答案 3 :(得分:0)

您可以通过选择范围[0 ... N [并将它们视为索引的K非重复随机数]从大小为N的群体中获取大小为K的样本。

选项a)

您可以使用众所周知的样本方法生成此类索引样本。

random.sample(xrange(N), K)

来自Python docs about random.sample

  

要从一系列整数中选择样本,请使用xrange()对象作为参数。这对于从大量人群中采样来说特别快且节省空间

选项b)

如果您不喜欢random.sample已经返回列表而不是非重复随机数的延迟生成器,那么您可以使用Format-Preserving Encryption来加密计数器。

通过这种方式,您可以获得一个真正的随机索引生成器,您可以随意选择并随时停止,而不会获得任何重复项,从而为您提供动态大小的样本集。

我们的想法是构建一个加密方案来加密从0到N的数字。现在,每次你想从你的人口中获取一个样本时,你为你的加密选择一个随机密钥并开始加密来自0,1,2,...开始(这是计数器)。由于每个良好的加密都会创建一个随机的1:1映射,因此最终会使用非重复的随机整数作为索引。 这个延迟生成期间的存储要求只是初始密钥加上计数器的当前值。

这个想法已经在Generating non-repeating random numbers in Python中讨论过了。甚至还有链接的python片段:formatpreservingencryption.py

使用此代码段的示例代码可以像这样实现:

def itersample(population):
    # Get the size of the population
    N = len(population)
    # Get the number of bits needed to represent this number
    bits = (N-1).bit_length()
    # Generate some random key
    key = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
    # Create a new crypto instance that encrypts binary blocks of width <bits>
    # Thus, being able to encrypt all numbers up to the nearest power of two
    crypter = FPEInteger(key=key, radix=2, width=bits)

    # Count up 
    for i in xrange(1<<bits):
        # Encrypt the current counter value
        x = crypter.encrypt(i)
        # If it is bigger than our population size, just skip it
        # Since we generate numbers up to the nearest power of 2, 
        # we have to skip up to half of them, and on average up to one at a time
        if x < N:
            # Return the randomly chosen element
            yield population[x]

答案 4 :(得分:0)

这是另一个想法。因此,对于庞大的人口,我们希望保留一些有关所选记录的信息。在您的情况下,您为每个选定的记录保留一个整数索引 - 32位或64位整数,以及一些代码来进行选择/未选择的合理搜索。如果选择了大量记录,则此记录保存可能会令人望而却步。我建议使用Bloom过滤器来选择所设置的indeces。假阳性匹配是可能的,但是假阴性不是,因此没有获得重复记录的风险。它确实引入了轻微的偏见 - 假阳性记录将被排除在抽样之外。但是内存效率很好,1%误报概率需要每个元素少于10位。因此,如果您选择5%的人口并且误报率为1%,那么您错过了0.0005的人口,具体取决于要求可能没问题。如果您想要较低的误报,请使用更多位。但是内存效率会好很多,不过我预计每个记录样本会有更多的代码执行。

抱歉,没有代码