---
name: "monte_carlo_simulation"
description: "Run a Monte Carlo simulation on a forecast or decision: turn single point estimates into honest input ranges, pick a distribution per input with a stated reason, model correlated inputs with a Gaussian copula instead of assuming independence, run enough trials with a convergence check, and read percentiles, downside probability, and a Spearman sensitivity ranking a decision can rest on. Use when someone wants a forecast with uncertainty, a probability of hitting or missing a target, a risk range for a budget, schedule, cash flow, or NPV, or asks how many trials a simulation needs."
---

# Monte Carlo Simulation

## Purpose

A single point forecast hides the only thing a decision needs: how wrong it could be, and in which direction. This skill turns a deterministic model into a distribution of outcomes. Each uncertain input gets a range and a distribution chosen for a stated reason, inputs that move together are sampled together, and the output is read as percentiles, a downside probability, and a ranked list of which inputs drive the result.

The bundled runner (`bin/monte_carlo.py`, needs numpy and scipy) does the sampling, the correlation, the convergence check, and the sensitivity ranking. A worked cash forecast spec ships in `references/cash-forecast-example.json`. The distribution selection guide is in `references/distribution-guide.md`.

## Workflow

1. **Write the model as one formula first.** The output must be a single expression of named inputs (for example `units * (price - var_cost) - fixed_cost`). If the model only exists as a spreadsheet tangle or a paragraph, pin it down before simulating. A simulation of a vague model produces precise looking noise.

2. **List the uncertain inputs and freeze the certain ones.** Anything known exactly (a signed contract price, a fixed rent) enters as a `fixed` input or a constant in the formula. Simulating a known value adds fake spread.

3. **Give every uncertain input a range before a distribution.** Ask for a realistic low, a most likely value, and a realistic high. Define the low and high out loud as roughly a 1 in 20 bad case and a 1 in 20 good case. Without that definition, people give best and worst ever seen, or a range so narrow it is just the point estimate in a costume.

4. **Pick a distribution per input and write the reason.** The short version: history that is symmetric and unbounded takes a normal; a positive quantity built from multiplied factors takes a lognormal; three point judgment takes triangular when the extremes deserve real weight and PERT when outcomes cluster near the mode; a hard bounded range with no interior knowledge takes a uniform. Full guide, including count, proportion, and wait time cases, is in `references/distribution-guide.md`. Never put a normal on a quantity that cannot go negative if its spread would draw negative values in a visible share of trials.

5. **Ask the correlation question for every pair that shares a driver.** Inputs that share a cause move together: volume and discount driven price, two costs priced off the same commodity, task durations hit by the same staff shortage. State each correlation in the spec as `[["units", "price", -0.35]]` with the reason. The runner applies a Gaussian copula: it draws correlated standard normals through a Cholesky factor of the correlation matrix, maps them to correlated uniforms with the normal CDF, then through each input's own inverse CDF. Independence is the default only when no shared driver exists, and that is a claim to state, not a default to slide past.

6. **Write the spec and run it.** Start at 10,000 trials for a first look and 20,000 to 100,000 for numbers a decision will rest on, always with a fixed seed so the run is reproducible. `python3 bin/monte_carlo.py spec.json` prints percentiles p5 to p95, the mean, the standard error of the mean, the sensitivity ranking, and a convergence check. Add `--threshold X` to get the probability the output lands below X (a loss line, a deadline expressed as a budget cap, a covenant floor).

7. **Run the convergence check before reading anything.** Error in a Monte Carlo mean shrinks with the square root of trials: the standard error is the output sd divided by sqrt(N), so doubling accuracy costs four times the trials. The runner reruns at half the trials on a fresh seed and reports how far p50 and p90 drift, as a share of the output sd. Drift above 5 percent of sd means raise the trials and rerun. Tail percentiles (p5, p95) settle last, so a decision that turns on a tail needs more trials than one that turns on the median.

8. **Read the output in decision order.** First the percentile band: p10 is a bad month you should expect to see, p50 is the middle outcome, p90 is a good month, none of them is a promise. Second the downside probability against the threshold that matters. Third the gap between the simulated median and the old point forecast: a wide gap usually means a skewed input is pulling the mean or median away from the most likely case, which is a finding, not an error. Report percentiles rounded to what the inputs can support.

9. **Do the sensitivity pass.** The runner ranks inputs by Spearman rank correlation between each input's sampled values and the output across all trials, printed as a text tornado, longest bar on top. Spearman is used because it catches monotone relationships of any shape and does not depend on units. The top of the ranking is where better data, a hedge, or a negotiation buys the most certainty. An input with a wide range but a near zero rank correlation does not drive this output, whatever intuition said.

10. **Stress the assumptions that could flip the decision.** Rerun with correlations set to zero, then doubled toward their limit. Rerun with the most contested input swapped to a different distribution family (triangular for PERT, lognormal for normal). If the decision survives those reruns, say so. If it flips, the honest deliverable is the flip point and what evidence would settle it.

## Worked example

Monthly cash flow for a small product business. Point forecast from most likely values: 1,000 units at a price of 49, variable cost 22, fixed cost 18,000, giving 9,000 for the month.

The spec (`references/cash-forecast-example.json`) replaces the points with ranges: units triangular 800 / 1,000 / 1,300, price normal mean 49 sd 3, variable cost lognormal with median 22 (mu 3.091, sigma 0.12, positive only with a right tail), fixed cost triangular 16,500 / 18,000 / 20,500. Units and price are correlated at -0.35 because high volume months are discount months.

At 20,000 trials, seed 42, the runner reports: mean 9,272, sd 4,450, p10 3,666, p50 9,181, p90 15,019, and a 1.6 percent chance the month loses money. Sensitivity ranking: variable cost -0.60, price +0.47, units +0.36, fixed cost -0.18. The decision grade reading: the month is very likely cash positive, a bad month still clears about 3,700 at p10, and variable cost is the input worth nailing down first because it drives more of the spread than volume does. None of that is visible in the 9,000 point forecast.

## Output Contract

A simulation report must contain: the model formula, every input with its distribution, its parameters, and the reason that distribution was chosen, every correlation with its reason (or an explicit statement that inputs were treated as independent and why), the trial count and seed, the percentile band (at least p10, p50, p90), the mean with its standard error, the downside probability against a named threshold when the decision has one, the convergence result, the sensitivity ranking, and the decision the numbers support with the assumption that would flip it.

Round percentiles in the report. A p90 of 15,019 from guessed ranges is reported as about 15,000.

## Operating Rules

- Never invent the ranges. Every low, mode, and high comes from data, a named expert judgment elicited with the 1 in 20 definition, or a stated assumption flagged as a placeholder. An unverifiable range presented as fact fails the whole exercise.
- The Gaussian copula has no tail dependence: it understates two inputs going extreme together. When the decision turns on joint extremes, add an explicit joint stress scenario beside the simulation and say why.
- A correlation set has to be internally consistent (positive definite). The runner rejects one that is not and reports the smallest eigenvalue. Weaken the least certain correlation, never silently drop the strongest one to make the math pass.
- Latin hypercube sampling (`--lhs`, no correlations) stratifies each input's range so every slice gets sampled, and its variance on a given trial count is lower than plain random sampling. Use it for expensive models where trials are scarce. It does not combine with the copula path in this runner, so correlated models use plain sampling with more trials.
- Fix the seed and report it. Two runs of the same spec and seed must give identical output. A result nobody can reproduce is an anecdote.
- Sensitivity by rank correlation measures association inside this model, not causation in the world, and a one at a time tornado (one input swung while others sit at base case) answers a different question. Do not mix the two rankings in one chart.
- Simulation describes the model, it does not validate it. If the formula is wrong, the percentiles are wrong with great precision. Spend at least as long checking the formula against one hand calculated case as on the sampling setup.
- Do not quote a percentile without its trial count and convergence status in the same report. A p95 from 1,000 trials with an unchecked drift is a guess with extra steps.
