1
\$\begingroup\$

Here's the problem from the book Automate the boring stuff:

write a program to find out how often a streak of six heads or a streak of six tails comes up in a randomly generated list of heads and tails.

The #comments are a hint by the author. This program I created ran just fine but the chance of streak is surprisingly high to me: ~152% - 153%. So I'm wondering if I got the logic right? Any help will be greatly appreciated.

import random

def flip():
    random_num = random.randint(0, 1)
    if random_num == 1:
        return "H"
    else:
        return 'T'
            
numberOfStreaks = 0 #author wrote this line
for experimentNumber in range(10000): #author wrote this line
# Code that creates a list of 100 'heads' or 'tails' values.

    head_tail_str = ""
    side = flip() 
    head_tail_str += side
    for _ in range(99):
        prev_side = side
        side = flip()
        if side != prev_side:
            head_tail_str += f",{side}"
        else:
            head_tail_str += side

# Code that checks if there is a streak of 6 heads or tails in a row.

    head_tail_lst = head_tail_str.split(',')
    for item in head_tail_lst:
        if len(item) >= 6:
            numberOfStreaks += (int(len(item)) // 6)

print('Chance of streak: %s%%' % (numberOfStreaks / 100)) #author wrote this line

P.S. I've been learning Python for 1 month

\$\endgroup\$
6
  • \$\begingroup\$ If you flip a coin 100 times, the probability that the flips contain a streak of 6 or more cannot be greater than 100%. That means the program logic is incorrect (and technically that your question is off-topic for CodeReview). Your primary error comes from the low quality book you are reading: it has you compute the probability by dividing by 100 rather than by the number of experiments. Your second error is in counting streaks within an experiment rather than simply checking whether the experiment contains a sufficiently large streak or not. \$\endgroup\$
    – FMc
    May 5, 2023 at 20:18
  • \$\begingroup\$ @FMc Yes. The numbers look off to me. I never suspected anything wrong with the suggestion but I don't full understand the last line of code. What do you mean by "counting streaks within an experiment rather than simply checking whether the experiment contains a sufficiently large streak or not"? I don't understand this part. Thank u for your insight The book is written by Al Sweigart. I'm new to Python but it seems to me like a reputable book, recommended by lots of sites for newbies so I'm surprised that the code would be faulty \$\endgroup\$
    – Nova
    May 5, 2023 at 20:41
  • \$\begingroup\$ As I interpret the question, it asks the following: what percentage of "experiments" result in "success"? An experiment is flipping a coin 100 times. And success is a streak of 6+ heads or tails. To answer this question, you want to count successful experiments, not the total number of streaks across all experiments. Your code does the latter, but that overcounts things because a single experiment can contain multiple long steaks. \$\endgroup\$
    – FMc
    May 6, 2023 at 0:16
  • \$\begingroup\$ Hmm.. That's an interesting interpretation. I suppose the only way to know exactly what he meant is to ask the author himself \$\endgroup\$
    – Nova
    May 6, 2023 at 0:45
  • \$\begingroup\$ That ambiguity is one small reason why I dislike the book (the primary reason is that it places too little emphasis on building programs with functions). I had forgotten about a review of this very question I wrote a while back. My interpretation of the question appears to be shared by others. If you're interested, here's the review. \$\endgroup\$
    – FMc
    May 6, 2023 at 1:00

1 Answer 1

2
\$\begingroup\$

Logical

            numberOfStreaks += (int(len(item)) // 6)

Does a streak of 12 H mean 2 streaks?


how often a streak of six heads or a streak of six tails comes up

Sounds like just numberOfStreaks to me, not sure why it's being suggested to compare to 100 in order to produce a percentage:

print('Chance of streak: %s%%' % (numberOfStreaks / 100)) #author wrote this line

for experimentNumber in range(10000): #author wrote this line

The above suggests the code is designed to run multiple trials. For this to happen, it's important that each trial is independent, meaning state (values/data) does not bleed from one trial to another.

You'll see that's not happening if you run the script with this line instead:

for experimentNumber in range(1):

Structural

Would suggest using list instead of str to collect the flips, which more naturally expresses "a bunch of items".

It also helps to organize code by splitting up the two things you're doing: flipping 100 times and grouping together streaks.

# flip 100 times
flips = []
for _ in range(100):
    flips.append(flip())

# group together streaks:
first_flip, *rest_flips = flips
streaks = [[first_flip]]

for side in rest_flips:
    prev_streak = streaks[-1]  # [-1] returns the last item
    prev_side = prev_streak[-1]

    if side == prev_side:
        streak = prev_streak
    else:
        streak = []  # new streak
        streaks.append(streak)

    streak.append(side)

For "nicer" code

numberOfStreaks = 0 #author wrote this line

The convention in Python is underscore naming for variables, i.e. number_of_streaks.


In Python, 0 is "fasley", which means flip() can be:

    if random.randint(0, 1):
        return "H"  # when 1, "truthy"
    else:
        return "T"  # when 0, "falsey"

When grouping, can do all the flips together rather than the first_flip and rest_flips separately by handling the first iteration edge case:

streaks = []
for side in flips:
    prev_side = streaks and streaks[-1][-1]  # `[]` is also falsey

    if side == prev_side:
        streak = streaks[-1]
    else:
        streak = []
        streaks.append(streak)

    streak.append(side)

Leveling up

One of the reasons to split up the flipping from the grouping is that grouping is so generally useful, it's already available in the standard library as itertools.groupby:

import itertools as it

flips = [flip() for _ in range(100)]
streaks = [list(grp) for _, grp in it.groupby(flips)]
\$\endgroup\$
10
  • \$\begingroup\$ Thank you so much for the very detailed and thorough answer. | Does a streak of 12 H mean 2 streaks? ----> Yes. Because the problem was asking for streaks of 6 I don't get this part either: 'print('Chance of streak: %s%%' % (numberOfStreaks / 100)) ' . It took me a few times to have an idea how the list grouping works and the logic of what's going on in that 'for' loop you wrote. That was very educational. And I love your 'nicer' code suggestions too. Did you think my results were off? Why are the chances so high? \$\endgroup\$
    – Nova
    May 5, 2023 at 21:30
  • \$\begingroup\$ Whoops, forgot to mention that part. Added another section to #Logical \$\endgroup\$
    – Kache
    May 5, 2023 at 21:50
  • \$\begingroup\$ thanks! I calculated number of streaks in each trial, then got the average of streaks across all trials, divided by 100. Now the chance is super low ~0.015% .It makes more sense now \$\endgroup\$
    – Nova
    May 5, 2023 at 22:21
  • 1
    \$\begingroup\$ Again, dividing the final count by 100 doesn't really make sense. Perhaps the 100 was the "dividing by the number of trials" to get the average, which you've already done. \$\endgroup\$
    – Kache
    May 8, 2023 at 18:28
  • \$\begingroup\$ Hi, I'm reviewing the solution of this exercise. Could you help me understand this: prev_side = streaks and streaks[-1][-1] . What is happening here? Thank you \$\endgroup\$
    – Nova
    May 9, 2023 at 14:47

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