Взвешенный выбор короткий и простой

Если у меня есть набор элементов в списке. Я хочу выбрать из этого списка в соответствии с другим списком весов.

Например, моя коллекция ['one', 'two', 'three'] и веса [0.2, 0.3, 0.5], я бы ожидал, что метод даст мне "три" примерно в половине всех ничьих.

Каков самый простой способ сделать это?

Ответ 1

Поскольку версия 1.7 вы можете используйте numpy.random.choice():

elements = ['one', 'two', 'three'] 
weights = [0.2, 0.3, 0.5]

from numpy.random import choice
print(choice(elements, p=weights))

Ответ 2

Эта функция принимает два аргумента: список весов и список, содержащий объекты на выбор:

from numpy import cumsum
from numpy.random import rand
def weightedChoice(weights, objects):
    """Return a random item from objects, with the weighting defined by weights 
    (which must sum to 1)."""
    cs = cumsum(weights) #An array of the weights, cumulatively summed.
    idx = sum(cs < rand()) #Find the index of the first weight over a random value.
    return objects[idx]

Он не использует петли python.

Ответ 3

С Python 3.6 вы можете сделать взвешенный случайный выбор (с заменой) с помощью random.choices.

> выбор (совокупность, вес = нет, *, cum_weights = нет, k = 1)

Пример использования:

import random
random.choices(['one', 'two', 'three'], [0.2, 0.3, 0.5], k=10)
# ['three', 'two', 'three', 'three', 'three',
#  'three', 'three', 'two', 'two', 'one']

Ответ 4

Вы можете использовать многочленное распространение (от numpy), чтобы делать то, что вы хотите. Например.

elements = ['one', 'two', 'three'] 
weights = [0.2, 0.3, 0.5]


import numpy as np

indices = np.random.multinomial( 100, weights, 1)
#=> array([[20, 32, 48]]), YMMV

results = [] #A list of the original items, repeated the correct number of times.
for i, count in enumerate(indices[0]):
    results.extend( [elements[i]]*count )

Итак, элемент в первой позиции поднялся в 20 раз, элемент во втором положении поднялся 32 раза, а элемент в третьей позиции поднялся 48 раз, примерно то, что вы ожидали бы с учетом веса.

Если вам нелегко обернуть вокруг многомиллиардного распространения, я нашел полезную документацию.

Ответ 5

Если вы не хотите использовать numpy, вы можете следовать тому же методу с чем-то вроде этого:

from random import random
from itertools import takewhile

def accumulate(iterator):
    """Returns a cumulative sum of the elements.
    accumulate([1, 2, 3, 4, 5]) --> 1 3 6 10 15"""
    current = 0
    for value in iterator:
        current += value
        yield current

def weightedChoice(weights, objects):
    """Return a random item from objects, with the weighting defined by weights 
    (which must sum to 1)."""
    limit = random()
    return objects[sum(takewhile(bool, (value < limit for value in accumulate(weights))))]

Мы используем itertools.takewhile(), чтобы избежать проверки значений, как только мы достигнем точки, которую хотим остановить, в противном случае это по сути та же идея, что и Ответ Mischa Obrecht, просто без numpy.

Ответ 6

Как просто инициализировать список, чтобы он соответствовал вашим выборам с ожидаемыми весами. Здесь я составляю список из 100 значений, представляющих желаемый процент "тянуть".

>>> import random
>>> elements = ['one', 'two', 'three'] 
>>> weights = [0.2, 0.3, 0.5]
>>>
>>> # get "sum" of result list of lists (flattens list)
>>> choices = sum([[element] * int(weight * 100)for element, weight in zip(elements, weights)], [])
>>> random.choice(choices)
three

Это не кумулятивный, но похоже, что это может быть то, что вы ищете.

Ответ 7

Чтобы построить ответ Maus, это здорово, если вы хотите повторно получать взвешенные случайные значения, если вы хотите только одно значение, вы можете сделать это очень просто путем объединения numpy.random.multinomial() и itertools.compress():

from itertools import compress
from numpy.random import multinomial

def weightedChoice(weights, objects):
    """Return a random item from objects, with the weighting defined by weights 
    (which must sum to 1)."""
    return next(compress(objects, multinomial(1, weights, 1)[0]))