Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lecture 12: Sampling a Random Variable

Open In Colab

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 numpy random generator, and use a seed to make results reproducible

The Inverse Transform Method

Every programming language can generate numbers uu that are approximately uniformly distributed on [0,1][0, 1]. The inverse transform method (ITM) turns them into samples from any distribution with cdf FF: sample uu and return the xx with F(x)=uF(x) = u. We write this xx as F−1(u)F^{-1}(u). Figure 1 shows the idea: go from uu on the vertical axis horizontally to the cdf, and then down to the xx-axis.

Loading...

Figure 1:The inverse transform method for the exponential distribution with rate 0.5: the uniform numbers u=0.3u = 0.3 and u=0.9u = 0.9 become the samples F−1(0.3)=0.71F^{-1}(0.3) = 0.71 and F−1(0.9)=4.61F^{-1}(0.9) = 4.61.

Why it works. Let UU be uniform on [0,1][0, 1], so P(U≤u)=uP(U \le u) = u. Because FF is non-decreasing, F−1(U)≤xF^{-1}(U) \le x holds exactly when U≤F(x)U \le F(x). So for every xx,

P(F−1(U)≤x)=P(U≤F(x))=F(x)=P(X≤x).P(F^{-1}(U) \le x) = P(U \le F(x)) = F(x) = P(X \le x).

The sample F−1(U)F^{-1}(U) has the same cdf as XX, so it has the same distribution.

Intuition. Uniform numbers are spread evenly over the vertical axis. Where FF is steep, a large part of the vertical axis maps to a short interval of xx-values, so many samples land there. FF is steep exactly where the density ff is high, so the samples follow the shape of the pdf. In Figure 1, most uu-values end up at small xx, where the exponential density is highest. Ross (2022) covers the ITM and other sampling methods in more detail.

Example: uniform[aa, bb]. The cdf is F(x)=(x−a)/(b−a)F(x) = (x - a)/(b - a) on [a,b][a, b]. Solving F(x)=uF(x) = u gives

F−1(u)=a+(b−a)u.F^{-1}(u) = a + (b - a)u.

Example: exponential. A component’s lifetime XX (in years) is exponential with rate λ=0.5\lambda = 0.5, so mean 1/λ=21/\lambda = 2 years, and F(x)=1−e−λxF(x) = 1 - e^{-\lambda x} for x≥0x \ge 0. Solving F(x)=uF(x) = u:

1−e−λx=u⟹x=−ln⁡(1−u)λ=F−1(u).1 - e^{-\lambda x} = u \quad\Longrightarrow\quad x = -\frac{\ln(1 - u)}{\lambda} = F^{-1}(u).

Since 1−U1 - U is uniform on [0,1][0, 1] as well, −ln⁡(u)/λ-\ln(u)/\lambda 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 F(1)=0.2F(1) = 0.2, F(2)=0.7F(2) = 0.7 and F(3)=1F(3) = 1. A value uu usually lies between two steps, so we take the smallest xx with F(x)≥uF(x) \ge u:

F−1(u)=min⁡{x:F(x)≥u}={1if 0≤u≤0.2,2if 0.2<u≤0.7,3if 0.7<u≤1.F^{-1}(u) = \min\{x : F(x) \ge u\} = \begin{cases} 1 & \text{if } 0 \le u \le 0.2, \\ 2 & \text{if } 0.2 < u \le 0.7, \\ 3 & \text{if } 0.7 < u \le 1. \end{cases}

In words: split [0,1][0, 1] into consecutive intervals whose lengths are the probabilities of the outcomes, sample uu, and return the outcome whose interval contains uu. For example, u=0.45u = 0.45 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:

[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:

[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 uu at once, as an array. Then we apply the formula F−1(u)=−ln⁡(1−u)/λF^{-1}(u) = -\ln(1 - u)/\lambda with λ=0.5\lambda = 0.5. A formula applied to an array is applied to every element separately, so this gives an array of 10,000 lifetimes:

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 1/λ=21/\lambda = 2, and the histogram in Figure 2 has the shape of the exponential density.

Loading...

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 u≤0.2u \le 0.2, delay 2 if 0.2<u≤0.70.2 < u \le 0.7 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>0.2u > 0.2:

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):

distributionnumpy code for n samplesnote
uniform[aa, bb]rng.uniform(a, b, n)
normalrng.normal(mean, sd, n)the SD, not the variance
exponentialrng.exponential(mean, n)the mean 1/λ1/\lambda, not the rate λ\lambda
lognormalrng.lognormal(mu, sigma, n)mu and sigma of the underlying normal distribution
Poissonrng.poisson(mean, n)
binomialrng.binomial(trials, p, n)
integers a,…,b−1a, \dots, b - 1, equally likelyrng.integers(a, b, n)b itself is excluded: a die roll is rng.integers(1, 7)
a discrete distributionrng.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:

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.