Reference · Analysis & decision
Analysis & Decision Tools
Load when **Analysis & decision**.
Part of the Financial Modeling skill · loaded on demand from SKILL.md
Three recipes that attach to any model with a live output cell: sensitivity tables, scenario manager, Monte Carlo. Shared mechanics in references/build-engine.md. The recurring tension here is native Excel What-If structures (which openpyxl can write but not populate) vs code-computed grids (which are populated but static) — build both where it matters.
Sensitivity Tables (sensitivity-tables)
One- and two-variable Data Tables showing how a single output (NPV, IRR, EPS, margin) moves as one or two drivers vary.
When: "sensitivity", "data table", "how does X move if Y changes", "tornado-style range".
Tabs: a Sensitivity tab attached to an existing model, plus a Checks block.
Prerequisite: the output must be a live formula chain back to the input cells. If the output is hardcoded, stop and wire the model first — a Data Table over a constant returns the constant.
One-variable block (inputs down a column):
- Corner
B3 =Model!Output; swept input valuesA4:A10(blue); results columnB4:B10. - Native: select
A3:B10→ Data Table with Column input cell =Model!Input; Excel writes{=TABLE(,Model!Input)}.openpyxlwrites the text; values populate only on recalc. - Code-computed fallback (recommended): for each input value, set the cell in Python, recompute the model logic, write the resulting output as a plain value to a labelled matrix below. Keep both: native proves liveness, computed guarantees numbers.
Two-variable block:
- Corner
E3 =Model!Output; row-input values down the left edgeE4:E10(driver 1); column-input values across the topF3:L3(driver 2). - Native: Data Table with Row input cell = the across driver, Column input cell = the down driver →
{=TABLE(across, down)}. Watch orientation — swapping the two transposes the grid silently. - Code-computed fallback: double loop over both axes writing a value matrix.
Axis ranges: centre each axis on the base case so the base sits in the middle row/column; use an odd step count (5/7/9) so a single middle cell lines up with the base; uniform increments; sweep rates in absolute points (8/10/12%), not relative.
Checks: corner = Model!Output (TRUE); axes labelled with driver names & cell addresses; centre grid cell = current output (base sits in the middle); computed fallback base cell = native cell at the same inputs (within rounding). Apply a 3-colour scale.
Inputs: source model + exact output cell; the one or two input cells; base values + range & step per axis; units/formats.
Example: Model!B40 NPV driven by discount rate (base 10%) and growth (base 3%); one-var sweeps rate 6–14% by 2%; two-var crosses growth 0–6% × rate 6–14%; centre cell at 3%/10% equals Model!B40. Illustrative.
Scenario Manager — Base/Bull/Bear (scenario-manager)
A visible, auditable scenario toggle: a selector drives CHOOSE/INDEX assumption pulls, with a side-by-side comparison of all three cases.
When: "scenario toggle", "Base/Bull/Bear", "show the downside", "switch cases".
Tabs: Cover (selector + summary) · Scenario Inputs · live links into the model · Outputs (comparison) · Checks.
Cover: B2 selector with data-validation list 1,2,3; B3 =CHOOSE(B2,"Base","Bull","Bear"); a small summary reading the live output under the active case.
Scenario Inputs (one row/driver, one column/case): A driver, B/C/D Base/Bull/Bear (blue), E active =CHOOSE(Cover!$B$2,B5,C5,D5) (or =INDEX(B5:D5,Cover!$B$2) — cleaner if cases may grow). Copy the one chosen formula down every driver row.
Links into the model: each model assumption cell points at the active value =ScenarioInputs!E5 (green link). Never let the model hold a typed assumption a scenario should control — value flows selector → active column → model.
Outputs (show all three at once, regardless of selector):
- Parallel calc (preferred): replicate the few output formulas three times, each fed directly by the Base, Bull, or Bear column (
...!B5:B12,...!C5:C12,...!D5:D12), not the active column. Layout: rows = key metrics, columns = Base/Bull/Bear + Bull−Base and Bear−Base deltas. - Read-each (large models): capture each case's live output by toggling the selector during recalc; label clearly as captured snapshots vs live.
Checks: selector in 1–3 and integer; every driver has all three cases (COUNTBLANK=0); active value matches the selector's case. Directional sanity: Bull should not produce a worse headline than Base, Bear not better — a violation means two case columns are swapped.
Trade-off vs Excel's built-in Scenario Manager: the built-in stores values in a hidden dialog, overwrites live inputs, and desyncs easily. A visible toggle keeps every case on the grid, makes the active pull a traceable formula, shows all three outcomes at once, and survives copy/review/version control.
Inputs: model + assumption cell per driver; Base/Bull/Bear value per driver; output metrics + cells; parallel-calc vs snapshots.
Example: growth (4%/8%/1%), gross margin (55%/58%/50%); selector 2 → Bull, E5 =CHOOSE(2,...) = 8% flows to Model!B6; Outputs show NPV 760/1,140/410 with a Bull−Base delta 380, all visible at once. Illustrative.
Monte Carlo (no add-in) (monte-carlo)
Simulate thousands of trials of an uncertain output and summarize the distribution — no Crystal Ball, no @RISK.
When: "Monte Carlo", "distribution of outcomes", "probability of", "risk simulation".
Tabs: Cover (run settings + summary) · Assumptions (each uncertain input + distribution) · Simulation (trial engine) · Results (trials, percentiles, histogram) · Checks.
Assumptions (one row/uncertain input, blue parameters):
- Normal: mean, sd →
=NORM.INV(RAND(),mean,sd). - Uniform: min, max →
=min+(max-min)*RAND(). - Triangular: min, mode, max → with
u=RAND(),Fc=(mode-min)/(max-min):=IF(u<Fc, min+SQRT(u*(max-min)*(mode-min)), max-SQRT((1-u)*(max-min)*(max-mode))). - Correlation caveat: independent
RAND()draws assume uncorrelated inputs; if drivers move together (price & volume, rate & default) independent sampling understates tail risk — flag it and drive correlated inputs from a shared factor.
Simulation — native path: live draw cells (one per input) feed the model; the output =Model!Output is the corner of a one-variable Data Table whose Column input cell points at an unused scratch cell. Each trial row forces a recalc, reseeding RAND(), so captured outputs differ per row → {=TABLE(,scratch)}. Populates only after a real-engine recalc.
Simulation/Results — numpy path (recommended via code): sample each distribution N times with numpy (np.random.normal/triangular/uniform), evaluate the output vectorized across all N trials, write the N outputs as plain values. Deterministic, avoids volatile RAND() churn. Seed with np.random.default_rng(seed) and note the seed on Cover.
Results: percentiles =PERCENTILE.INC(trials,0.05) (P5/P10/P50/P90/P95); =AVERAGE mean, =STDEV.S sd; threshold probability =COUNTIF(trials,"<"&hurdle)/COUNT(trials); histogram via FREQUENCY (or numpy counts) → BarChart.
Trial count: percentile standard error shrinks ~with √N, so 1,000→10,000 roughly halves the noise; lean to 10,000 for tail percentiles and small threshold probabilities; state the count on Cover.
Checks: trial count stated & matches (COUNT(trials)=Cover!B3); all summary stats present; percentiles monotonic (P5≤P10≤P50≤P90≤P95); histogram present. Distribution sanity: confirm sampled means/spreads match parameters (a triangular mode outside min/max, or negative sd, is a common error); for zero-bounded inputs (volumes, prices) prefer triangular or a truncated draw over a wide normal that can sample negatives. Note: sampled trial outputs are written as values, not formulas; percentile/probability cells stay as live formulas over the trial range.
Inputs: model + output cell + each uncertain input cell; a distribution & parameters per input; trial count + any correlation; threshold(s) for the probability metric.
Example: NPV with growth ~Normal(4%,2%) and gross margin ~Triangular(50%,55%,58%); 10,000 trials → P5 210, P50 760, P95 1,430, mean 770, sd 380, P(NPV<0) 6.5%; right-skewed histogram, monotonic percentiles. Illustrative.