【问题标题】:How to do weighted random sample of categories in python如何在python中进行类别的加权随机样本
【发布时间】:2011-09-19 22:09:13
【问题描述】:

给定一个元组列表,其中每个元组包含一个概率和一个项目,我想根据其概率对一个项目进行抽样。例如,给出列表 [ (.3, 'a'), (.4, 'b'), (.3, 'c')] 我想在 40% 的时间内对 'b' 进行采样。

在 python 中执行此操作的规范方法是什么?

我查看了 random 模块,它似乎没有适当的功能,并且在 numpy.random 中,虽然它具有多项式函数,但似乎并没有以一种很好的形式返回这个问题的结果。我基本上是在 matlab 中寻找类似 mnrnd 的东西。

非常感谢。

感谢您这么快给出所有答案。澄清一下,我不是在寻找如何编写采样方案的解释,而是指出一种简单的方法来从给定一组对象和权重的多项分布中采样,或者被告知不存在这样的函数在标准库中,所以应该自己编写。

【问题讨论】:

标签: python statistics numpy probability random-sample


【解决方案1】:

这可能会做你想要的:

numpy.array([.3,.4,.3]).cumsum().searchsorted(numpy.random.sample(5))

【讨论】:

  • 适用于数字选择,但可以通过将字典拆分为概率数组和值数组并返回 sampler 函数来概括。尽管很复杂,并且假设一个返回一个采样器因此不必重新计算累积和,+1 因为由于 numpy 进行二进制搜索,因此对于大型数组是有效的。
  • 我希望有人能更好地解释这一点。如果[.3,.4,.3] 是权重,我们应该如何获取附加到它们的值?
  • @DanielQuinn 在这种情况下,让采样概率为sampled,值为vals=['a','b','c']。那么,采样值就是map(lambda x:vals[x], sampled)
【解决方案2】:

由于没有人使用 numpy.random.choice 函数,这里有一个可以在一个紧凑的行中生成您需要的内容:

numpy.random.choice(['a','b','c'], size = 20, p = [0.3,0.4,0.3])

【讨论】:

  • 这是最简单的解决方案。 random.choice 的 p 参数是否相对较新?
  • @velotron 我相信已经存在了很长一段时间(鉴于答案在 2015 年 9 月有效)。
  • 这在 2017 年对我来说非常有用,我只是好奇,因为 2011 年原始问题出现时的解决方案都更长。
  • 一行,多年后直观易懂。这应该是奖励的答案。
【解决方案3】:
import numpy

n = 1000
pairs = [(.3, 'a'), (.3, 'b'), (.4, 'c')]
probabilities = numpy.random.multinomial(n, zip(*pairs)[0])
result = zip(probabilities, zip(*pairs)[1])
# [(299, 'a'), (299, 'b'), (402, 'c')]
[x[0] * x[1] for x in result]
# ['aaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccc']

您希望以何种方式收到结果?

【讨论】:

  • @John:我已将reduce()-madness 换成了更具可读性的列表综合。 (我不确定如果我现在编辑我的帖子,你是否会收到通知......)
  • @John:FWIW,恕我直言sholte 的答案要简单得多。并且可以扩展以非常简单的方式处理任意项目(如图所示)。谢谢
  • @eat:您可以修改我的代码以得到与 sholte 类似的结果:numpy.random.multinomial(5, [.3, .3, .4]) - 这可能会返回:array([2, 2, 1])。 sholte 的等效结果可能如下所示:array([1, 0, 2, 0, 1])。我看不出他的代码会比这更简单。如果您关心订单,他的结果会更有用,如果您不关心,我的结果会更有用。无论如何,我已经添加了代码来接受他的输入,将其处理到我的代码中,并将结果带回我认为可能对他有用的形式。
  • 我已经更新了我的答案。请注意,在评论时,我主要关心的是代码的可读性。无论如何,您的答案是正确的,经过一番精神角力,它揭示了multinomial 的美丽。谢谢
  • 是的,它真的不可读 :) 我只是坚持使用 John 提供的输入,这就是为什么它变得有点难看 :) - wrestling 是一个很好的表达方式.你的现在看起来很干净。
【解决方案4】:

如果您的概率非常适合百分比等,您可以做一些技巧。

例如,如果您对百分比没问题,以下将起作用(以高内存开销为代价):

但使用任意浮点概率的“真正”方法是在构建累积分布后对其进行采样。这相当于将单位区间[0,1]细分为3个线段,分别标记为'a'、'b'和'c';然后在单位间隔上随机选择一个点,看看它是哪条线段。

#!/usr/bin/python3
def randomCategory(probDict):
    """
        >>> dist = {'a':.1, 'b':.2, 'c':.3, 'd':.4}

        >>> [randomCategory(dist) for _ in range(5)]
        ['c', 'c', 'a', 'd', 'c']

        >>> Counter(randomCategory(dist) for _ in range(10**5))
        Counter({'d': 40127, 'c': 29975, 'b': 19873, 'a': 10025})
    """
    r = random.random() # range: [0,1)
    total = 0           # range: [0,1]
    for value,prob in probDict.items():
        total += prob
        if total>r:
            return value
    raise Exception('distribution not normalized: {probs}'.format(probs=probDict))

必须注意返回值的方法,即使它们的概率为 0。幸运的是,这种方法不会,但以防万一,可以插入 if prob==0: continue


为了记录,这里是做这件事的骇人听闻的方法:

import random

def makeSampler(probDict):
    """
        >>> sampler = makeSampler({'a':0.3, 'b':0.4, 'c':0.3})
        >>> sampler.sample()
        'a'
        >>> sampler.sample()
        'c'
    """
    oneHundredElements = sum(([val]*(prob*100) for val,prob in probDict.items()), [])
    def sampler():
        return random.choice(oneHundredElements)
    return sampler

但是,如果您没有解决问题……这实际上可能是最快的方法。 =)

【讨论】:

  • -1 表示带有百分比的“hackish”方式,但 +10 表示累积分布!
  • 我有一个疑问:probDict.items() 没有定义的顺序,它不会总是以相同的顺序返回 (k, v) 对吗?会导致分布不均?
  • @phant0m:这不是问题,因为您输入的顺序无关紧要。此处给出的任何算法都适用于[('a',0.2),('b',0.8)][('b',0.8),('a',0.2)]。另一种方法是选择一个随机顺序并始终使用该顺序,方法是返回通常的sample() 生成器。我以前的解决方案就是这样做的,而且内存更大。除非您可以利用一种策略将它们预排序为一些奇怪的分形结构,否则没有任何收获,这样执行二进制搜索会显着加快具有许多可能值的分布...
  • 我不确定这是不是我的意思:如果你第一次调用 randomCategory(),probDict.items() 可能会返回 [('a',0.2),('b',0.8)],但如果你第二次调用它,它可能会返回[('b',0.8),('a',0.2)]。可能是一个类比:假设你有一个大桶(b:0.8)和一个小桶(a:0.2)。你把硬币扔进去,总是命中一个,永远不会错过。如果您要连续移动桶(在 1d 中思考) - 或者更确切地说是切换 - 这会影响实验的结果吗?当我现在考虑这个类比时,我会说不:)
【解决方案5】:

如何在一个列表中创建 3 个“a”、4 个“b”和 3 个“c”,然后随机选择一个。通过足够的迭代,您将获得所需的概率。

【讨论】:

    【解决方案6】:

    我认为多项式函数仍然是一种以随机顺序获取分布样本的相当简单的方法。这只是一种方式

    import numpy
    from itertools import izip
    
    def getSamples(input, size):
        probabilities, items = zip(*input)
        sampleCounts = numpy.random.multinomial(size, probabilities)
        samples = numpy.array(tuple(countsToSamples(sampleCounts, items)))
        numpy.random.shuffle(samples)
        return samples
    
    def countsToSamples(counts, items):
        for value, repeats in izip(items, counts):
            for _i in xrange(repeats):
                yield value
    

    输入是指定的[(.2, 'a'), (.4, 'b'), (.3, 'c')],大小是您需要的样本数。

    【讨论】:

      【解决方案7】:

      我不确定这是否是按照你的要求做的 pythonic 方式,但你可以使用 random.sample(['a','a','a','b','b','b','b','c','c','c'],k) 其中 k 是您想要的样本数。

      对于更稳健的方法,根据累积概率将单位区间一分为二,并使用 random.random() 从均匀分布 (0,1) 中提取。在这种情况下,子区间将是 (0,.3)(.3,.7)(.7,1)。您可以根据元素所在的子区间来选择元素。

      【讨论】:

      • 关于你对unit-interval方法的描述,你必须处理它落在区间之间的情况,如果有0长度的区间。
      • 0到1之间的随机数位于区间之间的概率为0。0长度的区间出现的概率为0。
      • 数学上,是的。然而,浮点运算却不是这样。
      • 这只有在区间的端点可以用浮点数表示时才有意义,并且如果 1/(2^53) 的额外概率很重要,那么操作可能应该滚动他/她自己的函数。
      【解决方案8】:

      只是受到sholte 非常直接(且正确)的回答的启发:我将演示扩展它以处理任意项目是多么容易,例如:

      In []: s= array([.3, .4, .3]).cumsum().searchsorted(sample(54))
      In []: c, _= histogram(s, bins= arange(4))
      In []: [item* c[i] for i, item in enumerate('abc')]
      Out[]: ['aaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccc']
      

      更新
      根据phant0m的反馈,原来可以基于multinomial实现更直接的解决方案,比如:

      In []: s= multinomial(54, [.3, .4, .3])
      In []: [item* s[i] for i, item in enumerate('abc')]
      Out[]: ['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccc']
      

      恕我直言,我们对基于empirical cdfmultinomial 的采样进行了很好的总结,得出了相似的结果。因此,总而言之,选择最适合您目的的。

      【讨论】:

        【解决方案9】:

        这可能是边际收益,但我是这样做的:

        import scipy.stats as sps
        N=1000
        M3 = sps.multinomial.rvs(1, p = [0.3,0.4,0.3], size=N, random_state=None)
        M3a = [ np.where(r==1)[0][0] for r in M3 ] # convert 1-hot encoding to integers
        

        这类似于@eat 的回答。

        【讨论】:

          猜你喜欢
          • 2012-10-14
          • 1970-01-01
          • 1970-01-01
          • 2017-09-18
          • 2021-07-19
          • 2020-06-04
          • 2017-05-11
          • 2012-01-31
          • 2016-03-11
          相关资源
          最近更新 更多