Monte Carlo Β· for programmers

When the math is too hard, throw dice at it.

Some answers are brutal to derive with a formula. But running the same random experiment ten thousand times? That's just a loop with a counter. That's the entire trick behind Monte Carlo simulation β€” trade an exact derivation you can't do for a statistical estimate you can β€” and you already know how to write it.

Watch the darts find Ο€  β†“
Estimate of Ο€
3.000
Darts thrown
0
01The big idea

Stop solving. Start sampling.

Picture a jar of jellybeans. You could count every bean by hand β€” slow and error-prone. Or you could grab handfuls at random, look at what you got, and after enough handfuls you'd know the jar pretty well. Monte Carlo is the second approach, turned into code: model the situation, feed it random inputs, run it a huge number of times, and read the answer off the results.

🎲

It uses randomness on purpose

Instead of deriving a clean equation, you let random chance explore the problem for you β€” one possible outcome at a time. The dice aren't a bug; they're the method.

πŸ”

Repetition does the heavy lifting

One random trial tells you almost nothing. A million of them, averaged together, tells you almost everything. Volume is what converts noise into a signal.

πŸ“Š

You get odds, not just a number

Its real superpower: it hands you a whole distribution of possible outcomes and how likely each one is β€” not a single fragile point estimate you have to take on faith.

The name is pure history, not jargon. In the 1940s, working on neutron diffusion at Los Alamos, Stanislaw Ulam and John von Neumann needed answers to integrals that no formula could crack. Their colleague Nicholas Metropolis suggested a code name after the Monte Carlo casino in Monaco β€” where Ulam's uncle famously gambled. The label stuck, and so did the method: when you can't compute the answer, gamble your way to it.

02The classic example

Estimate Ο€ by throwing darts

Here's the famous one. Draw a square, and inside it a quarter-circle. Now throw darts at the square completely at random. The darts that land inside the curve, compared to the total, are directly linked to Ο€. No circle formula required at the throwing stage β€” just a coin-toss of geometry and a tally. Hit the buttons and watch the estimate firm up.

inside the curve outside the curve
Darts thrown
0
Landed inside
0
Ο€ estimate
β€”
Off true Ο€ by
β€”

True value: Ο€ = 3.14159… Notice how jumpy the estimate is with a handful of darts β€” and how it steadies as the count climbs. That's the whole point of the next section.

Why this works (in one breath)

The square has area 1. The quarter-circle inside it has area Ο€/4. Throw darts evenly across the square and the fraction that land inside the curve will hover around Ο€/4. Flip that around and you get your estimate:

Ο€ β‰ˆ 4 Γ— (inside Γ· total)

A dart is "inside" when its distance from the corner is ≀ 1 β€” i.e. xΒ² + yΒ² ≀ 1. That single comparison is the only geometry in the whole program. We never invoke Ο€ to find Ο€; it falls out of the counting.

// The entire simulation. Yes, really.
let inside = 0;
const N = 1_000_000;

for (let i = 0; i < N; i++) {
  const x = Math.random();   // 0 … 1
  const y = Math.random();   // 0 … 1
  if (x*x + y*y <= 1) inside++;
}

const pi = 4 * inside / N;
console.log(pi); // β‰ˆ 3.14159…
03Why throwing darts works

More trials, less noise

There's one theorem doing all the work here, and it has an intimidating name β€” the Law of Large Numbers β€” for a simple idea: as you repeat a random experiment, the average of your results converges to the true expected value. Each dart is a Bernoulli trial β€” inside with probability Ο€/4 β€” so the running fraction must drift toward Ο€/4. Watch the estimate stagger around early on, then lock onto Ο€ and ride down inside a narrowing funnel.

running estimate true value of Ο€ Β± typical error (β‰ˆ 1/√N)

The mental model

Think of each trial as a noisy vote. A few votes can swing wildly. But as votes pile up, the random over- and under-shoots tend to cancel, and the signal underneath rises to the surface. You're not making any single trial more accurate β€” every dart is exactly as random as the last. You're drowning the randomness in volume until what's left is the underlying truth.

Why the shaded funnel matters

That gold band isn't decoration. The width of an estimate's likely error scales like one over the square root of the number of samples β€” so the band pinches inward as N grows, but it never collapses to zero. Your estimate stays somewhere inside it. That single fact, error ∝ 1/√N, defines almost everything about how Monte Carlo behaves in practice. It's the next section.

04The convergence wall

Cheap to start, expensive to sharpen

Here is Monte Carlo's defining trade-off, measured directly. For each sample count N, we run many independent simulations and record the typical (root-mean-square) error. Plotted on log-log axes, the measured errors fall almost perfectly onto a straight line β€” the theoretical 1/√N curve. There's no escaping its slope.

measured typical error theory: β‰ˆ 1.64 / √N
Typical error @ 100
β€”
Typical error @ 30k
β€”

The catch every programmer should internalize

Because error shrinks like 1/√N, halving the error costs 4Γ— the samples, and squeezing out one more decimal digit (a 10Γ— drop in error) costs roughly 100Γ— the work. That's the wall. It's why Monte Carlo is wonderful for a fast "good enough, with honest error bars" answer and miserable when you need many guaranteed digits β€” for which a deterministic numerical method will usually crush it.

The redeeming feature

Notice what's missing from error ∝ 1/√N: the dimension of the problem. The convergence rate doesn't care whether you're integrating over 2 variables or 200. Classical grid methods care enormously β€” and that asymmetry is exactly where Monte Carlo stops being a toy and starts being indispensable. Hold that thought for section 07.

05Beyond Ο€

It's really just integration

The dart trick wasn't special to circles. Estimating Ο€ was secretly measuring an area by counting hits β€” and area under a curve is exactly what an integral is. Pick any function below, throw darts into the unit box, and the fraction that land under the curve estimates the integral. Try the parabola, the sine hump, the quarter-circle. The estimate converges to the true area the same way the Ο€ estimate converged to Ο€.

under the curve above it f(x)
Darts thrown
0
Landed under
0
Area estimate
β€”
Off true by
β€”

true area = β€”

Switching the function resets the board. The box has area 1, so the estimate is simply under Γ· total.

This "hit-or-miss" method is the gentlest form of Monte Carlo integration. In real code you'd usually skip the y-coordinate entirely: sample x at random, evaluate f(x), and average those values β€” the average value of the function times the width of the interval is the integral. Same idea, lower variance, and it generalizes to any number of dimensions without changing a line of the logic.

06The pattern you'll reuse

Four steps, every single time

Strip away the Ο€ and the integrals and what's left is a recipe that fits almost any uncertain system. Once you see the shape of it, you'll spot Monte Carlo problems everywhere β€” pricing, scheduling, queueing, reliability, games.

STEP 01

Model one trial

Describe a single run of the situation as a function, and pin down exactly where the randomness enters.

STEP 02

Feed it random inputs

Draw random values for the uncertain parts, from distributions that match reality β€” not just uniform noise.

STEP 03

Repeat a lot

Loop thousands or millions of times, recording the outcome of each independent run as you go.

STEP 04

Aggregate

Average them, count a proportion, or plot the spread. That collection β€” with its error bar β€” is your answer.

function monteCarlo(runs) {
  const results = [];

  for (let i = 0; i < runs; i++) {
    const outcome = simulateOneTrial();  // steps 1 & 2
    results.push(outcome);                // step 3
  }

  return summarise(results);            // step 4: mean, %, histogram, error bar…
}

The only hard part is honest in step 4: always report the spread, not just the mean. An estimate without an error bar is a guess wearing a lab coat.

07When randomness beats algebra

The curse of dimensions

For a one-dimensional integral, you'd never reach for Monte Carlo β€” a deterministic rule like the trapezoid or Simpson's converges far faster and lands on many digits cheaply. So where does randomness actually win? When the problem has many dimensions, or no formula at all.

Why grids fall apart

Suppose you cover each axis with a modest grid of 100 points to integrate a function. In 1 dimension that's 100 evaluations. In 2 dimensions it's 100Β² = 10,000. In 10 dimensions it's 100¹⁰ = 10²⁰ evaluations β€” more than any machine will finish before the heat death of your sprint. Grid cost explodes as points^dimensions. This is the "curse of dimensionality," and it kills exact and grid-based methods stone dead.

Why Monte Carlo shrugs

Monte Carlo's error is 1/√N β€” and N is the number of random samples, independent of the dimension. Ten dimensions or ten thousand, a million samples is a million samples. The convergence rate doesn't budge. So past a handful of dimensions, the slow-but-flat 1/√N method overtakes the fast-but-exploding grid, and there is simply no contest. High-dimensional integrals are Monte Carlo's home turf.

This is not an academic edge case. A financial derivative whose payoff depends on a hundred correlated price paths, a rendered film frame integrating light over every surface and bounce, a physics calculation over the states of millions of particles β€” these are integrals in absurdly high-dimensional spaces. No formula exists, and no grid will fit. Monte Carlo isn't the convenient choice there; it's frequently the only choice.

08A real one

Will the project ship on time?

Ο€ is cute, but here's where Monte Carlo earns its keep day to day. You've got five chunks of work, each with a best case, likely case, and worst case. Adding up the "likely" numbers gives one tidy estimate β€” and it lies to you, because work rarely all goes to plan at once. Instead, simulate the whole project thousands of times with random task durations and look at the distribution of outcomes.

Each bar = how often, out of all the simulated projects, the total landed in that range of days. The red line is your deadline.

Design & specs2 Β· 4 Β· 8 d
Backend5 Β· 9 Β· 18 d
Frontend4 Β· 7 Β· 14 d
Integration & QA3 Β· 6 Β· 15 d
The unknown unknowns1 Β· 3 Β· 10 d
Chance on time
β€”
Typical (p50)
β€”
"Safe" bet (p90)
β€”
Sum of likelies
29 d

Drag the deadline after running β€” the odds update instantly off the same simulated outcomes.

The naΓ―ve "add up the likely numbers" answer is 29 days. Run the simulation and you'll usually find the odds of actually hitting that are uncomfortably low β€” because each task draws from a skewed triangular distribution and the long tails stack up. That gap is risk you couldn't see in a single point estimate β€” and the reason people reach for Monte Carlo.

09Working smarter, not just harder

Variance, and how to tame it

Brute-forcing more samples is the obvious lever, but 1/√N makes it an expensive one. The smarter move is to attack the variance β€” the noisiness of each sample β€” so the same N buys a tighter answer. The simplest trick is stratification: instead of scattering points purely at random, divide the space into equal cells and place one jittered point per cell, so the samples can't clump or leave gaps.

256 samples Β· same dart count, different scatter

Hit Compare: we run 200 independent Ο€ estimates of 256 points each, both ways, and measure how much the estimates wobble (their standard deviation). Lower is better.

Οƒ β€” pure random
β€”
Οƒ β€” stratified
β€”

Stratified sampling typically cuts the wobble noticeably for the same number of samples β€” free accuracy, just from spreading the points fairly. It's one of a family of variance-reduction techniques: importance sampling (spend samples where the function is big), antithetic variates (pair each sample with its mirror image so errors cancel), and control variates (subtract off a related quantity you can compute exactly). All chase the same prize β€” bend the constant in front of 1/√N down, since you can't change the √N itself.

10Under the hood

Real random vs. fake random

Every dart you threw came from Math.random() β€” and that function isn't actually random at all. It's a pseudo-random number generator (PRNG): a deterministic formula that, from a starting seed, churns out a stream of numbers that merely looks random. Same seed, same stream, every time. For Monte Carlo that's a feature, not a flaw.

Pseudo-random

A PRNG is fast, repeatable, and portable. The toy below is a linear congruential generator β€” the textbook example. Real engines (xorshift, PCG, Mersenne Twister) are far better, but the principle is identical: a hidden integer state advances by a fixed rule each call. Because it's deterministic, you can seed it to reproduce a run exactly β€” essential for debugging a simulation, writing a regression test, or letting a teammate reproduce your numbers to the digit.

// A linear congruential generator (LCG).
let seed = 42;                 // same seed β†’ same stream
function rng() {
  seed = (1103515245*seed + 12345)
         % 2147483648;
  return seed / 2147483648;   // 0 … 1
}

True random

True random numbers come from physical entropy β€” thermal noise, radioactive decay, timing jitter β€” exposed by your OS (and wrapped by crypto.getRandomValues). They're unpredictable and unrepeatable, which is exactly what cryptography needs and exactly what makes them worse for simulation: you can't reproduce a run, and they're slower to draw.

The risk that bites Monte Carlo is a low-quality PRNG: short period (it loops and re-uses numbers), or hidden correlations (consecutive draws line up on planes in higher dimensions). A bad generator can quietly bias an answer that looks perfectly converged. The fix is a well-tested modern PRNG β€” not switching to "true" randomness.

11In the wild

Where it earns its keep

The same loop-and-tally you wrote for Ο€ runs quietly under a surprising amount of modern software and science. A few of the big ones:

πŸ’Ή

Finance & risk

Price exotic options by simulating thousands of random price paths; estimate Value-at-Risk by simulating a portfolio's worst plausible days. When the payoff has no closed-form formula, you simulate it.

βš›οΈ

Physics & engineering

The original use case: neutron transport, particle showers, and statistical mechanics. Reliability engineers simulate component failures to estimate how long a whole system survives.

🎬

Rendering

Path tracers in film and games estimate the light arriving at each pixel by firing thousands of random rays and averaging β€” a Monte Carlo integral over every possible path light could take.

πŸ“ˆ

Experiments & stats

Bootstrapping resamples data at random to put error bars on almost any statistic; permutation tests and Bayesian MCMC estimate things no formula will hand you in an A/B or causal analysis.

🚦

Operations

Queues, logistics, capacity planning, epidemic spread β€” simulate the system under random demand thousands of times to see the full range of outcomes, not just the rosy average.

πŸ€–

Search & games

Monte Carlo Tree Search powers strong game-playing agents by random-rollout-sampling possible futures from each move β€” randomness as a way to scout an intractable decision tree.

The traps β€” what it is not

"Run it long enough and it's exact."

It converges in probability, never lands exactly. You always quote an estimate ± an error bar that shrinks like 1/√N. Treat any Monte Carlo number without its uncertainty as half-finished.

"My Ο€ hit 3.14159, so my RNG is great."

Recovering digits of Ο€ mostly measures your sample count, not randomness quality β€” the result is dominated by 1/√N noise. A subtly biased generator can still produce a passable Ο€ while failing real randomness tests. It's a weak RNG check at best.

"More samples fixes everything."

1/√N has brutal diminishing returns, and it can't rescue a wrong model or a biased sampler. Garbage in, garbage out β€” just with tighter, more convincing error bars wrapped around the wrong answer.

βœ“ Reach for it when…

  • The system has real randomness or uncertainty baked in.
  • A clean formula is hard, impossible, or not worth deriving.
  • The problem lives in many dimensions, where grids explode.
  • Simulating one outcome is easy, even if the whole is complex.
  • You want a distribution and its odds β€” risk, not just a point.

! Think twice when…

  • You need many guaranteed digits β€” the cost balloons fast.
  • A direct or low-dimensional numerical method already nails it.
  • Your random model doesn't match reality β€” garbage in, garbage out.
  • Compute is tight β€” millions of independent runs aren't free.
  • You skipped the error bar β€” then you don't actually know your answer.
That's the whole idea

If you can simulate one outcome, you can estimate anything.

Model a trial. Feed it randomness. Loop it. Average it β€” and report the error bar. You walked in knowing how to write a loop and a counter, and that was always the hard part. The rest is letting randomness do the exploring while repetition sharpens the answer, on the iron schedule of 1/√N. Cheap to start, honest about what it doesn't know, and unbeatable when the formula simply isn't there.