random.choice 的加权版本

Posted

技术标签:

【中文标题】random.choice 的加权版本【英文标题】:A weighted version of random.choice 【发布时间】:2022-01-19 13:30:32 【问题描述】:

我需要编写一个加权版本的 random.choice(列表中的每个元素都有不同的被选中概率)。这是我想出的:

def weightedChoice(choices):
    """Like random.choice, but each element can have a different chance of
    being selected.

    choices can be any iterable containing iterables with two items each.
    Technically, they can have more than two items, the rest will just be
    ignored.  The first item is the thing being chosen, the second item is
    its weight.  The weights can be any numeric values, what matters is the
    relative differences between them.
    """
    space = 
    current = 0
    for choice, weight in choices:
        if weight > 0:
            space[current] = choice
            current += weight
    rand = random.uniform(0, current)
    for key in sorted(space.keys() + [current]):
        if rand < key:
            return choice
        choice = space[key]
    return None

这个函数对我来说似乎过于复杂,而且丑陋。我希望这里的每个人都可以提供一些改进它的建议或替代方法。对我来说,效率不如代码的简洁性和可读性重要。

【问题讨论】:

【参考方案1】:

从 1.7.0 版开始,NumPy 有一个支持概率分布的choice 函数。

from numpy.random import choice
draw = choice(list_of_candidates, number_of_items_to_pick,
              p=probability_distribution)

注意probability_distribution 是一个与list_of_candidates 顺序相同的序列。您还可以使用关键字replace=False 来更改行为,以便绘制的项目不会被替换。

【讨论】:

根据我的测试,这比 random.choices 对于单个调用慢了一个数量级。如果您需要大量随机结果,通过调整number_of_items_to_pick 一次性选择它们非常重要。如果这样做,速度会快一个数量级。 这不适用于元组等(“ValueError: a must be 1-dimensional”),所以在这种情况下,可以要求 numpy 选择 index 到列出,即len(list_of_candidates),然后做list_of_candidates[draw] 现在你在 random 模块中得到了选择方法 Document 说choices() 使用浮点算法提高速度choice() 使用整数算法减少偏差。这可能是 choices()choice() 更快的选择的原因【参考方案2】:

从 Python 3.6 开始,choices 模块中有一个方法 choices

In [1]: import random

In [2]: random.choices(
...:     population=[['a','b'], ['b','a'], ['c','b']],
...:     weights=[0.2, 0.2, 0.6],
...:     k=10
...: )

Out[2]:
[['c', 'b'],
 ['c', 'b'],
 ['b', 'a'],
 ['c', 'b'],
 ['c', 'b'],
 ['b', 'a'],
 ['c', 'b'],
 ['b', 'a'],
 ['c', 'b'],
 ['c', 'b']]

请注意,random.choices 将根据docs 对替换进行采样:

返回一个k 大小的元素列表,该列表是从具有替换的总体中选择的。

注意回答的完整性:

当一个抽样单元从一个有限的总体中抽取并返回时 对该人群,在其特征被记录后, 在抽取下一个单元之前,抽样被称为“与 替换”。这基本上意味着每个元素可以选择超过 一次。

如果您需要在不替换的情况下进行采样,那么正如@ronan-paixão's brilliant answer 所述,您可以使用numpy.choice,其replace 参数控制此类行为。

【讨论】:

这比 numpy.random.choice 快得多。从 8 个加权项目的列表中挑选 10,000 次,numpy.random.choice 耗时 0.3286 秒,而 random.choices 耗时 0.0416 秒,大约快 8 倍。 @AntonCodes 这个例子是精心挑选的。 numpy 将有一些 random.choices 没有的恒定时间开销,所以当然它在 8 个项目的小列表上会更慢,如果你从这样的列表中选择 10k 次,你是对的。但是对于列表较大的情况(取决于您的测试方式,我看到 100-300 个元素之间的断点),np.random.choice 开始以相当大的差距超越random.choices。例如,包括规范化步骤和 numpy 调用,对于 10k 个元素的列表,我得到了比 random.choices 快近 4 倍的速度。 这应该是基于@AntonCodes 报告的性能改进的新答案。【参考方案3】:
def weighted_choice(choices):
   total = sum(w for c, w in choices)
   r = random.uniform(0, total)
   upto = 0
   for c, w in choices:
      if upto + w >= r:
         return c
      upto += w
   assert False, "Shouldn't get here"

【讨论】:

您可以通过反转 for 循环中的语句来删除操作并节省一小段时间:upto +=w; if upto &gt; r 通过删除 upto 来保存一个变量,每次只减去 r 的权重。然后比较是if r &lt; 0 @JnBrymn 你需要检查r &lt;= 0。考虑一个包含 1 个项目的输入集和一个 1.0 的卷。断言将失败。我纠正了答案中的错误。 @Sardathrion 您可以使用编译指示将 for 循环标记为部分循环:# pragma: no branch @mLstudent33 我不使用 Udacity。【参考方案4】:
    将权重排列成一个 累积分布。 使用 random.random() 随机选择 浮动0.0 &lt;= x &lt; total。 搜索 使用 bisect.bisect 作为分布 在http://docs.python.org/dev/library/bisect.html#other-examples 的示例中显示。
from random import random
from bisect import bisect

def weighted_choice(choices):
    values, weights = zip(*choices)
    total = 0
    cum_weights = []
    for w in weights:
        total += w
        cum_weights.append(total)
    x = random() * total
    i = bisect(cum_weights, x)
    return values[i]

>>> weighted_choice([("WHITE",90), ("RED",8), ("GREEN",2)])
'WHITE'

如果您需要做出多个选择,请将其拆分为两个函数,一个用于构建累积权重,另一个用于平分到随机点。

【讨论】:

这比 Ned 的回答更有效率。基本上,他不是通过选择进行线性(O(n))搜索,而是进行二进制搜索(O(log n))。 +1! 如果 random() 碰巧返回 1.0,元组索引超出范围 由于累积分布计算,这仍然在O(n) 中运行。 此解决方案在需要多次调用 weighted_choice 来获得同一组选择的情况下效果更好。在这种情况下,您可以创建一次累积总和并在每次调用时进行二进制搜索。 @JonVaughan random() 不能返回 1.0。根据文档,它在半开区间 [0.0, 1.0) 中返回一个结果,也就是说它可以准确地返回 0.0,但 不能 准确地返回 1.0 .它可以返回的最大值是 0.99999999999999988897769753748434595763683319091796875(Python 打印为 0.99999999999999999,并且是小于 1 的最大 64 位浮点数)。【参考方案5】:

如果你不介意使用 numpy,可以使用numpy.random.choice。

例如:

import numpy

items  = [["item1", 0.2], ["item2", 0.3], ["item3", 0.45], ["item4", 0.05]
elems = [i[0] for i in items]
probs = [i[1] for i in items]

trials = 1000
results = [0] * len(items)
for i in range(trials):
    res = numpy.random.choice(items, p=probs)  #This is where the item is selected!
    results[items.index(res)] += 1
results = [r / float(trials) for r in results]
print "item\texpected\tactual"
for i in range(len(probs)):
    print "%s\t%0.4f\t%0.4f" % (items[i], probs[i], results[i])

如果您事先知道需要进行多少选择,则可以不使用这样的循环:

numpy.random.choice(items, trials, p=probs)

【讨论】:

【参考方案6】:

从 Python v3.6 开始,random.choices 可用于从给定总体中返回具有可选权重的指定大小元素的 list

random.choices(population, weights=None, *, cum_weights=None, k=1)

population : list 包含独特的观察结果。 (如果为空,则引发IndexError

权重:更准确地说是进行选择所需的相对权重。

cum_weights:进行选择所需的累积权重。

k : 要输出的list 的大小(len)。 (默认len()=1


几个注意事项:

1) 它使用带替换的加权抽样,因此抽出的项目将在以后被替换。权重序列中的值本身并不重要,但它们的相对比例很重要。

np.random.choice 不同,它只能将概率作为权重,并且必须确保单个概率的总和达到 1 个标准,这里没有这样的规定。只要它们属于数字类型(int/float/fraction 除了Decimal 类型),它们仍然会执行。

>>> import random
# weights being integers
>>> random.choices(["white", "green", "red"], [12, 12, 4], k=10)
['green', 'red', 'green', 'white', 'white', 'white', 'green', 'white', 'red', 'white']
# weights being floats
>>> random.choices(["white", "green", "red"], [.12, .12, .04], k=10)
['white', 'white', 'green', 'green', 'red', 'red', 'white', 'green', 'white', 'green']
# weights being fractions
>>> random.choices(["white", "green", "red"], [12/100, 12/100, 4/100], k=10)
['green', 'green', 'white', 'red', 'green', 'red', 'white', 'green', 'green', 'green']

2) 如果 weightscum_weights 均未指定,则以相等的概率进行选择。如果提供了 weights 序列,则它必须与 population 序列的长度相同。

同时指定 weightscum_weights 会引发 TypeError

>>> random.choices(["white", "green", "red"], k=10)
['white', 'white', 'green', 'red', 'red', 'red', 'white', 'white', 'white', 'green']

3) cum_weights 通常是itertools.accumulate 函数的结果,在这种情况下非常方便。

来自链接的文档:

在内部,将相对权重转换为累积权重 在进行选择之前,因此提供累积权重可以节省 工作。

因此,为我们设计的案例提供 weights=[12, 12, 4]cum_weights=[12, 24, 28] 都会产生相同的结果,而后者似乎更快/更高效。

【讨论】:

【参考方案7】:

粗略,但可能就足够了:

import random
weighted_choice = lambda s : random.choice(sum(([v]*wt for v,wt in s),[]))

有效吗?

# define choices and relative weights
choices = [("WHITE",90), ("RED",8), ("GREEN",2)]

# initialize tally dict
tally = dict.fromkeys(choices, 0)

# tally up 1000 weighted choices
for i in xrange(1000):
    tally[weighted_choice(choices)] += 1

print tally.items()

打印:

[('WHITE', 904), ('GREEN', 22), ('RED', 74)]

假设所有权重都是整数。它们不必加起来为 100,我这样做只是为了使测试结果更易于解释。 (如果权重是浮点数,则将它们全部乘以 10,直到所有权重 >= 1。)

weights = [.6, .2, .001, .199]
while any(w < 1.0 for w in weights):
    weights = [w*10 for w in weights]
weights = map(int, weights)

【讨论】:

很好,不过我不确定我是否可以假设所有权重都是整数。 在这个例子中你的对象似乎会被复制。那将是低效的(将权重转换为整数的函数也是如此)。然而,如果整数权重很小,这个解决方案是一个很好的单线解决方案。 基元将被复制,但对象只会复制引用,而不是对象本身。 (这就是为什么您不能使用 [[]]*10 创建列表列表的原因 - 外部列表中的所有元素都指向同一个列表。 @PaulMcG 不;只有引用才会被复制。 Python 的类型系统没有原语的概念。即使使用例如,您也可以确认。一个int,您仍然可以通过执行[id(x) for x in ([99**99] * 100)] 之类的操作来获得对同一对象的大量引用,并观察id 在每次调用时返回相同的内存地址。【参考方案8】:

如果你有一个加权字典而不是一个列表,你可以这样写

items =  "a": 10, "b": 5, "c": 1  
random.choice([k for k in items for dummy in range(items[k])])

请注意,[k for k in items for dummy in range(items[k])] 会生成此列表 ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'c', 'b', 'b', 'b', 'b', 'b']

【讨论】:

这适用于小总人口值,但不适用于大数据集(例如,按州划分的美国人口最终会创建一个包含 3 亿个项目的工作列表)。 @Ryan 确实。它也不适用于非整数权重,这是另一种现实情况(例如,如果您将权重表示为选择概率)。【参考方案9】:

这是包含在 Python 3.6 标准库中的版本:

import itertools as _itertools
import bisect as _bisect

class Random36(random.Random):
    "Show the code included in the Python 3.6 version of the Random class"

    def choices(self, population, weights=None, *, cum_weights=None, k=1):
        """Return a k sized list of population elements chosen with replacement.

        If the relative weights or cumulative weights are not specified,
        the selections are made with equal probability.

        """
        random = self.random
        if cum_weights is None:
            if weights is None:
                _int = int
                total = len(population)
                return [population[_int(random() * total)] for i in range(k)]
            cum_weights = list(_itertools.accumulate(weights))
        elif weights is not None:
            raise TypeError('Cannot specify both weights and cumulative weights')
        if len(cum_weights) != len(population):
            raise ValueError('The number of weights does not match the population')
        bisect = _bisect.bisect
        total = cum_weights[-1]
        return [population[bisect(cum_weights, random() * total)] for i in range(k)]

来源:https://hg.python.org/cpython/file/tip/Lib/random.py#l340

【讨论】:

【参考方案10】:

一种非常基本且简单的加权选择方法如下:

np.random.choice(['A', 'B', 'C'], p=[0.3, 0.4, 0.3])

【讨论】:

【参考方案11】:
import numpy as np
w=np.array([ 0.4,  0.8,  1.6,  0.8,  0.4])
np.random.choice(w, p=w/sum(w))

【讨论】:

【参考方案12】:

我可能来不及贡献任何有用的东西,但这里有一个简单、简短且非常有效的 sn-p:

def choose_index(probabilies):
    cmf = probabilies[0]
    choice = random.random()
    for k in xrange(len(probabilies)):
        if choice <= cmf:
            return k
        else:
            cmf += probabilies[k+1]

无需对您的概率进行排序或使用您的 cmf 创建一个向量,一旦找到它的选择,它就会终止。内存:O(1),时间:O(N),平均运行时间~N/2。

如果你有权重,只需添加一行:

def choose_index(weights):
    probabilities = weights / sum(weights)
    cmf = probabilies[0]
    choice = random.random()
    for k in xrange(len(probabilies)):
        if choice <= cmf:
            return k
        else:
            cmf += probabilies[k+1]

【讨论】:

这有几个问题。从表面上看,有一些打错的变量名,并且没有给出使用它的理由,比如np.random.choice。但更有趣的是,有一种失败模式会引发异常。执行 probabilities = weights / sum(weights) 并不能保证 probabilities 的总和为 1;例如,如果weights[1,1,1,1,1,1,1],那么probabilities 的总和将仅为0.9999999999999998,小于random.random 的最大可能返回值(即0.9999999999999999)。那么choice &lt;= cmf是永远不会满足的。【参考方案13】:

如果您的加权选择列表是相对静态的,并且您希望频繁采样,您可以执行一个 O(N) 预处理步骤,然后使用 this related answer 中的函数在 O(1) 中进行选择。

# run only when `choices` changes.
preprocessed_data = prep(weight for _,weight in choices)

# O(1) selection
value = choices[sample(preprocessed_data)][0]

【讨论】:

【参考方案14】:

如果您碰巧有 Python 3,并且害怕安装 numpy 或编写自己的循环,您可以这样做:

import itertools, bisect, random

def weighted_choice(choices):
   weights = list(zip(*choices))[1]
   return choices[bisect.bisect(list(itertools.accumulate(weights)),
                                random.uniform(0, sum(weights)))][0]

因为您可以用一袋管道适配器构建任何东西!虽然……我必须承认,内德的回答虽然稍长一些,但更容易理解。

【讨论】:

【参考方案15】:

我查看了其他线程,并在我的编码风格中提出了这种变化,这将返回选择的索引以进行计数,但返回字符串很简单(注释返回替代):

import random
import bisect

try:
    range = xrange
except:
    pass

def weighted_choice(choices):
    total, cumulative = 0, []
    for c,w in choices:
        total += w
        cumulative.append((total, c))
    r = random.uniform(0, total)
    # return index
    return bisect.bisect(cumulative, (r,))
    # return item string
    #return choices[bisect.bisect(cumulative, (r,))][0]

# define choices and relative weights
choices = [("WHITE",90), ("RED",8), ("GREEN",2)]

tally = [0 for item in choices]

n = 100000
# tally up n weighted choices
for i in range(n):
    tally[weighted_choice(choices)] += 1

print([t/sum(tally)*100 for t in tally])

【讨论】:

【参考方案16】:

这是另一个使用 numpy 的 weighted_choice 版本。传入权重向量,它将返回一个由 0 组成的数组,其中包含一个 1,表示选择了哪个 bin。代码默认只进行一次抽奖,但您可以传入要进行的抽奖次数,并将返回每个抽奖箱的计数。

如果权重向量的总和不为 1,则会对其进行归一化处理。

import numpy as np

def weighted_choice(weights, n=1):
    if np.sum(weights)!=1:
        weights = weights/np.sum(weights)

    draws = np.random.random_sample(size=n)

    weights = np.cumsum(weights)
    weights = np.insert(weights,0,0.0)

    counts = np.histogram(draws, bins=weights)
    return(counts[0])

【讨论】:

【参考方案17】:

这取决于您要对分布进行采样的次数。

假设您要对分布进行 K 次采样。那么,当n是分布中的项目数时,每次使用np.random.choice()的时间复杂度是O(K(n + log(n)))

在我的例子中,我需要多次采样相同的分布,数量级为 10^3,其中 n 的数量级为 10^6。我使用了下面的代码,它预先计算了累积分布并在O(log(n)) 中对其进行采样。总体时间复杂度为O(n+K*log(n))

import numpy as np

n,k = 10**6,10**3

# Create dummy distribution
a = np.array([i+1 for i in range(n)])
p = np.array([1.0/n]*n)

cfd = p.cumsum()
for _ in range(k):
    x = np.random.uniform()
    idx = cfd.searchsorted(x, side='right')
    sampled_element = a[idx]

【讨论】:

【参考方案18】:

Sebastien Thurn 在免费的 Udacity 课程 AI for Robotics 中对此进行了演讲。基本上,他使用 mod 运算符% 制作索引权重的圆形数组,将变量 beta 设置为 0,随机选择一个索引, for 循环通过 N 其中 N 是索引的数量,在 for 循环中首先通过公式递增 beta:

beta = beta + 来自 0...2* Weight_max 的均匀样本

然后嵌套在for循环中,下面是一个while循环:

while w[index] < beta:
    beta = beta - w[index]
    index = index + 1

select p[index]

然后根据概率(或课程中介绍的情况下的归一化概率)对下一个索引进行重新采样。

在 Udacity 上找到第 8 课,视频第 21 号人工智能机器人,他正在讲粒子过滤器。

【讨论】:

【参考方案19】:

一个通用的解决方案:

import random
def weighted_choice(choices, weights):
    total = sum(weights)
    treshold = random.uniform(0, total)
    for k, weight in enumerate(weights):
        total -= weight
        if total < treshold:
            return choices[k]

【讨论】:

【参考方案20】:

另一种方法,假设我们的权重与元素数组中的元素具有相同的索引。

import numpy as np
weights = [0.1, 0.3, 0.5] #weights for the item at index 0,1,2
# sum of weights should be <=1, you can also divide each weight by sum of all weights to standardise it to <=1 constraint.
trials = 1 #number of trials
num_item = 1 #number of items that can be picked in each trial
selected_item_arr = np.random.multinomial(num_item, weights, trials)
# gives number of times an item was selected at a particular index
# this assumes selection with replacement
# one possible output
# selected_item_arr
# array([[0, 0, 1]])
# say if trials = 5, the the possible output could be 
# selected_item_arr
# array([[1, 0, 0],
#   [0, 0, 1],
#   [0, 0, 1],
#   [0, 1, 0],
#   [0, 0, 1]])

现在假设,我们必须在 1 次试验中抽取 3 件商品。您可以假设存在三个球 R、G、B,按重量数组给出的重量比,可能会出现以下结果:

num_item = 3
trials = 1
selected_item_arr = np.random.multinomial(num_item, weights, trials)
# selected_item_arr can give output like :
# array([[1, 0, 2]])

您还可以将要选择的项目数视为一组中的二项式/多项式试验的数量。所以,上面的例子仍然可以作为

num_binomial_trial = 5
weights = [0.1,0.9] #say an unfair coin weights for H/T
num_experiment_set = 1
selected_item_arr = np.random.multinomial(num_binomial_trial, weights, num_experiment_set)
# possible output
# selected_item_arr
# array([[1, 4]])
# i.e H came 1 time and T came 4 times in 5 binomial trials. And one set contains 5 binomial trails.

【讨论】:

【参考方案21】:

一种方法是对所有权重的总和进行随机化,然后将这些值用作每个 var 的限制点。这是作为生成器的粗略实现。

def rand_weighted(weights):
    """
    Generator which uses the weights to generate a
    weighted random values
    """
    sum_weights = sum(weights.values())
    cum_weights = 
    current_weight = 0
    for key, value in sorted(weights.iteritems()):
        current_weight += value
        cum_weights[key] = current_weight
    while True:
        sel = int(random.uniform(0, 1) * sum_weights)
        for key, value in sorted(cum_weights.iteritems()):
            if sel < value:
                break
        yield key

【讨论】:

【参考方案22】:

我需要做这样的事情非常快速非常简单,从寻找想法我最终构建了这个模板。这个想法是从 api 以 json 的形式接收加权值,这里是由 dict 模拟的。

然后将其转换为一个列表,其中每个值与其权重成比例地重复,然后使用 random.choice 从列表中选择一个值。

我尝试过运行 10、100 和 1000 次迭代。分布似乎相当稳固。

def weighted_choice(weighted_dict):
    """Input example: dict(apples=60, oranges=30, pineapples=10)"""
    weight_list = []
    for key in weighted_dict.keys():
        weight_list += [key] * weighted_dict[key]
    return random.choice(weight_list)

【讨论】:

【参考方案23】:

我不喜欢其中任何一个的语法。我真的很想指定这些项目是什么以及每个项目的权重是多少。我意识到我本可以使用random.choices,但我很快就编写了下面的类。

import random, string
from numpy import cumsum

class randomChoiceWithProportions:
    '''
    Accepts a dictionary of choices as keys and weights as values. Example if you want a unfair dice:


    choiceWeightDic = "1":0.16666666666666666, "2": 0.16666666666666666, "3": 0.16666666666666666
    , "4": 0.16666666666666666, "5": .06666666666666666, "6": 0.26666666666666666
    dice = randomChoiceWithProportions(choiceWeightDic)

    samples = []
    for i in range(100000):
        samples.append(dice.sample())

    # Should be close to .26666
    samples.count("6")/len(samples)

    # Should be close to .16666
    samples.count("1")/len(samples)
    '''
    def __init__(self, choiceWeightDic):
        self.choiceWeightDic = choiceWeightDic
        weightSum = sum(self.choiceWeightDic.values())
        assert weightSum == 1, 'Weights sum to ' + str(weightSum) + ', not 1.'
        self.valWeightDict = self._compute_valWeights()

    def _compute_valWeights(self):
        valWeights = list(cumsum(list(self.choiceWeightDic.values())))
        valWeightDict = dict(zip(list(self.choiceWeightDic.keys()), valWeights))
        return valWeightDict

    def sample(self):
        num = random.uniform(0,1)
        for key, val in self.valWeightDict.items():
            if val >= num:
                return key

【讨论】:

【参考方案24】:

为 random.choice() 提供一个预先加权的列表:

解决方案与测试:

import random

options = ['a', 'b', 'c', 'd']
weights = [1, 2, 5, 2]

weighted_options = [[opt]*wgt for opt, wgt in zip(options, weights)]
weighted_options = [opt for sublist in weighted_options for opt in sublist]
print(weighted_options)

# test

counts = c: 0 for c in options
for x in range(10000):
    counts[random.choice(weighted_options)] += 1

for opt, wgt in zip(options, weights):
    wgt_r = counts[opt] / 10000 * sum(weights)
    print(opt, counts[opt], wgt, wgt_r)

输出:

['a', 'b', 'b', 'c', 'c', 'c', 'c', 'c', 'd', 'd']
a 1025 1 1.025
b 1948 2 1.948
c 5019 5 5.019
d 2008 2 2.008

【讨论】:

【参考方案25】:

使用 numpy

def choice(items, weights):
    return items[np.argmin((np.cumsum(weights) / sum(weights)) < np.random.rand())]

【讨论】:

NumPy 已经有np.random.choice,正如自 2014 年以来已接受的答案中所述。滚动你自己的有什么意义?

以上是关于random.choice 的加权版本的主要内容,如果未能解决你的问题,请参考以下文章

对随机矩阵的所有行进行快速随机加权选择

Pandas 随机加权选择

Pandas 随机加权选择

numpy.random.choice和random.choice的输入参数有区别吗?

命令'random.choice(list)'没有给出任何输出[关闭]

为啥我的 numpy.random.choice 实现更快?