D = import(new URL("./engine/dashboard.mjs", window.location.href).href)
UI = import(new URL("./panel.mjs", window.location.href).href)
Plot = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/+esm")
sens = fetch(new URL("./data/sensitivity.json", window.location.href).href).then(r => { if (!r.ok) throw new Error(`couldn't load the sensitivity results (${r.status})`); return r.json(); })
tipEl = UI.tipEl
chartW = Math.max(300, Math.min(900, width))
narrow = chartW < 600
fundName = k => D.FUND_BY_KEY[k].short
// Ratio to the baseline on a log axis; zero or negative ratios sit at the left edge.
EDGE = -3.3
tx = r => r > 0 ? Math.max(-3, Math.min(1.3, Math.log10(r))) : EDGE
fmtRatio = r => r <= 0 ? (r < -0.005 ? `${D.fmt(r, 2)}×: negative` : "about 0") : r < 0.01 ? "about 0" : `×${+r.toPrecision(2)}`
tornadoChart = (items, base, fk, key, title) => {
const b = base[fk][key];
const rows = items.map(it => {
const lo = it.lo[fk][key] / b, hi = it.hi[fk][key] / b;
return {label: D.INPUT_LABELS[it.name] || it.name, values: it.values || it.desc || "", lo, hi, swing: Math.abs(tx(hi) - tx(lo))};
}).filter(r => r.swing > 0.004).sort((a, b) => b.swing - a.swing).slice(0, 12);
const ends = rows.flatMap(r => [{...r, at: "Input at its 5th percentile", v: r.lo}, {...r, at: "Input at its 95th percentile", v: r.hi}]);
return Plot.plot({
width: chartW, height: 40 + 30 * rows.length, marginLeft: narrow ? 150 : 210, marginRight: 70, marginTop: 30, marginBottom: 36,
ariaLabel: title,
ariaDescription: rows.map(r => `${r.label}: ${fmtRatio(r.lo)} at its 5th percentile, ${fmtRatio(r.hi)} at its 95th`).join("; "),
x: {domain: [EDGE - 0.1, 1.4], ticks: [-3, -2, -1, 0, 1], tickFormat: k => k === 0 ? "×1 (baseline)" : `×${10 ** k}`, grid: true, label: "Result as a multiple of the baseline (log scale) →"},
y: {domain: rows.map(r => r.label), label: null},
color: {domain: ["Input at its 5th percentile", "Input at its 95th percentile"], range: ["#90cdf4", "#2c5282"], legend: true},
marks: [
Plot.rectX([0], {x1: EDGE - 0.1, x2: -3.05, fill: "#f7fafc"}),
Plot.barX(rows, {y: "label", x1: d => tx(Math.min(d.lo, d.hi)), x2: d => tx(Math.max(d.lo, d.hi)), fill: "#cbd5e0", insetTop: 7, insetBottom: 7}),
Plot.ruleX([0], {stroke: "#1a202c"}),
Plot.dot(ends, {y: "label", x: d => tx(d.v), fill: "at", r: 5, stroke: "white"}),
Plot.text(rows, {y: "label", x: d => tx(Math.max(d.lo, d.hi)), text: d => fmtRatio(Math.max(d.lo, d.hi)), dx: 8, textAnchor: "start", fontSize: 11, fill: "#1a202c"}),
Plot.text(rows, {y: "label", x: d => tx(Math.min(d.lo, d.hi)), text: d => fmtRatio(Math.min(d.lo, d.hi)), dx: -8, textAnchor: "end", fontSize: 11, fill: "#1a202c", filter: d => tx(Math.min(d.lo, d.hi)) > -2.4}),
Plot.text(rows, {y: "label", x: d => tx(Math.min(d.lo, d.hi)), text: d => fmtRatio(Math.min(d.lo, d.hi)), dx: 9, dy: -9, textAnchor: "start", fontSize: 10, fill: "#4a5568", filter: d => tx(Math.min(d.lo, d.hi)) <= -2.4}),
Plot.text([0], {x: EDGE + 0.1, frameAnchor: "top", dy: -18, text: () => "≤ 0", fontSize: 10, fill: "#4a5568"}),
Plot.tip(ends, Plot.pointer({y: "label", x: d => tx(d.v), title: d => `${d.label}\n${d.at}${d.values ? ` (${d.values})` : ""}: ${fmtRatio(d.v)} the baseline`}))
]
});
}What drives the result
Which inputs move each fund’s life-years per $1M, and where the far-future average comes from
Help improve this draftComment with Hypothes.is; flag @daaronr for a response.
Select any text, chart or number and annotate it with Hypothes.is (the tab at the top right) to flag errors, missing sources, or a clearer way to show something. David plans to respond, especially where a comment mentions @daaronr. Public Hypothes.is annotations can be read by anyone who opens this page, so don't post anything confidential.
How this was made: an AI agent (Claude, working for The Unjournal) rebuilt Rethink Priorities' GCR fund model from RP's public write-up, update log and Donor Compass data, and built these pages. David Reinstein set the task. Neither he nor RP has checked the numbers yet, and RP hasn't seen this page.
In short. The 500+ year number, which is almost all of each fund’s total, is a statement about a thin slice of possible futures: very low long-run risk, humanity settling the stars, fast expansion, and (for Sentinel Bio and the nuclear fund) a large share of the risk coming from their cause. The first 500 years depend on more ordinary things a donor can form a view on: the cause’s share of risk, how much and for how long the money cuts it, and the chance it backfires.
One input at a time
Each bar shows what happens to a fund’s expected life-years per $1M when one input is set to its 5th percentile (light dot) and then its 95th percentile (dark dot), with every other input drawn as usual. A long bar means the result is sensitive to that input. Inputs at the left edge drive the result to about zero; “negative” means the fund’s expected effect turns harmful.
Reading it. For the 500+ window, long-run risk dominates: at its 5th percentile (1 in 100 million a year) the result is about 7 times the baseline, at its 95th (5 in 100,000) it is close to zero. Next come the fund’s cause share (for bio and nuclear), settlement speed and the chance of settling the stars. Switch to the 20-100 window and everything about space and long-run risk drops out; what remains is the cause share, how long the effect lasts, how much it cuts risk, the 100-year catastrophe chance and the chances of no effect or backfire. For the AI fund, its chance of raising risk at the 95th percentile (45%) turns its expected value negative in both windows.
Where the far-future average comes from
The average in the 500+ window doesn’t come from typical draws. Half the draws are exactly zero (the money has no effect), and the rest mostly lie far below the average.
At RP’s values, 1 million draws. The top 0.1% of draws (about 1,000 of a million) carry roughly 60% of the average, and the top 1% carry all of it: the remaining 99% of draws add up to slightly less than zero, because draws in which a fund raises risk offset the small positive ones. The dashed line is 100%.
Which draws? The share of the 500+ average from draws in each tenth of an input, lowest tenth on the left:
What this means for a donor
- The far-future number is fragile in a specific way. It rests on a corner of the input space: very low long-run risk, settlement of the stars, fast expansion and, for bio and nuclear, a large cause share. If you think that corner is less likely than RP’s inputs say, or you don’t want your giving to rest on it, the 500+ number tells you little. Shifting the long-run risk distribution from log-uniform to lognormal (same 5th and 95th percentiles) moves it by five or more orders of magnitude (the 10^32 puzzle).
- The first 500 years are more robust, and they depend on things you can form a view on: how much of the risk comes from the fund’s cause, how much and for how long $1M cuts it, and the chance it backfires.
- For a typical draw, what matters is whether the fund has any effect, and its sign. That explains about 90% of where a draw ranks; everything else a few percent at most. So the percentiles in RP’s tables (zero at the median) say little about the long-run inputs, which drive the average, not the typical draw.
- The ranking is sturdier than the levels. The AI fund comes first unless its chance of backfiring is high or bio’s share of the risk is high. Bio and nuclear are effectively tied.
- How Donor Compass treats risk matters as much as the model. Because the average is carried by rare draws, any risk attitude that discounts the largest gains changes the answer a lot; see the score tab on the model page.
Recompute at your settings
The charts above are at RP’s values. To see them at other settings, set them on the model page and follow its “What drives the result at these settings” link, or press the button below to use the settings listed here.