Step 2 of a simulation study is to sample realizations of the random inputs from their distributions. This notebook first explains the method behind almost all sampling, the inverse transform method, which you can also apply by hand. It then shows how to sample in Python with numpy, and how a seed makes the results reproducible. Monte Carlo Simulation uses these samples.
Learning outcomes
On completion of this notebook, you will be able to:
explain the inverse transform method and why it works
sample by hand from a continuous or discrete distribution, given uniform[0, 1] numbers
sample from common distributions with a
numpyrandom generator, and use a seed to make results reproducible
The Inverse Transform Method¶
Every programming language can generate numbers that are approximately uniformly distributed on . The inverse transform method (ITM) turns them into samples from any distribution with cdf : sample and return the with . We write this as . Figure 1 shows the idea: go from on the vertical axis horizontally to the cdf, and then down to the -axis.
Figure 1:The inverse transform method for the exponential distribution with rate 0.5: the uniform numbers and become the samples and .
Why it works. Let be uniform on , so . Because is non-decreasing, holds exactly when . So for every ,
The sample has the same cdf as , so it has the same distribution.
Intuition. Uniform numbers are spread evenly over the vertical axis. Where is steep, a large part of the vertical axis maps to a short interval of -values, so many samples land there. is steep exactly where the density is high, so the samples follow the shape of the pdf. In Figure 1, most -values end up at small , where the exponential density is highest. Ross (2022) covers the ITM and other sampling methods in more detail.
Example: uniform[, ]. The cdf is on . Solving gives
Example: exponential. A component’s lifetime (in years) is exponential with rate , so mean years, and for . Solving :
Since is uniform on as well, also gives exponential samples, and is often used instead.
Example: a discrete distribution. A delivery arrives 1, 2 or 3 days late, with probabilities 0.2, 0.5 and 0.3. The cdf is a step function with , and . A value usually lies between two steps, so we take the smallest with :
In words: split into consecutive intervals whose lengths are the probabilities of the outcomes, sample , and return the outcome whose interval contains . For example, gives a delay of 2 days.
Sampling in Practice with numpy¶
Random Number Generators and Seeds¶
A computer generates uniform numbers with a pseudo-random number generator (PRNG): a formula that produces a sequence of numbers that looks random and passes statistical tests for randomness, but is completely determined by its starting value, the seed. True randomness exists too, for example based on atmospheric noise, but it is slower and rarely needed.
That a seed determines all numbers is useful. A simulation with a fixed seed gives the same results every time it runs, so you can reproduce and check results, find errors, and compare two decisions under exactly the same randomness (Lecture 13 makes use of this).
In numpy, you create a random generator with np.random.default_rng(seed) and draw all random numbers from it. Its method uniform(low, high, size) gives size uniform samples:
import numpy as np
rng = np.random.default_rng(seed=42)
print(rng.uniform(0, 1, 3))
print(rng.uniform(0, 1, 3))[0.77395605 0.43887844 0.85859792]
[0.69736803 0.09417735 0.97562235]
The second call continues the sequence, so it gives new numbers. A new generator with the same seed starts the same sequence again:
rng = np.random.default_rng(seed=42)
print(rng.uniform(0, 1, 3))[0.77395605 0.43887844 0.85859792]
A different seed gives a different sequence. Without a seed, np.random.default_rng() takes a seed from the operating system, so every run of the notebook gives different numbers.
Sampling with the Inverse Transform Method¶
With a generator for uniform numbers, we can apply the ITM ourselves. We redo the two examples from the previous section in code: the exponential lifetimes and the delivery delays. (For these distributions, numpy can also sample directly, but the ITM works for any cdf that we can invert.)
Exponential lifetimes. First we sample 10,000 uniform numbers at once, as an array. Then we apply the formula with . A formula applied to an array is applied to every element separately, so this gives an array of 10,000 lifetimes:
rate = 0.5
u = rng.uniform(0, 1, 10000)
lifetimes = -np.log(1 - u) / rate
print("first five lifetimes:", lifetimes[:5])
print("sample mean:", lifetimes.mean())first five lifetimes: [2.39047566 0.19782348 7.42817723 2.86375285 3.0841596 ]
sample mean: 1.9874632324862231
The sample mean is close to the expectation , and the histogram in Figure 2 has the shape of the exponential density.
Figure 2:Histogram of 10,000 exponential samples from the inverse transform method, with the exponential pdf (rate 0.5).
Delivery delays. The ITM returns delay 1 if , delay 2 if and delay 3 otherwise. The function np.where(condition, a, b) makes this choice for every element of the array u: it takes a where the condition holds and b where it does not. The inner np.where chooses between 2 and 3 for the values :
u = rng.uniform(0, 1, 10000)
delays = np.where(u <= 0.2, 1, np.where(u <= 0.7, 2, 3))
print("first ten delays:", delays[:10])
for days in [1, 2, 3]:
print(f"fraction of {days}-day delays:", (delays == days).mean())first ten delays: [1 2 1 2 3 1 2 3 2 1]
fraction of 1-day delays: 0.1977
fraction of 2-day delays: 0.4967
fraction of 3-day delays: 0.3056
delays == days gives an array of True/False values, and its mean is the fraction of True (which counts as 1). The fractions are close to 0.2, 0.5 and 0.3.
Sampling Directly¶
For common distributions, the generator has a method that samples directly (internally, often with the ITM or a similar transformation):
| distribution | numpy code for n samples | note |
|---|---|---|
| uniform[, ] | rng.uniform(a, b, n) | |
| normal | rng.normal(mean, sd, n) | the SD, not the variance |
| exponential | rng.exponential(mean, n) | the mean , not the rate |
| lognormal | rng.lognormal(mu, sigma, n) | mu and sigma of the underlying normal distribution |
| Poisson | rng.poisson(mean, n) | |
| binomial | rng.binomial(trials, p, n) | |
| integers , equally likely | rng.integers(a, b, n) | b itself is excluded: a die roll is rng.integers(1, 7) |
| a discrete distribution | rng.choice([1, 2, 3], n, p=[0.2, 0.5, 0.3]) | values and their probabilities |
Without n, each method returns a single number instead of an array. The direct versions of the two examples above:
lifetimes = rng.exponential(2, 10000)
delays = rng.choice([1, 2, 3], 10000, p=[0.2, 0.5, 0.3])
print("mean lifetime:", lifetimes.mean())
print("mean delay:", delays.mean())mean lifetime: 2.0106797892329413
mean delay: 2.1023
References¶
Koole, G. (2019). An Introduction to Business Analytics. Chapter 5, “Simulation.”
Ross, S.M. (2022). Simulation, 6th ed. Academic Press.