繁体   English   中英

python中的加权随机样本

[英]Weighted random sample in python

我正在寻找一个函数weighted_sample的合理定义,它不会为给定权重列表返回一个随机索引(这类似于

def weighted_choice(weights, random=random):
    """ Given a list of weights [w_0, w_1, ..., w_n-1],
        return an index i in range(n) with probability proportional to w_i. """
    rnd = random.random() * sum(weights)
    for i, w in enumerate(weights):
        if w<0:
            raise ValueError("Negative weight encountered.")
        rnd -= w
        if rnd < 0:
            return i
    raise ValueError("Sum of weights is not positive")

给出一个具有恒定权重的分类分布)但是随机抽样的那些k没有替换 ,就像random.sample行为与random.choice相比。

就像weighted_choice可以写成

lambda weights: random.choice([val for val, cnt in enumerate(weights)
    for i in range(cnt)])

weighted_sample可以写成

lambda weights, k: random.sample([val for val, cnt in enumerate(weights)
    for i in range(cnt)], k)

但我想要一个解决方案,不需要我将权重解析为(可能是巨大的)列表。

编辑:如果有任何好的算法可以返回一个直方图/频率列表(与参数weights格式相同)而不是一系列索引,这也是非常有用的。

从你的代码:..

weight_sample_indexes = lambda weights, k: random.sample([val 
        for val, cnt in enumerate(weights) for i in range(cnt)], k)

..我认为权重是正整数,而“没有替换”你的意思是没有替换解开的序列。

下面是基于random.sample和O溶液(log n)的__getitem__

import bisect
import random
from collections import Counter, Sequence

def weighted_sample(population, weights, k):
    return random.sample(WeightedPopulation(population, weights), k)

class WeightedPopulation(Sequence):
    def __init__(self, population, weights):
        assert len(population) == len(weights) > 0
        self.population = population
        self.cumweights = []
        cumsum = 0 # compute cumulative weight
        for w in weights:
            cumsum += w   
            self.cumweights.append(cumsum)  
    def __len__(self):
        return self.cumweights[-1]
    def __getitem__(self, i):
        if not 0 <= i < len(self):
            raise IndexError(i)
        return self.population[bisect.bisect(self.cumweights, i)]

total = Counter()
for _ in range(1000):
    sample = weighted_sample("abc", [1,10,2], 5)
    total.update(sample)
print(sample)
print("Frequences %s" % (dict(Counter(sample)),))

# Check that values are sane
print("Total " + ', '.join("%s: %.0f" % (val, count * 1.0 / min(total.values()))
                           for val, count in total.most_common()))

产量

['b', 'b', 'b', 'c', 'c']
Frequences {'c': 2, 'b': 3}
Total b: 10, c: 2, a: 1

您要创建的是非均匀随机分布。 这样做的一个坏方法是创建一个巨大的数组,其输出符号与权重成比例。 因此,如果a的可能性是b的5倍,则创建一个比b的数量多5倍的数组。 这适用于权重甚至是彼此的倍数的简单分布。 如果你想要99.99%a和.01%b怎么办? 你必须创建10000个插槽。

有个更好的方法。 具有N个符号的所有非均匀分布可以被分解为一系列n-1个二进制分布,每个分布都是相同的。

因此,如果你有这样的解压缩,你首先通过从1 - N-1生成一个统一的随机数来随机选择二进制分布

u32 dist = randInRange( 1, N-1 ); // generate a random number from 1 to N;

然后说所选择的分布是一个带有两个符号a和b的二元分布,a的概率为0-alpha,b的概率为alpha-1:

float f = randomFloat();
return ( f > alpha ) ? b : a;

如何分解任何非均匀随机分布有点复杂。 基本上你创造了N-1'水桶'。 选择具有最低概率的符号和具有最高概率的符号,并将它们的权重按比例分配到第一个二进制分布中。 然后删除最小的符号,并删除用于创建此二进制分发的较大的权重。 并重复此过程,直到您没有符号。

如果您想使用此解决方案,我可以为此发布c ++代码。

如果为random.sample()构造正确的数据结构以进行操作,则根本不需要定义新函数。 只需使用random.sample()

这里, __getitem__()是O(n),其中n是具有权重的不同项目的数量。 但它在内存中很紧凑,只需要存储(weight, value)对。 我在练习中使用了类似的课程,而且我的目的很快。 注意,此实现假定整数权重。

class SparseDistribution(object):
    _cached_length = None

    def __init__(self, weighted_items):
        # weighted items are (weight, value) pairs
        self._weighted_items = []
        for item in weighted_items:
            self.append(item)

    def append(self, weighted_item):
        self._weighted_items.append(weighted_item)
        self.__dict__.pop("_cached_length", None)

    def __len__(self):
        if self._cached_length is None:
            length = 0
            for w, v in self._weighted_items:
                length += w
            self._cached_length = length
        return self._cached_length

    def __getitem__(self, index):
        if index < 0 or index >= len(self):
            raise IndexError(index)
        for w, v in self._weighted_items:
            if index < w:
                return v
        raise Exception("Shouldn't have happened")

    def __iter__(self):
        for w, v in self._weighted_items:
            for _ in xrange(w):
                yield v

然后,我们可以使用它:

import random

d = SparseDistribution([(5, "a"), (2, "b")])
d.append((3, "c"))

for num in (3, 5, 10, 11):
    try:
        print random.sample(d, num)
    except Exception as e:
        print "{}({!r})".format(type(e).__name__, str(e))

导致:

['a', 'a', 'b']
['b', 'a', 'c', 'a', 'b']
['a', 'c', 'a', 'c', 'a', 'b', 'a', 'a', 'b', 'c']
ValueError('sample larger than population')

由于我目前最感兴趣的是结果的直方图,我想到了使用numpy.random.hypergeometric的以下解决方案(不幸的是,对于ngood < 1nbad < 1nsample < 1的边界情况,它有不良行为,所以这些案件需要单独检查。)

def weighted_sample_histogram(frequencies, k, random=numpy.random):
    """ Given a sequence of absolute frequencies [w_0, w_1, ..., w_n-1],
    return a generator [s_0, s_1, ..., s_n-1] where the number s_i gives the
    absolute frequency of drawing the index i from an urn in which that index is
    represented by w_i balls, when drawing k balls without replacement. """
    W = sum(frequencies)
    if k > W:
        raise ValueError("Sum of absolute frequencies less than number of samples")
    for frequency in frequencies:
        if k < 1 or frequency < 1:
            yield 0
        else:
            W -= frequency
            if W < 1:
                good = k
            else:
                good = random.hypergeometric(frequency, W, k)
            k -= good
            yield good
    raise StopIteration

我很乐意就如何改进这一点或为什么这可能不是一个好的解决方案发表任何意见。

实现这个(和其他加权随机事物)的python包现在在http://github.com/Anaphory/weighted_choice上

另一种方法

from typing import List, Any
import numpy as np

def weighted_sample(choices: List[Any], probs: List[float]):
    """
    Sample from `choices` with probability according to `probs`
    """
    probs = np.concatenate(([0], np.cumsum(probs)))
    r = random.random()
    for j in range(len(choices) + 1):
        if probs[j] < r <= probs[j + 1]:
            return choices[j]

例:

aa = [0,1,2,3]
probs = [0.1, 0.8, 0.0, 0.1]
np.average([weighted_sample(aa, probs) for _ in range(10000)])

Out: 1.0993

样品非常快。 因此,除非你有很多兆字节需要处理,所以sample()应该没问题。

在我的机器上,需要1.655秒来从10000个长度100中取出1000个样本。从10000000个元素遍历100000个长度为100的样本需要12.98秒。

from random import sample,random
from time import time

def generate(n1,n2,n3):
    w = [random() for x in range(n1)]

    print len(w)

    samples = list()
    for i in range(0,n2):
        s = sample(w,n3)
        samples.append(s)

    return samples

start = time()
size_set = 10**7
num_samples = 10**5
length_sample = 100
samples = generate(size_set,num_samples,length_sample)
end = time()

allsum=0
for row in samples:
    sum = reduce(lambda x, y: x+y,row)
    allsum+=sum

print 'sum of all elements',allsum

print '%f seconds for %i samples of %i length %i'%((end-start),size_set,num_sam\
ples,length_sample)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM