Взвешенная версия random.choice

Мне нужно было написать взвешенную версию 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, чтобы изменить поведение, чтобы нарисованные элементы не заменялись.

Ответ 2

Начиная с Python3.6, существует метод choices из random модуля.

Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.0.0 -- An enhanced Interactive Python. Type '?' for help.

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']]

И люди также упоминали, что есть numpy.random.choice, которые поддерживают весовые коэффициенты, НО он не поддерживает двухмерные массивы и так далее.

Таким образом, вы можете получить все, что захотите (см. обновление), со встроенным random.choices, если у вас есть 3.6.x Python.

UPDATE: Как @roganjosh любезно упомянуто, random.choices не может возвращать значения без замены, как это упоминалось в документации:

Вернуть список элементов размером k, выбранный из совокупности с заменой.

И @ronan-paixão блестящий ответ гласит, что numpy.choice имеет аргумент replace, который контролирует такое поведение.

Ответ 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"

Ответ 4

  • Упорядочить веса в кумулятивное распределение.
  • Используйте random.random(), чтобы выбрать случайный float 0.0 <= x < 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'

Если вам нужно сделать более одного выбора, разделите его на две функции: одну, чтобы собрать кумулятивные веса, а другую, чтобы делить пополам случайную точку.

Ответ 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

Сырой, но может быть достаточно:

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 раз, пока все весa >= 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)

Ответ 7

Если у вас есть взвешенный словарь вместо списка, вы можете написать это

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']

Ответ 8

В Python v3.6 random.choices можно использовать для возврата list элементов заданного размера из данной совокупности с дополнительными весами.

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

  • население: list, содержащее уникальные наблюдения. (Если пусто, поднимается IndexError)

  • веса: более точно относительные веса, необходимые для выбора.

  • cum_weights: кумулятивные веса, необходимые для выбора.

  • k: размер (len) list для вывода. (По умолчанию len()=1)


Немного оговорок:

1) Он использует взвешенную выборку с заменой, поэтому нарисованные элементы позже будут заменены. Значения в последовательности весов сами по себе не имеют значения, но их относительное отношение действительно.

В отличие от np.random.choice, который может принимать только вероятности в виде весов, а также должен обеспечивать суммирование индивидуальных вероятностей до 1 критерия, здесь нет таких правил. Пока они относятся к числовым типам (int/float/fraction except Decimal type), они все равно будут выполняться.

>>> 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) Если не указаны ни веса, ни cum_weights, выбор производится с равной вероятностью. Если задана последовательность весов, она должна быть такой же длины, как и последовательность популяции.

Задание как весов, так и cum_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] для нашего надуманного случая дает тот же результат, и последнее, кажется, работает быстрее/эффективнее.

Ответ 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

Мне понадобится сумма вариантов: 1, но это все равно работает

def weightedChoice(choices):
    # Safety check, you can remove it
    for c,w in choices:
        assert w >= 0


    tmp = random.uniform(0, sum(c for c,w in choices))
    for choice,weight in choices:
        if tmp < weight:
            return choice
        else:
            tmp -= weight
     raise ValueError('Negative values in input')

Ответ 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

Возможно, я слишком поздно внес что-нибудь полезное, но вот простой, короткий и очень эффективный фрагмент:

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]

Ответ 13

Если ваш список взвешенных вариантов относительно статичен и вам нужна частая выборка, вы можете сделать один шаг предварительной обработки O (N), а затем сделать выбор в 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

Я посмотрел на другую тему и придумал эту вариацию в моем стиле кодирования, это возвращает индекс выбора для целей подсчета голосов, но просто вернуть строку (прокомментированную альтернативу возврата):

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])

Ответ 15

Общее решение:

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]

Ответ 16

Вот еще одна версия weighted_choice, которая использует numpy. Перейдите в вектор весов и он вернет массив из 0, содержащий 1, указывающий, какой бункер был выбран. В коде по умолчанию используется только однократная ничья, но вы можете передать количество рисунков, которые будут сделаны, и будут возвращены отсчеты на каждый извлеченный бункер.

Если вектор весов не суммируется с 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 раз. Тогда временная сложность с использованием np.random.choice() каждый раз равна O(K(n + log(n))), когда 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

Один из способов - рандомизировать по сумме всех весов, а затем использовать значения в качестве предельных точек для каждого 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

Ответ 19

Используя NumPy

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

Ответ 20

Мне нужно было сделать что-то вроде этого очень быстро, очень просто, от поиска идей я наконец-то создал этот шаблон. Идея состоит в том, чтобы получить взвешенные значения в виде json из API, который здесь моделируется диктом.

Затем переведите его в список, в котором каждое значение повторяется пропорционально его весу, и просто используйте 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)

Ответ 21

Мне не понравился синтаксис любого из них. Я действительно хотел просто указать, что это были за вещи и какой вес у каждого из них. Я понимаю, что мог бы использовать 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

Ответ 22

Укажите 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