Prop 40, JR analysis: the reproduction

Jaros and Rauh (NBER, September 2026; first version by Rauh, Jaros, Kearney, Doran and Cosso, March 2026), reproduced step by step from the authors’ own public code and data

Published

October 14, 2026

R code for the setup
suppressPackageStartupMessages({
  library(targets); library(dplyr); library(knitr); library(ggplot2)
})
store <- "../_targets"                       # rjkdc-analysis/_targets
tr <- function(name) tar_read_raw(name, store = store)
for (f in list.files("../R", pattern = "\\.R$", full.names = TRUE)) source(f)

rc   <- tr("revenue_chain")
pf   <- tr("pareto_fit")
sch  <- tr("pareto_income_schedule")
t8   <- tr("income_tax_table8")
itmc <- tr("income_tax_mc")
t9   <- tr("npv_table9")
t10  <- tr("npv_table10")
mcs  <- tr("npv_mc_ssrn")
mcn  <- tr("npv_mc_nber")
nc   <- tr("nber_ceiling")
ncol <- tr("nber_collectible")
hl   <- tr("site_headlines")
grid <- tr("site_grid")
inputs  <- read.csv("../export/r/inputs.csv")
outputs <- read.csv("../export/r/outputs.csv")
printed <- read.csv("data/printed-tables.csv")
ss <- formals(compute_npv_mc_ssrn); nb <- formals(compute_npv_mc_nber)
# HTML tables: pandoc would read a pair of "$" across markdown table cells as math
options(knitr.table.format = "html")

# formatting: a real minus sign, fixed decimals
num <- function(x, d = 1) {
  s <- formatC(abs(x), format = "f", digits = d, big.mark = ",")
  ifelse(x < 0, paste0("−", s), s)
}
pct <- function(x, d = 1) paste0(num(x, d), "%")
out <- function(id, col = "value") outputs[[col]][outputs$output_id == id]
inp <- function(id) inputs$value[inputs$input_id == id]
h <- function(v, q, col = "reproduced") hl[[col]][hl$version == v & hl$quantity == q]
# raw HTML block: pandoc must not read "$B ... $B" inside a table as a math span
scroll <- function(x) cat("\n\n```{=html}\n<div class=\"table-scroll\">\n",
                          as.character(x), "\n</div>\n```\n\n", sep = "")

Introduction

This report rebuilds, step by step, JR’s fiscal estimate of California’s Proposition 40 (the Billionaire Tax Act): a one-time 5% tax on the net worth of billionaires resident in California on 1 January 2026. The authors argue that the tax would collect far less than the BSZ report estimates, and that billionaires who leave take their income tax with them for ever, so the state ends up poorer. Following the Open Policy Analysis framework:

  • Open Output: the explorer, the mean net present value and share of negative outcomes for every combination of seven inputs
  • Open Analysis: this document, and the slide deck
  • Open Materials: the repository, every script, test, CSV and source (guide, with all nine mismatches found along the way: the materials page)

What is reproduced. Jaros and Rauh, The Net Present Value of the California Billionaire Tax Act, NBER chapter c15504 (Tax Policy and the Economy, vol. 41; 22 September 2026; NBER is the National Bureau of Economic Research, which circulates working papers), the litigation-weighted revision of the first version by Rauh, Jaros, Kearney, Doran and Cosso (SSRN 6340778, March 2026; SSRN is a preprint site). Its headline: revenue of about $30B ($28.4B to $35.6B) and a mean NPV of −38.9 $B, with 85.2% of simulated outcomes negative (the Figure 3 note, PDF p.35; the text on p.1 says 85%). That mean NPV is the main estimate, derived step by step in the next section.1

The analysis is reproduced from the authors’ own public, MIT-licensed code and data (github.com/bjaros20/wealth_tax, pinned at commit 25e84dd): their Excel workbook is read cell by cell, their R scripts are ported line by line and also re-run unmodified, and the person-level file final.csv is summed directly. Every number in this document is read from the project’s {targets} pipeline at render time; the only numbers typed in are the paper’s printed values we compare against, each with its PDF page (page numbers are PDF pages throughout).

The shape of the argument, in one line: net present value = wealth tax collected once, minus the present value of the income tax that departing billionaires would have paid in every future year. Sections 4 and 5.1 of the paper estimate the two pieces; Sections 5.2 to 5.4 combine them. We follow that order.

Notation, in one place

Symbol Meaning Unit
\(\tau\) statutory wealth tax rate share
\(WT\) one-time wealth tax revenue actually collected $B
\(C\) annual California income tax paid by the billionaires $B per year
\(f\) share of that income tax base that leaves the state share
\(r\) real discount rate (the Monte Carlo’s draw) per year
\(g\) real growth of the lost income tax stream per year
\(\varepsilon\) migration semi-elasticity: % of the base lost per percentage point of tax
\(q\) probability the Act survives constitutional challenge probability
NPV \(WT - f \cdot C/r\) (or \(/(r-g)\)), the net fiscal effect today $B

The main estimate, step by step

The main estimate is the paper’s mean net present value, printed as −38.9 $B (PDF p.1 and p.35). The authors get it from a Monte Carlo: they draw four inputs at random 100,000 times, compute \(NPV = WT - f \cdot C / r\) for each draw, and average. Because the four draws are independent, the average has an exact value too, and we build it one input at a time. Every number below is read from the pipeline (R/npv_mc.R, R/nber_final.R); the draw ranges are the defaults of compute_npv_mc_nber(), which copies the authors’ script NPV_dist_v8.R.

Step 1: the wealth tax collected, \(WT\)

The script draws \(WT \sim U[0, 72]\) ($B). The top, 72, is the revenue if the Act survives in court, after removing the seven confirmed departures. Summing the authors’ person-level file final.csv gives $72.05B for the same quantity, so the script’s round 72 is close (mismatch 4). The bottom, 0, is the Act struck down. The mean of the draw is \[E[WT] = \frac{0 + 72}{2} = 36.00.\] This is the paper’s central revenue, \(q \times 72\) with a survival chance \(q\) = 0.50 (PDF p.33). The 0 lower bound is guesswork.

Step 2: the share of the income tax base that leaves, \(f\)

The script draws \(f \sim U[0.3, 0.6]\), on its own, unrelated to the revenue draw. Its mean is \(E[f]\) = 0.45: on average, 45 cents of every dollar of billionaire income tax leaves the state. The range is guesswork, “centered conservatively around the observed wealth-base departure fraction” (PDF p.31).

Step 3: the income tax the billionaires pay each year, \(C\)

The script draws \(C \sim U[3.3, 5.8]\) ($B a year), the two ends of the income tax Monte Carlo in Section 5.1. Its mean is \(E[C]\) = 4.55.

Step 4: the discount rate, \(r\)

The script draws \(r \sim U[0.015, 0.045]\), a rate between 1.5% and 4.5% a year. A stream of $1 a year for ever is worth \(1/r\) today, so what enters is the mean of \(1/r\), not \(1/E[r]\): \[E\!\left[\tfrac1r\right] = \frac{\ln(0.045 / 0.015)}{0.045 - 0.015} = 36.62.\] In words: at these rates, $1 lost every year for ever is worth about $37 today. The range is guesswork: its 1.5% anchor is a published market yield, which is why the export contract (the two CSV files, inputs.csv and outputs.csv, that each analysis’s pipeline writes for the comparison to read) and the comparison’s disputes row 2 call the range research, but the width of the band, to 4.5%, is the authors’ judgement, and the width is what moves the result.

Step 5: the present value of the income tax lost

The three loss inputs are independent, so the mean of their product is the product of their means: \[E\!\left[\frac{f\,C}{r}\right] = 0.45 \times 4.55 \times 36.62 = 74.98.\]

Step 6: the net present value

The revenue of Step 1 minus the loss of Step 5: \[E[NPV] = E[WT] - E\!\left[\frac{f\,C}{r}\right]\] \[= 36.00 - 74.98 = −38.98.\]

The authors do not print this exact expectation (the average of the Monte Carlo computed by formula rather than by drawing). They print the average of one seeded run of 100,000 draws (a seeded run: one run of the random draws started from a fixed seed number, 2026, so it gives the same draws every time; its mean differs from the exact expectation by sampling error), which our R port replays draw for draw: −38.886 $B, printed as −38.9. The two differ by 0.09 $B, less than one Monte Carlo standard error (37.6 / \(\sqrt{100{,}000}\) = 0.12). The overview and the explorer show the printed −38.9; the explorer’s grid holds the exact value for every other setting. The explorer moves each of the four draws, and two readings the code does not implement (a litigation multiplier and growth of the lost income).

R code for the main estimate
# R/npv_mc.R: the seeded Monte Carlo (the printed number) and the exact expectation.
compute_npv_mc_nber <- function(seed = 2026, n_sims = 100000,
                                 wt_min = 0, wt_max = 72,
                                 f_min = 0.30, f_max = 0.60,
                                 c_min = 3.3, c_max = 5.8,
                                 rg_min = 0.015, rg_max = 0.045) {
  set.seed(seed)
  wt <- stats::runif(n_sims, wt_min, wt_max)
  f  <- stats::runif(n_sims, f_min, f_max)
  c_income <- stats::runif(n_sims, c_min, c_max)
  rg_discount <- stats::runif(n_sims, rg_min, rg_max)

  npv <- wt - (f * c_income) / rg_discount

  list(
    draws = tibble::tibble(wt = wt, f = f, c_income = c_income,
                            rg_discount = rg_discount, npv = npv),
    summary = npv_mc_summary(npv)
  )
}

npv_mc_summary <- function(npv) {
  tibble::tibble(
    n = length(npv),
    mean = mean(npv),
    median = stats::median(npv),
    sd = stats::sd(npv),
    p05 = unname(stats::quantile(npv, 0.05)),
    p95 = unname(stats::quantile(npv, 0.95)),
    pct_negative = 100 * mean(npv < 0)
  )
}

npv_e_inv_rate <- function(lo, hi, g = 0) {
  if (hi > lo) log((hi - g) / (lo - g)) / (hi - lo) else 1 / (lo - g)
}

npv_expectation <- function(wt_min, wt_max, c_min, c_max, rate_min, rate_max,
                            f_spec, q = 1, g = 0) {
  e_wt <- (wt_min + wt_max) / 2
  e_f <- switch(f_spec$mode,
    implied = 1 - e_wt / f_spec$baseline,
    uniform = (f_spec$lo + f_spec$hi) / 2,
    fixed   = f_spec$value
  )
  pv_lost <- e_f * mean(c(c_min, c_max)) * npv_e_inv_rate(rate_min, rate_max, g)
  collected <- q * e_wt
  c(mean_npv = collected - pv_lost, wt_collected = collected, pv_lost = pv_lost)
}

# _targets.R: the targets this section reads
  tar_target(nber_ceiling, compute_nber_ceiling(nber_final)),
  tar_target(npv_mc_nber, compute_npv_mc_nber()),
  tar_target(npv_mc_nber_summary, npv_mc_nber$summary),
  tar_target(npv_mc_nber_analytic_mean,
  tar_target(export_inputs, compute_export_inputs(pareto_fit, revenue_chain, nber_ceiling)),
                                                     npv_mc_nber_summary, nber_ceiling)),
             compute_site_headlines(site_grid, npv_mc_ssrn_summary, npv_mc_nber_summary,
                                    npv_mc_ssrn_analytic_mean, npv_mc_nber_analytic_mean,
Python code for the main estimate
# py/npv_mc.py: the Python twin. numpy's random stream differs from R's, so its seeded
# mean agrees with R's within Monte Carlo error; the exact expectation agrees exactly.
def compute_npv_mc_nber(seed=2026, n_sims=100000, wt_min=0, wt_max=72,
                         f_min=0.30, f_max=0.60, c_min=3.3, c_max=5.8,
                         rg_min=0.015, rg_max=0.045):
    rng = np.random.default_rng(seed)
    wt = rng.uniform(wt_min, wt_max, n_sims)
    f = rng.uniform(f_min, f_max, n_sims)
    c_income = rng.uniform(c_min, c_max, n_sims)
    rg_discount = rng.uniform(rg_min, rg_max, n_sims)

    npv = wt - (f * c_income) / rg_discount
    return {"npv": npv, "summary": npv_mc_summary(npv)}

def npv_mc_summary(npv):
    return {
        "n": len(npv),
        "mean": float(np.mean(npv)),
        "median": float(np.median(npv)),
        "sd": float(np.std(npv, ddof=1)),
        "p05": float(np.percentile(npv, 5)),
        "p95": float(np.percentile(npv, 95)),
        "pct_negative": float(100 * np.mean(npv < 0)),
    }

def npv_mc_analytic_mean(wt_min, wt_max, c_min, c_max, rate_min, rate_max,
                          f_mode="ssrn", baseline_revenue=None, f_min=None, f_max=None):
    e_wt = (wt_min + wt_max) / 2
    e_c = (c_min + c_max) / 2
    e_inv_rate = math.log(rate_max / rate_min) / (rate_max - rate_min)
    if f_mode == "ssrn":
        e_f = 1 - e_wt / baseline_revenue
    else:
        e_f = (f_min + f_max) / 2
    return e_wt - e_f * e_c * e_inv_rate

Section 4: the wealth tax revenue

4.1 The baseline, before anyone leaves (eqs. 1 and 2)

\[R_{\text{base}} = \sum_{i=1}^{N} \tau \times \text{TaxBase}_i, \qquad \text{TaxBase}_i = \text{NetWorth}_i - \text{TRRE}_i\]

In words: take each of the \(N\) = 212 California billionaires on the 2025 Forbes list (with the authors’ corrections), subtract their directly held residential real estate (TRRE, total residential real estate; the Act exempts it), multiply what is left by the 5% rate, and add up. Their aggregate net worth is $1,894.8B. The authors’ workbook sums this row by row to $94.30B; the paper prints $94.20B (PDF p.19, eq. 2). The difference is mismatch 6.

The tax is assessed on net worth at 31 December 2026, so the authors grow every net worth by 7% to that valuation date and rebuild the base person by person in final.csv: 212 domestic rows, face value $100.91B (printed $100.9B, PDF p.19). That is exactly 1.0700 times the workbook’s $94.30B, so the person-level file and the workbook agree with each other, and both differ from the printed $94.20B. The 28 non-US-citizen residents ($146.3B of net worth) stay outside the base (PDF p.17).

4.2 The collectibility principle (eq. 3)

\[E[R] = q \times R_{\text{recognized}}\]

The authors count revenue only when it is “more likely than not” to be collected. They borrow this test from the accounting rule for uncertain tax positions in company accounts (ASC 740-10, PDF p.20). It works at two levels. For each taxpayer: confirmed departures, pre-filing departures and billionaires imputed from a Pareto curve are left out of the recognized base, \(R_{\text{recognized}}\). For the Act as a whole: \(q\) is the probability that the Act survives constitutional challenge, set at 0.50 (PDF pp.19-20). That value comes from the accounting standard’s 50% threshold, not from a legal assessment of the challenges, so it is guesswork.

4.3 The seven confirmed departures (Table 7, eq. 4)

Page, Brin, Thiel, Hankey, Spielberg, Kalanick and Sacks moved out between the initiative’s filing and the 1 January 2026 residency date (Table 7, PDF p.21). Removing their $28.85B leaves $72.05B in final.csv. The paper (eq. 4, p.22) and the authors’ README print $72.06B, the difference of two figures each rounded to two decimals (100.91 − 28.85), and the script hard-codes 72 (mismatch 4): one unrounded value, its rounding and a round literal. The paper then asks how big a behavioural response that is. It divides the share of revenue lost by the rate change, in percentage points (eq. 4, PDF p.22):

\[\varepsilon = \frac{\%\Delta \text{TaxBase}}{\Delta \text{TaxRate (p.p.)}} = \frac{(100.9 - 72.06)/100.9}{5}\]

R code for the implied semi-elasticity
eps7 <- 100 * (1 - nc$ceiling_recomputed / nc$domestic_total_grown) / (100 * inp("tax_rate"))
eps7
[1] 5.71903

This gives 5.72 (printed 5.73): a 1-point tax rate rise drives out about 5.7% of the base. The authors treat this as a floor, because it counts only departures that made the news (PDF pp.22-24).

4.4 The litigation discount and the collectible estimate (eqs. 5 to 8)

Removing three pre-filing departures as well ($0.88B) leaves $71.18B (printed $71.17B, PDF p.22). Mark Zuckerberg announced his move in February 2026, after the residency date. The authors exclude his $12.03B, because collecting it depends on the retroactive date surviving in court. That leaves a recognized base of $59.15B (eq. 5). Multiplying by \(q\) = 0.50 gives

\[E[R] = q \times R_{\text{recognized}} = 0.50 \times 59.15 = 29.57\]

(printed $29.6B, eqs. 6 and 8, PDF p.23). Keeping Zuckerberg gives $35.59B (eq. 7). Moving the gray-zone names out gives $28.40B: these are the seven billionaires with departure signals short of confirmation, plus Jan Koum, Reed Hastings and Andy Fang, reported as considering a move (PDF p.23). Hence the printed range, $28.4B to $35.6B, around “approximately $30 billion”.

The Monte Carlo does not use this point estimate. It draws revenue from 0 to 72, whose mean is the paper’s central $36B (Step 1 and Section 5.4).

4.5 Validation against the literature (Table 8)

\[\eta_{\text{mig}} = 0.24 \times 43 = 10.32, \qquad \text{base lost} = 1 - (1 - 0.05)^{10.32}\]

R code for the literature-implied base loss
lit_loss <- 1 - (1 - inp("tax_rate"))^inp("brulhart_semi_elasticity")
lit_loss
[1] 0.4110104

Brülhart et al. (2022) estimate that Swiss taxable wealth falls by about 43% per point of wealth tax, about 24% of it through people moving. The authors keep only the moving part, 10.32, and apply it to the 5-point rate in log form: the base shrinks by 41.1% (PDF p.24). Table 8 (PDF p.25) sets this beside the observed 28.6% from the seven confirmed departures. This check does not enter the main estimate. Two things to notice. First, the Swiss estimate is for a recurring annual tax. The paper argues that taxpayers price the Act as the start of a recurring regime, so the recurring-tax estimate is the right benchmark (PDF p.25). Second, the BSZ report (Boll, Saez and Zucman) disputes exactly this transfer (disputes row 1).

Section 5.1: the income tax the billionaires pay

The Pareto upper bound (eqs. 9 and 10)

\[N(y) = A\,y^{-\alpha}, \qquad S(k, n) = \left(\frac{k}{n}\right)^{(\alpha - 1)/\alpha}\]

The state publishes how many tax returns report income above $1M, $2M, $3M, $4M, $5M and $10M (Franchise Tax Board, FTB below: Personal Income Tax Annual Report 2024, Table B-4A). If top incomes follow a Pareto distribution, the log of those counts falls on a straight line against the log of the thresholds, with slope \(-\alpha\). The fitted line gives \(\hat\alpha\) = 1.438 (printed 1.44, \(R^2\) = 0.9987, PDF p.26).

R code for the Pareto fit figure
d <- data.frame(y = pf$thresholds, n = pf$filer_counts)
ggplot(d, aes(y, n)) +
  geom_abline(intercept = log10(pf$A), slope = -pf$alpha, linetype = 2, colour = "grey50") +
  geom_point(size = 2.5, colour = "#1f6fce") +
  scale_x_log10(labels = scales::label_dollar(scale = 1e-6, suffix = "M")) +
  scale_y_log10(labels = scales::label_comma()) +
  labs(x = "AGI threshold", y = "Returns above the threshold") +
  theme_minimal(base_size = 11)

The six published filer counts against their AGI thresholds, log-log, with the fitted Pareto line (paper Figure 1, PDF p.27).

\(S(k, n)\) is the share of the bracket’s income that goes to its top \(k\) of \(n\) filers. With the top 212 of the 4,729 filers above $10M, the continuous formula gives 38.8% of the bracket’s $11.1B of tax. The script then builds a rank-by-rank income schedule and rescales it by 1.118 so that its top 212 match the continuous share (footnote 28, PDF p.28: the discrete sum undershoots by about 11%). Scaling Tax Year 2023 to FY 2024-25 collections ($130B in total) multiplies by 1.335. If the billionaires are the 212 highest earners, they pay $5.76B a year. That is the upper bound, and the top of \(C\)’s range.

The dispersion Monte Carlo (Table 9)

Billionaires are ranked by wealth, not income, and many realise little income in a given year. So the script places the 212 billionaires at random among the top \(K\) filers (without replacement, 100,000 draws for each \(K\) from 212 to 2,000) and sums their tax. The wider the pool, the lower the expected total. The authors take \(K \approx 500\) (“roughly the top decile of the bracket”, PDF p.28) as a reasonable floor, which gives the bottom of \(C\)’s range, $3.3B. The choice of \(K\) = 500 is a judgement, not an estimate, so the range \(C \in [3.3, 5.8]\) is labelled guesswork below.

R code for Table 9
p8 <- printed[printed$table == "table8", ]
pv <- function(k, col) p8$printed[p8$row == k & p8$column == col]
tab8 <- t8 %>% transmute(
  `Pool size K` = format(K, big.mark = ","),
  `Mean` = num(mean_fy25, 3), `printed` = sapply(K, pv, "mean"),
  `P5` = num(p05_fy25, 3), `printed ` = sapply(K, pv, "p05"),
  `P95` = num(p95_fy25, 3), `printed  ` = sapply(K, pv, "p95"))
m8 <- sum(round(t8$mean_fy25, 2) == sapply(t8$K, pv, "mean")) +
      sum(round(t8$p05_fy25, 2) == sapply(t8$K, pv, "p05")) +
      sum(round(t8$p95_fy25, 2) == sapply(t8$K, pv, "p95"))
scroll(kable(tab8, align = "rrrrrrr", caption = "Table 9, $B per year (PDF p.30)"))
Table 9, $B per year (PDF p.30)
Pool size K Mean printed P5 printed P95 printed
212 5.757 5.76 5.757 5.76 5.757 5.76
250 5.181 5.18 4.785 4.79 5.453 5.45
300 4.609 4.61 4.136 4.14 5.007 5.01
400 3.828 3.83 3.331 3.33 4.314 4.31
500 3.308 3.31 2.829 2.83 3.818 3.82
750 2.536 2.54 2.116 2.12 3.033 3.03
1,000 2.099 2.10 1.726 1.73 2.572 2.57

21 of the 21 cells round to the printed value; the tests hold this at the printed rounding (tests/testthat/test-income-tax-mc.R).

Section 5.2: from an annual loss to a present value (eqs. 11 to 14)

\[PV = \frac{C}{r}, \qquad NPV = WT - f \cdot \frac{C}{r}\]

\[\text{or, with growth:}\qquad PV = \frac{C}{r - g}, \qquad NPV = WT - f \cdot \frac{C}{r - g}\]

This is the step that makes the result. The wealth tax is collected once. The income tax of anyone who leaves is lost every year, for ever, and a perpetuity paying \(C\) a year is worth \(C/r\) today. At \(r\) = 1.5% a perpetuity is worth 1/0.015, about 67 years of payments; at 4.5%, about 22. The paper’s text then argues the lost stream grows with the billionaires’ wealth, making it a growing perpetuity, \(C/(r-g)\) (PDF p.31). It calibrates \(r - g\) at about 1.5% from the S&P 500’s dividend yield, and presents scenarios at 1.5%, 3.0% and 4.5% (PDF p.32).

Three things are worth knowing before reading the rest:

  • Which rate is drawn. The Monte Carlo’s eq. 18 and the code (NPV_dist_v8.R) call the 1.5% to 4.5% draw \((r-g)\). The sentence after eq. 19 then computes \(C/r\) (PDF p.35) (mismatch 3). Numerically nothing changes, because the same interval is used whatever it is called. Conceptually, the interval is asked to stand for two different things. The explorer’s growth dial shows what the ambiguity is worth.
  • The loss is not scaled by \(q\). People who left do not come back if the Act is struck down, so the income tax loss stays even when the wealth tax collects nothing (PDF p.33).
  • The horizon. Treating the loss as permanent is itself one of the disputed choices: the BSZ report estimates the loss as lasting about the five payment years (disputes row 2).

Section 5.3: the break-even departure share (Table 10, eq. 15)

\[x^* = \frac{WT \cdot (r - g)}{C}\]

Set NPV to zero and solve for the departure share: the tax pays for itself only if less than \(x^*\) of the income tax base leaves. The paper evaluates it at its central revenue, $36B (\(q \times 72\)), and the midpoint income tax, $4.55B (PDF p.33).

R code for Table 10
wt36 <- 0.5 * nb$wt_max
n10 <- compute_npv_table10(scenarios = list(central = list(label = "Central", WT = wt36, C = 4.55)))
pn <- printed[printed$table == "nber_table10", ]
mn10 <- sum(abs(round(100 * n10$break_even_pct, 1) - pn$printed[match(paste0("central@", n10$r), pn$row)]) < 1e-9)
scroll(kable(n10 %>% transmute(`WT ($B)` = num(WT, 0), `C ($B)` = num(C, 2), `r - g` = pct(100 * r),
                               `x*` = pct(100 * break_even_pct),
                               printed = pct(pn$printed[match(paste0("central@", r), pn$row)])),
             align = "rrrrr", caption = "Table 10 (PDF p.34)"))
Table 10 (PDF p.34)
WT ($B) C ($B) r - g x* printed
36 4.55 1.5% 11.9% 11.9%
36 4.55 3.0% 23.7% 23.7%
36 4.55 4.5% 35.6% 35.6%

3 of the 3 cells match the printed digit. The central departure share, \(E[f]\) = 0.45, is above every one of them, which is why the mean NPV comes out negative (PDF p.34). A check that is not an input: plugging each \(x^*\) back into the NPV formula must give exactly zero (see internal identities).

Section 5.4: the Monte Carlo behind the headline (eqs. 16 to 19)

\[WT \sim U[\$0\text{B}, \$72\text{B}], \quad C \sim U[\$3.3\text{B}, \$5.8\text{B}], \quad (r - g) \sim U[0.015, 0.045], \quad f \sim U[0.30, 0.60]\]

Draw the four inputs independently and uniformly, 100,000 times, and average the NPV (PDF p.35). Two details matter:

  1. Revenue is drawn from zero. The mean of \(U[0, 72]\), $36B, is \(0.5 \times 72\), the paper’s central estimate (PDF p.33). This is the only place \(q\) appears in the code: there is no explicit multiplier (mismatch 5). A uniform draw from 0 is a different assumption from a survive-or-not coin flip on a fixed base. The two share a mean, not a distribution.
  2. The departure share is drawn on its own, not tied to revenue. Revenue lost and income tax lost do not move together in a draw.
R code for Section 5.4
s <- tr("npv_mc_nber_summary")
scroll(kable(data.frame(
  ` ` = c("reproduced (seed 2026)", "printed (PDF p.35)"),
  `Mean` = c(num(s$mean, 2), num(h("nber", "mc_mean_npv", "printed"))),
  `Median` = c(num(s$median, 2), num(h("nber", "mc_median_npv", "printed"))),
  `SD` = c(num(s$sd, 2), num(h("nber", "mc_sd_npv", "printed"))),
  `% negative` = c(num(s$pct_negative, 2), num(h("nber", "mc_pct_negative", "printed"))),
  check.names = FALSE), align = "lrrrr",
  caption = "NPV Monte Carlo, $B, seed 2026, 100,000 draws"))
NPV Monte Carlo, $B, seed 2026, 100,000 draws
Mean Median SD % negative
reproduced (seed 2026) −38.89 −35.26 37.62 85.17
printed (PDF p.35) −38.9 −35.3 37.6 85.2

Our R port replays the authors’ script exactly (same seed, same draw order) and agrees with an unmodified run of NPV_dist_v8.R to machine precision (tests/testthat/test-npv-mc.R). The printed summary reproduces to within Monte Carlo error, with no systematic gap. Its exact expectation is −38.98 $B, which rounds to −39.0 rather than the printed −38.9: the printed value is the seeded run’s, not the expectation. The explorer shows the exact expectations and says so.

R code for the NPV distribution figure
ggplot(data.frame(npv = mcn$draws$npv), aes(npv, fill = npv < 0)) +
  geom_histogram(binwidth = 5, boundary = 0) +
  geom_vline(xintercept = mean(mcn$draws$npv), linetype = 2) +
  scale_fill_manual(values = c(`TRUE` = "#eb6834", `FALSE` = "#1f6fce"), guide = "none") +
  coord_cartesian(xlim = c(-250, 70)) +
  labs(x = "NPV, $B", y = "draws") + theme_minimal(base_size = 11)

The distribution of simulated NPV (paper Figure 3, PDF p.35). Draws below zero in orange; the dashed line is the mean.

Every input, by origin

This section is not in the paper: it re-sorts every input by how much evidence stands behind it, the question a reader who wants to argue with the result has to answer first.

  • data: read off a public dataset or the authors’ own person-level file, and recomputable from it.
  • research: an estimate taken from a published paper.
  • guesswork: set by assumption. Some are bounded by evidence, but no dataset or estimate pins the value itself.
  • scenario: a choice about which policy or counterfactual is being estimated.
  • derived: a value another row implies.
R code for the input table
dials <- site_dials(rc)
dial_rows <- do.call(rbind, lapply(dials, function(d) data.frame(
  input = d$key, what = d$label, `value(s)` = paste(d$labels, collapse = "; "),
  origin = d$origin, where = "explorer dial (R/site.R)", check.names = FALSE)))
# The rows only the first version uses (version "ssrn") go to the appendix.
first_only <- inputs$version == "ssrn"
inp_row <- function(x) data.frame(input = x$input_id, what = x$description,
  `value(s)` = ifelse(is.na(x$value), "1.5% to 4.5%", vapply(x$value, function(v) format(signif(v, 6)), "")),
  origin = x$label, where = x$source, check.names = FALSE)
odf <- rbind(inp_row(inputs[!first_only, ]), dial_rows)
scroll(kable(odf, caption = "Inputs by origin (export/r/inputs.csv, then the explorer's dials)"))
Inputs by origin (export/r/inputs.csv, then the explorer's dials)
input what value(s) origin where
n_billionaires CA billionaires in the domestic base 212 data CA_Billionaires_Revenues_and_Migration_final.xlsx!Summary_Preferred!D15
baseline_net_worth Aggregate net worth, 212 domestic billionaires 1894.8 data CA_Billionaires_Revenues_and_Migration_final.xlsx!Summary_Preferred!E15
tax_rate Statutory wealth tax rate 0.05 data CA_Billionaires_Revenues_and_Migration_final.xlsx!Calculations_Preferred!N3
pareto_alpha Pareto tail parameter fit to FTB AGI thresholds 1.43789 research monte_carlo_sim.R:33-44 (FTB PIT Annual Report 2024, Table B-4A)
bracket_tax_ty23 TY2023 tax liability, $10M+ AGI bracket 11.1 data monte_carlo_sim.R:51 (FTB Table B-4A)
total_pit_fy25 Total FY2024-25 PIT collections 130 data monte_carlo_sim.R:89 (CA State Controller cash receipts)
rauh_mc_c_min Annual CA income tax of billionaires: lower bound of the draw 3.3 guesswork NPV_dist.R; paper eq.23 (from the K=500 dispersion draw); NPV_dist_v8.R:21 (NBER chapter p.1, pp.29-31)
rauh_mc_c_max Annual CA income tax of billionaires: upper bound of the draw 5.8 guesswork NPV_dist.R; paper eq.23 (Pareto upper bound); NPV_dist_v8.R:21 (NBER chapter p.1, pp.29-31)
rauh_mc_r_min Discount rate draw: lower bound 0.015 research NPV_dist.R:47; paper eq.24 (S&P 500 dividend yield anchor p.18; eq.19-20 instead specify r-g); NPV_dist_v8.R:22 (NBER chapter p.32, r-g)
rauh_mc_r_max Discount rate draw: upper bound 0.045 research NPV_dist.R:47; paper eq.24; NPV_dist_v8.R:22 (NBER chapter p.32)
nber_mc_f_min NBER departure fraction of the income tax base: lower bound (drawn independently of WT) 0.3 guesswork NPV_dist_v8.R; Jaros and Rauh NBER c15504 Sec 5.4
nber_mc_f_max NBER departure fraction of the income tax base: upper bound 0.6 guesswork NPV_dist_v8.R; Jaros and Rauh NBER c15504 Sec 5.4
q_litigation_survival Probability the Act survives constitutional challenge (ASC 740-10 more-likely-than-not) 0.5 guesswork NBER_2026_litigation_weighted/README.md (assumptions table); not an explicit multiplier in NPV_dist_v8.R - see MISMATCHES.md #5
nber_growth_rate Growth applied to the Jan-1-2026 snapshot to the Dec-31-2026 valuation date 0.07 data final.csv (net_worth_usd_7pct column); NBER README assumptions table
nber_ceiling_hardcoded wt_max literal in NPV_dist_v8.R (script uses this, not the value re-derived from final.csv) 72 data NPV_dist_v8.R:19
nber_ceiling_recomputed wt_max re-derived from final.csv: domestic total minus 7 removed_departed rows 72.0515 derived R/nber_final.R:compute_nber_ceiling() from final.csv
nber_international_net_worth 28 international billionaires excluded from the domestic base 146.3 data final.csv (panel == international, net_worth_usd)
wt_lo Revenue, lowest value the model's random pick can take ($B) 0 (as coded); 35; 45.59 guesswork explorer dial (R/site.R)
wt_hi Revenue, highest value the model's random pick can take ($B) 55.10; 67.51; 72 (as coded) data explorer dial (R/site.R)
q Litigation survival, applied to revenue 1 (as coded); 0.75 (OPA reading); 0.50 (OPA reading) guesswork explorer dial (R/site.R)
c Annual billionaire income tax, C ($B) 3.3 to 5.8; 3.3 only; 5.8 only guesswork explorer dial (R/site.R)
rate Discount rate, the range the model picks from, r 1.5% to 4.5%; 1.5% only; 4.5% only guesswork explorer dial (R/site.R)
f Share of the income tax base that leaves, f 1 - WT/94.2; 0.30 to 0.60 (as coded); 0.283 fixed guesswork explorer dial (R/site.R)
g Growth of lost income, g (r vs r - g reading) 0 (as coded); 0.5% (illustrative); 1.0% (illustrative) guesswork explorer dial (R/site.R)
R code for the input counts
cnt <- as.data.frame(table(factor(odf$origin, levels = c("data", "research", "guesswork", "scenario", "derived"))))
names(cnt) <- c("origin", "rows")
n_guess_dials <- sum(vapply(dials, function(d) d$origin == "guesswork", logical(1)))
scroll(kable(cnt))
origin rows
data 9
research 3
guesswork 11
scenario 0
derived 1

The headline: 6 of the 7 inputs that move the result in the explorer are guesswork. The one labelled data, the revenue ceiling, rests on a legal judgement about who counts as having left (disputes row 3). The data and research inputs (the Forbes base, the FTB brackets, the Pareto fit) fix the scale; whether the NPV is negative is decided by the guesswork: how wide the revenue draw is, where the income tax range stops, which rate is used, and whether the lost income tax is permanent.2

What reproduces

R code for the output checks
# The chapter's PDF page for each figure it prints (typed, like the printed values).
page_nber <- c(revenue_baseline = "19", nber_domestic_grown = "19", nber_ceiling = "21",
               nber_international = "17", pareto_alpha = "26", income_tax_mc_k212 = "30",
               income_tax_mc_k300 = "30", income_tax_mc_k500 = "30", npv_mc_nber_mean = "35",
               npv_mc_nber_median = "35", npv_mc_nber_pct_negative = "35")
ob <- outputs[outputs$output_id %in% names(page_nber), ]
o <- ob %>% transmute(output = output_id, reproduced = signif(value, 6),
  printed = printed_value, `abs diff` = signif(abs_diff, 2), page = page_nber[output_id])
scroll(kable(o, row.names = FALSE))
output reproduced printed abs diff page
pareto_alpha 1.43789 1.44 0.00210 26
income_tax_mc_k212 5.75702 5.76 0.00300 30
income_tax_mc_k300 4.60902 4.61 0.00098 30
income_tax_mc_k500 3.30815 3.31 0.00180 30
revenue_baseline 94.30430 94.20 0.10000 19
nber_domestic_grown 100.90600 100.90 0.00560 19
nber_ceiling 72.05150 72.06 0.00850 21
nber_international 146.30000 146.30 0.00000 17
npv_mc_nber_mean -38.88630 -38.90 0.01400 35
npv_mc_nber_median -35.25550 -35.30 0.04500 35
npv_mc_nber_pct_negative 85.17200 85.20 0.02800 35
R code for the output checks
top_id <- ob$output_id[which.max(ob$abs_diff)]
stopifnot(top_id == "revenue_baseline")   # the prose below names it as mismatch 6

Every figure reproduces from the authors’ own code to within the tolerances in tests/testthat/ (printed rounding for deterministic figures, four Monte Carlo standard errors for simulated ones). The largest gap is revenue_baseline (0.10), the baseline of mismatch 6. Table 9’s 21 cells and Table 10’s 3 cells, shown above, match as well.3

R code for the mismatch list
mm_lines <- sub("^## ", "", grep("^## [0-9]+\\.", readLines("../MISMATCHES.md"), value = TRUE))
mm_first <- grepl("^[0-9]+\\. First version: ", mm_lines)
cat(length(mm_lines), " discrepancies were found between the authors' text, their READMEs and their code. ",
    sum(!mm_first), " of them concern the chapter. We record them rather than fix them:",
    "^[The other ", sum(mm_first), " concern only the authors' first version: ",
    paste(sub("First version: ", "", mm_lines[mm_first]), collapse = "; "), ".]\n\n", sep = "")

9 discrepancies were found between the authors’ text, their READMEs and their code. 4 of them concern the chapter. We record them rather than fix them:4

R code for the mismatch list
cat(paste0(mm_lines[!mm_first], collapse = "\n"), "\n\nFull text: [the materials page](materials.html#mismatches).\n")
  1. One 1.5% to 4.5% interval for two discount concepts, (r-g) and plain r
  2. NBER ceiling: hard-coded 72 vs. 72.05/72.06 derivable from final.csv
  3. q = 0.50 litigation-survival weight has no explicit multiplier in the code
  4. Baseline revenue: the workbook’s precise 94.30 vs. the printed 94.20

Full text: the materials page.

Internal identities

Checks against things that are not inputs to anything:

R code for the consistency checks
n_cell <- site_grid_cell(grid, as.list(site_named()$nber))
se_n <- mcn$summary$sd / sqrt(mcn$summary$n)
checks <- data.frame(
  identity = c(
    "Table 10's x* plugged into NPV = WT - x* C/r gives 0 (max |NPV|, $B)",
    "Grid's main-estimate cell equals the pipeline's analytic mean (abs diff, $B)",
    "Seeded MC mean vs exact expectation, in MC standard errors",
    "Share negative: quadrature vs seeded MC (points)",
    "Ceiling: domestic total minus the 7 departures, recomputed, minus the script's 72 ($B)"),
  value = c(max(abs(with(n10, WT - break_even_pct * C / r))),
    abs(n_cell$mean_npv - tr("npv_mc_nber_analytic_mean")),
    (mcn$summary$mean - tr("npv_mc_nber_analytic_mean")) / se_n,
    n_cell$pct_negative - mcn$summary$pct_negative,
    nc$ceiling_recomputed - nb$wt_max))
checks$value <- vapply(checks$value, function(v) format(signif(v, 3)), "")
scroll(kable(checks))
identity value
Table 10's x* plugged into NPV = WT - x* C/r gives 0 (max |NPV|, $B) 7.11e-15
Grid's main-estimate cell equals the pipeline's analytic mean (abs diff, $B) 0
Seeded MC mean vs exact expectation, in MC standard errors 0.79
Share negative: quadrature vs seeded MC (points) 0.149
Ceiling: domestic total minus the 7 departures, recomputed, minus the script's 72 ($B) 0.0515

The seeded mean sits within about one Monte Carlo standard error of the exact expectation. The printed −38.9 equals the rounded seeded run, not the rounded expectation, −39.0. Which one a printed digit matches is a rounding coincidence.5

Reproducing this document

cd rjkdc-analysis
export RAUH_REPO_DIR=/path/to/wealth_tax     # clone of github.com/bjaros20/wealth_tax at 25e84dd
Rscript -e 'targets::tar_make()'             # writes the targets store, export/r/, site/data/, site/explorer/grid.js
Rscript tests/testthat.R                     # full suite
quarto render site/repro.qmd

The authors’ repository is MIT licensed but not redistributed here; the tests that re-run the authors’ own scripts skip cleanly without it, and the rest of the suite does not need it.

Appendix: the authors’ first version (March 2026)

This appendix is not part of the account above. It records what the pipeline also reproduces: the authors’ first version, Rauh, Jaros, Kearney, Doran and Cosso, The Net Present Value of the Billionaire Tax Act, Hoover Institution, SSRN 6340778 (17 March 2026). Page numbers in this appendix are that version’s PDF pages. Its Pareto fit and income tax Monte Carlo are the ones in Section 5.1 above (its Table 8 is the chapter’s Table 9). Its revenue part and its Monte Carlo differ.

Revenue. It starts from the same $94.20B baseline, without growth to a valuation date or a litigation discount. Six confirmed departures leave $67.51B (printed $67.51B, p.11); ten reported departures leave $55.10B (printed $55.1B, p.13). The Swiss semi-elasticity, applied linearly (10.32 × 5 = 51.6% of the base), gives $45.59B (printed $45.59B, p.14). The preferred “approximately $40 billion (range: $35-46 billion)” (p.14) is not computed anywhere in the authors’ code. Every step after the baseline uses a typed 94.2 rather than the workbook’s 94.30 (mismatch 6).

NPV by scenario and break-even (its Tables 9 and 10). The departure share is tied to revenue, \(f = 1 - WT/94.2\), so revenue lost and income tax lost move together.

R code for the first version’s Table 9
p9 <- printed[printed$table == "table9", ]
t9p <- t9 %>% mutate(key = paste0(scenario, "@", r)) %>% rowwise() %>% mutate(
  pv_printed = p9$printed[p9$row == key & p9$column == "pv_lost"],
  npv_printed = p9$printed[p9$row == key & p9$column == "npv"]) %>% ungroup()
tab9 <- t9p %>% transmute(Scenario = label, `WT ($B)` = num(WT, 2), `C ($B)` = num(C, 2),
  `f` = pct(100 * f), `r - g` = pct(100 * r),
  `PV lost` = num(pv_lost), `printed` = num(pv_printed),
  `NPV` = num(npv), `printed ` = num(npv_printed))
m9 <- sum(abs(round(t9p$pv_lost, 1) - t9p$pv_printed) < 1e-9) + sum(abs(round(t9p$npv, 1) - t9p$npv_printed) < 1e-9)
scroll(kable(tab9, align = "lrrrrrrrr", caption = "First version, Table 9, $B (its PDF p.19)"))
First version, Table 9, $B (its PDF p.19)
Scenario WT ($B) C ($B) f r - g PV lost printed NPV printed
Central Scenario 42.00 4.55 55.4% 1.5% 168.1 168.1 −126.1 −126.1
Central Scenario 42.00 4.55 55.4% 3.0% 84.0 84.0 −42.0 −42.0
Central Scenario 42.00 4.55 55.4% 4.5% 56.0 56.0 −14.0 −14.0
Only 6 Confirmed Departures 67.51 3.30 28.3% 1.5% 62.3 62.3 5.2 5.2
Only 6 Confirmed Departures 67.51 3.30 28.3% 3.0% 31.2 31.2 36.3 36.3
Only 6 Confirmed Departures 67.51 3.30 28.3% 4.5% 20.8 20.8 46.7 46.7
Literature Calibrated Departures 35.00 5.80 62.8% 1.5% 243.0 243.0 −208.0 −208.0
Literature Calibrated Departures 35.00 5.80 62.8% 3.0% 121.5 121.5 −86.5 −86.5
Literature Calibrated Departures 35.00 5.80 62.8% 4.5% 81.0 81.0 −46.0 −46.0
R code for the first version’s Table 10
p10 <- printed[printed$table == "table10", ]
tab10 <- t10 %>% mutate(key = paste0(scenario, "@", r)) %>% rowwise() %>%
  mutate(printed = p10$printed[p10$row == key]) %>% ungroup() %>%
  transmute(Scenario = label, `r - g` = pct(100 * r), `x*` = pct(100 * break_even_pct),
            printed = pct(printed))
m10 <- sum(abs(round(100 * t10$break_even_pct, 1) -
  sapply(paste0(t10$scenario, "@", t10$r), function(k) p10$printed[p10$row == k])) < 1e-9)
scroll(kable(tab10, align = "lrrr", caption = "First version, Table 10 (its PDF p.20)"))
First version, Table 10 (its PDF p.20)
Scenario r - g x* printed
Central Scenario 1.5% 13.8% 13.8%
Central Scenario 3.0% 27.7% 27.7%
Central Scenario 4.5% 41.5% 41.5%
Only 6 Confirmed Departures 1.5% 30.7% 30.7%
Only 6 Confirmed Departures 3.0% 61.4% 61.4%
Only 6 Confirmed Departures 4.5% 92.1% 92.1%
Literature Calibrated Departures 1.5% 9.1% 9.1%
Literature Calibrated Departures 3.0% 18.1% 18.1%
Literature Calibrated Departures 4.5% 27.2% 27.2%

18 of the 18 Table 9 cells and 9 of the 9 Table 10 cells match the printed digit. The central scenario’s $42B is a typed literal with no derivation (mismatch 8). The headline is not a cell of Table 9: the central cell at 1.5% is −126.1 $B.

Monte Carlo (its Sec 5.5). It draws \(WT \sim U[35, 67.51]\), \(C \sim U[3.3, 5.8]\) and \(r \sim U[0.015, 0.045]\), with \(f = 1 - WT/94.2\). The paper prints the upper bound as 67.1, a typo for 67.51 (mismatch 1). The text derives a growing perpetuity, but the code divides by the plain rate (mismatch 2). The draw’s revenue has a mean of $51.26B, above the preferred $40B.

R code for the first version’s Monte Carlo
s1 <- tr("npv_mc_ssrn_summary")
scroll(kable(data.frame(
  ` ` = c("reproduced (seed 2026)", "printed (its PDF p.20)"),
  `Mean` = c(num(s1$mean, 2), num(h("ssrn", "mc_mean_npv", "printed"))),
  `Median` = c(num(s1$median, 2), num(h("ssrn", "mc_median_npv", "printed"))),
  `SD` = c(num(s1$sd, 2), num(h("ssrn", "mc_sd_npv", "printed"))),
  `% negative` = c(num(s1$pct_negative, 2), num(h("ssrn", "mc_pct_negative", "printed"))),
  check.names = FALSE), align = "lrrrr",
  caption = "First version, NPV Monte Carlo, $B, seed 2026, 100,000 draws"))
First version, NPV Monte Carlo, $B, seed 2026, 100,000 draws
Mean Median SD % negative
reproduced (seed 2026) −24.77 −19.10 38.60 71.02
printed (its PDF p.20) −24.7 −19.1 38.4 71.0

Our R port agrees with an unmodified run of NPV_dist.R to machine precision. The median and share negative match the printed values. The mean (−24.77 against −24.7) and the standard deviation (38.60 against 38.4) do not quite (mismatch 9). The draws are independent, so the mean has a closed form:

\[E[NPV] = E[WT] - \left(1 - \frac{E[WT]}{94.20}\right) E[C]\, E\!\left[\tfrac{1}{r}\right]\]

It equals −24.707 $B, which rounds to the printed −24.7. So the printed mean is the model’s exact expectation, while the shipped seed gives −24.77. The printed SD stays unexplained.

Its outputs, inputs and checks.

R code for the first version’s outputs, inputs and checks
of <- outputs[!outputs$output_id %in% names(page_nber) & outputs$version != "nber", ]
scroll(kable(of %>% transmute(output = output_id, reproduced = signif(value, 6),
  printed = printed_value, `abs diff` = signif(abs_diff, 2), `its page` = printed_page),
  row.names = FALSE, caption = "First version: outputs"))
First version: outputs
output reproduced printed abs diff its page
revenue_confirmed6 67.5106 67.51 0.00065 11
revenue_expanded10 55.0964 55.10 0.00360 13
revenue_literature_calibrated 45.5928 45.59 0.00280 14
table9_central_npv_r015 -126.0890 -126.10 0.01100 19
npv_mc_ssrn_mean -24.7681 -24.70 0.06800 20
npv_mc_ssrn_median -19.1004 -19.10 0.00045 20
npv_mc_ssrn_sd 38.5955 38.40 0.20000 20
npv_mc_ssrn_pct_negative 71.0240 71.00 0.02400 20
R code for the first version’s outputs, inputs and checks
scroll(kable(inp_row(inputs[first_only, ]), row.names = FALSE, caption = "First version: inputs"))
First version: inputs
input what value(s) origin where
brulhart_semi_elasticity Migration semi-elasticity, Brulhart et al. (2022) 10.32 research Summary_Preferred!I8 (paper eq.12)
wt_central_scenario Table 9 Central Scenario wealth tax revenue (literal input, not linked to Sec 4's chain) 42 scenario NPV_calculations_5.2.xlsx!B12
rauh_mc_wt_min Monte Carlo wealth tax floor (literature-calibrated scenario; semi-elasticity about 12.6) 35 guesswork NPV_dist.R (wt ~ U[35, 67.51]); paper eq.22 and Table 9
R code for the first version’s outputs, inputs and checks
x10 <- t10 %>% left_join(t9 %>% select(scenario, r, WT, C), by = c("scenario", "r", "WT", "C"))
s_cell <- site_grid_cell(grid, as.list(site_named()$ssrn))
se_s <- mcs$summary$sd / sqrt(mcs$summary$n)
checks1 <- data.frame(
  identity = c(
    "Table 10's x* plugged into Table 9's formula gives NPV = 0 (max |NPV|, $B)",
    "Grid's first-version cell equals the pipeline's analytic mean (abs diff, $B)",
    "Seeded MC mean vs exact expectation, in MC standard errors",
    "Share negative: quadrature vs seeded MC (points)"),
  value = c(max(abs(with(x10, WT - break_even_pct * C / r))),
    abs(s_cell$mean_npv - tr("npv_mc_ssrn_analytic_mean")),
    (mcs$summary$mean - tr("npv_mc_ssrn_analytic_mean")) / se_s,
    s_cell$pct_negative - mcs$summary$pct_negative))
checks1$value <- vapply(checks1$value, function(v) format(signif(v, 3)), "")
scroll(kable(checks1, caption = "First version: internal identities"))
First version: internal identities
identity value
Table 10's x* plugged into Table 9's formula gives NPV = 0 (max |NPV|, $B) 7.11e-15
Grid's first-version cell equals the pipeline's analytic mean (abs diff, $B) 0
Seeded MC mean vs exact expectation, in MC standard errors -0.501
Share negative: quadrature vs seeded MC (points) 0.00385

Footnotes

  1. The authors released a first version in March 2026: Rauh, Jaros, Kearney, Doran and Cosso, Hoover Institution, SSRN 6340778. It printed revenue of about $40B and a mean NPV of −24.7 $B, with 71% of outcomes negative (its PDF p.1). The pipeline reproduces it too; the appendix at the end of this report sets out what it printed and which checks concern it.↩︎

  2. The pipeline’s input file also lists 3 rows sourced to the authors’ first version (March 2026): its revenue floor, its central scenario and the Swiss semi-elasticity as its workbook applies it. The appendix shows them.↩︎

  3. The first version’s own outputs are in the appendix.↩︎

  4. The other 5 concern only the authors’ first version: 1. the printed WT upper bound (67.1) vs. the code’s literal (67.51); 2. growing perpetuity in the text (r-g) vs. plain-r code; 7. Summary_Expanded!D23:D24 reads from the wrong sheet (stale copy-paste); 8. the Table 9 central scenario (WT=$42.0B) has no derivation; 9. the printed Monte Carlo summary (mean -$24.7B, sd $38.4B) does not exactly reproduce.↩︎

  5. In the authors’ first version it is the other way round: its printed mean equals the rounded expectation (see the appendix).↩︎