HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Monty Hall simulation with any number of doors

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
numbersimulationwithanydoorsmontyhall

Problem

After answering Monty hall python simulation I wondered what the outcome were to be if there were more than three doors, say four or seven. And so I decided to modify the problem slightly to adjust for this.

There are \$x\$ amount of doors. Behind one door is a car, the others are goats. You pick a door, and the host reveals a goat in a different door. You're then given the choice to change from the selected door to any door except the open door, or the already selected door.

And so I decided to find out how many times a person is likely to win if they always pick a random door when asked if they want to switch door. And so programmed the following:

import random

def monty_hall(amount, doors=3):
    if doors < 3:
        raise ValueError(f'doors must be greater than three, not {doors}')

    wins = 0
    for _ in range(amount):
        player_choice = random.randrange(doors)
        car_placement = random.randrange(doors)
        other_doors = set(range(doors)) - {player_choice, car_placement}

        shown_door = random.choice(list(other_doors))
        swap_doors = set(range(doors)) - {player_choice, shown_door}
        final_choice = random.choice(list(swap_doors))

        wins += final_choice == car_placement
    return wins

print(monty_hall(1000000, 3))
print(monty_hall(1000000, 4))
print(monty_hall(1000000, 5))


I then decided to optimize the above code, and came up with the following ways to do this:

  • Change player_choice to doors - 1. The first player choice doesn't have to be random.



  • Change other_doors and swap_doors to account for (1). Rather than removing an item that will always be in the set, remove it from the range and the second set. And so other_doors becomes: set(range(doors - 1)) - {car_placement}.



  • Remove player_choice as it's no longer used.



  • Change shown_door to always be the first door. However if the car is the first door, show the second. shown_door = car_placement == 0.



  • Remove other_doors as it

Solution

I would have used an helper function in the first version so that your monty_hall function is:

def monty_hall(amount, doors=3):
    if doors < 3:
        raise ValueError(f'doors must be greater than three, not {doors}')

    return sum(player_wins_a_car(doors) for _ in range(amount))


This also applies to the second version but removes some of the speed optimizations you made there; so not sure if it’s worth keeping.

However, in the first version, I would use list-comprehensions to reduce a bit the memory needed. It would also exhibit the actions more like a real player would do. Since creating the set from range will already iterate over each elements, using the list-comp won't be much more costy:

import random

def player_wins_a_car(doors):
    player_choice = random.randrange(doors)
    car_placement = random.randrange(doors)
    shown_door = random.choice([door for door in range(doors)
                                if door not in (player_choice, car_placement)])
    final_choice = random.choice([door for door in range(doors)
                                  if door not in (player_choice, shown_door)])

    return final_choice == car_placement


For the second version, if you were to keep two functions that could lead to more explanatory stuff like:

def player_wins_a_car(doors):
    # Choose a door that hide a car. Car is never behind door 0,
    # if it were behind door 0, consider door 0 and 1 to be
    # swapped so that host always open door 0.
    car_placement = random.randrange(doors) or 1
    # Consider an initial choice of door `doors - 1` and host
    # showing door 0 unconditionaly. Always choosing a random
    # door out of these two.
    final_choice = random.randrange(1, doors-1)

    return car_placement == final_choice


I’m using or here as the only value triggering max would be 0, not sure which one performs better.

Code Snippets

def monty_hall(amount, doors=3):
    if doors < 3:
        raise ValueError(f'doors must be greater than three, not {doors}')

    return sum(player_wins_a_car(doors) for _ in range(amount))
import random

def player_wins_a_car(doors):
    player_choice = random.randrange(doors)
    car_placement = random.randrange(doors)
    shown_door = random.choice([door for door in range(doors)
                                if door not in (player_choice, car_placement)])
    final_choice = random.choice([door for door in range(doors)
                                  if door not in (player_choice, shown_door)])

    return final_choice == car_placement
def player_wins_a_car(doors):
    # Choose a door that hide a car. Car is never behind door 0,
    # if it were behind door 0, consider door 0 and 1 to be
    # swapped so that host always open door 0.
    car_placement = random.randrange(doors) or 1
    # Consider an initial choice of door `doors - 1` and host
    # showing door 0 unconditionaly. Always choosing a random
    # door out of these two.
    final_choice = random.randrange(1, doors-1)

    return car_placement == final_choice

Context

StackExchange Code Review Q#161088, answer score: 5

Revisions (0)

No revisions yet.