Eliminating useless code
Enumerating a range is really pointless
In [6]: sample_size = 5
In [7]: for i, experimentNumber in enumerate(range(sample_size)):
...: print(i, experimentNumber)
...:
0 0
1 1
2 2
3 3
4 4
So we can easily replace one by the other. We do not even need to replace as experimentNumber
is not used anywhere. Next we notice that i
is also used only once where we can replace results[i-5:]
by superior construct results[-6:]
.
We also eliminate the superfluous exception handling. So far this is already covered by @ppperys answer.
Efficiency
you create a complet list of length sample_size
of random values in memory. This is not required and may be a problem on big sample sizes. As you always need the last 6 values only you could go for collections.deque
which can maintain a maxlen
.
from collections import deque
results = deque(maxlen=6)
For the evaluation made easy we do not use ('H', 'T')
but numbers. We do not need to comare with a streak any more but do it arithmetically. Here is the only pitfall - we must check if the queue is filled completely to not accidentally accept a short sequence of zeros.
for _ in range(sample_size):
results.append(random.choice((0, 1)))
if len(results) == 6 and sum(results) in (0, 6):
numberOfStreaks += 1
This not only saves memory but we also get rid of a temporary temp
and the predifined head_streak
and tail_streak
. We notice the magic number 6
appearing multiple times - use a variable. We also make a testable function. We end up with
import random
from collections import deque
def streak_probability(streak_len, sample_size):
results = deque(maxlen=streak_len)
numberOfStreaks = 0
for _ in range(sample_size):
results.append(random.choice((0, 1)))
if len(results) == streak_len and sum(results) in (0, streak_len):
numberOfStreaks += 1
return numberOfStreaks / sample_size
print('Chance of streak: %s%%' % (streak_probability(6, 1000000))
Algorithm
This simulation will give good results for big numbers of sample_size
. However if the sample size was smaller than 6
it will always return 0
. As you divide the final streak count by the sample size you indicate, that you would like to get the probability of a streak per "additional" coin toss. So we should fill the queue before starting to count. That way an average of a large number of runs with a small sample size would match a single run of a large sample size. If we prefill we do not have to check the fill state of the queue (yes I filled to the max while one less would be sufficient).
def prefilled_streak_probability(streak_len, sample_size):
results = deque((random.choice((0, 1)) for _ in range(streak_len)), maxlen=streak_len)
numberOfStreaks = 0
for _ in range(sample_size):
results.append(random.choice((0, 1)))
if sum(results) in (0, streak_len):
numberOfStreaks += 1
return numberOfStreaks / sample_size
Now test the difference - we compare the original sample size of 1.000.000 to 100.000 repetitions of sample size 10
s=10
n=100000
print('no prefill')
print('Single big sample - Chance of streak: %s%%' % (streak_probability(6, s*n)))
probs = [streak_probability(6, s) for _ in range(n)]
print('Multiple small samples - Chance of streak: %s%%' % (sum(probs)/len(probs)))
print('with prefill')
print('Single big sample - Chance of streak: %s%%' % (prefilled_streak_probability(6, s*n)))
probs = [prefilled_streak_probability(6, s) for _ in range(n)]
print('Multiple small samples - Chance of streak: %s%%' % (sum(probs)/len(probs)))
we get
no prefill
Single big sample - Chance of streak: 0.031372%
Multiple small samples - Chance of streak: 0.01573599999999932%
with prefill
Single big sample - Chance of streak: 0.031093%
Multiple small samples - Chance of streak: 0.031131999999994574%