Ответ 1
Кумулятивное суммирование и деление пополам
В любом общем случае представляется целесообразным вычислить суммарную сумму весов и использовать bisect из модуля bisect, чтобы найти случайную точку в результирующем отсортированном массиве
def weighted_choice(weights):
cs = numpy.cumsum(weights)
return bisect.bisect(cs, numpy.random.random() * cs[-1])
если скорость вызывает беспокойство. Более подробный анализ приведен ниже.
Примечание. Если массив не является плоским, numpy.unravel_index
можно использовать для преобразования плоского индекса в форменный индекс, как показано в fooobar.com/info/28987/...
Экспериментальный анализ
Существует четыре более или менее очевидных решения с использованием встроенных функций numpy
. Сравнение всех из них с помощью timeit
дает следующий результат:
import timeit
weighted_choice_functions = [
"""import numpy
wc = lambda weights: numpy.random.choice(
range(len(weights)),
p=weights/weights.sum())
""",
"""import numpy
# Adapted from /info/28987/efficient-way-of-sampling-from-indices-of-a-numpy-array/212123#212123
def wc(weights):
cs = numpy.cumsum(weights)
return cs.searchsorted(numpy.random.random() * cs[-1], 'right')
""",
"""import numpy, bisect
# Using bisect mentioned in https://stackoverflow.com/a/13052108/1274613
def wc(weights):
cs = numpy.cumsum(weights)
return bisect.bisect(cs, numpy.random.random() * cs[-1])
""",
"""import numpy
wc = lambda weights: numpy.random.multinomial(
1,
weights/weights.sum()).argmax()
"""]
for setup in weighted_choice_functions:
for ps in ["numpy.ones(40)",
"numpy.arange(10)",
"numpy.arange(200)",
"numpy.arange(199,-1,-1)",
"numpy.arange(4000)"]:
timeit.timeit("wc(%s)"%ps, setup=setup)
print()
Результирующий результат
178.45797914802097
161.72161589498864
223.53492237901082
224.80936180002755
1901.6298267539823
15.197789980040397
19.985687876993325
20.795070077001583
20.919113760988694
41.6509403079981
14.240949985047337
17.335801470966544
19.433710905024782
19.52205040602712
35.60536142199999
26.6195822560112
20.501282756973524
31.271995796996634
27.20013752405066
243.09768892999273
Это означает, что numpy.random.choice
на удивление очень медленный, и даже выделенный метод numpy searchsorted
медленнее, чем вариант наивного типа bisect
. (Эти результаты были получены с использованием Python 3.3.5 с numpy 1.8.1, поэтому для других версий могут быть разные.) Функция, основанная на numpy.random.multinomial
, менее эффективна для больших весов, чем методы, основанные на суммарном суммировании. Предположительно, тот факт, что argmax должен перебирать весь массив и выполнять сравнения, каждый шаг играет значительную роль, что также видно из четырех секундного разницы между увеличивающимся и уменьшающимся списком веса.