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: Monte Carlo Simulation

Open In Colab

Monte Carlo simulation estimates the expected output E[r(X)]E[r(X)] of a model rr with random inputs XX by averaging the outputs of many samples. It applies when rr is a known function of a fixed number of random inputs, as in the project of Why Simulate?. This notebook explains why the method works and how accurate it is, shows how to simulate with numpy arrays instead of loops, and then works out the project planning example. It uses the sampling methods from Sampling a Random Variable. Processes that are too complex for a single function rr follow in Discrete-Event Simulation.

Learning outcomes

On completion of this notebook, you will be able to:

  • explain why the average of simulated outputs converges to E[r(X)]E[r(X)]

  • compute a confidence interval for an expectation or a probability from simulation output

  • use numpy arrays to perform a Monte Carlo simulation, and read such code

  • simulate the finish time of a project with random activity durations

The Method

Monte Carlo simulation approximates E[r(X)]E[r(X)] in three steps:

  1. sample independent realizations x1,…,xnx_1, \dots, x_n of XX;

  2. compute the outputs r(x1),…,r(xn)r(x_1), \dots, r(x_n);

  3. average them: (r(x1)+⋯+r(xn))/n\big(r(x_1) + \dots + r(x_n)\big)/n.

Why does this converge? Write Y=r(X)Y = r(X) and Yi=r(Xi)Y_i = r(X_i) for the output of run ii, so Y1,…,YnY_1, \dots, Y_n are independent with the same distribution as YY. By the rules for sums and averages,

E[Y1+⋯+Ynn]=EY,σ2[Y1+⋯+Ynn]=σ2(Y)n.E\left[\frac{Y_1 + \dots + Y_n}{n}\right] = EY, \qquad \sigma^2\left[\frac{Y_1 + \dots + Y_n}{n}\right] = \frac{\sigma^2(Y)}{n}.

The average is centered on EYEY, and its variance goes to 0 as nn grows. This is the law of large numbers: the average output of the simulation converges to the expected output.

How accurate is the estimate? With nn runs, sample mean mm and sample SD ss of the outputs, a 95% confidence interval for E[r(X)]E[r(X)] is

[m−2sn, m+2sn].\left[m - \frac{2s}{\sqrt n}, \ m + \frac{2s}{\sqrt n}\right].

The width shrinks with n\sqrt n: twice as precise requires four times as many runs.

Probabilities. Sometimes we want a probability such as P(r(X)>α)P(r(X) > \alpha) instead of an expectation. A probability is the expectation of an indicator: I{r(X)>α}I\{r(X) > \alpha\} is 1 if r(X)>αr(X) > \alpha and 0 otherwise, and

E[I{r(X)>α}]=1⋅P(r(X)>α)+0⋅P(r(X)≤α)=P(r(X)>α).E[I\{r(X) > \alpha\}] = 1 \cdot P(r(X) > \alpha) + 0 \cdot P(r(X) \le \alpha) = P(r(X) > \alpha).

So we simulate the 0/1 outputs I{r(xi)>α}I\{r(x_i) > \alpha\}: their average is the fraction of runs with r(xi)>αr(x_i) > \alpha, and the same formula gives a CI.

Simulating with numpy Arrays

A simulation can loop over the runs, but in Python it pays to use numpy wherever possible: it is faster and shorter to sample every random input as an array with one value per run, and to compute with whole arrays. A Python for loop handles one number per step, and each step has overhead: Python checks the type of every number and looks up every operation again. A numpy operation on an array is a single call to compiled code that processes all values in a row.

As an example, we estimate the probability that the sum of two dice is 7, which we know is 1/6≈0.1671/6 \approx 0.167, once with a loop and once with arrays:

loop:   0.190 s, fraction 0.1662
arrays: 0.0013 s, fraction 0.1655
the arrays are 143 times faster

Both versions give about 1/6, but the array version is many times faster. That matters once a simulation is repeated many times, as in Lecture 13, where a whole simulation is run for every decision we want to compare.

Below are the numpy features that are useful to perform a Monte Carlo simulation, grouped by the three steps of the method.

Sampling the inputs. Every sampling method of a random generator returns an array of nn values, one per run, when it gets the number of samples nn as its last argument, for example rng.uniform(a, b, n) or rng.lognormal(mu, sigma, n). Sampling a Random Variable lists these methods. With several random inputs, a dictionary with one array per input keeps them together, for example one array of durations per activity of a project: durations = {"A": rng.uniform(2, 4, n), "B": ...}.

Evaluating the function rr. Operations on arrays of the same length work run by run, so the code for rr looks like the formula for a single run:

whatcoderesult
arithmetic per runx + y, price * sales - costs, x ** 2array, computed value by value
maximum or minimum per runnp.maximum(x, y), np.minimum(x, 45)array; a number such as 45 is used for every run
a condition per runx > 25, (x > 25) & (y < 3)array of True/False

Analyzing the output. These turn the array of outputs into one number:

whatcoderesult
summary statisticsx.mean(), x.std(ddof=1)ddof=1 divides by n−1n - 1, giving the sample SD ss used in a CI
a probability(x > 25).mean()fraction of runs in which the condition holds (True counts as 1)
the largest valuex.max() or np.max(x)the largest value in the whole array

Note the difference between np.maximum(x, y), which compares two arrays run by run, and np.max(x), which gives the largest value of one array.

Example: Project Planning

We return to the project of Why Simulate?: activities A and B run at the same time, and C starts when both are finished. The durations XAX_A, XBX_B and XCX_C (in days) are independent and lognormal with parameters μ=1\mu = 1 and σ=1\sigma = 1, which gives each activity an expected duration of e1.5≈4.48e^{1.5} \approx 4.48 days. The finish time is

r(XA,XB,XC)=max⁡(XA,XB)+XC,r(X_A, X_B, X_C) = \max(X_A, X_B) + X_C,

and plugging in the expected durations gives 4.48+4.48=8.964.48 + 4.48 = 8.96 days. We simulate the project 10,000 times, with one array of durations per activity:

first five finish times: [ 8.23  4.51  2.67 20.81 15.54]
plug-in estimate r(EX): 8.963378140676129
simulated estimate of E[r(X)]: 11.348139603963084

The expected finish time is more than two days later than the plug-in estimate: E[r(X)]E[r(X)] is larger than r(EX)r(EX), the flaw of averages, equation (2) in Why Simulate?. Figure 1 shows the distribution of the simulated finish times. It is skewed to the right: most projects finish within 15 days, but a few take much longer, and these long runs pull the expected finish time up.

Loading...

Figure 1:Histogram of the 10,000 simulated finish times, with the plug-in estimate of 8.96 days and the simulated mean. The few runs longer than 40 days are not shown.

Figure 2 shows the average finish time after the first nn runs, with nn on a logarithmic scale so that both the first runs and the later ones are visible. The first runs move the average a lot, but it settles down as nn grows, as the law of large numbers predicts. The jumps come from rare, very long durations, typical of the right-skewed lognormal distribution.

Loading...

Figure 2:Average finish time of the first nn simulated projects, with the plug-in estimate of 8.96 days. The horizontal axis has a logarithmic scale.

The 95% CI quantifies how accurate the estimate is:

95% CI: [11.16, 11.54]

The plug-in estimate of 8.96 days is far outside it. The probability that the project takes more than 15 days follows from the same code, applied to a True/False array:

P(finish time > 15): 0.215
95% CI: [0.207, 0.224]

With the expected durations, the project would never take more than 15 days, but in about one in five simulated projects it does.

References

  • Koole, G. (2019). An Introduction to Business Analytics. Chapter 5, “Simulation.”