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

Monte Carlo coin flip simulation

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

Problem

I've been learning about Monte Carlo simulations on MIT's intro to programming class, and I'm trying to implement one that calculates the probability of flipping a coin heads side up 4 times in a row out of ten flips.

Basically, I calculate if the current flip in a 10 flip session is equal to the prior flip, and if it is, I increment a counter. Once that counter has reached 3, I exit the loop even if I haven't done all 10 coin flips since subsequent flips have no bearing on the probability. Then I increment a counter counting the number of flip sessions that successfully had 4 consecutive heads in a row. At the end, I divide the number of successful sessions by the total number of trials.

The simulation runs 10,000 trials.

def simThrows(numFlips):
    consecSuccess = 0   ## number of trials where 4 heads were flipped consecutively
    coin = 0  ## to be assigned to a number between 0 and 1
    numTrials = 10000
    for i in range(numTrials):
        consecCount = 0
        currentFlip = ""
        priorFlip = ""
        while consecCount  .5:
                    currentFlip = "Tails"
                    if currentFlip == priorFlip:
                        consecCount = 0
                        priorFlip = "Tails"
                    else:
                        consecCount = 0
                        priorFlip = "Tails"
            break
        if consecCount >= 3:
            print("Success!")
            consecSuccess += 1
    print("Probability of getting 4 or more heads in a row: " + str(consecSuccess/numTrials))

simThrows(10)


The code seems to work (gives me consistent results each time that seem to line up to the analytic probability), but it's a bit verbose for such a simple task. Does anyone see where my code can get better?

Solution

When you're writing code for educational purposes (or sometimes other purposes), verbose is good because it helps you understand what's really going on. So making the code shorter or snappier or whatever is not necessarily going to make it better.

With that disclaimer out of the way: one of the most common ways to condense Python code is to use list comprehensions or generators instead of loops. A list comprehension is what you use when you're constructing a list element by element: in its simplest form, instead of this,

the_list = []
for something in something_else:
    the_list.append(func(something))


you write this:

the_list = [func(something) for something in something_else]


If you're doing something else instead of creating a list, you can have Python create an object that generates the elements on demand, rather than actually creating a list out of them. An object of that sort is called a generator and you can create one like this:

the_generator = (func(something) for something in something_else)


You can omit the parentheses when the generator is passed to another function as an argument, though.

the_sum = sum(func(something) for something in something_else)


would be equivalent to, but better than,

count = 0
for something in something_else:
    count += func(something)


There are a lot of functions in Python that take iterables (list, generators, etc.) and "condense" them into one value using some sort of operation. You can also create your own, corresponding to whatever you would be doing to the result of the loop. You can convert most loops into generator expressions this way.

So let's investigate how you could use a generator to represent the sequences of consecutive throws in each trial. You can create a generator that produces 10 random numbers easily:

(random.random() for i in xrange(10))


(this is for Python 2.x; xrange was renamed to range for Python 3). Or you can create a generator that produces 10 random values which are either 0 or 1:

(random.randint(0,1) for i in xrange(10))


That saves you from having to check each random number against 0.5. In fact, you could produce a generator that produces 10 randomly chosen words, "Heads" or "Tails", like so:

(random.choice(("Heads","Tails")) for i in xrange(10))


but it'll be easier to stick with numbers. (It's usually better to represent things with numbers or objects than with strings.)

But perhaps you're thinking, "why are you telling me to make 10 numbers when I only have to check until I find a group of four consecutive heads?" For one thing, if you're just flipping 10 coins each time, it really doesn't matter because you'll make the computer flip at most 6, and on average 3, extra coins in each trial. That doesn't take very long - it'll extend the runtime of this part of your program by 50%, but we're talking 50% of a fraction of a second. It's not worth the effort to figure out how to do it for such a small number of flips. But if each flip had, say, a billion trials, then you would definitely want to stop early. Fortunately, a generator can do this for you! Since generators produce their elements only on demand, you can stop taking elements from it once you get what you want, and not waste much of any computation. I'll address this more later.

Anyway, suppose we have our generator that produces 10 binary values 0 (tails) or 1 (heads). Is there a way to go through this and check to see whether there is a sequence of four or more heads? It turns out that just such a function is provided in itertools.groupby, which takes any iterable (list, generator, etc.) and groups consecutive identical elements. An example of its usage is

for k, g in itertools.groupby([1,0,0,1,1,1,1,0,0,0]):
    print k, list(g)


and this would print out something like

1 [1]
0 [0,0]
1 [1,1,1,1]
0 [0,0,0]


So you can check for four or more consecutive heads by just looking at the length of the group and whether the key is heads or tails.

for k, g in itertools.groupby(random.randint(0,1) for i in xrange(10)):
    if k and len(g) >= 4:
        # got a run of 4 or more consecutive heads!
        # wait, what now?


(In Python, 1 is true and 0 is false in a boolean context, so if k is equivalent to if k == 1.) OK, what shall we do with our run of 4 or more consecutive heads? Well, you're trying to find the number of trials in which this occurs. So it probably makes sense to set a success flag if this happens.

success = False
for k, g in itertools.groupby(random.randint(0,1) for i in xrange(10)):
    if k and len(g) >= 4:
        success = True
        break # this stops asking the generator for new values


But wait! This is starting to look a lot like the kind of loop that can be converted to a generator expression, isn't it? The only catch is that we're not adding anything up or constructing a list. But there is another function, any, that will go th

Code Snippets

the_list = []
for something in something_else:
    the_list.append(func(something))
the_list = [func(something) for something in something_else]
the_generator = (func(something) for something in something_else)
the_sum = sum(func(something) for something in something_else)
count = 0
for something in something_else:
    count += func(something)

Context

StackExchange Code Review Q#16310, answer score: 9

Revisions (0)

No revisions yet.