Chapter 2: Conditional probability

This Jupyter notebook is the Python equivalent of the R code in section 2.10 R, pp. 80 - 83, Introduction to Probability, Second Edition, Blitzstein & Hwang.


In [1]:
import numpy as np

Simulating the frequentist interpretation

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.

In [2]:
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

In [3]:
np.random.choice(["girl", "boy"], n, replace=True)
Out[3]:
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)$.

In [4]:
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).

In [5]:
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$.

Monty Hall simulation

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:

In [6]:
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.

In [7]:
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:

  • The Monty class represents a simple state model for the game.
  • When an instance of the Monty game is created, game state-holding variables are initialized and a cardoor randomly chosen.
  • After the player initially picks a door, Monty will choose a remaining door that does not have car behind it.
  • The player can then choose to switch to the other, remaining unopened door, or stick with her initial choice.
  • Monty will then see if the player wins or not, and updates the state-holding variables for num. wins and num. plays.
  • The player can continue playing, or stop and reset the game to its original state.

As a short simulation program

Here is an example showing how to use the Monty class above to run a simulation to see how often the switching strategy succeeds.

In [8]:
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

As an interactive widget in this Jupyter notebook

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
In [9]:
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.

In [10]:
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).

In [11]:
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.

In [12]:
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.

  • The reveal button is used to show what's behind all of the doors after the player makes her final choice.
  • After the player completes a round of play, she can click the continue button to keep counting game state (num. wins and num. plays)
  • The reset button lets the player return the game to its original state after completing a round of play.
In [13]:
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.

In [14]:
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.

In [15]:
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)

How to play:

  • Click a door to select.
  • Monty will select a remaining door and open to reveal a goat.
  • Click the reveal button to open your selected door.
  • Or click the remaining unopened Door button to switch your door choice, and then click reveal.
  • Click the continue button to keep playing.
  • You may click the reset button at any time to return the game back to its initial state.

Joseph K. Blitzstein and Jessica Hwang, Harvard University and Stanford University, © 2019 by Taylor and Francis Group, LLC