- Mon 09 December 2019
- Data Science
- Michael Lehotay
- #probability, #simulation, #python
I've never really liked the Monty Hall problem. The soution is counterintuative and my brain kind of breaks when I try to wrap my head around the conditional probabilities. In situations like this it can be beneficial to look at the problem empirically and estimate the probabilities of the outcomes in the event space in code. So, let's play the game ourselves and see what happens!
We start by creating a model of the game to simulate one iteration, or one time playing the game. I've included print statements to describe what happens are every step.
import random
# game setup
NUM_DOORS = 3
assert NUM_DOORS > 2, 'not enough doors'
random.seed()
winning_door = random.randint(1, NUM_DOORS) # randomly select the winning door
print(f'Game begins. (The car is behind door {winning_door}).')
# player chooses one of the doors
player_door = random.randint(1, NUM_DOORS)
print(f'Player chooses door {player_door}.')
# Monty opens a different door to reveal a goat
while(True):
goat_door = random.randint(1, NUM_DOORS)
if(goat_door != winning_door and goat_door != player_door):
break
print(f'Monty opens door {goat_door} to reveal a goat.')
# player chooses another door, if they want to
switch = random.choice([False, True])
if(switch):
while(True):
new_door = random.randint(1, NUM_DOORS)
if(new_door != player_door and new_door != goat_door):
break
player_door = new_door
print(f'Player switches to door {player_door}.')
else:
print(f'Player still chooses door {player_door}.')
# determine the outcome
print(f'Monty opens door {winning_door} to reveal the car.')
if player_door == winning_door:
print('Player wins!')
else:
print('Player loses.')
Game begins. (The car is behind door 2).
Player chooses door 1.
Monty opens door 3 to reveal a goat.
Player switches to door 2.
Monty opens door 2 to reveal the car.
Player wins!
At this point we have a working system that can play the game once. After testing it thoroughly to verify that it implements the behaviour described in the problem, let's turn it into a function so we can re-use it more easily.
def monty_hall(switch, n=NUM_DOORS):
assert n > 2, 'not enough doors'
# game setup
winning_door = random.randint(1, n) # randomly select the winning door
print(f'Game begins. (The car is behind door {winning_door}).')
# player chooses one of the doors
player_door = random.randint(1, n)
print(f'Player chooses door {player_door}.')
# Monty opens a different door to reveal a goat
while(True):
goat_door = random.randint(1, n)
if(goat_door != winning_door and goat_door != player_door):
break
print(f'Monty opens door {goat_door} to reveal a goat.')
# player chooses another door, if they want to
if(switch):
while(True):
new_door = random.randint(1, n)
if(new_door != player_door and new_door != goat_door):
break
player_door = new_door
print(f'Player switches to door {player_door}.')
else:
print(f'Player still chooses door {player_door}.')
# determine the outcome
print(f'Monty opens door {winning_door} to reveal the car.')
if player_door == winning_door:
print('Player wins!')
else:
print('Player loses.')
We can call our function with a randomized strategy like this:
monty_hall(random.choice([False, True]))
Game begins. (The car is behind door 2).
Player chooses door 2.
Monty opens door 3 to reveal a goat.
Player still chooses door 2.
Monty opens door 2 to reveal the car.
Player wins!
Now lets clean it up a bit and remove the print statements so it runs quietly.
def monty_hall(switch, n=NUM_DOORS):
assert n > 2, 'not enough doors'
winning_door = random.randint(1, n) # randomly select the winning door
# player chooses one of the doors
player_door = random.randint(1, n)
# Monty opens a different door to reveal a goat
while(True):
goat_door = random.randint(1, n)
if(goat_door != winning_door and goat_door != player_door):
break
# player chooses another door, if they want to
if(switch):
while(True):
new_door = random.randint(1, n)
if(new_door != player_door and new_door != goat_door):
break
player_door = new_door
# determine the outcome
return (player_door == winning_door)
monty_hall(random.choice([False, True]))
True
Now that we have create a model for the problem, we can estimate the probabilites with simulation. We run the model repeatedly and measure the frequency of each outcome over many iterations of the game. These frequencies represent our probabilites!
It looks like the probability of winning is about 0.5 when the player chooses to switch or stay with equal probability.
N = 10000
results = []
for _ in range(N):
results.append(monty_hall(random.choice([False, True])))
sum(results)/N
0.4926
But when the player always sticks to their original guess, their win rate drops to about 0.33.
results = []
for _ in range(N):
results.append(monty_hall(False))
sum(results)/N
0.3215
And if the player always changes their guess, their win rate increases to about 0.67.
results = []
for _ in range(N):
results.append(monty_hall(True))
sum(results)/N
0.6642
We can change our loop to a list comprehension to make it python-y. (I'm hoping this will seem more readable to me after I get more practice with list comprehensions!)
results = [monty_hall(True) for _ in range(N)]
sum(results)/N
0.6717
And for conciseness, lets eliminate the results variable and run each simulation as a single expression.
sum([monty_hall(True) for _ in range(N)])/N
0.663
Now with our model complete, we can easily simulate the Monty Hall problem with different parameters. Let's see what increasing the number of doors does:
for doors in range(3, 10+1):
for strategy in [False, True]:
print(f'{doors}, {strategy}: {sum([monty_hall(strategy, doors) for _ in range(N)])/N}')
3, False: 0.3337
3, True: 0.6684
4, False: 0.249
4, True: 0.3699
5, False: 0.2046
5, True: 0.266
6, False: 0.1666
6, True: 0.2072
7, False: 0.1525
7, True: 0.1725
8, False: 0.127
8, True: 0.1396
9, False: 0.1107
9, True: 0.1275
10, False: 0.0961
10, True: 0.1143
Hmmm, it looks like no matter how many doors there are, it is always better to change your guess. See how easy it is to estimate this programatically!