Section 5.3 The Exponential Distribution
In the previous section, we simulated a simple model of a hockey game where a goal has the same probability of being scored during any second of the game. Under the same model, it turns out, the time until the first goal follows an exponential distribution.
To demonstrate, letβs assume again that the teams score a total of 6 goals, on average, and compute the probability of a goal during each second.
$ n = 3600
m = 6
p = m / 3600
p
0.0016666666666666668
The following function simulates
n seconds and uses argmax to find the time of the first goal. This works because the result from flip is a sequence of 1s and 0s, so the maximum is almost always 1. If there is at least one goal in the sequence, argmax returns the index of the first. If there are no goals, it returns 0, but that happens seldom enough that weβll ignore it.
def simulate_first_goal(n, p):
return flip(n, p).argmax()
Weβll use
simulate_first_goal to simulate 1001 games and make a list of the times until the first goal. The average time until the first goal is close to 600 seconds, or 10 minutes. And that makes sense β if we expect 6 goals per sixty-minute game, we expect one goal every 10 minutes, on average.
np.random.seed(3)
$ first_goal_times = [simulate_first_goal(n, p) for i in range(1001)]
mean = np.mean(first_goal_times)
mean
np.float64(597.7902097902098)
When
n is large and p is small, we can show mathematically that the expected time until the first goal follows an exponential distribution. The CDF of the exponential distribution is easy to compute.
def exponential_cdf(x, lam):
"""Compute the exponential CDF.
x: float or sequence of floats
lam: rate parameter
returns: float or NumPy array of cumulative probability
"""
return 1 - np.exp(-lam * x)
The value of
lam is the average number of events per unit of time β in this example it is goals per second. We can use the mean of the simulated results to compute lam.
$ lam = 1 / mean
lam
np.float64(0.0016728276636563566)
If we call this function with a range of time values, we can approximate the distribution of first goal times. The NumPy function
linspace creates an array of equally-spaced values; in this example, it computes 201 values from 0 to 3600, including both.
from empiricaldist import Cdf
ts = np.linspace(0, 3600, 201)
ps = exponential_cdf(ts, lam)
cdf_expo = Cdf(ps, ts, name="exponential model")
The following figure compares the simulation results to the exponential distribution we just computed.
cdf_sim = Cdf.from_seq(first_goal_times, name="simulation")
cdf_expo.plot(ls=":", color="gray")
cdf_sim.plot()
decorate(xlabel="Time of first goal (seconds)", ylabel="CDF")

The exponential model fits the results from the simulation very well β but a stronger test is to see how it does with real data.
The following loop reads the results for all games, gets the time of the first goal, and stores the result in a list. If no goals were scored, it adds
nan to the list.
filename = "nhl_2023_2024.hdf"
with pd.HDFStore(filename, "r") as store:
keys = store.keys()
firsts = []
for key in keys:
times = pd.read_hdf(filename, key=key)
if len(times) > 0:
firsts.append(times[0])
else:
firsts.append(np.nan)
To estimate the goal-scoring rate, we can use
nanmean, which computes the mean of the times, ignoring nan values.
$ lam = 1 / np.nanmean(firsts)
lam
np.float64(0.0015121567467720825)
Now we can compute the CDF of an exponential distribution with the same goal-scoring rate as the data, and compare it to the CDF of the data, using the
dropna=False argument to include nan values at the end.
ps = exponential_cdf(ts, lam)
cdf_expo = Cdf(ps, ts, name="exponential model")
$ cdf_firsts = Cdf.from_seq(firsts, name="data", dropna=False)
cdf_firsts.tail()
3286.0 0.996951
3581.0 0.997713
NaN 1.000000
Name: data, dtype: float64
cdf_expo.plot(ls=":", color="gray")
cdf_firsts.plot()
decorate(xlabel="Time of first goal (seconds)", ylabel="CDF")

The data deviate from the model in some places β it looks like there are fewer goals in the first 1000 seconds than the model predicts. But still, the model fits the data well.
The underlying assumption of these models β the Poisson model of goals and the exponential model of times β is that a goal is equally likely during any second of a game. If you ask a hockey fan whether thatβs true, they would say no, and they would be right β the real world violates assumptions like these in many ways. Nevertheless, theoretical distributions often fit real data remarkably well.
