#!/usr/bin/env python # coding: utf-8 # # Chapter 2: Conditional probability # # This Jupyter notebook is the Python equivalent of the R code in section 2.10 R, pp. 72 - 74, [Introduction to Probability, 1st Edition](https://www.crcpress.com/Introduction-to-Probability/Blitzstein-Hwang/p/book/9781466575578), 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`](https://docs.scipy.org/doc/numpy-1.15.0/reference/generated/numpy.random.choice.html) 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)) # 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) # 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)) # 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)) # 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)) # 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())) # ### 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` ](https://ipywidgets.readthedocs.io/en/stable/user_guide.html). # # 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`](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#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`](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#IntText) (for the `num_wins` and `num_plays`); and [`ipywidgets.FloatText`](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#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`](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#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`](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Styling.html#The-Grid-layout) 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](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Events.html#Example) 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 to switch doors, 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. # ---- # # © Blitzstein, Joseph K.; Hwang, Jessica. Introduction to Probability (Chapman & Hall/CRC Texts in Statistical Science).