Как я могу получить взвешенный случайный выбор из класса Python Counter?
У меня есть программа, в которой я отслеживаю успех различных вещей, используя collections.Counter
- каждый успех вещи увеличивает соответствующий счетчик:
import collections
scoreboard = collections.Counter()
if test(thing):
scoreboard[thing]+ = 1
Затем, для будущих тестов, я хочу перекоситься в сторону вещей, которые принесли наибольший успех. Counter.elements()
казался идеальным для этого, так как он возвращает элементы (в произвольном порядке), повторяемые несколько раз, равные счетчику. Поэтому я решил, что могу просто сделать:
import random
nextthing=random.choice(scoreboard.elements())
Но нет, что вызывает TypeError: объект типа 'itertools.chain' не имеет len(). Хорошо, поэтому random.choice
не может работать с итераторами. Но в этом случае длина известна (или познаваема) - она sum(scoreboard.values())
.
Я знаю базовый алгоритм для итерации через список неизвестной длины и довольно аккуратный выбор элемента наугад, но я подозреваю, что там что-то более элегантное. Что мне делать здесь?
Ответы
Ответ 1
Вы можете сделать это довольно легко, используя itertools.islice
, чтобы получить N-й элемент итерации:
>>> import random
>>> import itertools
>>> import collections
>>> c = collections.Counter({'a': 2, 'b': 1})
>>> i = random.randrange(sum(c.values()))
>>> next(itertools.islice(c.elements(), i, None))
'a'
Ответ 2
Вы можете обернуть итератор в list()
, чтобы преобразовать его в список для random.choice()
:
nextthing = random.choice(list(scoreboard.elements()))
Недостатком здесь является то, что это расширяет список в памяти, вместо того, чтобы обращаться к нему по отдельности, как обычно с итератором.
Если вы хотите решить эту проблему, этот алгоритм, вероятно, хороший выбор.
Ответ 3
В следующем получится случайный элемент, в котором оценка - это вес для того, как часто возвращать этот элемент.
import random
def get_random_item_weighted(scoreboard):
total_scoreboard_value = sum(scoreboard.values())
item_loc = random.random() * total_scoreboard_value
current_loc = 0
for item, score in scoreboard.items():
current_loc += score
if current_loc > item_loc:
return item
например, если имеется 2 элемента:
item1 имеет оценку 5
item2 имеет оценку 10
item2 будет возвращен дважды так же часто, как item1
Ответ 4
Другой вариант с итерацией:
import collections
from collections import Counter
import random
class CounterElementsRandomAccess(collections.Sequence):
def __init__(self, counter):
self._counter = counter
def __len__(self):
return sum(self._counter.values())
def __getitem__(self, item):
for i, el in enumerate(self._counter.elements()):
if i == item:
return el
scoreboard = Counter('AAAASDFQWERQWEQWREAAAAABBBBCCDDVBSDF')
score_elements = CounterElementsRandomAccess(scoreboard)
for i in range(10):
print random.choice(score_elements)
Ответ 5
Другой вариант,
Настройка немного громоздка, но поиск выполняется с логарифмической сложностью (подходит, когда требуется несколько запросов):
import itertools
import random
from collections import Counter
from bisect import bisect
counter = Counter({"a": 5, "b": 1, "c": 1})
#setup
most_common = counter.most_common()
accumulated = list(itertools.accumulate([x[1] for x in most_common])) # i.e. [5, 6, 7]
total_size = accumulated[-1]
# lookup
i = random.randrange(total_size)
print(most_common[bisect(accumulated, i)])
Ответ 6
Много датированных ответов здесь.
Имея словарь вариантов выбора с соответствующими относительными вероятностями (в вашем случае это может быть число), вы можете использовать новый random.choices, добавленный в Python 3.6, следующим образом:
import random
my_dict = {
"choice a" : 1, # will in this case be chosen 1/3 of the time
"choice b" : 2, # will in this case be chosen 2/3 of the time
}
choice = random.choices(*zip(*my_dict.items()))[0]