Jupyter Notebook Viewer
Jupyter Notebook Viewer
Laplace really nailed it, way back then! If you want to untangle a probability problem, all you
have to do is be methodical about defining exactly what the cases are, and then careful in
counting the number of favorable and total cases. We'll start being methodical by defining some
vocabulary:
Experiment (https://en.wikipedia.org/wiki/Experiment_(probability_theory%29): An
occurrence with an uncertain outcome that we can observe.
For example, rolling a die.
Outcome (https://en.wikipedia.org/wiki/Outcome_(probability%29): The result of an
experiment; one particular state of the world. What Laplace calls a "case."
For example: 4 .
Sample Space (https://en.wikipedia.org/wiki/Sample_space): The set of all possible
outcomes for the experiment.
For example, {1, 2, 3, 4, 5, 6} .
Event (https://en.wikipedia.org/wiki/Event_(probability_theory%29): A subset of
possible outcomes that together have some property we are interested in.
For example, the event "even die roll" is the set of outcomes {2, 4, 6} .
Probability (https://en.wikipedia.org/wiki/Probability_theory): As Laplace said, the
1 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Code for P
P is the traditional name for the Probability function:
Read this as implementing Laplace's quote directly: "Probability is thus simply a fraction whose
numerator is the number of favorable cases and whose denominator is the number of all the
cases possible."
We can define the sample space D and the event even , and compute the probability:
In [2]: D = {1, 2, 3, 4, 5, 6}
even = { 2, 4, 6}
P(even, D)
Out[2]: Fraction(1, 2)
You may ask: Why does the definition of P use len(event & space) rather than
len(event) ? Because I don't want to count outcomes that were specified in event but
aren't actually in the sample space. Consider:
P(even, D)
Out[3]: Fraction(1, 2)
Here, len(event) and len(space) are both 6, so if just divided, then P would be 1,
which is not right. The favorable cases are the intersection of the event and the space, which in
Python is (event & space) . Also note that I use Fraction rather than regular division
because I want exact answers like 1/3, not 0.3333333333333333.
2 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Urn Problems
Around 1700, Jacob Bernoulli wrote about removing colored balls from an urn in his landmark
treatise Ars Conjectandi (https://en.wikipedia.org/wiki/Ars_Conjectandi), and ever since then,
explanations of probability have relied on urn problems (https://www.google.com
/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=probability%20ball%20urn).
(You'd think the urns would be empty by now.)
An urn contains 23 balls: 8 white, 6 blue, and 9 red. We select six balls at
random (each possible selection is equally likely). What is the probability of each
of these possible outcomes:
So, an outcome is a set of 6 balls, and the sample space is the set of all possible 6 ball
combinations. We'll solve each of the 3 parts using our P function, and also using basic
arithmetic; that is, counting. Counting is a bit tricky because:
To account for the first issue, I'll have 8 different white balls labelled 'W1' through 'W8' ,
rather than having eight balls all labelled 'W' . That makes it clear that selecting 'W1' is
different from selecting 'W2' .
The second issue is handled automatically by the P function, but if I want to do calculations by
hand, I will sometimes first count the number of permutations of balls, then get the number of
combinations by dividing the number of permutations by c!, where c is the number of balls in a
combination. For example, if I want to choose 2 white balls from the 8 available, there are 8
ways to choose a first white ball and 7 ways to choose a second, and therefore 8 × 7 = 56
permutations of two white balls. But there are only 56 / 2 = 28 combinations, because (W1,
W2) is the same combination as (W2, W1) .
3 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
urn
Out[4]: {'B1',
'B2',
'B3',
'B4',
'B5',
'B6',
'R1',
'R2',
'R3',
'R4',
'R5',
'R6',
'R7',
'R8',
'R9',
'W1',
'W2',
'W3',
'W4',
'W5',
'W6',
'W7',
'W8'}
In [5]: len(urn)
Out[5]: 23
Now we can define the sample space, U6 , as the set of all 6-ball combinations. We use
itertools.combinations to generate the combinations, and then join each combination
into a string:
U6 = combos(urn, 6)
len(U6)
Out[6]: 100947
I don't want to print all 100,947 members of the sample space; let's just peek at a random
sample of them:
4 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
random.sample(U6, 10)
Is 100,947 really the right number of ways of choosing 6 out of 23 items, or "23 choose 6", as
mathematicians call it (https://en.wikipedia.org/wiki/Combination)? Well, we can choose any of
23 for the first item, any of 22 for the second, and so on down to 18 for the sixth. But we don't
care about the ordering of the six items, so we divide the product by 6! (the number of
permutations of 6 things) giving us:
23 ⋅ 22 ⋅ 21 ⋅ 20 ⋅ 19 ⋅ 18
23 choose 6 = = 100947
6!
Note that 23 ⋅ 22 ⋅ 21 ⋅ 20 ⋅ 19 ⋅ 18 = 23! / 17!, so, generalizing, we can write:
n!
n choose c =
(n − c)! ⋅ c!
And we can translate that to code and verify that 23 choose 6 is 100,947:
In [9]: choose(23, 6)
Out[9]: 100947
P(red6, U6)
Let's investigate a bit more. How many ways of getting 6 red balls are there?
In [11]: len(red6)
Out[11]: 84
5 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Why are there 84 ways? Because there are 9 red balls in the urn, and we are asking how many
ways we can choose 6 of them:
In [12]: choose(9, 6)
Out[12]: 84
So the probabilty of 6 red balls is then just 9 choose 6 divided by the size of the sample space:
Out[13]: True
P(b3w2r1, U6)
We can get the same answer by counting how many ways we can choose 3 out of 6 blues, 2 out
of 8 whites, and 1 out of 9 reds, and dividing by the number of possible selections:
Out[15]: True
Here we don't need to divide by any factorials, because choose has already accounted for
that.
We can get the same answer by figuring: "there are 6 ways to pick the first blue, 5 ways to pick
the second blue, and 4 ways to pick the third; then 8 ways to pick the first white and 7 to pick
the second; then 9 ways to pick a red. But the order 'B1, B2, B3' should count as the
same as 'B2, B3, B1' and all the other orderings; so divide by 3! to account for the
permutations of blues, by 2! to account for the permutations of whites, and by 100947 to get a
probability:
Out[16]: True
6 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
In [17]: w4 = {s for s in U6 if
s.count('W') == 4}
P(w4, U6)
Out[18]: True
Out[19]: True
even = {2, 4, 6}
But that's inelegant—I had to explicitly enumerate all the even numbers from one to six. If I ever
wanted to deal with a twelve or twenty-sided die, I would have to go back and change even . I
would prefer to define even once and for all like this:
Now in order to make P(even, D) work, I'll have to modify P to accept an event as either a
set of outcomes (as before), or a predicate over outcomes—a function that returns true for an
outcome that is in the event:
is_predicate = callable
Here we see how such_that , the new even predicate, and the new P work:
In [22]: such_that(even, D)
Out[22]: {2, 4, 6}
7 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
In [23]: P(even, D)
Out[23]: Fraction(1, 2)
such_that(even, D12)
Out[25]: Fraction(1, 2)
Note: such_that is just like the built-in function filter , except such_that returns a set.
We can now define more interesting events using predicates; for example we can determine the
probability that the sum of a three-dice roll is prime (using a definition of is_prime that is
efficient enough for small n ):
def is_prime(n): return n > 1 and not any(n % i == 0 for i in range(2, n))
P(prime_sum, D3)
Card Problems
Consider dealing a hand of five playing cards. We can define deck as a set of 52 cards, and
Hands as the sample space of all combinations of 5 cards:
Out[27]: 52
random.sample(Hands, 5)
Now we can answer questions like the probability of being dealt a flush (5 cards of the same
suit):
8 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
P(flush, Hands)
P(four_kind, Hands)
9 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Consider a gambling game consisting of tossing a coin. Player H wins the game if 10 heads
come up, and T wins if 10 tails come up. If the game is interrupted when H has 8 heads and T
has 7 tails, how should the pot of money (which happens to be 100 Francs) be split? In 1654,
Blaise Pascal and Pierre de Fermat corresponded on this problem, with Fermat writing
(http://mathforum.org/isaac/problems/prob1.html):
Dearest Blaise,
As to the problem of how to divide the 100 Francs, I think I have found a
solution that you will find to be fair. Seeing as I needed only two points to win
the game, and you needed 3, I think we can establish that after four more tosses
of the coin, the game would have been over. For, in those four tosses, if you did
not get the necessary 3 points for your victory, this would imply that I had in fact
gained the necessary 2 points for my victory. In a similar manner, if I had not
achieved the necessary 2 points for my victory, this would imply that you had in
fact achieved at least 3 points and had therefore won the game. Thus, I believe
the following list of possible endings to the game is exhaustive. I have denoted
'heads' by an 'h', and tails by a 't.' I have starred the outcomes that indicate a
win for myself.
h h h h * h h h t * h h t h * h h t t *
h t h h * h t h t * h t t h * h t t t
t h h h * t h h t * t h t h * t h t t
t t h h * t t h t t t t h t t t t
I think you will agree that all of these outcomes are equally likely. Thus I believe
that we should divide the stakes by the ration 11:5 in my favor, that is, I should
receive (11/16)*100 = 68.75 Francs, while you should receive 31.25 Francs.
Pierre
10 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
In [32]: continuations(2, 3)
In [33]: win_unfinished_game(2, 3)
Our answer agrees with Pascal and Fermat; we're in good company!
11 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
We define ProbDist to take the same kinds of arguments that dict does: either a mapping
or an iterable of (key, val) pairs, and/or optional keyword arguments.
We also need to modify the functions P and such_that to accept either a sample space or a
probability distribution as the second argument.
12 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
And here are some predicates that will allow us to answer some questions:
Out[38]: 0.4866706335070131
Out[39]: 0.4872245557856497
The above says that the probability of a girl is somewhere between 48% and 49%, but that it is
slightly different between the first or second child.
13 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
The above says that the sex of the second child is more likely to be the same as the first child,
by about 1/2 a percentage point.
The blue M&M was introduced in 1995. Before then, the color mix in a bag of
plain M&Ms was (30% Brown, 20% Yellow, 20% Red, 10% Green, 10% Orange,
10% Tan). Afterward it was (24% Blue , 20% Green, 16% Orange, 14% Yellow,
13% Red, 13% Brown). A friend of mine has two bags of M&Ms, and he tells me
that one is from 1994 and one from 1996. He won't tell me which is which, but
he gives me one M&M from each bag. One is yellow and one is green. What is
the probability that the yellow M&M came from the 1994 bag?
To solve this problem, we'll first represent probability distributions for each bag: bag94 and
bag96 :
Next, define MM as the joint distribution—the sample space for picking one M&M from each
bag. The outcome 'yellow green' means that a yellow M&M was selected from the 1994
bag and a green one from the 1996 bag.
14 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
First we'll look at the "One is yellow and one is green" part:
such_that(yellow_and_green, MM)
Now we can answer the question: given that we got a yellow and a green (but don't know which
comes from which bag), what is the probability that the yellow came from the 1994 bag?
15 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Out[45]: 0.7407407407407408
16 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
So there is a 74% chance that the yellow comes from the 1994 bag.
Answering this question was straightforward: just like all the other probability problems, we
simply create a sample space, and use P to pick out the probability of the event in question,
given what we know about the outcome. But in a sense it is curious that we were able to solve
this problem with the same methodology as the others: this problem comes from a section titled
My favorite Bayes's Theorem Problems, so one would expect that we'd need to invoke Bayes
Theorem to solve it. The computation above shows that that is not necessary.
Of course, we could solve it using Bayes Theorem. Why is Bayes Theorem recommended?
Because we are asked about the probability of an event given the evidence, which is not
immediately available; however the probability of the evidence given the event is.
Before we see the colors of the M&Ms, there are two hypotheses, A and B , both with equal
probability:
P(A | E)
That's not easy to calculate (except by enumerating the sample space). But Bayes Theorem
says:
17 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Which of the following three propositions has the greatest chance of success?
1. Six fair dice are tossed independently and at least one “6” appears.
2. Twelve fair dice are tossed independently and at least two “6”s appear.
3. Eighteen fair dice are tossed independently and at least three “6”s appear.
Newton was able to answer the question correctly (although his reasoning was not quite right);
let's see how we can do. Since we're only interested in whether a die comes up as "6" or not,
we can define a single die and the joint distribution over n dice as follows:
Now we are ready to determine which proposition is more likely to have the required number of
sixes:
18 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Out[49]: 0.6651020233196161
Out[50]: 0.6186673737323009
Out[51]: 0.5973456859478227
We reach the same conclusion Newton did, that the best chance is rolling six dice.
Simulation
Sometimes it is inconvenient to explicitly define a sample space. Perhaps the sample space is
infinite, or perhaps it is just very large and complicated, and we feel more confident in writing a
program to simulate one pass through all the complications, rather than try to enumerate the
complete sample space. Random sampling from the simulation can give an accurate estimate of
the probability.
Simulating Monopoly
A game of Monopoly can go on forever, so the sample space is infinite. But even if we limit the
sample space to say, 1000 rolls, there are 211000 such sequences of rolls (and even more
possibilities when we consider drawing cards). So it is infeasible to explicitly represent the
sample space.
But it is fairly straightforward to implement a simulation and run it for, say, 400,000 rolls (so the
average square will be landed on 10,000 times). Here is the code for a simulation:
19 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
def monopoly(steps):
"""Simulate given number of steps of Monopoly game,
yielding the number of the current square after each step."""
goto(0) # start at GO
CC_deck = Deck('GO JAIL' + 14 * ' ?')
CH_deck = Deck('GO JAIL C1 E3 H2 R1 R R U -3' + 6 * ' ?')
doubles = 0
jail = board.index('JAIL')
for _ in range(steps):
d1, d2 = random.randint(1, 6), random.randint(1, 6)
goto(here + d1 + d2)
doubles = (doubles + 1) if (d1 == d2) else 0
if doubles == 3 or board[here] == 'G2J':
goto(jail)
elif board[here].startswith('CC'):
do_card(CC_deck)
elif board[here].startswith('CH'):
do_card(CH_deck)
yield here
def goto(square):
"Update the global variable 'here' to be square."
global here
here = square % len(board)
def Deck(names):
"Make a shuffled deck of cards, given a space-delimited string."
cards = names.split()
random.shuffle(cards)
return deque(cards)
def do_card(deck):
"Take the top card from deck and do what it says."
global here
card = deck[0] # The top card
deck.rotate(-1) # Move top card to bottom of deck
if card == 'R' or card == 'U':
while not board[here].startswith(card):
goto(here + 1) # Advance to next railroad or utility
elif card == '-3':
goto(here - 3) # Go back 3 spaces
elif card != '?':
goto(board.index(card))# Go to destination named on card
I'll show a histogram of the squares, with a dotted red line at the average:
20 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
plt.hist(results, bins=40)
avg = len(results) / 40
plt.plot([0, 39], [avg, avg], 'r--');
21 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
There is one square far above average: JAIL , at a little over 6%. There are four squares far
below average: the three chance squares, CH1 , CH2 , and CH3 , at around 1% (because 10 of
the 16 chance cards send the player away from the square), and the "Go to Jail" square, square
number 30 on the plot, which has a frequency of 0 because you can't end a turn there. The other
squares are around 2% to 3% each, which you would expect, because 100% / 40 = 2.5%.
22 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
The Central Limit Theorem states that if you have a collection of random variables and sum
them up, then the larger the collection, the closer the sum will be to a normal distribution (also
called a Gaussian distribution or a bell-shaped curve). The theorem applies in all but a few
pathological cases.
As an example, let's take 5 random variables reprsenting the per-game scores of 5 basketball
players, and then sum them together to form the team score. Each random variable/player is
represented as a function; calling the function returns a single sample from the distribution:
And here is a function to sample a random variable k times, show a histogram of the results, and
return the mean:
The two top-scoring players have scoring distributions that are slightly skewed from normal:
23 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Out[58]: 30.09618
Out[59]: 22.1383
The next two players have bi-modal distributions; some games they score a lot, some games
not:
Out[60]: 14.02429
24 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Out[61]: 11.70888
The fifth "player" (actually the sum of all the other players on the team) looks like this:
Out[62]: 36.31564
Now we define the team score to be the sum of the five players, and look at the distribution:
25 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Out[63]: 114.31262
Sure enough, this looks very much like a normal distribution. The Central Limit Theorem appears
to hold in this case. But I have to say "Central Limit" is not a very evocative name, so I propose
we re-name this as the Strength in Numbers Theorem, to indicate the fact that if you have a lot
of numbers, you tend to get the expected result.
Conclusion
We've had an interesting tour and met some giants of the field: Laplace, Bernoulli, Fermat,
Pascal, Bayes, Newton, ... even Mr. Monopoly and The Count.
The conclusion is: be explicit about what the problem says, and then methodical about defining
the sample space, and finally be careful in counting the number of outcomes in the numerator
and denominator. Easy as 1-2-3.
26 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
But I was asked about continuous sample spaces, such as the space of real numbers. The
principles are the same: probability is still the ratio of the favorable cases to all the cases, but
now instead of counting cases, we have to (in general) compute integrals to compare the sizes
of cases. Here we will cover a simple example, which we first solve approximately by simulation,
and then exactly by calculation.
Two players go on a hot new game show called Higher Number Wins. The two
go into separate booths, and each presses a button, and a random number
between zero and one appears on a screen. (At this point, neither knows the
other’s number, but they do know the numbers are chosen from a standard
uniform distribution.) They can choose to keep that first number, or to press the
button again to discard the first number and get a second random number,
which they must keep. Then, they come out of their booths and see the final
number for each player on the wall. The lavish grand prize — a case full of gold
bullion — is awarded to the player who kept the higher number. Which number
is the optimal cutoff for players to discard their first number and choose
another? Put another way, within which range should they choose to keep the
first number, and within which range should they reject it and try their luck with a
second number?
For example, if player A chooses a cutoff of A = 0.6, that means that A would accept any first
number greater than 0.6, and reject any number below that cutoff. The question is: What cutoff,
A, should player A choose to maximize the chance of winning, that is, maximize P(a > b)?
First, simulate the number that a player with a given cutoff gets (note that random.random()
returns a float sampled uniformly from the interval [0..1]):
In [65]: number(.5)
Out[65]: 0.643051044503982
27 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
Now compare the numbers returned with a cutoff of A versus a cutoff of B, and repeat for a large
number of trials; this gives us an estimate of the probability that cutoff A is better than cutoff B:
Out[67]: 0.49946666666666667
Now define a function, top , that considers a collection of possible cutoffs, estimate the
probability for each cutoff playing against each other cutoff, and returns a list with the N top
cutoffs (the ones that defeated the most number of opponent cutoffs), and the number of
opponents they defeat:
28 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
We get a good idea of the top cutoffs, but they are close to each other, so we can't quite be sure
which is best, only that the best is somewhere around 0.60. We could get a better estimate by
increasing the number of trials, but that would consume more time.
The total area of the sample space is 0.5 × 0.4 = 0.20, and in general it is (1 - A) · (1 - B). What
about the favorable cases, where A beats B? That corresponds to the shaded triangle below:
The area of a triangle is 1/2 the base times the height, or in this case, 0.42 / 2 = 0.08, and in
general, (1 - B)2 / 2. So in general we have:
29 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
We're now ready to tackle the full game. There are four cases to consider, depending on whether
A and B gets a first number that is above or below their cutoff choices:
a>A b>B (1 - A) · (1 - B) Phigher(A, B) Both above cutoff; both keep first numbers
a<A b<B A·B Phigher(0, 0) Both below cutoff, both get new numbers from [0..1]
a>A b<B (1 - A) · B Phigher(A, 0) A keeps number; B gets new number from [0..1]
a<A b>B A · (1 - B) Phigher(0, B) A gets new number from [0..1]; B keeps number
For example, the first row of this table says that the event of both first numbers being above
their respective cutoffs has probability (1 - A) · (1 - B), and if this does occur, then the probability
of A winning is Phigher(A, B). We're ready to replace the old simulation-based Pwin with a new
calculation-based version:
That was a lot of algebra. Let's define a few tests to check for obvious errors:
test()
It is good to see that the simulation and the exact calculation are in rough agreement; that gives
me more confidence in both of them. We see here that 0.62 defeats all the other cutoffs, and
0.61 defeats all cutoffs except 0.62. The great thing about the exact calculation code is that it
runs fast, regardless of how much accuracy we want. We can zero in on the range around 0.6:
30 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
This says 0.618 is best, better than 0.620. We can get even more accuracy:
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1, 1, 1, projection='3d')
ax.set_xlabel('A')
ax.set_ylabel('B')
ax.set_zlabel('Pwin(A, B)')
ax.plot_surface(A, B, map2(Pwin, A, B));
So A could win 62.5% of the time if only B would chose a cutoff of 0. But, unfortunately for A, a
rational player B is not going to do that. We can ask what happens if the game is changed so
that player A has to declare a cutoff first, and then player B gets to respond with a cutoff, with
full knowledge of A's choice. In other words, what cutoff should A choose to maximize
Pwin(A, B) , given that B is going to take that knowledge and pick a cutoff that minimizes
Pwin(A, B) ?
And what if we run it the other way around, where B chooses a cutoff first, and then A
responds?
31 of 32 5/19/18, 3:34 PM
Jupyter Notebook Viewer http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability...
In both cases, the rational choice for both players in a cutoff of 0.61803, which corresponds to
the "saddle point" in the middle of the plot. This is a stable equilibrium; consider fixing B =
0.61803, and notice that if A changes to any other value, we slip off the saddle to the right or
left, resulting in a worse win probability for A. Similarly, if we fix A = 0.61803, then if B changes
to another value, we ride up the saddle to a higher win percentage for A, which is worse for B.
So neither player will want to move from the saddle point.
The moral for continuous spaces is the same as for discrete spaces: be careful about defining
your space; count/measure carefully, and let your code take care of the rest.
32 of 32 5/19/18, 3:34 PM