The Monte Carlo Family · Lesson 1 of 7

Throwing darts
at the impossible

Estimate the impossible by throwing darts — how random sampling computes integrals, prices options, traces light, and powers modern AI, all governed by one iron law: error shrinks like 1/√N.

Prerequisites: basic algebra + the idea of an average (see ml-maths for probability refreshers). That's it.
12
Chapters
12+
Simulations
0
Assumed Knowledge

Chapter 0: Throwing Darts at Pi

You want the number π but you have no formula handy — no infinite series, no calculus, nothing. What you do have is a dartboard. Specifically, a square board one unit wide, with a quarter-circle of radius 1 drawn inside it, arcing from the top-left corner to the bottom-right.

Here is the trick. Throw darts uniformly at random across the whole square — every spot equally likely. Some land inside the quarter-circle; some land outside it, in the corner. Count the fraction that land inside. That fraction, multiplied by 4, is an estimate of π.

Why 4? The square has area 1. The quarter-circle has area π/4 (a quarter of the full circle's πr² with r=1). So the probability a random dart lands inside the quarter-circle equals area-inside ÷ area-total = π/4. The fraction of darts inside estimates that probability, so 4 × fraction estimates π.

This is the entire idea of Monte Carlo: replace a quantity you cannot compute directly (here, π, or really the area of a curved region) with the average outcome of random trials. No calculus. No series. Just throwing darts and counting hits.

It feels like cheating, and in a sense it is — you are buying an answer with randomness and patience instead of cleverness. The astonishing thing, which the rest of this lesson unpacks, is that this cheap trick scales all the way up to pricing financial options, simulating nuclear reactors, rendering Pixar films, and sampling from diffusion models.

But there is a catch, and it is the single most important fact in this whole lesson. More darts help — but slowly. The error does not shrink in proportion to the number of darts. It shrinks like one over the square root of the number of darts. We will meet this iron law again and again.

The trap to avoid: "More darts always means a proportionally better estimate." It does NOT. With only 8 darts an estimate of π=3.0 is plain luck, and going from 100 to 200 darts barely helps. Doubling the darts cuts the error by only about 1.4×, not 2×. That diminishing return — error ~ 1/√N — is the spine of the entire lesson.
The Dartboard Estimator

Darts rain down on the unit square. Teal = inside the quarter-circle (x²+y² ≤ 1), warm = outside. Watch the running estimate 4 × hits/total hunt for the true π line, and the error band shrink like 1/√N.

Darts / sec30
N (darts) = 0  |  inside = 0  |  fraction = 0.000
estimate 4×fraction = 0.000  |  true π = 3.14159  |  |error| =

Worked example — 8 darts by hand

Let's do this with pencil and paper, on a tiny deterministic sample so the arithmetic is reproducible. Here are eight fixed dart positions in the unit square (chosen once, not re-rolled):

(0.1, 0.2)   (0.4, 0.5)   (0.3, 0.6)   (0.5, 0.5)   (0.9, 0.5)   (0.7, 0.6)   (0.2, 0.4)   (0.95, 0.6)

For each dart we test whether it lands inside the quarter-circle by computing r² = x² + y² and asking whether r² ≤ 1. Watch every one:

#(x, y)x² + y²inside?
1(0.1, 0.2)0.01 + 0.040.05yes
2(0.4, 0.5)0.16 + 0.250.41yes
3(0.3, 0.6)0.09 + 0.360.45yes
4(0.5, 0.5)0.25 + 0.250.50yes
5(0.9, 0.5)0.81 + 0.251.06NO
6(0.7, 0.6)0.49 + 0.360.85yes
7(0.2, 0.4)0.04 + 0.160.20yes
8(0.95, 0.6)0.9025 + 0.361.2625NO

Dart 5 gives r² = 0.9² + 0.5² = 0.81 + 0.25 = 1.06, which is greater than 1, so it lands outside. Dart 8 gives 0.95² + 0.6² = 0.9025 + 0.36 = 1.2625, also outside. The other six all have r² ≤ 1.

So 6 of the 8 darts land inside. The fraction inside is 6/8 = 0.75. The Monte Carlo estimate of π is therefore 4 × 0.75 = 3.0.

The true value is 3.14159, and the true target fraction is π/4 = 0.7854. Our 8-dart estimate of 3.0 is in the right ballpark but visibly off — because eight darts is a tiny sample, and tiny samples are noisy. That noise is exactly what the next chapters tame.

Key insight: The estimate 3.0 is not "wrong" — it is one noisy draw from a process that is correct on average. The fix is never "compute more carefully"; it is "throw more darts," because the random scatter shrinks as the sample grows.

How far off should 8 darts be? A quick error budget

Before we throw more darts, let's quantify how much scatter eight darts should show, so the 3.0-vs-3.14 gap stops looking alarming. Each dart is a coin flip: it lands inside with probability p = π/4 ≈ 0.7854, outside otherwise. The number of hits in 8 throws is a binomial count, and the fraction inside has a known spread.

For a single inside/outside indicator, the per-dart standard deviation is √(p(1−p)). With p = 0.7854:

σ = √( 0.7854 × (1 − 0.7854) ) = √( 0.7854 × 0.2146 ) = √0.1685 = 0.4105

The fraction-inside over N = 8 darts therefore has standard error σ/√N = 0.4105/√8 = 0.4105/2.828 = 0.1451. But our estimate is the fraction, so its spread is 4× larger:

spread of (4 × fraction) = 4 × 0.1451 = 0.5805

So a typical 8-dart estimate of π scatters by about ±0.58 around the true 3.14159. Our value of 3.0 sits only 0.14 below the truth — well inside one standard error. Far from a red flag, it is a perfectly ordinary draw. The friend's 3.5 (0.36 high) is likewise unremarkable. Both are exactly what an 8-dart experiment is expected to produce.

Verified by the math: at N = 8 the estimate's standard error is ~0.58, so estimates anywhere in roughly [2.6, 3.7] are typical. To shrink that band to ±0.058 — one decimal of reliable accuracy — you would need 100× the darts (N = 800), because the spread falls only as 1/√N. That is the entire economics of Monte Carlo in one number.

Here is the same computation from scratch in code — sample N points, test each, average the indicator, multiply by 4:

# From scratch: estimate pi by sampling the unit square
import numpy as np
rng = np.random.default_rng(0)

N = 100_000
pts = rng.random((N, 2))            # N points uniform in [0,1)x[0,1)
inside = (pts**2).sum(1) <= 1.0    # x^2 + y^2 <= 1  ->  True/False
pi_est = 4 * inside.mean()           # fraction inside, times 4
print(pi_est)                          # ~3.14 (jitters with the seed)

And the whole thing as a single library one-liner:

pi_est = 4 * np.mean(np.sum(rng.random((N, 2))**2, axis=1) <= 1)

To see the 1/√N law rather than trust it, accumulate the running estimate and its error band as darts arrive. This is exactly what the widget above plots — the estimate hunting for π while the band narrows:

# From scratch: running pi estimate + its 1/sqrt(N) error band
import numpy as np
rng = np.random.default_rng(0)

N = 20_000
inside = (rng.random((N, 2))**2).sum(1) <= 1.0   # True/False per dart
frac = np.cumsum(inside) / np.arange(1, N+1)    # running fraction inside
est  = 4 * frac                              # running pi estimate
n    = np.arange(1, N+1)
band = 4 * np.sqrt(frac * (1 - frac) / n)        # 4 * SE of the fraction
print(est[-1], band[-1])                  # ~3.14, band ~0.023 at N=20k

Notice the band uses only the data — frac and n — never π itself. The estimator measures its own uncertainty as it goes, the theme of Chapter 3.

Eight darts give a π estimate of 3.0. A friend throws eight more and gets 3.5. What is the most honest takeaway?

Chapter 1: An Average Is an Integral in Disguise

In Chapter 0 we estimated π by counting darts inside a region. That was secretly an integral — the area of the quarter-circle. Now let's make the connection explicit, because it is the engine of everything that follows.

Suppose you want the area under the curve f(x) = x² on the interval [0, 1]. Pretend you forgot the antiderivative (it is x³/3, giving the answer 1/3, but pretend). How would Monte Carlo find it?

Sample x uniformly at random on [0, 1], evaluate f(x) = x² at each point, and average those heights. That average is the integral. This is the central identity of Monte Carlo: the expected value of a function over a distribution equals the integral of that function weighted by the distribution.

Ex~U(0,1)[ f(x) ]  =  ∫01 f(x) dx  ≈  (1/N) ∑i=1N f(xi)

Read that right to left. The sample mean (1/N)∑f(xi) approximates the expectation, and for a uniform distribution the expectation is the integral. So averaging function values at random points estimates the integral. An average is an integral in disguise.

The deep property of this estimator is that it is unbiased. Its expected value equals the true integral exactly — for any N, even N = 1. The estimate is correctly centered on the truth; it does not systematically over- or under-shoot. That is a guarantee about the long-run average of many independent estimates, not about any single run.

This distinction trips up almost everyone, so let's be precise. Unbiased does not mean "close." A three-sample estimate can land far from 1/3. Unbiased means that if you repeated the whole three-sample experiment a million times and averaged all those estimates together, you would get 1/3 exactly. How far any single run scatters from the truth is a different question — that is variance, the subject of the next few chapters.

The trap to avoid: "Unbiased means each estimate is close to the truth." No — unbiased means the estimates are centered on the truth, not that any one of them is close. A single estimate can be wildly off and the estimator is still perfectly unbiased. Centering is about the average over infinitely many repetitions; closeness is about variance.
An Average Becomes an Area

The curve is f(x)=x² on [0,1]; the true area is 1/3. Each random sample drops a stick of height f(x). Their average height (the warm bar) times the width 1 is the estimate. Hit resample to watch it wobble around the true line (green) while staying centered.

N samples20
sample mean = 0.000  |  true 1/3 = 0.3333  |  signed error = 0.000
mean of 50 repeats = (hugs 1/3 — that's unbiasedness)

Worked example — three fixed points

Estimate ∫01 x² dx (true value 1/3) using three fixed sample points: u = 0.1, 0.5, 0.9. Evaluate f = x² at each:

xf(x) = x²
0.10.1² = 0.01
0.50.5² = 0.25
0.90.9² = 0.81

The Monte Carlo estimate is the sample mean of these heights:

estimate = (0.01 + 0.25 + 0.81) / 3 = 1.07 / 3 = 0.3567

The true integral is 0.3333. This particular triple reads 0.3567 — a single realization that happens to sit a bit high. Yet the estimator is unbiased: if you averaged over all possible 3-point samples drawn uniformly, the result would be exactly 1/3.

Notice we never integrated anything. We evaluated a function at three points and divided by three. That is the whole method — and it works identically for functions whose integral has no closed form at all.

What if the interval isn't [0,1]? The width factor

The clean identity above hid a convenience: on [0,1] the uniform density is exactly 1, so the average of heights is the integral. On a general interval [a, b] the uniform density is 1/(b−a), and undoing that requires multiplying the average height by the width (b−a):

ab f(x) dx ≈ (b − a) · (1/N) ∑i=1N f(xi),   xi ~ U(a, b)

The logic is "average height × width = area," exactly the rectangle picture. On [0,1] the width is 1 and silently disappears, which is why we never wrote it before. Get this factor wrong and every estimate is off by a constant multiple — a classic Monte Carlo bug.

Worked check on [1, 3]. Estimate ∫13 x dx (true value = [x²/2] from 1 to 3 = 4.5) with three fixed points u = 1.5, 2.0, 2.5. The mean height is (1.5 + 2.0 + 2.5)/3 = 2.0; the width is 3 − 1 = 2; so the estimate is 2.0 × 2 = 4.0. The true 4.5 is one realization away — and again, averaging over all triples would land exactly on 4.5.

This width factor generalizes to the deepest form of the identity: for any density q(x) you actually sample from, the integral of f equals the expectation of f(x)/q(x). Uniform sampling is just the special case q = 1/(b−a). Reweighting by a cleverer q is the whole subject of the importance-sampling lesson; here we stay with plain uniform draws.

# From scratch: Monte Carlo estimate of integral of x^2 on [0,1]
import numpy as np
rng = np.random.default_rng(0)

N = 100_000
x = rng.random(N)                # x_i ~ Uniform(0,1)
est = np.mean(x**2)             # average of f(x_i) = average height
print(est)                       # ~0.3333  (true integral of x^2 is 1/3)
est = np.mean(rng.random(N)**2)

And the general-interval version, with the width factor made explicit so it works on any [a, b]:

# From scratch: MC integral on a general interval [a, b]
def mc_integral(f, a, b, N, rng):
    x = rng.uniform(a, b, N)        # x_i ~ Uniform(a, b)
    return (b - a) * np.mean(f(x))  # width * average height = area

print(mc_integral(lambda x: x, 1, 3, 100_000, rng))  # ~4.5 (true integral of x on [1,3])
A Monte Carlo estimator of an integral is called unbiased. What does that guarantee?

Chapter 2: Why the Average Settles Down

Chapter 1 promised that the sample mean estimates the integral. But why should a pile of random numbers settle onto a single true value rather than wandering forever? The answer is a theorem so important it has a name: the Law of Large Numbers (LLN).

Picture rolling a fair die over and over, tracking the running average of all rolls so far. Early on it lurches: a 2, then a 5, then a 1 — the average jumps from 2.0 to 3.5 to 2.67. But as the rolls pile up, the average creeps inexorably toward 3.5, the true mean of a fair die.

The LLN is the formal promise that this settling is not luck. It states that the running sample mean converges to the true expectation as the number of samples grows without bound. Any quantity you can write as an expectation can be estimated by averaging, and the average will find the truth.

This is the theorem that makes Monte Carlo legitimate. Without it, averaging random trials would be a hopeful guess. With it, averaging is a guaranteed-convergent procedure: throw enough darts, and you will land arbitrarily close to the answer.

Why does it work? Because the growing denominator dilutes the early noise. After a million rolls, one unlucky streak of low numbers near the start is divided by a million — its influence on the average is crushed. The mean settles not because later rolls "correct" earlier ones, but because each new roll matters less and less to the accumulated average.

That mechanism is also why the LLN does not rescue gamblers. The die is never "due" for a 6. Past rolls are never corrected. The average converges purely because the denominator grows, never because future rolls push back against past imbalance. Independence between rolls is completely untouched.

The trap to avoid (the gambler's fallacy): "A die that's been cold is due for a 6, and future rolls compensate for past imbalance." No. Past rolls are never corrected and future rolls never push back. The running average converges only because the growing denominator dilutes early deviations. The raw count imbalance can even grow forever; it is just dwarfed by N.

The Running Average Finds the Truth

Each trace is the cumulative mean of an independent stream. It swings wildly on the left and tightens onto the true mean (green dashed) on the right; the faint funnel is the typical deviation 1/√N. Add traces — all funnel toward the truth from different early paths.

Speed20
N = 0  |  running mean =  |  true mean = 3.5
|deviation| =  |  funnel 1/sqrt(N) =

Worked example — six fixed die rolls

Take a fair six-sided die (true mean 3.5) and a fixed sequence of first rolls: 2, 5, 1, 6, 3, 4. Track the running mean after each roll:

roll #valuecumulative sumrunning mean = sum / n
1222 / 1 = 2.000
2577 / 2 = 3.500
3188 / 3 = 2.667
461414 / 4 = 3.500
531717 / 5 = 3.400
642121 / 6 = 3.500

The early values lurch between 2.000 and 3.500 — that is the random scatter of a tiny sample. But by six rolls the running mean has settled onto 3.500, and the mean of these six rolls is (2+5+1+6+3+4)/6 = 21/6 = 3.5, matching the true die mean exactly. The LLN says it stays near 3.5 forever as more rolls accumulate.

The key word is "running." Each new roll updates the average a little, and the updates shrink as N grows. That shrinkage — the funnel narrowing — is the LLN in action.

Why the denominator wins: the count can run away while the fraction settles

The subtlest part of the LLN is that it does not say the raw imbalance shrinks. It says the fraction does. These are different claims, and conflating them is the gambler's fallacy. Let's make the gap concrete with coin flips, where heads = 1, tails = 0, true mean p = 0.5.

The typical size of the head-count imbalance (heads minus N/2) grows like √N — it gets bigger, not smaller. But the fraction's deviation from 0.5 is that imbalance divided by N, so it shrinks like √N / N = 1/√N. Watch both columns move in opposite directions:

N flipstypical |count imbalance| ~ 0.5√Ntypical |fraction − 0.5| ~ 0.5/√N
1000.5 × 10 = 50.5 / 10 = 0.0500
10,0000.5 × 100 = 500.5 / 100 = 0.0050
1,000,0000.5 × 1000 = 5000.5 / 1000 = 0.0005

From 100 to a million flips the typical raw imbalance grows 100-fold (5 → 500), yet the fraction's error shrinks 100-fold (0.05 → 0.0005). The coin is never "due"; the deficit can widen forever. The fraction settles only because the denominator N outruns the √N imbalance. That is the engine of the LLN — and it is the same 1/√N rate we will name the "iron law" two chapters from now.

Verified by the math: the 470/1000 heads from the upcoming quiz is a deficit of 30, larger than the typical 0.5√1000 ≈ 16 — a mildly unlucky run, not a broken coin. As N grows the fraction will converge to 0.5 while the raw head deficit need never close. Independence between flips is untouched throughout.
# From scratch: running mean of die rolls converges to 3.5
import numpy as np
rng = np.random.default_rng(0)

N = 100_000
rolls = rng.integers(1, 7, N)            # N rolls in {1..6}
running = np.cumsum(rolls) / np.arange(1, N+1)  # running mean after each roll
print(running[-1])                         # ~3.5
running_mean = np.cumsum(rng.integers(1, 7, N)) / np.arange(1, N+1)

And the experiment that shows the count running away while the fraction settles — the LLN's most counterintuitive face:

# From scratch: head COUNT imbalance grows, head FRACTION converges
import numpy as np
rng = np.random.default_rng(0)

N = 1_000_000
flips = (rng.random(N) < 0.5).astype(int)   # 1=head, 0=tail
heads = flips.cumsum()
imbalance = heads - np.arange(1, N+1) / 2   # heads minus expected n/2
frac_err  = heads / np.arange(1, N+1) - 0.5  # fraction minus 0.5
print(abs(imbalance[-1]), abs(frac_err[-1]))  # imbalance ~100s, frac_err ~0.0005
After 1000 fair coin flips you have 470 heads (30 short of 500). The LLN implies that as you keep flipping...

Chapter 3: Measuring Your Own Uncertainty

The LLN promises the average converges — eventually. But you never have infinite samples. You have 100, or 10,000. So the practical question is: how wrong might my finite estimate be? Remarkably, Monte Carlo grades its own homework.

Say you flip a coin 100 times and report a heads-probability of 0.50. How sure are you? Could the true value be 0.43? The beautiful answer is that you can compute your own uncertainty from the samples alone, without ever knowing the true value.

The quantity that captures this is the standard error — the spread of the average, written σ/√N. Here σ is the standard deviation of a single sample, and N is the number of samples. As N grows, the standard error shrinks like one over the square root of N.

Crucially, the standard error is not the same as the standard deviation. The standard deviation σ measures how spread out your raw samples are — and it does not shrink with more samples. The standard error measures how spread out the average is, and it does shrink. Confusing the two is the single most common Monte Carlo mistake.

Think of it physically. Each coin flip is wildly variable — it is 0 or 1, never 0.5. That per-flip spread σ is fixed no matter how many times you flip. But the average of many flips clusters tightly around the true probability, and that cluster tightens as you add flips. The standard error is the width of that tight cluster.

Because you can estimate σ from the same samples you already drew, you get the standard error for free. This is what lets a path tracer report "this pixel is converged to within 1%," or a trader report "this price is good to two cents" — without an oracle.

The trap to avoid: "Standard error measures how spread out my raw samples are." No — that is the standard deviation σ, and it stays fixed as N grows. The standard error is the spread of the average, σ/√N, which shrinks. Confusing them makes you wildly over- or under-state your confidence.
Raw Spread vs Spread of the Mean

Left: the raw sample histogram — its width σ never shrinks. Right: the sampling distribution of the mean — a bell that contracts as N rises, width labeled sigma/sqrt(N). Slide N up and watch only the right panel narrow.

N samples100
sample std sigma_hat =  |  std error sigma/sqrt(N) =
empirical std of redrawn means = (matches sigma/sqrt(N))

Worked example — a fair coin, N = 100

Estimate the probability a fair coin lands heads (true p = 0.5) from N = 100 flips. For a Bernoulli outcome with p = 0.5, the per-sample standard deviation is:

σ = √( p(1−p) ) = √( 0.5 × 0.5 ) = √0.25 = 0.5

The standard error at N = 100 is therefore:

SE = σ / √N = 0.5 / √100 = 0.5 / 10 = 0.05

So your 100-flip estimate of p is good to about ±0.05. Now push to N = 10,000:

SE = 0.5 / √10000 = 0.5 / 100 = 0.005

One hundred times the samples gave ten times the precision (0.05 → 0.005, a factor of exactly 10). That ratio — 100× the work for 10× the accuracy — is the exact signature of the √N law, and it is so important it gets its own chapter next.

The point of this chapter: at N = 100 you already know your answer is good to about ±0.05, computed entirely from p and N. Monte Carlo always tells you how wrong it might be.

Where the √N comes from — in three lines

The formula σ/√N is not a definition to memorize; it falls out of one fact: variances of independent things add. Let each sample Xi have variance σ². The estimate is the average, μ̂ = (1/N)∑Xi. Pull the constant 1/N out of the variance — constants come out squared:

Var(μ̂) = Var( (1/N) ∑ Xi ) = (1/N)² · Var( ∑ Xi ) = (1/N²) · Nσ² = σ² / N

The middle step used independence: the variance of the sum is the sum of the N individual variances, Nσ². Now take the square root to get the standard deviation of the estimate — the standard error:

SE = √( Var(μ̂) ) = √( σ² / N ) = σ / √N

That square root is the whole story. The variance of the average shrinks like 1/N (fast), but the error we actually feel is its square root, which shrinks only like 1/√N (slow). Chapter 4 is entirely about living with that square root.

Worked example — a non-binary sample (estimating σ from data)

The coin had a known σ = 0.5. Usually you don't — you estimate σ from the very samples you drew. Take five fixed function values from a Monte Carlo integration: f = [0.2, 0.9, 0.4, 0.7, 0.3]. Estimate the mean, the sample standard deviation, and the standard error.

Step 1 — sample mean. (0.2 + 0.9 + 0.4 + 0.7 + 0.3)/5 = 2.5/5 = 0.50.

Step 2 — squared deviations from the mean. (−0.3)², (0.4)², (−0.1)², (0.2)², (−0.2)² = 0.09, 0.16, 0.01, 0.04, 0.04; their sum is 0.34.

Step 3 — sample variance (divide by N−1 = 4, the unbiased "ddof=1" rule).

s² = 0.34 / 4 = 0.085,   s = √0.085 = 0.2915

Step 4 — standard error.

SE = s / √N = 0.2915 / √5 = 0.2915 / 2.236 = 0.1304

So this five-sample run reports 0.50 ± 0.13 — and every number came from the data alone, no oracle. Dividing by N−1 instead of N is the small "Bessel" correction that keeps s² unbiased on tiny samples; with large N it makes no practical difference, but on N = 5 it matters.

# From scratch: estimate p and its standard error from samples alone
import numpy as np
rng = np.random.default_rng(0)

N = 100
samples = (rng.random(N) < 0.5).astype(float)   # 1 = heads, 0 = tails
est = samples.mean()                          # point estimate of p
se  = samples.std(ddof=1) / np.sqrt(N)        # standard error from the SAME data
print(est, se)                            # ~0.5, ~0.05
se = samples.std(ddof=1) / np.sqrt(len(samples))
Your Monte Carlo estimate has standard error 0.04. What does increasing N from 2,500 to 10,000 do to it?

Chapter 4: The Iron Law — 100× Work for 10× Accuracy

Chapter 3 showed the standard error is σ/√N. Stare at that √N and you arrive at the single most important — and most painful — fact about Monte Carlo. The error does not shrink in proportion to N. It shrinks in proportion to √N. This is the iron law.

Spell out the consequences. To halve the error, you need √N to double, which means N must quadruple. To gain a single decimal digit of accuracy — a 10× reduction in error — you need √N to grow tenfold, which means N must grow a hundredfold.

Concretely: your π estimate is accurate to one decimal after a thousand darts. You want a second decimal. The math demands a hundred thousand darts. Brute force alone can never give high precision cheaply — the returns diminish too steeply.

This is not a defect of any particular sampler or random number generator. It is baked into the variance of an average. No amount of cleverness in how you sample can change the √N rate itself. The only lever is the constant σ sitting in front — and shrinking that constant is exactly what the variance-reduction chapter is about.

Why √N and not N? Because variances of independent things add, but standard deviations do not. The variance of a sum of N independent samples grows like N; the variance of their average (dividing by N) shrinks like 1/N; and the standard deviation — the square root of variance — shrinks like 1/√N. The square root is unavoidable.

Once you internalize the iron law you stop being surprised by Monte Carlo's costs. Grainy renders, jittery option prices, slow-converging Bayesian integrals — all are the same 4×-per-halving tax, paid in samples. Every later chapter in this lesson pays it.

The trap to avoid: "Doubling my samples halves my error." It does NOT. Doubling N divides the error by √2 ≈ 1.41 — only about a 30% improvement. Halving the error requires the samples. People badly underestimate how expensive extra Monte Carlo precision is.
The Cost Ladder of Precision

Left: a log-log plot of error vs N — the measured MC error (warm) tracks the reference slope −1/2 (teal). Right: the cost ladder — each step halves the error and quadruples N. Drag the target-error slider to see the required N light up.

Target error0.050
target error = 0.050  |  required N = (sigma/target)^2 =
(using sigma=0.5)  |  fitted MC slope ≈ -0.50

Worked example — the price of decimals

A Monte Carlo estimate has standard error 0.02 at N = 1000. We want to (a) halve the error to 0.01 and (b) cut it tenfold to 0.002. Because SE = σ/√N, the required N scales as 1/SE², so the factor by which N must grow is the square of the factor by which error must shrink.

(a) Halve the error. Error must shrink by a factor of 2, so N must grow by 2² = 4:

Nnew = 1000 × 2² = 1000 × 4 = 4000

(b) Cut the error tenfold. Error must shrink by a factor of 10, so N must grow by 10² = 100:

Nnew = 1000 × 10² = 1000 × 100 = 100000

And the often-misjudged case: doubling N from 1000 to 2000 improves the error by only:

factor = √(2000/1000) = √2 = 1.4142

So one more decimal digit of accuracy costs a hundredfold more work, and doubling the samples buys a mere 41% improvement. That is the defining pain of Monte Carlo — and the reason variance reduction (Chapter 10) earns its keep.

The cost ladder, rung by rung

Tabulate the same σ = 0.5 estimator at a sequence of target errors. Each row uses N = (σ/target)². Read down the table and watch N explode while the error merely halves step to step:

target errorrequired N = (0.5/target)²vs previous row
0.05(0.5/0.05)² = 10² = 100
0.025(0.5/0.025)² = 20² = 400error ×½, N ×4
0.0125(0.5/0.0125)² = 40² = 1,600error ×½, N ×4
0.005(0.5/0.005)² = 100² = 10,000error ×¼, N ×16

Every halving of the error costs exactly 4× the samples; cutting the error to a quarter (0.025 → 0.00625 would be) costs 16×. The "vs previous" column never lies: the work factor is always the square of the precision factor. There is no row where you get precision cheaply.

Verified by the math: going from error 0.05 to 0.005 is a 10× precision gain, and 100 → 10,000 is exactly the predicted 10² = 100× cost. The square is unavoidable because it is the square root in SE = σ/√N, read backwards.
# From scratch: how many samples to hit a target error?
import numpy as np

sigma = 0.5            # per-sample std (estimated from a pilot run)
target_error = 0.01    # desired standard error
N_needed = int(np.ceil((sigma / target_error)**2))  # invert SE = sigma/sqrt(N)
print(N_needed)        # 2500 — note: tighten target 2x -> N grows 4x
N_needed = int(np.ceil((sigma / target_err)**2))

And the cost ladder printed rung by rung, so the 4×-per-halving tax is impossible to miss:

# From scratch: print the cost ladder for sigma=0.5
import numpy as np
sigma = 0.5
for target in [0.05, 0.025, 0.0125, 0.00625]:
    N = (sigma / target)**2
    print(f"target {target:.5f} -> N = {N:,.0f}")
# target 0.05000 -> N = 100   (each halving of target multiplies N by 4)
# target 0.02500 -> N = 400
# target 0.01250 -> N = 1,600
# target 0.00625 -> N = 6,400
A render is too grainy; you want HALF the noise. By what factor must you increase the number of ray samples?

Chapter 5: Error Bars from the CLT

You have a point estimate and a standard error. But a bare number with a spread is still not quite an honest report. A trader asks: "Your option price is 10.45 — but how confident are you?" To answer, you need a confidence interval, and that requires one more theorem.

The Central Limit Theorem (CLT) says that the Monte Carlo average, whatever the messy shape of the underlying samples, is approximately a bell curve (a normal distribution) centered on the true value, with width equal to the standard error. This holds even when individual samples are wildly non-normal — binary coin flips, lumpy option payoffs, anything.

Because the average is approximately normal, you can attach a calibrated interval. A 95% confidence interval is simply the estimate plus or minus 1.96 standard errors, where 1.96 is the normal multiplier that captures 95% of the bell's mass.

95% CI = estimate ± 1.96 × SE

Different confidence levels use different multipliers: 1.645 for 90%, 1.96 for 95%, 2.576 for 99%. Higher confidence means a wider interval — you trade precision for certainty. The CLT supplies the bell shape that justifies these exact numbers.

Now the subtle part, the part that trips up even working scientists. A 95% confidence interval does not mean "there is a 95% probability the true value lies in this interval." The true value is fixed; it is not random. It is the interval that is random — it shifts every time you rerun the experiment.

The correct reading: if you repeated the whole experiment many times, building a fresh interval each time, about 95% of those intervals would contain the true value. This particular interval either does or does not — you just do not know which. "95% confidence" is a property of the procedure, not of one fixed interval.

The trap to avoid: "A 95% CI means there's a 95% probability the truth is in THIS interval." Subtle but wrong. The truth is fixed; the interval is random. Across many repeated experiments, 95% of the intervals you build will cover the truth — but this single interval either covers it or it doesn't.
95% Coverage, Made Tangible

"Run 20 experiments" stacks 20 independent confidence intervals against the true value (green line). About one in twenty misses — drawn red. Change N (bands narrow) or the confidence level (multiplier changes).

N samples400
SE =  |  half-width z*SE =
coverage this batch = / 20 intervals caught the truth

Worked example — a 95% interval by hand

A Monte Carlo run estimates a probability as μ̂ = 0.30, with sample standard deviation s = 0.46, from N = 400 samples. Build a 95% confidence interval (z = 1.96).

Step 1 — standard error.

SE = s / √N = 0.46 / √400 = 0.46 / 20 = 0.023

Step 2 — the 95% half-width.

half-width = 1.96 × SE = 1.96 × 0.023 = 0.0451

Step 3 — the interval.

0.30 − 0.0451 = 0.2549    to    0.30 + 0.0451 = 0.3451

So we report: "0.30, 95% CI [0.255, 0.345]." And by the iron law of Chapter 4, halving that band to ±0.0225 would require 4× the samples — 1600 instead of 400.

Where the multipliers come from

Why 1.96 for 95% and not some other number? It is a pure property of the standard bell curve. The CLT says the estimate is approximately normal, and for a normal distribution a fixed fraction of the mass lies within a fixed number of standard deviations of the center. The multiplier z is exactly the number of standard errors that brackets the desired fraction:

confidencetail left in each sidemultiplier zhalf-width at SE = 0.023
90%5.0%1.6451.645 × 0.023 = 0.0378
95%2.5%1.9601.960 × 0.023 = 0.0451
99%0.5%2.5762.576 × 0.023 = 0.0592

Higher confidence pushes z outward to capture more of the bell's tails, so the interval widens: 90% gives ±0.038, but 99% costs ±0.059 for the same data. You are not getting more accurate — you are casting a wider net to be more often right. That is the precision-for-certainty trade, and the CLT's bell shape is what fixes these exact z-values.

One honest caveat: the CLT is an approximation that improves with N. For small N or wildly skewed integrands the bell is imperfect and these intervals are only roughly calibrated — coverage might be 92% when you asked for 95%. With the N in the thousands that real Monte Carlo runs use, the approximation is excellent.

# From scratch: 95% confidence interval for a Monte Carlo mean
import numpy as np

mu = 0.30; s = 0.46; N = 400
se = s / np.sqrt(N)            # 0.023
lo, hi = mu - 1.96*se, mu + 1.96*se   # [0.2549, 0.3451]
print(lo, hi)
from scipy import stats
lo, hi = stats.norm.interval(0.95, loc=mu, scale=s/np.sqrt(N))

The interval is a procedure, so its 95% claim is testable: build many intervals from fresh data and count how many cover the truth. It should be about 95%:

# From scratch: verify that ~95% of CIs cover the true p
import numpy as np
rng = np.random.default_rng(0)

true_p, N, trials = 0.30, 400, 10_000
covered = 0
for _ in range(trials):
    x  = (rng.random(N) < true_p).astype(float)
    se = x.std(ddof=1) / np.sqrt(N)
    lo, hi = x.mean() - 1.96*se, x.mean() + 1.96*se
    covered += (lo <= true_p <= hi)
print(covered / trials)        # ~0.95 — the procedure's coverage
You build a 95% confidence interval [0.255, 0.345] for an MC estimate. Which statement is correct?

Chapter 6: The Curse Grid Quadrature Can't Escape

So far Monte Carlo looks slow — that brutal 1/√N rate. Why would anyone choose it over the obvious alternative, a regular grid of evaluation points? The answer is the single deepest reason Monte Carlo exists, and it only reveals itself in high dimensions.

Here is the astonishing fact: the 1/√N rate is dimension-independent. Monte Carlo error does not care whether you integrate over a 1-dimensional interval or a 1000-dimensional cube. The dimension d simply does not appear in the error formula σ/√N.

Now contrast a grid. To keep m points along each axis in d dimensions, you need md points total. With just 10 points per axis in 10 dimensions, that is 1010 = ten billion evaluations — and even then the accuracy per point is poor.

The grid's accuracy scales like N−1/d: in high d, even a vast number of grid points barely helps, because the exponent 1/d is tiny. Monte Carlo's exponent is 1/2 for every dimension. Since 1/2 > 1/d whenever d > 2, Monte Carlo's rate beats the grid for any dimension above 2 — and the cost gap is astronomical.

This is why high-dimensional integration runs on Monte Carlo and never on grids. Option pricing over 50 correlated assets, path tracing over the effectively infinite-dimensional space of light paths, Bayesian posterior integrals over thousands of parameters — all are Monte Carlo, because no grid could ever cover those spaces.

One careful caveat. Monte Carlo does not get more accurate in higher dimensions — its rate stays put at 1/√N. What changes is the competition: the grid degrades catastrophically as d grows while Monte Carlo holds steady, so Monte Carlo wins by comparison. The constant σ can still grow with d, but the rate never does.

The trap to avoid: "Higher dimensions make Monte Carlo more accurate because there's more room to sample." No — the rate 1/√N is unchanged by dimension, not improved. What changes is the grid: it collapses (md cost, N−1/d rate) while MC stays steady, so MC wins by comparison — but its own error does not shrink with d.
Grid Explosion vs Constant MC Error

Slide the dimension d up. The grid point count 10d explodes off the chart; the MC error bar stays the same height. "Matched budget" mode gives both the same N and shows the grid's per-axis resolution collapsing.

Dimension d3
grid points for 10/axis = 10^d =
MC rate exponent = 0.5 (any d)  |  grid rate exponent 1/d =

Worked example — cost and rate as d grows

Compare grid quadrature against Monte Carlo as the dimension d rises, requiring 10 grid points per axis (m = 10). The grid cost is md = 10d:

dgrid points 10dgrid rate N−1/dMC rate
110N−1 (fast)N−1/2
31,000N−1/3N−1/2
61,000,000N−1/6N−1/2
1010,000,000,000N−1/10N−1/2

At d = 6 the grid already needs 106 = 1,000,000 points; at d = 10 it needs 1010 = 10,000,000,000 — ten billion. Meanwhile the Monte Carlo rate exponent stays fixed at 0.5 for every d.

The crossover is sharp. The grid rate exponent at d = 10 is 1/10 = 0.1, far worse than MC's 0.5. Since 1/2 > 1/d for any d > 2, Monte Carlo's rate beats the grid above two dimensions — and the grid's astronomical point count makes it hopeless long before that.

Where the grid's N−1/d rate comes from

The grid's collapse isn't asserted — it follows from counting. Lay m points along each of d axes; the total is N = md. Invert that to find the per-axis resolution as a function of the budget N:

N = md  ⇒  m = N1/d  ⇒  spacing h = 1/m = N−1/d

A grid quadrature rule has error proportional to a power of its spacing h (for the midpoint rule, error ~ h² per dimension; even in the best case the error is tied to h, hence to N−1/d). The exponent 1/d is the killer: as d grows, 1/d → 0, so the error barely responds to more points. Monte Carlo's exponent is frozen at 1/2 forever.

Worked crossover. Set the two rates equal to find where MC overtakes the grid. MC error ~ N−1/2, grid error ~ N−1/d. They cross when the exponents match: 1/2 = 1/d, i.e. d = 2. For every d > 2 the grid's exponent 1/d is smaller (worse), so MC wins on rate — and on raw point count it wins catastrophically earlier:

dgrid spacing at N = 106 (= N−1/d)per-axis points N1/d
210−3 = 0.001 (fine)1000
610−1 = 0.1 (coarse)10
2010−0.3 ≈ 0.50 (useless)≈ 2

A million points buys a 1000-per-axis grid in 2-D but only a 2-per-axis grid in 20-D — barely two slabs per dimension. The same million Monte Carlo points give SE ~ 1/√106 = 0.001 in every dimension. That is the gap that makes high-dimensional integration synonymous with Monte Carlo.

# From scratch: the SAME code and SAME 1/sqrt(N) error for ANY dimension d
import numpy as np
rng = np.random.default_rng(0)

def f(x):                      # e.g. integrate ||x||^2 over the unit cube
    return (x**2).sum(axis=1)

N, d = 100_000, 10          # d=1 or d=1000 — identical convergence rate
x = rng.random((N, d))         # N points in the d-dimensional cube
est = f(x).mean()             # a grid here would need 10**10 points!
print(est)
est = f(rng.random((N, d))).mean()   # d=1 or d=1000, identical rate

And a head-to-head: give the grid and Monte Carlo the same budget in 5-D and compare their errors against the known answer (the integral of ||x||² over the unit cube is d/3):

# From scratch: grid vs MC on the SAME budget in d=5
import numpy as np, itertools
rng = np.random.default_rng(0)
d = 5; truth = d / 3          # exact integral of sum(x_i^2)

m = 7; N = m**d                  # grid: 7 per axis -> 16,807 points
axis = (np.arange(m) + 0.5) / m       # midpoints of m bins
grid = np.array(list(itertools.product(axis, repeat=d)))
grid_est = (grid**2).sum(1).mean()

x = rng.random((N, d))             # MC: SAME N points, but random
mc_est = (x**2).sum(1).mean()
print(abs(grid_est-truth), abs(mc_est-truth))  # MC error is competitive even here
Why does Monte Carlo beat grid integration for a smooth function in 12 dimensions?

Chapter 7: Pricing an Option by Averaging Futures

Time for the payoff. You sold a European call option: the buyer has the right to purchase a stock at a fixed strike price K = 100 in one year. What is that right worth today? This is a real, money-on-the-table question — and it is an expectation, so Monte Carlo answers it.

The fair price is the average, over all possible futures, of the discounted payoff. The payoff at expiry is max(ST − K, 0): if the stock ends above the strike, you profit by the difference; if below, the option is worthless. Average that payoff over many simulated futures, discount back to today, and you have the price.

There is one deep subtlety that makes or breaks the answer. You must simulate the stock under the risk-neutral measure, where the drift is the risk-free rate r, not the asset's real-world expected return. Plugging in the return you would actually forecast gives the wrong price — this is the heart of arbitrage-free pricing.

Under risk-neutral dynamics, the year-end stock price follows geometric Brownian motion:

ST = S0 · exp( (r − σ²/2) T + σ√T · Z )

Here S0 is today's price, r the risk-free rate, σ the volatility, T the time to expiry, and Z a standard normal draw. The (r − σ²/2) term is the drift; the σ√T·Z term is the random diffusion. Each draw of Z gives one possible future.

Run thousands of these, take each payoff, discount each by e−rT, and average. The result converges to the famous Black-Scholes analytic price — to within the standard error. This is the showcase: watch the simulated stock-price fan spread, the in-the-money paths light up, and the running average lock onto the analytic line.

Why this matters: Black-Scholes has a closed form only for the simplest options. Change the payoff — Asian options, barriers, baskets of 50 assets — and the formula vanishes, but the Monte Carlo recipe is unchanged: simulate, payoff, discount, average. That generality is why trading desks run Monte Carlo by the billions.
Monte Carlo Option Pricer

A fan of simulated stock paths spreads from S0=100. Paths ending above the strike K (dashed) are in-the-money (teal). The running mean discounted payoff (warm) converges toward the Black-Scholes price (green) with its ±1.96 SE band.

Volatility σ0.20
Strike K100
Paths / sec30
paths N = 0  |  in-the-money = 0%
MC price =  |  Black-Scholes =  |  95% half-width =

Worked example — one path by hand

Price a one-year European call with S0 = 100, K = 100, r = 0.05, σ = 0.20, T = 1. The Black-Scholes analytic anchor (computed once, exactly) is 10.4506. Now trace ONE simulated path using a fixed standard-normal draw Z = 0.5.

Step 1 — the drift term. (r − σ²/2)T = (0.05 − 0.20²/2)(1) = (0.05 − 0.02)(1) = 0.03.

Step 2 — the diffusion term. σ√T · Z = 0.20 × 1 × 0.5 = 0.10.

Step 3 — the year-end price. Add the exponents: 0.03 + 0.10 = 0.13, then

ST = 100 · exp(0.13) = 100 × 1.138828 = 113.8828

Step 4 — the payoff. max(ST − K, 0) = max(113.8828 − 100, 0) = 13.8828.

Step 5 — discount to today. e−rT × payoff = e−0.05 × 13.8828 = 0.951229 × 13.8828 = 13.2058.

This single path's discounted payoff is 13.21 — well above the average. That makes sense: Z = 0.5 is an above-average draw, pushing the stock high. Average many such discounted payoffs (most with smaller or even zero payoff) and the mean converges to the Black-Scholes 10.4506.

A second path — where the option expires worthless

One path is not Monte Carlo; it is a single sample. The whole method lives in the averaging, and the average only behaves because the estimator mixes large payoffs with many zeros. Trace a second path with a below-average draw Z = −1.0 to see the other half of the distribution.

Step 1 — the exponent. drift + diffusion = (r − σ²/2)T + σ√T·Z = 0.03 + (0.20)(1)(−1.0) = 0.03 − 0.20 = −0.17.

ST = 100 · exp(−0.17) = 100 × 0.843665 = 84.3665

Step 2 — the payoff. max(84.3665 − 100, 0) = max(−15.63, 0) = 0. The stock finished below the strike, so the call is worthless. Discounting zero is still zero.

Step 3 — average the two hand paths. ( 13.2058 + 0 ) / 2 = 6.6029. With N = 2 our estimate is 6.60 — far from the true 10.45, because two samples carry enormous standard error. This is the iron law from Chapter 4 in action: only as N climbs into the thousands does the running mean settle onto the analytic line.

The asymmetry that makes pricing hard: the payoff is capped below at zero but unbounded above. A few deep-in-the-money paths dominate the average while the majority contribute nothing. That heavy right tail is precisely why the standard error of an option estimator is large — and why importance-sampling (reweighting draws toward the paying region) pays off so dramatically for far out-of-the-money strikes.

The analytic anchor, dissected

Where does the magic number 10.4506 come from? The Black-Scholes formula is itself just the risk-neutral expectation done in closed form. For a call it reads C = S0·N(d1) − K·e−rT·N(d2), where N is the standard-normal CDF and

d1 = [ ln(S0/K) + (r + σ²/2)T ] / (σ√T),   d2 = d1 − σ√T

Plug in our numbers: the log-moneyness ln(100/100) = 0, so d1 = (0 + (0.05 + 0.02)(1)) / 0.20 = 0.07/0.20 = 0.35, and d2 = 0.35 − 0.20 = 0.15. Reading a normal table, N(0.35) = 0.6368 and N(0.15) = 0.5596. Then

C = 100(0.6368) − 100·e−0.05(0.5596) = 63.68 − 95.123×0.5596 = 63.68 − 53.23 = 10.45

The Monte Carlo run does not need this formula — that is the whole point — but having it lets us measure whether the simulation is right. The two factors N(d1) and N(d2) are roughly the chance the option finishes in the money under two related probability measures; you will meet that measure-change idea again as reweighting in importance-sampling.

Sanity check via put–call parity: the matching put must satisfy P = C − S0 + K·e−rT = 10.4506 − 100 + 95.123 = 5.5735. A second Monte Carlo run pricing the put payoff max(K − ST, 0) should land on 5.57 within its standard error — an independent cross-check that costs you nothing but a different payoff line.

How wrong is the estimate? Read the standard error

Chapter 3 taught that every Monte Carlo answer ships with its own error bar: SE = s/√N, where s is the sample standard deviation of the discounted payoffs. Run N = 200,000 paths and you measure a sample mean of about 10.4645 with s ≈ 14.7, giving

SE = 14.7 / √200000 ≈ 14.7 / 447.2 ≈ 0.033

So the 95% confidence half-width is 1.96 × 0.033 ≈ 0.064, and the estimate 10.46 ± 0.06 comfortably brackets the true 10.4506. The widget reports exactly this band live; watch it shrink as √N grows. Notice the deep lesson: to halve that ±0.06 you must quadruple the paths to 800,000 — the iron law again, now with money attached.

# From scratch: Monte Carlo European call price
import numpy as np
rng = np.random.default_rng(0)

S0, K, r, sig, T, N = 100, 100, 0.05, 0.20, 1.0, 200_000
Z  = rng.standard_normal(N)                              # one draw per future
ST = S0 * np.exp((r - 0.5*sig**2)*T + sig*np.sqrt(T)*Z) # risk-neutral GBM
payoff = np.maximum(ST - K, 0)                       # call payoff
price  = np.exp(-r*T) * payoff.mean()                   # discount + average -> ~10.45
print(price)
price = np.exp(-r*T)*np.maximum(S0*np.exp((r-sig**2/2)*T+sig*np.sqrt(T)*rng.standard_normal(N))-K,0).mean()

The generality the callout promised is one line of code. Reuse the same simulated ST array and only swap the payoff function — the put for the parity check, an Asian option averaging the path, a digital that pays a fixed amount. The simulate-and-discount skeleton never changes; the payoff is the only knob.

# Reuse ST, change only the payoff. Standard error ships for free.
put     = np.exp(-r*T) * np.maximum(K - ST, 0)   # put payoff -> ~5.57 (parity check)
se      = put.std(ddof=1) / np.sqrt(N)               # its own error bar
print(f"put = {put.mean():.4f} +/- {1.96*se:.4f}")  # 5.5735 +/- 0.04
Where this goes next: deep out-of-the-money options — where almost every path pays zero — converge agonizingly slowly under plain MC. The fix is to reweight the samples toward the rare payoff region. That trick has its own lesson: importance-sampling.

Chapter 8: Los Alamos and the Neutron's Random Walk

Where did this whole method come from? In the 1940s at Los Alamos, Stanislaw Ulam was recovering from illness, playing solitaire, and wondering about his odds of winning. Rather than solve the combinatorics, he realized he could just deal many games and count. The same thought, applied to neutrons, became Monte Carlo.

The nuclear problem was this: a neutron entering a slab of shielding material either scatters off a nucleus, gets absorbed, or leaks out a boundary. The transport equations describing this are brutal to solve analytically. So Ulam, with John von Neumann and Nicholas Metropolis, proposed: don't solve them — simulate the neutrons and count what happens.

Each neutron's history is a random walk. At every collision, a random draw decides its fate: absorbed (the history ends), leaked out (it escaped), or scattered (it continues in a new random direction). Run thousands of these histories and tally the escapes, and you have estimated the shielding probability — the very quantity no formula could deliver.

The codename "Monte Carlo," after the famous casino, came from this work — a nod to Ulam's gambling uncle and the method's reliance on chance. It is literally where the field got its name.

The crucial conceptual point: a single neutron history is cheap and crude — a few random collisions, ending in one binary verdict (escaped or not). One neutron tells you nothing about the escape probability. The probability emerges only from the fraction over thousands of independent histories.

This is the same sample-and-count idea as the dartboard, dressed in physics. The dart's "inside vs outside" becomes the neutron's "escaped vs absorbed." The answer is a probability, and a probability is estimated by a fraction, and a fraction converges by the Law of Large Numbers at the iron 1/√N rate.

The trap to avoid: "Each neutron history must be physically detailed and long to be useful." No — individual histories are cheap and crude, just a few random collisions each. The answer emerges only from averaging thousands of them. One neutron tells you nothing; ten thousand tell you the escape probability. It is the count, not any single particle, that carries the physics.
Neutron Histories Through a Slab

Neutrons enter from the left and random-walk. At each collision a draw decides: absorb (history ends), leak/escape, or scatter (continue). The running escape estimate is compared to the analytic value p_leak / (p_leak + p_absorb).

P(absorb)0.30
P(leak)0.20
released = 0  |  escaped = 0  |  absorbed = 0
running escape est =  |  analytic =  |  SE =

Worked example — one history by hand

Use a simplified per-collision model: at each collision a neutron is absorbed with probability 0.3, leaks out with probability 0.2, or scatters (continues) with probability 0.5. We want P(eventually LEAKS) rather than being absorbed.

The analytic answer. Scattering merely postpones the verdict — it does not resolve the neutron's fate, just sends it onward. So the eventual outcome is a race between absorb (0.3) and leak (0.2). Conditioning on a resolving event:

P(leak) = 0.2 / (0.2 + 0.3) = 0.2 / 0.5 = 0.4
P(absorb) = 0.3 / (0.2 + 0.3) = 0.3 / 0.5 = 0.6

One fixed history. Use thresholds [0, 0.3) = absorb, [0.3, 0.5) = leak, [0.5, 1) = scatter, with fixed uniform draws U = 0.6 then U = 0.1.

First draw U = 0.6 falls in [0.5, 1), so the neutron scatters and continues. Second draw U = 0.1 falls in [0, 0.3), so the neutron is absorbed. This single history ends "absorbed" — it contributes a 0 to the escape tally.

One history gives one outcome and tells you nothing about the 0.4 escape probability. Only averaging thousands of such histories — counting the fraction that leak — recovers the 0.4. The physics lives in the count.

Why "race between absorb and leak" is exactly right

The clean formula P(leak) = 0.2/(0.2+0.3) deserves a derivation, because it shows why scattering drops out. A history leaks if it survives some number of scatters and then resolves to "leak." Summing over every possible number of scatters k = 0, 1, 2, ... gives a geometric series, where each scatter carries probability 0.5:

P(leak) = 0.2 + 0.5·0.2 + 0.5²·0.2 + … = 0.2 · (1 + 0.5 + 0.5² + …)

The bracket is a geometric series with ratio 0.5, summing to 1/(1−0.5) = 2. So P(leak) = 0.2 × 2 = 0.4 — matching the race formula exactly. The same series gives P(absorb) = 0.3 × 2 = 0.6, and the two sum to 1 as they must (every history eventually resolves). Scattering only stretches the series; it never changes the ratio of the two resolving outcomes.

How many collisions does a typical history take? Each collision resolves with probability 0.2 + 0.3 = 0.5, so the number of collisions until resolution is geometric with mean 1/0.5 = 2. Histories are short and cheap — usually one or two collisions — which is exactly why you can afford to run millions of them. Cheap-but-crude per-history, precise-in-aggregate: the Monte Carlo bargain in physical clothing.

Verified by the math: the geometric sum 0.2/(1−0.5) = 0.4 and the race ratio 0.2/(0.2+0.3) = 0.4 agree because they are two ways of writing the same conditional probability — "given the history resolved, what fraction of that resolution mass is leak?" Scattering is the part that cancels.
# From scratch: estimate the neutron escape probability
import numpy as np
rng = np.random.default_rng(0)

def history(rng):
    while True:                 # walk until resolved
        u = rng.random()
        if u < 0.3: return 0     # absorbed
        if u < 0.5: return 1     # leaked / escaped
        # else u in [0.5,1): scatter, loop again

esc = np.mean([history(rng) for _ in range(100_000)])
print(esc)                      # ~0.4  (analytic 0.2/(0.2+0.3))
escape_prob = np.mean([history(rng) for _ in range(N)])

A vectorized version makes the count-not-the-particle point vivid: simulate all histories' collision counts and outcomes at once, then report the escape fraction and its standard error — the same SE = σ/√N from Chapter 3, now grading a reactor-shielding estimate:

# From scratch: escape fraction + its standard error
import numpy as np
rng = np.random.default_rng(0)

N = 200_000
esc = np.zeros(N, dtype=bool)
alive = np.ones(N, dtype=bool)
while alive.any():                      # resolve every live history
    u = rng.random(N)
    absorbed = alive & (u < 0.3)
    leaked   = alive & (u >= 0.3) & (u < 0.5)
    esc[leaked] = True
    alive = alive & ~absorbed & ~leaked   # scatterers stay alive
p_hat = esc.mean()
se = esc.std(ddof=1) / np.sqrt(N)
print(p_hat, se)                       # ~0.400, SE ~0.0011
In the Los Alamos neutron simulation, why do you need thousands of histories rather than one very careful one?

Chapter 9: Why Renders Are Grainy

Every frame in a Pixar film, and increasingly every frame in a real-time path-traced game, asks one question per pixel: of all the light bouncing around the scene, how much reaches this point on the sensor? That question is an integral — and a hard one.

It is called the rendering equation, and it integrates incoming light over the entire hemisphere of directions above a surface, accounting for light that bounced off other surfaces first. The space of possible light paths is enormous — effectively infinite-dimensional. No closed form exists.

So renderers do exactly what we have done all lesson: they throw random rays into the scene, gather the light each one carries back, and average. Each ray is a random sample of a light path; the pixel's color is the Monte Carlo estimate of that path integral.

And here is the punchline that ties the whole lesson together: when you use too few rays, the image is grainy — salt-and-pepper noise scattered across the picture. That grain is not a display bug or a rendering glitch. It is Monte Carlo estimation variance, the same σ/√N spread we have studied since Chapter 3, made visible as pixel noise.

Because it is genuine standard error, it obeys the iron law of Chapter 4. To halve the visible grain you must quadruple the rays. Going from a noisy 64 rays-per-pixel to half the noise means 256 rays-per-pixel. Clean path-traced images are expensive for exactly the reason clean π estimates are expensive.

Modern denoisers (often neural) can clean up the grain — but they do it by trading variance for bias, smoothing the image based on learned priors. The principled, unbiased fix is always the same: more rays, paid at the 4×-per-halving rate. There is no free lunch in the rate.

The trap to avoid: "Render noise is a bug or a display artifact you can just filter away cleanly." No — the grain is genuine Monte Carlo estimation variance from too few light-path samples. It shrinks only as 1/√N (denoisers help but trade bias for variance). Adding rays is the principled fix, and it obeys the same iron 4×-per-halving law as everything else here.
A Pixel Converging Out of the Grain

Random rays sample a pixel's incoming light; each returns a radiance value. The running average colors the pixel, and the grain patch (left) visibly smooths as rays accumulate. Watch the SE = sigma/sqrt(N) shrink, and the "rays to halve noise" = 4N.

Samples / pixel16
spp N = 16  |  radiance est =  |  sample std =
pixel SE = sigma/sqrt(N) =  |  rays to halve noise = 4N =

Worked example — four rays into one pixel

Estimate one pixel's radiance from four fixed ray samples returning normalized radiance values L = [0.2, 0.8, 0.5, 0.1]. Then ask how many rays halve the noise.

Step 1 — the pixel estimate. The mean of the four returns:

estimate = (0.2 + 0.8 + 0.5 + 0.1) / 4 = 1.6 / 4 = 0.40

Step 2 — the sample standard deviation. The spread of the four ray returns (using the ddof=1 sample formula) is 0.3162.

Step 3 — the per-pixel standard error — this is the visible noise:

SE = sample-std / √N = 0.3162 / √4 = 0.3162 / 2 = 0.1581

Step 4 — rays to halve the noise. By the iron law, halving SE needs 4× the rays: from 4 to 16. From a typical 64 rays you would need:

64 × 4 = 256 rays to halve the grain

Same 1/√N iron law from Chapter 4, now wearing the clothes of pixel noise. The dartboard, the option price, and the rendered frame all pay the identical 4×-per-halving tax.

Why some pixels are grainier than others

Look at any noisy render and the grain is uneven: smooth walls converge fast, but caustics, glossy highlights, and edges of bright light sources stay speckled long after. The reason is in the numerator of SE = σ/√N. The N (rays per pixel) is the same everywhere, but σ — the spread of radiance values a pixel's rays return — varies enormously.

A pixel on a flat matte wall gets nearly the same radiance from every ray: σ is tiny, so even a handful of rays converge. A pixel that might or might not catch a tight specular glint gets mostly dark returns with the occasional blazing hit: σ is huge, so it needs vastly more rays for the same SE. The visible noise is σ/√N pixel by pixel, and high-σ pixels are the last to clean up.

Worked contrast. Two pixels, both at N = 64 rays. Pixel A (matte) has ray-return spread σ = 0.05; pixel B (glint) has σ = 0.6. Their standard errors:

SEA = 0.05/√64 = 0.05/8 = 0.00625,    SEB = 0.6/√64 = 0.6/8 = 0.0750

Pixel B is 12× noisier than A on identical rays. To bring B down to A's noise level you would need to scale N by (0.0750/0.00625)² = 12² = 144× the rays — the iron law's square again. This is why renderers spend their ray budget adaptively, dumping rays into the high-σ pixels and sparing the easy ones.

# From scratch: Monte Carlo estimate of a pixel's radiance
import numpy as np

L = np.array([0.2, 0.8, 0.5, 0.1])   # radiance from 4 sampled light paths
pixel = L.mean()                        # 0.40 — the rendering-equation MC estimate
se    = L.std(ddof=1) / np.sqrt(len(L))  # 0.1581 — the visible grain
print(pixel, se)
pixel_color = np.mean([trace_ray(pixel, rng) for _ in range(spp)])

And the adaptive idea in code: estimate each pixel's σ from a pilot batch, then assign rays where the noise is — the same fixed budget spent smarter:

# From scratch: pixel SE drives an adaptive ray budget
import numpy as np
rng = np.random.default_rng(0)

def pixel_se(radiances):              # visible noise of one pixel
    return radiances.std(ddof=1) / np.sqrt(len(radiances))

matte = 0.4 + 0.05*rng.standard_normal(64)   # low-variance pixel
glint = 0.4 + 0.60*rng.standard_normal(64)   # high-variance pixel
se_m, se_g = pixel_se(matte), pixel_se(glint)
extra_rays = (se_g / se_m)**2          # how much more N the glint needs
print(se_m, se_g, extra_rays)        # glint ~12x noisier -> ~144x rays to match
A path-traced frame is grainy at 64 samples per pixel. To cut the visible noise in half, you should render at roughly...

Chapter 10: Same Answer, a Tenth of the Samples

The iron law says you cannot beat the 1/√N rate. But look again at the standard error: σ/√N. There are two factors. You cannot touch the √N. But you absolutely can shrink the constant σ sitting in front — and shrinking it is exactly as good as buying more samples, only far cheaper.

This is the art of variance reduction. Four classic techniques each exploit some structure in the problem to lower the variance of the estimator without changing what it estimates. Done right, all four are unbiased — same answer on average, tighter spread for the same N.

Antithetic variates. Pair each random draw u with its mirror 1−u. For a monotone integrand, when f(u) is high, f(1−u) is low — they are negatively correlated, so their average cancels variance. This is the technique we will compute by hand below.

Control variates. Subtract off a related quantity whose exact answer you already know, then add that known answer back. The estimator now only has to account for the small residual, which has far less variance than the original. A path tracer, for instance, can use a cheap analytic approximation of the lighting as a control and Monte-Carlo only the correction.

Stratified sampling. Partition the domain into bins and force one sample into each, guaranteeing coverage of every region instead of leaving it to chance. No region is accidentally over- or under-sampled, which tightens the estimate. A renderer stratifies the pixel and the hemisphere of directions for exactly this reason.

Common random numbers. When comparing two configurations (this reward function vs that one in an RL evaluation), reuse the same random seed across both. The shared randomness cancels in the difference, so the comparison is far less noisy than two independent runs would allow.

Why this matters: On the simple integral below, antithetic variates cut the variance by a factor of ~31× — the same accuracy for a thirtieth of the work, with zero bias. That is the difference between a render finishing in an hour and finishing in a day, or an RL evaluation that needs ~30× fewer episodes to call a difference significant.
Four Variance-Reduction Techniques Race

All four estimate ∫01 ex dx (true e−1 = 1.7183). Plain MC, antithetic (mirror pairs u, 1−u), control variate (linear control removed), stratified (one sample per bin). The bar chart ranks their variances — watch antithetic and stratified crush the noise.

N samples64
true e-1 = 1.7183
plain =   anti =   control =   strat =
variance-reduction factor (anti vs plain) ≈

Worked example — antithetic variance, analytically

Estimate ∫01 ex dx (true value e−1 = 1.7183). Compare the variance of an antithetic pair-average against a plain two-sample average — no simulation, pure analysis, so it is exactly reproducible.

Step 1 — variance of a single sample. For f(U) = eU with U uniform on [0,1]:

Var(f(U)) = (e² − 1)/2 − (e − 1)² = 0.2420

Step 2 — plain two-sample average. Two independent samples, averaged, have variance Var(f)/2:

Var(plain 2-sample) = 0.2420 / 2 = 0.1210

Step 3 — the antithetic covariance. Pair u with 1−u. Because ex is increasing, f(U) and f(1−U) move oppositely — the covariance is negative:

Cov(f(U), f(1−U)) = e − (e − 1)² = −0.2342

Step 4 — antithetic pair-average variance. The average of the pair has variance ( Var(f) + Cov ) / 2:

Var(antithetic) = (0.2420 + (−0.2342)) / 2 = 0.00783 / 2 = 0.0039

Step 5 — the reduction factor. Compare antithetic to plain, for the same two function evaluations:

0.0039 / 0.1210 = 0.0323

The antithetic variance is 0.0323 of the plain variance — a ~31× reduction (1 / 0.0323 = 30.9). Same two evaluations, same unbiased answer, roughly one thirty-first the variance. The negative covariance did all the work.

Worked example — control variates, and the optimal coefficient

Antithetic is a clever trick, but control variates are the workhorse, so let us derive one in full. Suppose alongside f(U) = eU we have a cheap helper g(U) = U whose exact mean we know: E[U] = 1/2. The control-variate estimator subtracts a scaled copy of the helper's error:

θ̂ = f(U) − c·( g(U) − E[g] ) = eU − c·( U − ½ )

Because we subtract a quantity with mean zero, θ̂ is unbiased for every choice of c — its expectation is still e−1. We get to pick c purely to minimize variance. Differentiating Var(θ̂) and setting it to zero gives the optimal coefficient:

c* = Cov(f, g) / Var(g)

Step 1 — the helper's variance. For U uniform on [0,1], Var(U) = 1/12 = 0.0833.

Step 2 — the covariance. Cov(eU, U) = E[U·eU] − E[U]·E[eU]. The first moment integrates by parts: ∫01 u·eu du = [u·eu − eu]01 = (e − e) − (0 − 1) = 1. So Cov = 1 − (½)(e−1) = 1 − 0.8591 = 0.1409.

Step 3 — the optimal coefficient. c* = 0.1409 / 0.0833 = 1.6903.

Step 4 — the reduced variance. Plugging c* back in, the control-variate variance collapses to Var(f)·(1 − ρ²), where ρ = Corr(f, g):

Var(θ̂) = Var(f) − Cov(f,g)²/Var(g) = 0.2420 − (0.1409)²/0.0833 = 0.2420 − 0.2381 = 0.0039

Step 5 — the reduction factor. 0.2420 / 0.0039 = 61.4×. Here the correlation between eU and U is ρ = 0.9918, so ρ² = 0.9837 — the control explains 98.4% of f's variance, and only the 1.6% residual remains to be Monte-Carlo'd. The better your free helper correlates with the target, the more variance vanishes.

The graphics connection: a path tracer estimating the light arriving at a pixel can use a cheap analytic approximation of the lighting (ambient occlusion, a low-order spherical-harmonic fit) as g, whose integral it knows in closed form. Then it only Monte-Carlos the difference between true and approximate lighting — a small, low-variance residual. Same idea powers control-variate baselines in RL policy gradients, where a learned value function V(s) is the control that de-noises the return estimate.

Worked example — stratified sampling

The fourth technique forces coverage. Split [0,1] into two equal strata, [0, ½] and [½, 1], and place exactly one uniform sample in each, then average. Because each draw is confined to half the domain, neither can wander into the other's territory, and the chunk of variance caused by where the samples land is eliminated.

Step 1 — within-stratum variances. For f = ex restricted to a stratum [a,b], Var = E[f²] − E[f]². On [0, ½] this is 0.0349; on [½, 1] it is 0.0949 (the function is steeper there, so more variance).

Step 2 — the stratified estimator's variance. The estimate is ½(f1 + f2), one sample per stratum, so

Var(strat) = ¼( 0.0349 + 0.0949 ) = ¼(0.1298) = 0.0325

Step 3 — compare to plain. A plain two-sample average had variance 0.1210. So stratification gives 0.1210 / 0.0325 = 3.7× less variance — more modest than antithetic here, but it never hurts and it shines in higher dimensions where coverage gaps are the dominant error. The widget's bar chart shows all four side by side so you can see antithetic and control crush the noise while stratified takes a steady, reliable bite.

# From scratch: antithetic variates for integral of exp(x) on [0,1]
import numpy as np
rng = np.random.default_rng(0)

N = 10_000
u = rng.random(N // 2)              # half the draws...
anti = (np.exp(u) + np.exp(1 - u)) / 2  # ...each paired with its mirror
est = anti.mean()                   # ~1.7183, far less variance than plain MC
print(est)
est = ((np.exp(u:=rng.random(N//2)) + np.exp(1-u))/2).mean()

The other two estimators are equally short. The control variate estimates c from the same draws (using the known E[g] = ½) — in practice you never know c* exactly, so you plug in the sample covariance, which costs a negligible bias and recovers nearly the full 61×:

# Control variate: helper g(u)=u with known mean 1/2
u  = rng.random(N)
f  = np.exp(u)
g  = u
c  = np.cov(f, g)[0, 1] / np.var(g)     # estimate c* ~ 1.69
cv = f - c * (g - 0.5)               # subtract the known-mean error
print(cv.mean())                          # ~1.7183, variance ~61x smaller

# Stratified: one uniform draw forced into each of K equal bins
K     = N
edges = (np.arange(K) + rng.random(K)) / K   # one point per 1/K-wide bin
print(np.exp(edges).mean())               # ~1.7183, no bin left to chance
Where this goes next: there is a fifth, more powerful technique — reweighting the samples to draw them from a smarter proposal distribution. That is importance-sampling, the next lesson in the family, and it can rescue the deep-out-of-the-money options and rare-event problems that defeat plain MC.

Chapter 11: Monte Carlo Is Eating Modern AI

The dartboard estimator you built in Chapter 0 is not a quaint numerical trick from the 1940s. It is the silent engine under a huge swath of 2024–2026 machine learning. Once you can see the sample-and-average pattern, you start spotting it everywhere.

When a probabilistic programming language like NumPyro, Pyro, or Stan computes a Bayesian posterior expectation, it is doing Monte Carlo integration — averaging over samples of the parameters. When a reinforcement-learning agent estimates a state's value by averaging episode returns, the "MC" in "Monte Carlo methods" is literally this estimator.

When a neural network reports calibrated uncertainty via MC-dropout, it is averaging many stochastic forward passes — sample and average. When a diffusion model denoises pure noise into an image, each step is a Monte Carlo move along a sampling trajectory. The image is, in a real sense, a Monte Carlo sample.

And path-traced rendering, which we met in Chapter 9, now runs in real time in game engines on consumer GPUs — billions of Monte Carlo light-path samples per frame. Far from legacy, the method is more central to computing now than at any point since Los Alamos.

From here the family branches, and each branch lifts one limitation of plain Monte Carlo. Importance sampling reweights samples drawn from a smarter proposal. MCMC lets you sample from distributions you cannot draw from directly. Quasi-Monte Carlo replaces random points with cleverly structured ones to beat the 1/√N rate. Monte Carlo Tree Search turns sampling into planning and powers game-playing and LLM reasoning.

One diffusion sampler even reframes denoising as annealed Langevin Monte Carlo — see diffusion and hamiltonian-mc for that gradient-guided lineage. The same dartboard, scaled all the way up.

The trap to avoid: "Monte Carlo is an old numerical-integration trick, separate from modern deep learning." No — it is foundational and active. The "MC" in MC return estimation, the sampling in every diffusion model, the integral inside variational inference and probabilistic programming are all Monte Carlo. It is more central to AI now than ever.
The Monte Carlo Family Map

A central Monte Carlo node radiates to four live mini-demos, each running the same average-of-samples motion: an RL agent averaging returns, an MC-dropout band, a diffusion denoiser, and a posterior histogram. The shared slider drives samples across all panels — one 1/√N convergence underneath them all.

Shared samples40
shared N = 40  |  every panel converges as 1/sqrt(N) — one engine, four faces

Worked example — the "MC" in MC policy evaluation

Estimate a reinforcement-learning state's value as the average discounted return over episodes. One episode yields rewards r = [1, 0, 2] at times t = 0, 1, 2, with discount γ = 0.9.

Step 1 — the discounted return of episode 1. Weight each reward by γ raised to its time index:

G = 1×0.90 + 0×0.91 + 2×0.92 = 1 + 0 + 2×0.81 = 1 + 0 + 1.62 = 2.62

The γ2 weight on the t=2 reward is 0.92 = 0.81, so the reward of 2 contributes 1.62.

Step 2 — a second episode. Suppose another episode gives return G2 = 1.0.

Step 3 — the Monte Carlo value estimate. Average the returns — the very same sample-and-average estimator from Chapter 1, now estimating a value function:

V̂ = (2.62 + 1.0) / 2 = 3.62 / 2 = 1.81

As more episodes accumulate, this average converges to the true state value by the Law of Large Numbers — the identical machinery as the dartboard, with episodes playing the role of darts. See policy-gradients and particle-filter for where this leads.

The same iron law, in RL clothing

This RL estimator inherits everything from this lesson, including the cost. The two-episode estimate V̂ = 1.81 has a standard error you can read off exactly as in Chapter 3. With episode returns G = [2.62, 1.0], the sample spread and standard error are:

s = √( (2.62 − 1.81)² + (1.0 − 1.81)² ) / (2−1) = √(0.6561 + 0.6561) = 1.1455
SE = s / √N = 1.1455 / √2 = 0.8100

An SE of 0.81 on an estimate of 1.81 is enormous — two episodes barely constrain the value at all. To trust the value to ±0.08 you would need (0.81/0.08)² ≈ 100× the episodes, the iron law of Chapter 4 charging its usual tax. This is precisely why RL is sample-hungry, and why the variance-reduction tricks of Chapter 10 — baselines, control variates, common random numbers across policy comparisons — are everywhere in modern RL.

Verified by the math: the √N law you derived on a dartboard sets the sample budget of a path tracer, an option desk, a reactor sim, and a reinforcement-learning agent identically. One estimator, one cost law, a dozen fields.
# From scratch: Monte Carlo policy evaluation (average discounted returns)
import numpy as np
gamma = 0.9
episodes = [[1, 0, 2], [1]]      # two episodes of rewards

returns = [sum(gamma**t * r for t, r in enumerate(ep)) for ep in episodes]
V_est = np.mean(returns)            # 1.81 — the MC value estimate
print(returns, V_est)            # [2.62, 1.0] 1.81
V_est = np.mean([np.polyval(ep[::-1], gamma) for ep in episodes])
LessonWhat it adds beyond plain MC
importance-samplingreweight samples from a smarter proposal; rescue rare events
quasi-monte-carlostructured points that beat the 1/√N rate
mcmcsample from distributions you cannot draw from directly
mctsturn sampling into planning and search
diffusiongenerative sampling as iterated Monte Carlo moves
You now own the spine of the whole family. The Law of Large Numbers, the standard error σ/√N, the iron 1/√N law, CLT confidence intervals, dimension-independence, and variance reduction — every other lesson in this family builds on exactly these ideas. As John Tukey put it, "the combination of some data and an aching desire for an answer does not ensure that a reasonable answer can be extracted" — but Monte Carlo, with enough darts, comes remarkably close.
In which of these modern systems is Monte Carlo (sample-and-average) NOT doing the core work?