PEP 8
The Style Guide for Python Code "gives coding conventions for the Python code ... intended to improve the readability of code and make it consistent across the wide spectrum of Python code."
Since a large majority of Python projects follow PEP-8 guideline, it behooves you to follow those conventions as much as possible (except when you shouldn't, as outlined in section 2 of the document).
These conventions include:
- using
snake_case
for variable names, instead of mixedCase
. Eg, numberOfStreaks
should be named number_of_streaks
.
- imports should be followed by a blank line
- functions should appear after import and before main code. Eg)
number_of_streaks = 0
, list_of_100 = []
, and streaks = 0
should appear after def toss():
- the mainline code should be inside a "main-guard" (
if __name__ == '__main__':
) statement.
Correctness
Task
I fear you have interpreted the practice task incorrectly, or at least, implemented it wrong.
The task is to check "if there is a streak of 6 'heads' or 'tails' in 100 coin tosses, not "how many" streaks occurred. It asks for a percentage success rate. If you had an unfair coin, with your code you may find several dozen streaks in each experiment, and well over 10,000 streaks in the course of the 10,000 experiments, which would lead to a "percentage success rate" that exceeds 100%, which is suspect.
(Incorrect, but left in to support Heap Overflow's answer)
Math
print('Chance of streak: %s%%' % (numberOfStreaks / 10000))
Simply dividing a count by the total possible does not yield a percentage; 95 / 100 = 0.95 ... you must multiply by 100 to compute the result as a percentage.
WET -vs- DRY and locality of reference.
Your code reads (roughly):
listOf100 = []
# ...
for experiment ...:
for flipCoin in range(100):
listOf100.append(toss())
...
listOf100 = []
You see the listOf100 = []
? WET stands for "Write Everything Twice". In contrast, DRY stands for "Don't Repeat Yourself". In general, with less code, the code is easier to understand and maintain. If variables are defined near where they are used, the code is also easier to understand and maintain.
Let's DRY this code up.
# ...
for experiment ...:
listOf100 = []
for flipCoin in range(100):
listOf100.append(toss())
...
Now, listOf100 = []
exists only once, and it exists right before it is being used.
Now, as demonstrated in the other two answers, you can replace the initialization and repeated .append()
with a more concise list comprehension.
Magic Numbers
I see several numbers in the code: 10000
, 100
, listOf100
, 1
, 5
, 0
. What do these numbers mean?
If you wanted to change the number of experiments from 10000
to 20000
how many changes would you have to make? Two?
If you wanted to changed the number of tosses per experiment from 100 to 200, how many changes do you have to make? Change a number once, and a variable name 6 times??? That seems awkward and unmaintainable. And wrong, because there is also the comment.
Named constants go a long way to improving maintainability.
NUM_EXPERIMENTS = 10_000
...
for experiementNumber in range(NUM_EXPERIMENTS):
...
print('Change of streak: %s%%' % (numberOfStreaks / NUM_EXPERIMENTS))
Finally, 5
is the length of the streak. No, wait, 6 is the length of the streak. Uh. It would be nice to have a STREAK_LENGTH = 6
named constant, and then the algorithm could use if streak == STREAK_LENGTH - 1:
, with perhaps a comment explaining the "why".
Unused variables
The variable created in this statement:
for experimentNumber in range(10000):
is never used anywhere. It only serves two purposes.
- to make a syntactically valid
for
statement.
- indicate this loop is executed once per experiment.
The second reason is obsoleted by changing the magic number 10000
into the named constant NUM_EXPERIMENTS
. By convention, _
is used as the throw-away variable, used only to satisfy syntactical reasons. So this for
statement could become:
for _ in range(NUM_EXPERIMENTS):
Ditto for the for flipCoin in range(100):
statement; it could become (say):
for _ in range(COIN_TOSSES_PER_EXPERIMENT):
Formatting numbers
Using the %s
format code for a number is not a good habit to get into. It may produce ok results here; you are dividing by 10,000 so likely will get a number with only 4 decimal points. But if you were asked to perform a different number of experiments, such as 7, you could get a lot of digits after the decimal point.
Using the format code %.4f
produces four digits after the decimal point, regardless of the actual number of experiments.
Improved Code
Others have answered with advanced -- or at best, tricky, and at worst, confusing -- methods of detecting the streaks including:
- string concatenation and substring searching
- functional programming
- converting head/tail coin values into same/different values
In the spirit of the beginner tag, let's investigate a clearer way.
You are currently testing listOf100[listItem] == listOf100[listItem + 1]
to check if a coin face is the same as the next. The [listItem + 1]
is the awkward part here, necessitating stopping our loop one element before the end of the list. Let's rethink this. Instead of comparing two coins at a time, how about examining only one coin at a time? Simply remember the whether the streak is currently heads or tails, and ask if the current coin matches that streak:
for coin_face in coin_tosses:
if coin_face == current_streak_face:
streak_length += 1
When we find a coin that doesn't match the current streak, we have to start the streak with one instance of the new face.
else:
current_streak_face = coin_face
streak_length = 1
Of course, we have to initialize our state variables. The first coin won't match any previous value, so we should start off with some value which is neither heads nor tails.
current_streak_face = None
streak_length = 0
Using this, we can create a simple coin streak detector function:
def contains_a_streak(coin_tosses, minimum_length):
current_streak_face = None
streak_length = 0
for coin_face in coin_tosses:
if coin_face == current_streak_face:
streak_length += 1
else:
current_streak_face = coin_face
streak_length = 1
if streak_length >= minimum_length:
return True
return False
Notice that since we are initialize the streak_length
to 1
when we find a different coin face, and adding 1
when we find a matching face, our streak_length
counter is actually the length of the streak, and not one less. No more 5 -vs- 6, confusion, which is a huge win for clarity.
Actually, there is nothing about this detector that is specific to coin tosses. We could use it for dice rolls, win-loss streaks, and so on. Just need to change some variable names ... and change the initial value from None
to a different sentinel, so it could even properly detect a streak of None
values at the start of a sequence of values.
def contains_a_streak(iterable, minimum_length):
current = object() # a unique value that can't possibly match this first
streak_length = 0
for value in iterable:
if current == value:
streak_length += 1
else:
current = value
streak_length = 1
if streak_length >= minimum_length:
return True
return False
Now, our code for one experiment could become:
def coin_toss_experiment(number_of_tosses, streak_length):
tosses = []
for _ in range(number_of_tosses):
tosses.append(toss())
return contains_a_streak(tosses, streak_length)
As noted elsewhere, the list initialization and repeated appending could be replaced with list comprehension:
def coin_toss_experiment(number_of_tosses, streak_length):
tosses = [toss() for _ in range(number_of_tosses)]
return contains_a_streak(tosses, streak_length)
(Actually, a generator expression might be even better, but since we're focusing at the beginner level, we'll just note it in passing. When you're a bit more comfortable with Python, look up what it is and what it would do for you, and why you might want to use one.)
We need to run multiple experiments to compute the streak success rate:
def repeated_coin_toss_experiment(num_experiments, num_tosses, streak_length):
successes = 0
for _ in range(num_experiments):
if coin_toss_experiment():
successes += 1
print(f"Chance of streak: {successes/num_experiments*100:.2f}%")
Finally, we need to run our experiment:
if __name__ == '__main__':
repeated_coin_toss_experiment(10_000, 100, 6)
If you want to change the number of tosses, you only have to change one number. If you want to change the number of experiments, again, you just have to change one number. Change the streak length? Well, you get the idea.