Skip to main content

Section 5.1 The Binomial Distribution

As a first example, we’ll consider the sport of skeet shooting, in which competitors use shotguns to shoot clay disks that are thrown into the air. In international competition, including the Olympics, there are five rounds with 25 targets per round, with additional rounds as needed to determine a winner.
As a model of a skeet-shooting competition, suppose that every participant has the same probability, p, of hitting every target. Of course, this model is a simplification β€” in reality, some competitors have a higher probability than others, and even for a single competitor, it might vary from one attempt to the next. But even if it is not realistic, this model makes some surprisingly accurate predictions, as we’ll see.
To simulate the model, I’ll use the following function, which takes the number of targets, n, and the probability of hitting each one, p, and returns a sequence of 1s and 0s to indicate hits and misses.
Listing 5.1.1. Python Code
def flip(n, p):
    choices = [1, 0]
    probs = [p, 1 - p]
    return np.random.choice(choices, n, p=probs)
Here’s an example that simulates a round of 25 targets where the probability of hitting each one is 90%.
Listing 5.1.2. Python Code
# Seed the random number generator so we get the same results every time
np.random.seed(1)
Listing 5.1.3. Python Code
$ flip(25, 0.9)
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
       1, 1, 1])
If we generate a longer sequence and compute the Pmf of the results, we can confirm that the proportions of 1s and 0s are correct, at least approximately.
Listing 5.1.4. Python Code
$ from empiricaldist import Pmf

seq = flip(1000, 0.9)
pmf = Pmf.from_seq(seq)
pmf
0    0.101
1    0.899
Name: , dtype: float64
Now we can use flip to simulate a round of skeet shooting and return the number of hits.
Listing 5.1.5. Python Code
def simulate_round(n, p):
    seq = flip(n, p)
    return seq.sum()
In a large competition, suppose 200 competitors shoot 5 rounds each, all with the same probability of hitting the target, p=0.9. We can simulate a competition like that by calling simulate_round 1000 times.
Listing 5.1.6. Python Code
n = 25
p = 0.9
results_sim = [simulate_round(n, p) for i in range(1000)]
The average score is close to 22.5, which is the product of n and p.
Listing 5.1.7. Python Code
$ np.mean(results_sim), n * p
(np.float64(22.522), 22.5)
Here’s what the distribution of the results looks like.
Listing 5.1.8. Python Code
from empiricaldist import Pmf

pmf_sim = Pmf.from_seq(results_sim, name="simulation results")

pmf_sim.bar()
decorate(xlabel="Hits", ylabel="PMF")
Figure 5.1.9.
The peak is near the mean, and the distribution is skewed to the left.
Instead of running a simulation, we could have predicted this distribution. Mathematically, the distribution of these outcomes follows a binomial distribution, which has a PMF that is easy to compute.
Listing 5.1.10. Python Code
from scipy.special import comb


def binomial_pmf(k, n, p):
    return comb(n, k) * (p**k) * ((1 - p) ** (n - k))
SciPy provides the comb function, which computes the number of combinations of n things taken k at a time, often pronounced "n choose k".
binomial_pmf computes the probability of getting k hits out of n attempts, given p. If we call this function with a range of k values, we can make a Pmf that represents the distribution of the outcomes.
Listing 5.1.11. Python Code
ks = np.arange(16, n + 1)
ps = binomial_pmf(ks, n, p)
pmf_binom = Pmf(ps, ks, name="binomial model")
And here’s what it looks like compared to the simulation results.
Listing 5.1.12. Python Code
from thinkstats import two_bar_plots

two_bar_plots(pmf_sim, pmf_binom)
decorate(xlabel="Hits", ylabel="PMF")
Figure 5.1.13.
They are similar, with small differences because of random variation in the simulation results. This agreement should not be surprising, because the simulation and the model are based on the same assumptions β€” particularly the assumption that every attempt has the same probability of success. A stronger test of a model is how it compares to real data.
From the Wikipedia page for the men’s skeet shooting competition at the 2020 Summer Olympics, we can extract a table that shows the results for the qualification rounds.
Listing 5.1.14. Python Code
filename = "Shooting_at_the_2020_Summer_Olympics_Mens_skeet"
Listing 5.1.15. Python Code
download("https://github.com/AllenDowney/ThinkStats/raw/v3/data/" + filename)
Listing 5.1.16. Python Code
$ tables = pd.read_html(filename)
table = tables[6]
table.head()
   Rank              Athlete        Country   1   2   3   4   5 Total[3]  \
0     1        Γ‰ric Delaunay         France  25  25  25  24  25      124   
1     2    Tammaro Cassandro          Italy  24  25  25  25  25      124   
2     3      Eetu Kallioinen        Finland  25  25  24  25  24      123   
3     4      Vincent Hancock  United States  25  25  25  25  22      122   
4     5  Abdullah Al-Rashidi         Kuwait  25  25  24  25  23      122   

   Shoot-off  Notes  
0        +6  Q, OR  
1        +5  Q, OR  
2       NaN      Q  
3        +8      Q  
4        +7      Q
The table has one row for each competitor, with one column for each of five rounds. We’ll select the columns that contain these results and use the NumPy function flatten to put them into a single array.
Listing 5.1.17. Python Code
columns = ["1", "2", "3", "4", "5"]
results = table[columns].values.flatten()
With 30 competitors, we have results from 150 rounds of 25 shots each, with 3750 hits out of a total of 3575 attempts.
Listing 5.1.18. Python Code
$ total_shots = 25 * len(results)
total_hits = results.sum()
n, total_shots, total_hits
(25, 3750, np.int64(3575))
So the overall success rate is 95.3%.
Listing 5.1.19. Python Code
$ p = total_hits / total_shots
p
np.float64(0.9533333333333334)
Now let’s compute a Pmf that represents the binomial distribution with n=25 and the value of p we just computed, and compare that to the Pmf of the actual results.
Listing 5.1.20. Python Code
ps = binomial_pmf(ks, n, p)
pmf_binom = Pmf(ps, ks, name="binomial model")
Listing 5.1.21. Python Code
pmf_results = Pmf.from_seq(results, name="actual results")

two_bar_plots(pmf_results, pmf_binom)
decorate(xlabel="Hits", ylabel="PMF")
Figure 5.1.22.
The binomial model is a good fit for the distribution of the data β€” even though it makes the unrealistic assumption that all competitors have the same, unchanging capability.