This Jupyter notebook is the Python equivalent of the R code in section 2.10 R, pp. 72 - 74, Introduction to Probability, 1st Edition, Blitzstein & Hwang.
import numpy as np
Recall that the frequentist interpretation of conditional probability based on a large number n
of repetitions of an experiment is $P(A|B) ≈ n_{AB}/n_{B}$, where $n_{AB}$ is the number of times that $A \cap B$ occurs and $n_{B}$ is the number of times that $B$ occurs. Let's try this out by simulation, and verify the results of Example 2.2.5. So let's use numpy.random.choice
to simulate n
families, each with two children.
np.random.seed(34)
n = 10**5
child1 = np.random.choice([1,2], n, replace=True)
child2 = np.random.choice([1,2], n, replace=True)
print('child1:\n{}\n'.format(child1))
print('child2:\n{}\n'.format(child2))
child1: [2 1 1 ... 1 2 1] child2: [2 2 2 ... 2 2 1]
Here child1
is a NumPy array
of length n
, where each element is a 1 or a 2. Letting 1 stand for "girl" and 2 stand for "boy", this array
represents the gender of the elder child in each of the n
families. Similarly, child2
represents the gender of the younger child in each family.
Alternatively, we could have used
np.random.choice(["girl", "boy"], n, replace=True)
array(['boy', 'boy', 'boy', ..., 'boy', 'boy', 'boy'], dtype='<U4')
but it is more convenient working with numerical values.
Let $A$ be the event that both children are girls and $B$ the event that the elder is a girl. Following the frequentist interpretation, we count the number of repetitions where $B$ occurred and name it n_b
, and we also count the number of repetitions where $A \cap B$ occurred and name it n_ab
. Finally, we divide n_ab
by n_b
to approximate $P(A|B)$.
n_b = np.sum(child1==1)
n_ab = np.sum((child1==1) & (child2==1))
print('P(both girls | elder is girl) = {:0.2F}'.format(n_ab / n_b))
P(both girls | elder is girl) = 0.50
The ampersand &
is an elementwise $AND$, so n_ab
is the number of families where both the first child and the second child are girls. When we ran this code, we got 0.50, confirming our answer $P(\text{both girls | elder is a girl}) = 1/2$.
Now let $A$ be the event that both children are girls and $B$ the event that at least one of the children is a girl. Then $A \cap B$ is the same, but n_b
needs to count the number of families where at least one child is a girl. This is accomplished with the elementwise $OR$ operator |
(this is not a conditioning bar; it is an inclusive $OR$, returning True
if at least one element is True
).
n_b = np.sum((child1==1) | (child2==2))
n_ab = np.sum((child1==1) & (child2==1))
print('P(both girls | at least one girl) = {:0.2F}'.format(n_ab / n_b))
P(both girls | at least one girl) = 0.33
For us, the result was 0.33, confirming that $P(\text{both girls | at least one girl}) = 1/3$.
Many long, bitter debates about the Monty Hall problem could have been averted by trying it out with a simulation. To study how well the never-switch strategy performs, let's generate 105 runs of the Monty Hall game. To simplify notation, assume the contestant always chooses door 1. Then we can generate a vector specifying which door has the car for each repetition:
np.random.seed(55)
n = 10**5
cardoor = np.random.choice([1,2,3] , n, replace=True)
print('The never-switch strategy has success rate {:.3F}'.format(np.sum(cardoor==1) / n))
The never-switch strategy has success rate 0.331
At this point we could generate the vector specifying which doors Monty opens, but that's unnecessary since the never-switch strategy succeeds if and only if door 1 has the car! So the fraction of times when the never-switch strategy succeeds is numpy.sum(cardoor==1)/n
, which was 0.331in our simulation. This is very close to 1/3.
What if we want to play the Monty Hall game interactively? We can do this by programming a Python class that would let us play interactively or let us run a simulation across many trials.
class Monty():
def __init__(self):
""" Object creation function. """
self.state = 0
self.doors = np.array([1, 2, 3])
self.prepare_game()
def get_success_rate(self):
""" Return the rate of success in this series of plays: num. wins / num. plays. """
if self.num_plays > 0:
return 1.0*self.num_wins / self.num_plays
else:
return 0.0
def prepare_game(self):
""" Prepare initial values for game play, and randonly choose the door with the car. """
self.num_plays = 0
self.num_wins = 0
self.cardoor = np.random.choice(self.doors)
self.players_choice = None
self.montys_choice = None
def choose_door(self, door):
""" Player chooses a door at state 0. Monty will choose a remaining door to reveal a goat. """
self.state = 1
self.players_choice = door
self.montys_choice = np.random.choice(self.doors[(self.doors!=self.players_choice) & (self.doors!=self.cardoor)])
def switch_door(self, do_switch):
""" Player has the option to switch from the door she has chosen to the remaining unopened door.
If the door the player has selected is the same as the cardoor, then num. of wins is incremented.
Finally, number of plays will be incremented.
"""
self.state = 2
if do_switch:
self.players_choice = self.doors[(self.doors!=self.players_choice) & (self.doors!=self.montys_choice)][0]
if self.players_choice == self.cardoor:
self.num_wins += 1
self.num_plays += 1
def continue_play(self):
""" Player opts to continue playing in this series.
The game is returned to state 0, but the counters for num. wins and num. plays
will be kept intact and running.
A new cardoor is randomly chosen.
"""
self.state = 0
self.cardoor = np.random.choice(self.doors)
self.players_choice = None
self.montys_choice = None
def reset(self):
""" The entire game state is returned to its initial state.
All counters and variable holdling state are re-initialized.
"""
self.state = 0
self.prepare_game()
In brief:
Monty
class represents a simple state model for the game.Monty
game is created, game state-holding variables are initialized and a cardoor
randomly chosen.Monty
will choose a remaining door that does not have car behind it.Monty
will then see if the player wins or not, and updates the state-holding variables for num. wins and num. plays.Here is an example showing how to use the Monty
class above to run a simulation to see how often the switching strategy succeeds.
np.random.seed(89)
trials = 10**5
game = Monty()
for _ in range(trials):
game.choose_door(np.random.choice([1,2,3]))
game.switch_door(True)
game.continue_play()
print('In {} trials, the switching strategy won {} times.'.format(game.num_plays, game.num_wins))
print('Success rate is {:.3f}'.format(game.get_success_rate()))
In 100000 trials, the switching strategy won 66730 times. Success rate is 0.667
Optionally, the Monty
Python class above can also be used as an engine to power an interactive widget that lets you play the three-door game in the browser using ipywidgets
.
To run the interactive widget, make sure you have the ipywidgets
package installed (v7.4.2 or greater).
To install with the conda
package manager, execute the following command:
conda install ipywidgets
To install with the pip
package manager, execute the following command:
pip install ipywidgets
from ipywidgets import Box, Button, ButtonStyle, FloatText, GridBox, IntText, Label, Layout, HBox
from IPython.display import display
The doors in the game are represented by ipywidgets.Button
.
door1 = Button(description='Door 1', layout=Layout(flex='1 1 auto', width='auto'))
door2 = Button(description='Door 2', layout=door1.layout)
door3 = Button(description='Door 3', layout=door1.layout)
doors_arr = [door1, door2, door3]
doors = Box(doors_arr, layout=Layout(width='auto', grid_area='doors'))
State-holding variables in the Monty
object are displayed using ipywidgets.IntText
(for the num_wins
and num_plays
); and ipywidgets.FloatText
(for the success rate).
label1 = Label(value='number of plays', layout=Layout(width='auto', grid_area='label1'))
text1 = IntText(disabled=True, layout=Layout(width='auto', grid_area='text1'))
label2 = Label(value='number of wins', layout=Layout(width='auto', grid_area='label2'))
text2 = IntText(disabled=True, layout=Layout(width='auto', grid_area='text2'))
label3 = Label(value='success rate', layout=Layout(width='auto', grid_area='label3'))
text3 = FloatText(disabled=True, layout=Layout(width='auto', grid_area='text3'))
ipywidgets.Label
is used to display the title and descriptive text in the game widget.
banner = Box([Label(value='Interactive widget: Monty Hall problem',
layout=Layout(width='50%'))],
layout=Layout(width='auto', justify_content='center', grid_area='banner'))
status = Label(value='Pick a door...', layout=Layout(width='auto', grid_area='status'))
Buttons allowing for further user actions are located at the bottom of the widget.
reveal
button is used to show what's behind all of the doors after the player makes her final choice.continue
button to keep counting game state (num. wins and num. plays)reset
button lets the player return the game to its original state after completing a round of play.button_layout = Layout(flex='1 1 auto', width='auto')
reveal = Button(description='reveal', tooltip='open selected door', layout=button_layout, disabled=True)
contin = Button(description='continue', tooltip='continue play', layout=button_layout, disabled=True)
reset = Button(description='reset', tooltip='reset game', layout=button_layout, disabled=True)
actions = Box([reveal, contin, reset], layout=Layout(width='auto', grid_area='actions'))
ipywidgets.GridBox
helps us lay out the user interface elements for the Monty
game widget.
ui = GridBox(children=[banner, doors, label1, text1, label2, text2, label3, text3, status, actions],
layout=Layout(
width='50%',
grid_template_rows='auto auto auto auto auto auto auto',
grid_template_columns='25% 25% 25% 25%',
grid_template_areas='''
"banner banner banner banner"
"doors doors doors doors"
"label1 label1 text1 text1"
"label2 label2 text2 text2"
"label3 label3 text3 text3"
"status status status status"
". . actions actions"
'''
)
)
We lastly create some functions to connect the widget to the Monty
game object. These functions adapt player action events to state changes in the Monty
object, and then update the widget user interface accordingly.
uigame = Monty()
def reset_ui(disable_reset=True):
""" Return widget elements to their initial state.
Do not disable the reset button in the case of continue.
"""
for i,d in enumerate(doors_arr):
d.description = 'Door {}'.format(i+1)
d.disabled = False
d.icon = ''
d.button_style = ''
reveal.disabled = True
contin.disabled = True
reset.disabled = disable_reset
def update_status(new_status):
""" Update the widget text fields for displaying present game status. """
text1.value = uigame.num_plays
text2.value = uigame.num_wins
text3.value = uigame.get_success_rate()
status.value = new_status
def update_ui_reveal():
""" Helper function to update the widget after the player clicks the reveal button. """
if uigame.players_choice == uigame.cardoor:
new_status = 'You win! Continue playing?'
else:
new_status = 'Sorry, you lose. Continue playing?'
for i,d in enumerate(doors_arr):
d.disabled = True
if uigame.cardoor == i+1:
d.description = 'car'
else:
d.description = 'goat'
if uigame.players_choice == i+1:
if uigame.players_choice == uigame.cardoor:
d.button_style = 'success'
d.icon = 'check'
else:
d.button_style = 'danger'
d.icon = 'times'
update_status(new_status)
reveal.disabled = True
contin.disabled = False
reset.disabled = False
def on_button_clicked(b):
""" Event-handling function that maps button click events in the widget
to corresponding functions in Monty, and updates the user interface
according to the present game state.
"""
if uigame.state == 0:
if b.description in ['Door 1', 'Door 2', 'Door 3']:
c = int(b.description.split()[1])
uigame.choose_door(c)
b.disabled = True
b.button_style = 'info'
m = doors_arr[uigame.montys_choice-1]
m.disabled = True
m.description = 'goat'
unopened = uigame.doors[(uigame.doors != uigame.players_choice) &
(uigame.doors != uigame.montys_choice)][0]
status.value = 'Monty reveals a goat behind Door {}. Click Door {} to switch, or \'reveal\' Door {}.' \
.format(uigame.montys_choice, unopened, uigame.players_choice)
reveal.disabled = False
reset.disabled = False
elif b.description == 'reset':
uigame.reset()
reset_ui()
update_status('Pick a door...')
elif uigame.state == 1:
if b.description in ['Door 1', 'Door 2', 'Door 3']:
prev_choice = uigame.players_choice
uigame.switch_door(True)
pb = doors_arr[prev_choice-1]
pb.icon = ''
pb.button_style = ''
b.disabled = True
b.button_style = 'info'
status.value = 'Now click \'reveal\' to see what\'s behind Door {}.'.format(uigame.players_choice)
elif b.description == 'reset':
uigame.reset()
reset_ui()
update_status('Pick a door...')
elif b.description == 'reveal':
uigame.switch_door(False)
update_ui_reveal()
elif uigame.state == 2:
if b.description == 'reveal':
update_ui_reveal()
else:
if b.description == 'continue':
uigame.continue_play()
reset_ui(False)
update_status('Pick a door once more...')
elif b.description == 'reset':
uigame.reset()
reset_ui()
update_status('Pick a door...')
# hook up all buttons to our event-handling function
door1.on_click(on_button_clicked)
door2.on_click(on_button_clicked)
door3.on_click(on_button_clicked)
reveal.on_click(on_button_clicked)
contin.on_click(on_button_clicked)
reset.on_click(on_button_clicked)
display(ui)
GridBox(children=(Box(children=(Label(value='Interactive widget: Monty Hall problem', layout=Layout(width='50%…
How to play:
reveal
button to open your selected door.reveal
.continue
button to keep playing.reset
button at any time to return the game back to its initial state.© Blitzstein, Joseph K.; Hwang, Jessica. Introduction to Probability (Chapman & Hall/CRC Texts in Statistical Science).