monty is a pure-Python library for computing and analyzing discrete distributions. It is useful for exploring hypothetical scenarios and solving tricky statistical problems. The name monty comes from the Monty Hall problem, a probability puzzle.
The workhorse of this library is the Distribution class, which internally stores the discrete distribution as a list of pairs (value, odds). The Distribution class has functions for changing the values, updating the odds, generating random values, plotting, computing utility functions, and more. A number of built-in distributions and helper functions is also included.
Note on terminology: this library uses the term odds to mean any non-negative number that somehow associates a scenario with a likelihood. Odds are not normalized like probabilities, so you can have Distribution(Tails=25, Heads=50), where the numbers 25 and 50 are used only for relative comparison (heads appears twice more than tails, that is, 66% vs 33%). Those numbers are converted to probabilities when plotting, but under the hood they are kept as-is. I'm not sure if odds is the correct term, but it's an important distinction.
- Constructing
- Joining
- Reading
- Updating
- Simulating
- Expected value / utility function
- Batteries included
- Exmples
There are several ways to construct a distribution. The following three examples are all equivalent:
- Explicit pairs (list):
Distribution([('Heads', 0.5), ('Tails', 0.5)]). - Explicit pairs (*args):
Distribution(('Heads', 0.5), ('Tails', 0.5)). - Dictionary:
Distribution({'Heads': 0.5, 'Tails': 0.5}). - Keyword arguments:
Distribution(Heads=0.5, Tails=0.5).
Since the library operates on odds, not probabilities, the total doesn't have to sum up to 1: Distribution(Heads=9, Tails=1): 9/10 chance of heads.
If you do choose to enter probabilities (odds summing to 1), you can use the special value REST to avoid computing the probability of the last value: Distribution(Heads=0.499, Tails=0.499, Sideways=REST).
Additionally, the classes Uniform, Fixed, Range, Count, Permutations are constructred differently, but behave exactly like Distribution after initialization:
Uniform('Heads', 'Tails'): automatically distributes the odds equally between all items.Fixed('Heads'): only allows one value, with 100% probability.Range(10): uniform distribution of values[0, 1, ... 8, 9].Count(10): uniform distribution of values[1, 2, ... 8, 10].Permutations('Red', 'Blue', 'Green'): uniform distribution of all possible orderings (red blue greenorblue red greenorblue green red, etc).
Finally, the values may also be distributions, in a nested manner:
# 99% chance of the coin being legitimate, with an unknown monetary
# value uniformly distributed between the possible coin types.
coin_value = Distribution({
Uniform(1, 5, 10, 25, 50, 100): 0.99,
'Counterfeit': REST,
})
coin_value.plot()
# 1 16.50% [======= ]
# 5 16.50% [======= ]
# 10 16.50% [======= ]
# 25 16.50% [======= ]
# 50 16.50% [======= ]
# 100 16.50% [======= ]
# Counterfeit 1.00% [ ]Cheatsheet
| Input | Resulting distribution |
|---|---|
Distribution(a=0.5, b=0.1, c=0.4) |
(('a', 0.5), ('b', 0.1), ('c', 0.4)) |
Distribution(a=0.5, b=0.1, c=REST) |
(('a', 0.5), ('b', 0.1), ('c', 0.4)) |
Distribution({'a': 0.5, 'b': 0.1, 'c': 0.4}) |
(('a', 0.5), ('b', 0.1), ('c', 0.4)) |
Distribution([('a', .5), ('b', .1), ('c', .4)]) |
(('a', 0.5), ('b', 0.1), ('c', 0.4)) |
Distribution(('a', .5), ('b', .1), ('c', .4)) |
(('a', 0.5), ('b', 0.1), ('c', 0.4)) |
Uniform('a', 'b', 'c', 'd') |
(('a', 0.25), ('b', 0.25), ('c', 0.25), ('d', 0.25)) |
Uniform(['a', 'b', 'c', 'd']) |
(('a', 0.25), ('b', 0.25), ('c', 0.25), ('d', 0.25)) |
Uniform('abcd') |
(('a', 0.25), ('b', 0.25), ('c', 0.25), ('d', 0.25)) |
Fixed('Highlander') |
(('Highlander', 1),) |
Range(4) |
((0, 0.25), (1, 0.25), (2, 0.25), (3, 0.25)) |
Range(2, 4) |
((2, 0.5), (3, 0.5)) |
Count(4) |
((1, 0.25), (2, 0.25), (3, 0.25), (4, 0.25)) |
Count(2, 5) |
((2, 0.25), (3, 0.25), (4, 0.25), (5, 0.25)) |
Permutations('A', 'B') |
((('A', 'B'), 0.5), (('B', 'A'), 0.5)) |
Permutations(['A', 'B']) |
((('A', 'B'), 0.5), (('B', 'A'), 0.5)) |
Distribution((Uniform('ab'), 0.6), (None, REST)) |
(('a', 0.3), ('b', 0.3), (None, 0.4)) |
You can compute the combinations of two or more distributions with the function join, creating tuples of combinations with multiplied probability:
join(coin, dice).plot()
# ('Heads', 1) 8.33% [=== ]
# ('Heads', 2) 8.33% [=== ]
# ('Heads', 3) 8.33% [=== ]
# ('Heads', 4) 8.33% [=== ]
# ('Heads', 5) 8.33% [=== ]
# ('Heads', 6) 8.33% [=== ]
# ('Tails', 1) 8.33% [=== ]
# ('Tails', 2) 8.33% [=== ]
# ('Tails', 3) 8.33% [=== ]
# ('Tails', 4) 8.33% [=== ]
# ('Tails', 5) 8.33% [=== ]
# ('Tails', 6) 8.33% [=== ]Additionally, the multiplication operator * has been overloaded to join a distribution with itself n times.
(coin*2).plot()
# ('Heads', 'Heads') 25.00% [========== ]
# ('Heads', 'Tails') 25.00% [========== ]
# ('Tails', 'Heads') 25.00% [========== ]
# ('Tails', 'Tails') 25.00% [========== ]Warning: joining two distributions, join(A, B), results in a distribution where the values are pairs (a, b). Joining this resulting distribution with another one, join(join(A, B), C), will not result in a distribution of triples (a, b, c), but of nested pairs ((a, b), c). In the same vein, A*1 results in values wrapped in a single-value tuple (a,). This is why the addition operator was not overloaded, otherwise A+B+C would result in a confusingly nested distribution. Use join(A, B, C) in this case.
If printed or iterated over, a Distribution acts like a list of pairs (value, odds)
print(len(coin), coin)
# 2 (('Heads', 0.5), ('Tails', 0.5))
for value, odds in coin:
print(value)
# 'Heads'
# 'Tails'
dict(coin)
# {'Heads': 0.5, 'Tails': 0.5}Access single values by using distribution[value], distribution.expected_value (weighted average of numeric values), distribution.utility(fn), distribution.mode (the most common value).
Finally, you can also plot to the terminal: distribution.plot(sort=True, filter=True).
card_suits.plot()
# Clubs 25.00% [========== ]
# Diamonds 25.00% [========== ]
# Hearts 25.00% [========== ]
# Spades 25.00% [========== ]Distributions are immutable objects, but they support creating modified copies. Remember the distribution is modeled as a list of pairs (value, odds). There are two main functions to update a distribution: distribution.map updates each value; and distribution.filter updates each odds.
Note on flexibility: both map and filter usually take a function, but you can also pass a dictionary, list/tuple, keyword arguments, or even None, as follows:
- Function: invoked for each value, expects a new value (for
map) or odds multiplier (forfilter). - Dictionary: behaves like
lambda v: dictionary[v]. - Keyword args: behaves like
lambda v: kwargs[v]. - List or tuple: behaves like
lambda v: v in list. - None: behaves like
lambda v: bool(v).
Also, the Distribution class implements starmap and starfilter, which invoke fn(*value) instead of fn(value). This is useful when your value is a tuple representing a "state". For example:
join(coin, dice).starmap(lambda toss, roll: toss == 'Heads' and roll > 4).plot()
# False 83.33% [================================= ]
# True 16.67% [======= ]distribution.map applies a function to each value and returns a new distribution of pairs (fn(value), odds). Duplicated values are merged, and sub-distributions flattened, with the same rules as usual construction.
Example:
dice.map(lambda v: v+1).plot()
# 2 16.67% [======= ]
# 3 16.67% [======= ]
# 4 16.67% [======= ]
# 5 16.67% [======= ]
# 6 16.67% [======= ]
# 7 16.67% [======= ]
dice.map(lambda v: v > 4).plot()
# False 66.67% [=========================== ]
# True 33.33% [============= ]
coin.map(Tails='A', Heads='B').plot()
# B 50.00% [==================== ]
# A 50.00% [==================== ]
dice.map({1: 0, 2: 0, 3: 1, 4: 1, 5: 2, 6: 2}).plot()
# 0 33.33% [============= ]
# 1 33.33% [============= ]
# 2 33.33% [============= ]Because of this grouping behavior, map is aliased to group and group_by.
This functions returns a copy of the distribution, with modified odds for each value. Each pair (value, odds) is replaced with (value, odds*fn(value)). Note that the odds are updating in relation to the previous odds, like a refinement, and not a replacement.
distribution.filter can be used in several ways, depending on the type of the argument:
- List: allows only values present in the list, changing the odds of any other value to 0. Think of it as focusing the distribution.
dice.filter([1, 2, 5, 6]).plot(sort=False)
# 1 25.00% [========== ]
# 2 25.00% [========== ]
# 3 0.00% [ ]
# 4 0.00% [ ]
# 5 25.00% [========== ]
# 6 25.00% [========== ]- Dictionary, Distribution or keyword arguments: multiplies the odds by the corresponding value.
dice.filter({1: 1, 2: 1, 3: 0.5, 4: 0.5, 5: 1, 6: 1}).plot(sort=False)
# 1 16.67% [======= ]
# 2 16.67% [======= ]
# 3 8.33% [=== ]
# 4 8.33% [=== ]
# 5 16.67% [======= ]
# 6 16.67% [======= ]- Function: calls
fn(value)and expects a multiplier back (note thatTrue == 1andFalse == 0, so boolean results are ok).
# Functions returns boolean:
dice.filter(lambda v: v % 2 == 1).plot(sort=False)
# 1 33.33% [============= ]
# 2 0.00% [ ]
# 3 33.33% [============= ]
# 4 0.00% [ ]
# 5 33.33% [============= ]
# 6 0.00% [ ]# Function returns multiplier
dice.filter(lambda v: 1 if v % 2 else 0.5).plot(sort=False)
# 1 22.22% [========= ]
# 2 11.11% [==== ]
# 3 22.22% [========= ]
# 4 11.11% [==== ]
# 5 22.22% [========= ]
# 6 11.11% [==== ]- No argument: equivalent to
filter(lambda v: bool(v)), filters away "falsy" values (i.e. removesFalse,0,None,[],{},"").
Note: the built-in comparison operators (lt, le, eq, ne (aliased to not_equal(s)), gt, ge) are also useful here. But the result from mapping versus filtering based on them is completely different. Mapping a condition means asking "divide the values into the ones that obey or not this condition". Filtering on a condition, on the other hand, means asking "ignore the values that don't obey this condition". Both are useful in their own ways. For example:
(2*coin).map(not_equals).plot()
# False 50.00% [==================== ]
# True 50.00% [==================== ](2*coin).filter(not_equals).plot()
# ('Heads', 'Tails') 50.00% [==================== ]
# ('Tails', 'Heads') 50.00% [==================== ]The function distribution.generate(n) returns n random values sampled from the distribution (or infinite values, if n==-1 or not specified).
for card in deck.generate(10):
print('Is this your card?', card)
# Is this your card? (9, 'Clubs')
# Is this your card? (2, 'Diamonds')
# Is this your card? (5, 'Hearts')
# Is this your card? ('Jack', 'Spades')
# Is this your card? (4, 'Hearts')
# Is this your card? (8, 'Clubs')
# Is this your card? ('King', 'Hearts')
# Is this your card? (9, 'Diamonds')
# Is this your card? (5, 'Diamonds')
# Is this your card? (2, 'Clubs')Additionally, sometimes operations are too complex to fit in a pattern of map and filter, such as conditions that depend on consecutive draws. In these cases, the method distribution.monte_carlo(fn, n=100000) generates n examples from the distribution, feeds them as a generator to fn, and creates a new distribution from the list of values returned by fn. Note that operations performed this way are probabilistic, therefore the result may not be precise.
def remove_doubles(nums):
# Omits numbers that are twice as big as the previous one.
last = next(nums)
yield last
for num in nums:
if num != 2 * last:
yield num
last = num
dice.monte_carlo(remove_doubles).plot()
# 5 18.24% [======= ]
# 1 18.20% [======= ]
# 3 18.18% [======= ]
# 6 15.21% [====== ]
# 4 15.14% [====== ]
# 2 15.02% [====== ]Sometimes you want to summarize a complex distribution into a single value. For this purpose, the Distribution class implements the distribution.expected_value property and the distribution.utlity(fn) method.
# How much should you pay for a ticket to a $400,000,000 jackpot at a 1/13,983,816 chance?
lottery.map(Win=400_000_000, Loss=0).expected_value
# 28.604495368074065
# A dollar is less useful for a millionaire than for a poorer person.
# Use a utility function with a logarithmic scale.
import math
lottery.map(Win=400_000_000, Loss=0).utility(lambda v: math.log(v, 1.1) if v else 0)
# 1.4861175606104777e-05The monty library comes with a number of distributions commonly used in examples:
coin = Uniform('Heads', 'Tails')
dice = die = d6 = Count(6)
d4 = Count(4)
d8 = Count(8)
d10 = Count(10)
d12 = Count(12)
d20 = Count(20)
d100 = Count(100)
card_ranks = Uniform('Ace', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'Jack', 'Queen', 'King')
card_suits = Uniform('Clubs', 'Diamonds', 'Hearts', 'Spades')
deck = join(card_ranks, card_suits)
rock_paper_scissors = Uniform('Rock', 'Paper', 'Scissors')
monty_hall_doors = Permutations('Goat', 'Goat', 'Car')
# https://en.wikipedia.org/wiki/Lottery_mathematics
# Typical 6/49 game.
lottery = Distribution(Win=1/13983816, Loss=REST)
powerball = Distribution(Win=1/292201338, Loss=REST)
# http://www.lightningsafety.noaa.gov/odds.shtml
# Chance of being struck by lightning in your lifetime.
lightning_strike = Distribution({'Struck by lightning': 1/13500, 'Safe': REST})
# http://news.nationalgeographic.com/2016/02/160209-meteorite-death-india-probability-odds/
# Chance of being killed by meteorite in your lifetime.
meteorite = Distribution({'Killed by meteorite': 1/700000, 'Safe': REST})Shorthands for distribution names:
D = Distribution
U = Uniform
R = Range
C = Count
P = Permutations
F = FixedIt's common for values to be tuples of numbers. To add two numbers one would use distribution.map(lambda v: v[0] + v[1]), or with starmap: distribution.starmap(lambda a, b: a + b). To simplify operations like these, one can use one of the following pre-made functions, as in distribution.map(add):
| Name | Behavior | Aliases |
|---|---|---|
lt |
v[0] < v[1] |
|
le |
v[0] <= v[1] |
|
eq |
v[0] == v[1] |
equal, equals |
ne |
v[0] != v[1] |
not_equal, not_equals |
gt |
v[0] > v[1] |
|
ge |
v[0] >= v[1] |
|
contains |
v[0] in v[1] |
|
add |
v[0] + v[1] + v[2] + ... |
same as Python's builtin sum |
sub |
v[0] - v[1] |
|
difference |
abs(v[0] - v[1]) |
|
mul |
v[0] * v[1] * v[2] * ... |
product |
first |
v[0] |
|
second |
v[1] |
|
third |
v[2] |
|
last |
v[-1] |
from monty import *# Taken from https://betterexplained.com/articles/an-intuitive-and-short-explanation-of-bayes-theorem/ :
# 80% of mammograms detect breast cancer when it is there.
# 9.6% of mammograms detect breast cancer when it’s not there.
positive_mammogram = {'Cancer': 0.8, 'No cancer': 0.096}
# 1% of candidates have breast cancer. What's the likelihood after a
# positive test?
Distribution({'Cancer': 0.01, 'No cancer': REST}).filter(positive_mammogram).plot()
# No cancer [===================================== ] 92.24%
# Cancer [=== ] 7.76%
# Alternative solution: model the test results in the distribution itself.
Distribution({
# Cancer.
Distribution({'True positive': 0.8, 'False negative': REST}): 0.01,
# No cancer.
Distribution({'False positive': 0.096, 'True negative': REST}): REST,
}).filter(['True positive', 'False positive']).plot()
# If the test was positive, what's the likelihood of having cancer?
# False positive [===================================== ] 92.24%
# True positive [=== ] 7.76%# From https://www.gwern.net/docs/statistics/1994-falk#standard-problems-and-their-solution
# It's 23:30, you are at the bus stop. Buses usually run at an interval of
# 30 minutes, but you are only 60% sure they are operating at all at this
# time.
bus_distribution = Distribution({
Uniform(
'Will arrive at 23:35',
'Will arrive at 23:40',
'Will arrive at 23:45',
'Will arrive at 23:50',
'Will arrive at 23:55',
'Will arrive at 00:00',
): 0.6,
'Not operating': REST,
})
# 5 minutes pass. It's now 23:35, and the bus has not yet arrived. What
# are the new likelihoods?
bus_distribution.filter(lambda e: '23:35' not in e).plot()
# Not operating [================== ] 44.44%
# Will arrive at 23:55 [==== ] 11.11%
# Will arrive at 23:50 [==== ] 11.11%
# Will arrive at 23:45 [==== ] 11.11%
# Will arrive at 23:40 [==== ] 11.11%
# Will arrive at 00:00 [==== ] 11.11%
# It's now 23:55, and the bus has not yet arrived.
bus_distribution.filter(lambda e: '23:' not in e).plot()
# Not operating [================================ ] 80.00%
# Will arrive at 00:00 [======== ] 20.00%# A car is put behind one of three doors. The participant chooses door number 1.
# The show host (Monty Hall) opens one of the remaining doors, revealing it's
# empty (if the car is behind door number 1, Monty opens door 2 or 3 at random).
# The participant is then asked if they want to stay with their choice, or
# switch to the other unopened door. Which strategy wins?
car_position = Uniform(1, 2, 3)
# Model the game as a pair `(car_position, alternative_door)`.
game = car_position.map({1: Uniform((1, 2), (1, 3)), 2: (2, 2), 3: (3, 3)})
game.plot()
# (2, 2) 33.33% [============= ]
# (3, 3) 33.33% [============= ]
# (1, 2) 16.67% [======= ]
# (1, 3) 16.67% [======= ]
best_strategy = lambda car, alternative: 'Switch' if car == alternative else 'Stay'
game.starmap(best_strategy).plot()
# Switch 66.67% [=========================== ]
# Stay 33.33% [============= ]# Same setup as classic Monty Hall, now with host opening door 2 or 3 at
# random (i.e. offering door 3 or 2 at random).
car_position = Uniform(1, 2, 3)
alternative_door = Uniform(3, 2)
# But we only look at scenarios where the opened door *just happened* to
# not be the car door (i.e. the car *just happened* to be in door 1 or
# the alternative door).
is_allowed = lambda car, alternative: car in (1, alternative)
game = join(car_position, alternative_door).starfilter(is_allowed)
best_strategy = lambda car, alternative: 'Switch' if car == alternative else 'Stay'
game.starmap(best_strategy).plot()
# Stay 50.00% [==================== ]
# Switch 50.00% [==================== ]# From http://www.mathteacherctk.com/blog/2013/01/13/a-pair-of-probability-games-for-beginners/
# Throw two dice. I win if the difference is 0,1,2. You win if it is 3,4,5.
# Wanna play?
(2*dice).map(difference).map(lambda d: 'No' if d <= 2 else 'Yes').plot()
# No [=========================== ] 66.67%
# Yes [============= ] 33.33%
# I win if a 2 or a 5 shows on either die. (Not a sum of 2 or 5, just an
# occurrence of a 2 or a 5.) Otherwise, you win. Wanna play?
(2*dice).map(lambda pair: 'No' if 2 in pair or 5 in pair else 'Yes').plot()
# No [====================== ] 55.56%
# Yes [================== ] 44.44%# From John von Neuman (1951)
# I want a fair coin flip, but I don't trust this coin. Can I "unbias" it?
b_coin = Distribution(Heads=0.6, Tails=REST)
# Yes! Flip it twice, and retry until they are different. Then look at
# the first one.
(2*b_coin).filter(not_equals).map(first).plot()
# Tails 50.00% [==================== ]
# Heads 50.00% [==================== ]# Fun fact: you can also use likelihood distributions to keep track of
# concentrations in solutions. `Solution` is a subclass of `Distribution`
# that overloads `+`, `*`, and `/` to behave like a physical mix. This is
# possible because odds are not normalized like probabilities, so we use
# them to keep track of total volume.
#
# Think of the probabilities as "what is the chance of a random molecule
# of this mix being of type X?".
# 200 units of water and 600 units of pure orange.
juice = Solution(water=200, orange=600).plot()
# orange 75.00% [============================== ]
# water 25.00% [========== ]
# 100 units of sugar water at 5%
sugar_water = Solution(water=95, sugar=5).plot()
# water 95.00% [====================================== ]
# sugar 5.00% [== ]
# Mix all of the juice with half of the sugar water.
mix = (juice + sugar_water/2).plot()
# orange 70.59% [============================ ]
# water 29.12% [============ ]
# sugar 0.29% [ ]
# Remove most of the orange and some of the sugar.
filtered = mix.filter(water=1, orange=0.01, sugar=0.80).plot()
# water 96.87% [======================================= ]
# orange 2.35% [= ]
# sugar 0.78% [ ]
print(filtered, filtered.total)
# (('water', 247.5), ('orange', 6.0), ('sugar', 2.0)) 255.5
# Mix 1 unit of juice and sugar water at 50/50, resulting in 2.5% sugar.
Solution({juice: 1, sugar_water: 1}).plot()
# water 60.00% [======================== ]
# orange 37.50% [=============== ]
# sugar 2.50% [= ]# We got a coin from a factory of biased coins. The factory makes 11 different
# coins, each type flipping heads anywhere from 0% to 100% of the time.
all_coin_types = [Distribution(Heads=i/10, Tails=REST) for i in range(11)]
# We get one of these coins, but don't know which type.
# (`force_flatten=False` is required so `Uniform` doesn't merge all Heads/Tails
# probabilities. We could also just not use `Distribution` for the coin types.)
coins = Uniform(*all_coin_types, force_flatten=False)
coins.plot()
# (('Heads', 0.0), ('Tails', 1.0)) 9.09% [==== ]
# (('Heads', 0.1), ('Tails', 0.9)) 9.09% [==== ]
# (('Heads', 0.2), ('Tails', 0.8)) 9.09% [==== ]
# (('Heads', 0.3), ('Tails', 0.7)) 9.09% [==== ]
# (('Heads', 0.4), ('Tails', 0.6)) 9.09% [==== ]
# (('Heads', 0.5), ('Tails', 0.5)) 9.09% [==== ]
# (('Heads', 0.6), ('Tails', 0.4)) 9.09% [==== ]
# (('Heads', 0.7), ('Tails', 0.3)) 9.09% [==== ]
# (('Heads', 0.8), ('Tails', 0.1)) 9.09% [==== ]
# (('Heads', 0.9), ('Tails', 0.0)) 9.09% [==== ]
# (('Heads', 1.0), ('Tails', 0.0)) 9.09% [==== ]
# We don't know yet, but our coin is the 70%-Heads coin. Toss it 10 times.
tosses = ['Heads'] * 7 + ['Tails'] * 3
random.shuffle(tosses)
# Update the chance of each coin type according to their predicted probability
# for that coin toss.
for toss in tosses:
# Must be normalized to avoid losing precision.
coins = coins.filter(lambda c: c[toss]).normalize()
coins.plot(sort=False)
# (('Heads', 0.0), ('Tails', 1.0)) 0.00% [ ]
# (('Heads', 0.1), ('Tails', 0.9)) 0.00% [ ]
# (('Heads', 0.2), ('Tails', 0.8)) 0.09% [ ]
# (('Heads', 0.3), ('Tails', 0.7)) 0.99% [ ]
# (('Heads', 0.4), ('Tails', 0.6)) 4.67% [== ]
# (('Heads', 0.5), ('Tails', 0.5)) 12.88% [===== ]
# (('Heads', 0.6), ('Tails', 0.4)) 23.63% [========= ]
# (('Heads', 0.7), ('Tails', 0.3)) 29.32% [============ ]
# (('Heads', 0.8), ('Tails', 0.2)) 22.12% [========= ]
# (('Heads', 0.9), ('Tails', 0.1)) 6.31% [=== ]
# (('Heads', 1.0), ('Tails', 0.0)) 0.00% [ ]# I need a D20 roll, but all I have are D4. Can I just add 5xD4?
d20.plot()
# 1 5.00% [== ]
# 2 5.00% [== ]
# 3 5.00% [== ]
# 4 5.00% [== ]
# 5 5.00% [== ]
# 6 5.00% [== ]
# 7 5.00% [== ]
# 8 5.00% [== ]
# 9 5.00% [== ]
# 10 5.00% [== ]
# 11 5.00% [== ]
# 12 5.00% [== ]
# 13 5.00% [== ]
# 14 5.00% [== ]
# 15 5.00% [== ]
# 16 5.00% [== ]
# 17 5.00% [== ]
# 18 5.00% [== ]
# 19 5.00% [== ]
# 20 5.00% [== ]
(5 * d4).map(sum).plot(sort=False)
# 5 0.10% [ ]
# 6 0.49% [ ]
# 7 1.46% [= ]
# 8 3.42% [= ]
# 9 6.35% [=== ]
# 10 9.86% [==== ]
# 11 13.18% [===== ]
# 12 15.14% [====== ]
# 13 15.14% [====== ]
# 14 13.18% [===== ]
# 15 9.86% [==== ]
# 16 6.35% [=== ]
# 17 3.42% [= ]
# 18 1.46% [= ]
# 19 0.49% [ ]
# 20 0.10% [ ]
# Nope.# Roll a D20, a D12 and a D4. What's the probability of the D20 and the D12
# being less than the D4 away from each other?
join(d20, d12, d4).map(lambda s: abs(s[0]-s[1]) < s[2]).plot()
# False 81.04% [================================ ]
# True 18.96% [======== ]
# Equivalently, in two steps:
join(join(d20, d12).map(difference), d4).map(lt).plot()
# False 81.04% [================================ ]
# True 18.96% [======== ]# If a family has two children...
children = Uniform('Son', 'Daughter') * 2
# ... at least one of which is a daughter, what is the probability that
# both of them are daughters?
children.filter(lambda s: 'Daughter' in s).map(eq).plot()
# False 66.67% [=========================== ]
# True 33.33% [============= ]
# ... the elder of which is a daughter, what is the probability that both
# of them are the daughters?
children.filter(lambda s: s[1] == 'Daughter').map(eq).plot()
# False 50.00% [==================== ]
# True 50.00% [==================== ]# From https://en.wikipedia.org/wiki/Nontransitive_dice#Example
# Three 6-sided dices with modified numbers.
dice_a = Uniform(2, 2, 4, 4, 9, 9)
dice_b = Uniform(1, 1, 6, 6, 8, 8)
dice_c = Uniform(3, 3, 5, 5, 7, 7)
# Expected value is the same (within float tolerance).
import math
assert math.isclose(dice_a.expected_value, dice_b.expected_value)
assert math.isclose(dice_b.expected_value, dice_c.expected_value)
# But they behave like rock paper scissors:
join(dice_a, dice_b).map(gt).map({True: 'A wins', False: 'B wins'}).plot()
# A wins 55.56% [====================== ]
# B wins 44.44% [================== ]
join(dice_b, dice_c).map(gt).map({True: 'B wins', False: 'C wins'}).plot()
# B wins 55.56% [====================== ]
# C wins 44.44% [================== ]
join(dice_c, dice_a).map(gt).map({True: 'C wins', False: 'A wins'}).plot()
# C wins 55.56% [====================== ]
# A wins 44.44% [================== ]# From: https://brilliant.org/discussions/thread/rationality-revisited-the-sleeping-beauty-paradox/
# Today is Sunday. Sleeping Beauty drinks a powerful sleeping potion and
# falls asleep. Her attendant tosses a fair coin and records the result.
# - The coin lands in Heads. Beauty is awakened only on Monday and
# interviewed. Her memory is erased and she is again put back to sleep.
# - The coin lands in Tails. Beauty is awakened and interviewed on Monday.
# Her memory is erased and she's put back to sleep again. On Tuesday, she is
# once again awaken, interviewed and finally put back to sleep.
# The most important question she's asked in the interviews is
# "What is your credence (degree of belief) that the coin landed in heads?""
days = join(coin, Uniform('Monday', 'Tuesday'))
def add_guess(state):
if state == ('Heads', 'Tuesday'):
# Sleeping beauty is not awakened in this case.
return Uniform()
else:
# She tries to guess the coin toss.
return Uniform(state+('Heads',), state+('Tails',))
guesses = days.map(add_guess).plot()
# ('Heads', 'Monday', 'Heads') 16.67% [======= ]
# ('Heads', 'Monday', 'Tails') 16.67% [======= ]
# ('Tails', 'Monday', 'Heads') 16.67% [======= ]
# ('Tails', 'Monday', 'Tails') 16.67% [======= ]
# ('Tails', 'Tuesday', 'Heads') 16.67% [======= ]
# ('Tails', 'Tuesday', 'Tails') 16.67% [======= ]
def verify_guess(actual, day, guess):
if actual == guess:
return 'Correct ' + guess
else:
return 'Incorrect'
guesses.starmap(verify_guess).plot()
# Incorrect 50.00% [==================== ]
# Correct Tails 33.33% [============= ]
# Correct Heads 16.67% [======= ]
# She is right more often by guessing tails. But no event gave her any
# evidence. Should she believe the coin landed tails?