#python #numpy #montecarlo
Вопрос:
Я пытаюсь оптимизировать (векторизировать?) Создание моделирования в стиле Монте-Карло, и я не смог понять, как создавать вложенные взвешенные случайные значения с использованием numpy
или аналогичных библиотек. Рассмотрим сценарий, вдохновленный interviewqs
вопросом: «Учащиеся в трех классах должны проголосовать за одного из двух кандидатов в президенты класса. В классе А 40% учащихся, и они разделены 50/50 на кандидатов X и Y. В имеет 25% студентов и делится на 60/40. C насчитывает 35% студентов и делится на 35/65»
Создание этих данных с помощью ванильного Python может выглядеть примерно так,
import random
nsimulations = 10_000_000
def choose_classroom():
"returns A, B, or C based on percentages"
value = random.random()
if value < .4:
return 'A'
elif value < .65:
return 'B'
else:
return 'C'
def choose_support(classroom):
"return X or Y based on support percentage by classroom"
value = random.random()
if classroom == 'A':
return "X" if value < .5 else "Y"
elif classroom == 'B':
return "X" if value < .6 else "Y"
elif classroom == 'C':
return "X" if value < .35 else "Y"
results = []
for i in range(nsimulations):
classroom = choose_classroom()
support = choose_support(classroom)
results.append({'classroom': classroom, 'support': support})
Для запуска 10-метрового моделирования на моей машине требуется около 10 секунд. Я бы хотел улучшить это время. Первое, на что я посмотрел, был тупой.случайный.выбор, fast_classrooms = np.random.choice(['A', 'B', 'C'], size=nsimulations, p=[.4, .25, .35])
. Это действительно выполняется быстро, около 350 мс. Однако я не знаю, как оттуда генерировать последующие X
/ Y
дистрибутивы.
Одна вещь , которую я пробовал, — это Панды apply
, и, похоже, она выполняет какие-то оптимизации под прикрытием. Приведенный ниже фрагмент кода Pandas выполняется за ~2,5 секунды, в то время как понимание списка ( [choose_support(record) for record in fast_classrooms]
занимает ~4 секунды. Тем не менее, мне кажется, что это не «правильный» способ делать вещи.
import pandas
import numpy as np
fast_classrooms = np.random.choice(['A', 'B', 'C'], size=nsimulations, p=[.4, .25, .35])
df = pandas.DataFrame({'classroom': fast_classrooms})
df['support'] = df.classroom.apply(choose_support)
То, что я надеялся найти, — это что-то, что могло бы генерировать вложенные взвешенные распределения, что-то вроде — np.random.choice([['A', 'B', 'C'], ['X', 'Y']], p=[[.4, .25, .35], [[.5, .5], [.6, .4], [.35, .65]]])
Каковы некоторые способы получения этих данных?
Ответ №1:
Вы можете использовать np.random.choice
на парах вместо того, чтобы запускать функцию дважды. Это означает, что вы можете рассчитать вероятность наличия пары («класс», «поддержка»). Например, вероятность выбора класса » А «И получения поддержки «Х» составляет 0,4*0,5 = 0,2 и так далее.
Приведенный ниже код работает для меня довольно быстро.
import numpy as np
nsimulations = 10000000
#Construct the probabilities and pairs
p = [.4*.5, .4*.5, .25*.6, .25*.4, .35*.35, .35*.65]
pairs = [{'classroom': 'A', 'support': 'X'},
{'classroom': 'A', 'support': 'Y'},
{'classroom': 'B', 'support': 'X'},
{'classroom': 'B', 'support': 'Y'},
{'classroom': 'C', 'support': 'X'},
{'classroom': 'C', 'support': 'Y'}]
# Sample the pairs based on the probabilities
fast_classrooms = np.random.choice(pairs, size=nsimulations, p=p)
Редактировать:
Этот код был взят 0.6193864345550537 seconds
по сравнению с методом, опубликованным OP с. 7.815439224243164 seconds
Также подтверждено @Tom McLean в комментарии.
Комментарии:
1. Именно так я и думал, но не мог придумать, как это реализовать
2. Это такой элегантный ответ! Я хотел бы добавить, что можно использовать
itertools.product
для получения декартова произведения (комбинацииX
иY
и вероятностей), если у вас слишком много пар для ввода вручную.3. @iffanh Мне очень нравится этот ответ, он такой умный! Есть ли быстрый способ прочитать множество словарей в панд, чтобы
classroom
иsupport
они были в своих собственных столбцах?pandas.DataFrame(fast_classrooms)
выполняется быстро, но дает мне один столбец словарей. ПробуетDataFrame(fast_classrooms.tolist())
илиDataFrame.from_records(fast_classroom)
разбивает его на столбцы, но занимает несколько секунд4. Для справки этот метод занял 0,42 с на моей машине для моделирования 10 м против 7,7 с из метода, описанного в вопросе
5. @FBruzzesi Спасибо за предложение. Добавлено 🙂
Ответ №2:
Вы можете значительно сократить количество циклов python, сделав код более векторизованным:
import numpy as np
nsimulations = 12
uniquerooms = ['A','B','C']
supportprobs = [0.5, 0.6, 0.35]
classrooms = np.random.choice(uniquerooms, size=nsimulations, p=[.4, .25, .35])
supports = np.empty_like(classrooms, dtype=classrooms.dtype)
for room, prob in zip(uniquerooms, supportprobs):
mask = classrooms == room
supports[mask] = np.random.choice(['X','Y'], size=mask.sum(), p=[prob, 1-prob])
print(classrooms)
# ['C' 'A' 'C' 'A' 'C' 'C' 'A' 'C' 'B' 'C' 'B' 'A']
print(supports)
# ['X' 'Y' 'Y' 'Y' 'Y' 'X' 'Y' 'X' 'X' 'X' 'Y' 'X']
Ответ №3:
Я думаю, что текущий лучший ответ является самым элегантным, но я хочу добавить numpy.по частям в смесь из-за расширяемости условий:
import numpy as np
fast_classrooms = np.random.choice(['A', 'B', 'C'], size=nsimulations, p=[.4, .25, .35])
np.piecewise(fast_classrooms, [fast_classrooms == 'A', fast_classrooms =='B', fast_classrooms=='C'],
[lambda X: "X" if np.random.random() < .5 else "Y",
lambda X: "X" if np.random.random() < .6 else "Y",
lambda X: "X" if np.random.random() < .35 else "Y"
])
out: array(['X', 'X', 'Y', ..., 'Y', 'X', 'X']
~660 мс на 10-миллиметровом моделировании на моей машине, YMMV
Комментарии:
1. Спасибо @Michael Karman. Я думаю
np.random.random()
, что это может быть рассчитано только один раз в ваших лямбда-функциях. Когда я читаю вfast_classrooms
кадре данных иout
в него, всеA
они имеют одинаковоеX
Y
значение или и т. Д.