5
\$\begingroup\$

I have the following code that simulates the monty hall problem (see google for more details). I used sets to do it but is there a more intuitive or efficient way to do it?

import random as r
from sets import Set
def montysim(N):
  K = 0
  for i in range(N):
    s = Set([1,2,3])
    doorstoswitch = Set([1,2,3])
    cardoor = r.randint(1,3)
    chosendoor = r.randint(1,3)
    doorstoswitch.remove(chosendoor)
    if chosendoor != cardoor:
        s.remove(chosendoor)
    s.remove(cardoor)

    montydoor = r.sample(s, 1)[0]
    doorstoswitch.remove(montydoor)
    newdoor = r.sample(doorstoswitch, 1)[0]
    if newdoor == cardoor:
        K+=1
  return float(K) / float(N)

print montysim(10000)
\$\endgroup\$
  • 4
    \$\begingroup\$ /OT Can't help that sounds like "Monty Python's flying circus" ;-) No offence. \$\endgroup\$ – πάντα ῥεῖ Apr 12 '17 at 17:34
6
\$\begingroup\$

There are a couple of changes you can make:

  1. You can use random.choice(...) on a list, rather than random.sample(...)[0] on a set.
  2. You shouldn't use sets:

    Deprecated since version 2.6: The built-in set/frozenset types replace this module.

    This means you can change Set to just set, or instead use the syntactic sugar: {1, 2, 3}.

  3. You can make doorstoswitch at the end. You can do this by inverting the check. So rather than cardoor == r.choice(doorstoswitch) you can use cardoor not in {chosendoor, montydoor}.

  4. You can then simplify the above to just cardoor != chosendoor, as montydoor can't be the car.

  5. You can remove the sets, as there's no need for them anymore.

And so your code can be:

import random

def montysim(n):
    k = 0
    for _ in range(n):
        k += random.randrange(3) != random.randrange(3)
    return float(k) / n
\$\endgroup\$
  • 1
    \$\begingroup\$ I think this is a bit extreme from a readability perspective as it doesnt simulate the switching of doors etc. I would want the code to be self annotating to the point where somebody who reads it can understand what the monty hall problem is. \$\endgroup\$ – user3079275 Apr 13 '17 at 0:35
  • \$\begingroup\$ @user3079275 I agree with you, but performance optimizations normally come at the cost of readability. Feel free to follow as many of my steps to achieve code you're happy with. I think to step 3 would be best for that. :) Also it could further be changed to something like: montysim = lambda n: float(sum(randrange(3) != 0 for _ in range(n))) / n, if you want to see what the extreme is. \$\endgroup\$ – Peilonrayz Apr 13 '17 at 0:47
  • 1
    \$\begingroup\$ @user3079275 I agree with you; this code wouldn't convince anybody that it was simulating the Monty Hall problem. The goal of code isn't always optimum performance! \$\endgroup\$ – ShreevatsaR Apr 13 '17 at 3:06
  • 1
    \$\begingroup\$ For instance, for even faster performance one could just change the function to return numpy.random.binomial(n, 2.0/3) / float(n) :-) It would give the same results, and much faster, but claiming that it is a simulation of the Monty Hall would be absurd (the same with the current code in this answer). \$\endgroup\$ – ShreevatsaR Apr 13 '17 at 3:33
6
\$\begingroup\$

Two things stick out to me:

First, Python has an official style-guide, PEP8. It recommends consistently using 4 spaces as indentation (you use a mix of two and four). It also recommends using lower_case_with_underscores for names.

Second, the sets module has been deprecated. You can just use the built-in set, instead of sets.Set.

import random


def monty_sim(n):
    k = 0
    for i in range(n):
        s = set([1, 2, 3])
        doors_to_switch = set([1, 2, 3])
        car_door = random.randint(1, 3)
        chosen_door = random.randint(1, 3)
        doors_to_switch.remove(chosen_door)
        if chosen_door != car_door:
            s.remove(chosen_door)
        s.remove(car_door)

        monty_door = random.sample(s, 1)[0]
        doors_to_switch.remove(monty_door)
        newdoor = random.sample(doors_to_switch, 1)[0]
        if newdoor == car_door:
            k += 1
      return float(k) / float(n)

if __name__ == "__main__":
    print monty_sim(10000)

This even comes with a nice speed-boost for free!

In [2]: %timeit montysim(10000)
10 loops, best of 3: 109 ms per loop

In [4]: %timeit monty_sim(10000)
10 loops, best of 3: 54 ms per loop
\$\endgroup\$
  • \$\begingroup\$ Try this for a bigger speed boost: import numpy and then def montySim(n): return numpy.random.binomial(n, 2.0/3) / float(n) :-) (See the comments on the other answer.) \$\endgroup\$ – ShreevatsaR Apr 13 '17 at 7:25
2
\$\begingroup\$

I think this is a bit extreme from a readability perspective as it doesnt simulate the switching of doors etc. I would want the code to be self annotating to the point where somebody who reads it can understand what the monty hall problem is.

I remember being bemused by this puzzle many years ago. And in the spirit of your quote I offer this rather hacked-together implementation. It runs through the entire game-show scenario in text before repeating it silently 10,000 times.

It's not efficient. It's not polished. It's not even particularly good, but I think it does the explaining bit. And if you squint you may not even notice the latent divide-by-zero error.

# Simulate the Monty Hall Problem

import random

class Stats:
    def __init__(self,title):
        self.title = title
        self.games = 0
        self.wins = 0
    def printit(self):
        print(self.title+": "+str(self.wins)+"/"+str(self.games)+" = " + str(self.wins*100//self.games)+"% " )

class Door:
    """ One for each door. """
    def __init__(self):
        self.status = "closed"
        self.contents = None
        self.chosen = False

class MontyHallProblem:
    """ Simulate one play of the Monty Hall problem """
    stats = None
    chosen_door = None

    def __init__(self,silent=False):
        """ Initialise the doors. """
        # this happens before the game starts, the player does not know
        # what is behind the doors. We put a car behind one door, and
        # fill the other two with goats.
        self.three_doors = [Door(), Door(), Door()]
        self.three_doors[random.randint(0,2)].contents = "FERRARI"
        for door in self.three_doors:
            if not door.contents:
                door.contents = "goat"
        if not silent:
            print("\nMonty says 'After that brief message from our sponsors WELCOME BACK to 'Let's Make a Deal'")
            print("As you can see, there are three doors in front of you ...")

    def look(self):
        """ Inspect the doors from the player's point of view. """
        for i, door in enumerate(self.three_doors):
            print("\nDoor "+str(i+1)+" is "+door.status)
            if door.status == "open":
                print("... there is a "+door.contents+" behind it!!")
            if door.chosen:
                print("... a green light shows that this is the door you have chosen")

    def choose_door(self,silent=False):
        """ Player chooses a door. """
        if silent:
            i = random.randint(0,2)
            self.chosen_door = self.three_doors[i]
            self.chosen_door.chosen = True
            return
        else:
            i = input("\nMonty says 'Choose a door - 1, 2 or 3': ")
            while i not in "123":
                i = input("Choose a door - 1, 2 or 3: ")
            self.chosen_door = self.three_doors[int(i)-1]
            self.chosen_door.chosen = True

    def monty_opens_a_door(self, silent=False):
        """ Monty opens one of the remaining two doors, revealing ..."""
        # The exact procedure here is crucial to the outcome of the problem.
        # Some descriptions of the problem don't make it clear that Monty
        # *knows* what is behind the doors and deliberately opens one that
        # does not reveal the Grand Prize (whatever that might be)
        if not silent:
            print("\nMonty says 'Thank you for choosing a door. Your choice is shown by the ")
            print("green light. Now I am going to open one of the two remaining doors ...")
        remaining_doors = [door for door in self.three_doors if not door.chosen]
        for i, door in enumerate(remaining_doors):
            if door.contents == "FERRARI":           # so he opens the *other* door
                remaining_doors[(i+1) % 2].status = "open"
                return
        remaining_doors[random.randint(0,1)].status = "open"

    def stick_or_switch(self,silent=False):
        """ Player chooses whether to switch choice of door. """
        global switched, non_switched
        if silent:
            switch = random.choice([True,False])
        else:
            print("\nMonty says 'There are now two doors remaining closed. You have already chosen one of them.")
            s = input("Would you like to switch your choice?' Y/N: ")
            while s not in "YyNn":
                s = input("Do you want to switch doors? Y/N: ")
            if s in ["Yy"]:
                switch = True
            else:
                switch = False

        if switch:
            self.stats = switched
            for door in self.three_doors:
                if door.status == "closed" and not door.chosen:
                    door.chosen = True
                    self.chosen_door.chosen = False
                    self.chosen_door = door
                    break
        else:
            self.stats = non_switched

    def denoument(self,silent=False):
        """ Heart-stopping stuff this. Did we win or not? """
        if not silent:
            print("\nMonty says 'You have made your choice. Come forward and open the door you have chosen.'")
            print("... You walk unsteadily towards the door and reach for the handle ...")
        self.chosen_door.status = "open"
        self.stats.games += 1
        if self.chosen_door.contents == "FERRARI":
            self.stats.wins += 1
            if not silent:
                print("\n... LIGHTS FLASH! The audience SCREAMS!! You jump for joy!!!")
        else:
            if not silent:
                print("\n... as the door swings open, you hear a disconsolate 'Baa' ...")



switched = Stats("Switched")
non_switched = Stats("Not switched")

def play_monty_hall(silent=False):
    M = MontyHallProblem(silent=silent)
    if not silent: M.look()
    M.choose_door(silent=silent)
    if not silent: M.look()
    M.monty_opens_a_door(silent=silent)
    if not silent: M.look()
    M.stick_or_switch(silent=silent)
    if not silent: M.look()
    M.denoument(silent=silent)
    if not silent: M.look()

play_monty_hall()

for i in range(10000):
    play_monty_hall(silent=True)
switched.printit()
non_switched.printit()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.