Prop 40, JR analysis: the materials

Every script, test, data file and source behind the explorer, the report and the slides, and what did not match

Published

October 14, 2026

This page is a guide to the Open Materials layer (the repository) of the JR analysis (Jaros and Rauh, NBER chapter c15504, Tax Policy and the Economy vol. 41, September 2026; first version by Rauh, Jaros, Kearney, Doran and Cosso, March 2026): everything needed to rebuild every number on this site, in the repository github.com/fhoces/opa-prop40, folder rjkdc-analysis/. The analysis is an R {targets} pipeline with a Python twin; the site’s explorer grid, report and deck read that pipeline’s store and exports, so none of them retypes a number.

Rebuild everything

git clone https://github.com/bjaros20/wealth_tax && git -C wealth_tax checkout 25e84dd   # the authors' MIT repo
cd rjkdc-analysis
export RAUH_REPO_DIR=/absolute/path/to/wealth_tax
Rscript -e 'targets::tar_make()'        # pipeline, export/r/*.csv, site/data/*.csv, site/explorer/grid.js
Rscript tests/testthat.R                # the test suite (console summary)
Rscript tests/count_tests.R             # the same suite, counted: site/data/test-summary.csv
python3 py/run_export.py     # the Python twin: export/py/*.csv
quarto render site/repro.qmd && quarto render site/materials.qmd
Rscript -e 'rmarkdown::render("site/slides/slides.Rmd")'

RAUH_REPO_DIR is the single place that knows where the authors’ repository lives (R/rauh_repo.R, py/rauh_repo.py). Tests that re-run the authors’ own scripts skip cleanly without it; everything else runs.

Tests

Counted with testthat's ListReporter by tests/count_tests.R
file tests expectations tests passed tests failed tests skipped
test-income-tax-mc.R 2 25 2 0 0
test-nber-final.R 3 10 3 0 0
test-npv-mc.R 5 12 5 0 0
test-npv-tables.R 3 7 3 0 0
test-pareto.R 3 6 3 0 0
test-parity.R 3 39 3 0 0
test-revenue-chain.R 2 8 2 0 0
test-site.R 8 52 8 0 0
total 29 159 29 0 0

29 tests (159 expectations): 29 tests passed, 0 failed, 0 skipped, run on 2026-10-14 with R 4.4.0, with the authors’ repository available. They come in four kinds:

  1. Against the paper’s printed number, at printed rounding for deterministic figures, or within four Monte Carlo standard errors for simulated ones.
  2. Against the authors’ own scripts, run unmodified from a scratch copy (plot and CSV paths redirected, no logic changed; tests/testthat/helper-author-scripts.R).
  3. Parity between the R and Python exports (test-parity.R): deterministic steps to 1e-6, Monte Carlo steps within a documented tolerance (numpy cannot share R’s random stream).
  4. The site (test-site.R): the explorer’s grid.js and site/data/headlines.csv must be byte-identical to the exporter’s output; grid cells must equal the pipeline’s analytic means and brute-force Monte Carlo; the printed-value transcription must agree with the pipeline; every number typed into the landing page must still match its source.

A continuous-integration workflow for this folder is configured to rebuild the pipeline and run the suite on each push that touches it (.github/workflows/rjkdc-ci.yml, cloning the authors’ repository at the pinned commit).

Scripts

R pipeline (R/), Python twin (py/), and the runners
file what it does
R/export.R export/{r,py}/inputs.csv + outputs.csv.
R/income_tax_mc.R Income-tax Monte Carlo (first version numbering: Sec 5.1.2, Table 8, p.17; in the chapter Table 9, p.30).
R/io.R Small shared helper for writing the export/ CSVs (both export/r and, indirectly via the schema it fixes, export/py).
R/nber_final.R NBER bridge inputs, rebuilt from RAUH/NBER_2026_litigation_weighted/ NPV_data/final.csv (240 rows: 212 domestic + 28 international; see extract_nber_final()) rather than hard-coded, by design.
R/npv_mc.R Sec 5.5 NPV Monte Carlo, both versions.
R/npv_tables.R First version numbering: Sec 5.2-5.4, Table 9 (NPV by departure scenario, p.19) and Table 10 (break-even departure fraction, p.20; the chapter's break-even table is Table 10, p.34).
R/pareto.R Sec 5.1.1: Pareto tail fit to the FTB's cumulative AGI-bracket filer counts, and the rank-implied income tax schedule used as the input to the income-tax Monte Carlo (R/income_tax_mc.R).
R/rauh_repo.R Single resolver for the authors' MIT-licensed repo (bjaros20/wealth_tax, pinned SHA 25e84ddbb67fe0293a64cd9108ed4d35d8ae37ab).
R/revenue_chain.R First version numbering: Sec 4, the revenue chain 94.20 -> 67.51 -> 55.10, plus the 45.59 literature-calibrated figure (March 2026 version pp.10-14).
R/site.R Exports for the public site (site/): the explorer grid, the NBER collectible-revenue figure, and the headline table the landing page and the prose-number test read.
py/income_tax_mc.py Python twin of R/income_tax_mc.R (Table 8, p.17).
py/nber_final.py Python twin of R/nber_final.R.
py/npv_mc.py Python twin of R/npv_mc.R.
py/npv_tables.py Python twin of R/npv_tables.R (Table 9, Table 10).
py/pareto.py Python twin of R/pareto.R.
py/rauh_repo.py Single resolver for the authors' MIT-licensed repo.
py/revenue_chain.py Python twin of R/revenue_chain.R.
py/run_export.py Entry point for the Python twin: runs every step and writes export/py/inputs.csv + export/py/outputs.csv, matching the schema and row ids of export/r/{inputs,outputs}.csv exactly (same input_id/output_id values) so tests...
_targets.R The pipeline definition: every target below, in one list.
tests/testthat.R
tests/count_tests.R Run the full suite with a ListReporter and write one row per test file to site/data/test-summary.csv, which the materials page reads.

The pipeline’s targets (tar_manifest()):

target command (first 90 characters)
npv_table10 compute_npv_table10()
npv_mc_nber_analytic_mean npv_mc_analytic_mean(0, 72, 3.3, 5.8, 0.015, 0.045, f_mode = "nber", f_min = 0.3, f_max =
npv_mc_ssrn_analytic_mean npv_mc_analytic_mean(35, 67.51, 3.3, 5.8, 0.015, 0.045, f_mode = "ssrn", baseline_revenue
pareto_fit compute_pareto_fit()
nber_final_csv rauh_nber_final_csv()
npv_table9 compute_npv_table9()
rauh_workbook rauh_workbook_path()
npv_mc_ssrn compute_npv_mc_ssrn()
npv_mc_nber compute_npv_mc_nber()
pareto_income_schedule compute_pareto_income_schedule(pareto_fit)
nber_final extract_nber_final(nber_final_csv)
calc_expanded extract_calculations_expanded(rauh_workbook)
calc_preferred extract_calculations_preferred(rauh_workbook)
npv_mc_ssrn_summary npv_mc_ssrn$summary
npv_mc_nber_summary npv_mc_nber$summary
income_tax_mc compute_income_tax_mc(pareto_income_schedule$tax_by_rank)
nber_collectible compute_nber_collectible(nber_final)
nber_ceiling compute_nber_ceiling(nber_final)
revenue_chain compute_revenue_chain(calc_preferred, calc_expanded)
income_tax_table8 income_tax_mc_table8(income_tax_mc)
site_grid compute_site_grid(revenue_chain)
export_inputs compute_export_inputs(pareto_fit, revenue_chain, nber_ceiling)
export_outputs compute_export_outputs(pareto_fit, income_tax_table8, revenue_chain, npv_table9, npv_mc_ss
site_headlines compute_site_headlines(site_grid, npv_mc_ssrn_summary, npv_mc_nber_summary, npv_mc_ssrn_an
export_inputs_csv { readr_write(export_inputs, "export/r/inputs.csv") "export/r/inputs.csv" }
export_outputs_csv { readr_write(export_outputs, "export/r/outputs.csv") "export/r/outputs.csv" }
site_headlines_csv { readr_write(site_headlines, "site/data/headlines.csv") "site/data/headlines.csv" }
site_grid_js write_site_grid_js(site_grid, revenue_chain, site_headlines)

Data files the site publishes

file rows contents
export/r/inputs.csv 20 every input: value, OPA label (data / research / guesswork / scenario / derived), source, page
export/r/outputs.csv 19 every headline figure: reproduced value, printed value, page, absolute difference
export/py/inputs.csv 20 the Python twin's inputs (same schema)
export/py/outputs.csv 19 the Python twin's outputs (same schema)
site/data/headlines.csv 19 the main estimate: printed, reproduced, exact (target site_headlines)
site/data/printed-tables.csv 51 hand transcription of the printed income tax table (Table 9) and Table 10, with PDF pages (checked by test-site.R)
site/data/test-summary.csv 8 test counts per file (tests/count_tests.R)
DATA-SOURCES.csv 5 the source manifest (below)

The explorer’s grid, site/explorer/grid.js, is also generated (target site_grid_js): 2,187 cells, never edited by hand.1

Sources

None of these files is committed; each is listed with where it came from and its SHA-256 hash. The papers are not redistributed; the authors’ repository is MIT licensed and read from a local clone.2

file provider obtained_via date_obtained url_or_contact sha256 terms feeds
original-materials/ssrn-6340778.pdf Hoover Institution (Rauh, Jaros, Kearney, Doran, Cosso) public-download 2026-09-22 link edb4ad1e9c165b34… do-not-redistribute reference only (page citations in MISMATCHES.md and export/{r,py}/inputs.csv, outputs.csv); not read programmatically - the pipeline reads the authors' own repo…
original-materials/additional-documentation/caltax-walczak-2026-04-22.pdf Jared Walczak, California Tax Foundation, "Ongoing State Tax Revenue Implications of the 2026 California Billionaire Tax Act" public-download 2026-09-23 link 06fd06567974866e… do-not-redistribute same-side work entering as credited alternative dials (ongoing income tax losses incl. spillovers); not the JR headline
original-materials/additional-documentation/hoover-california-decides-prop40-2026-08.pdf Hoover Institution, "California Decides: Proposition 40" (Proposition Lab brief) public-download 2026-09-23 link 1281bed69488de02… do-not-redistribute evidence that the JR anchor (Rauh et al., Mar 17) was not revised through Aug 2026
original-materials/author-shared/2026-09-23_wealth_tax-repo/ Jaros, Rauh (bjaros20/wealth_tax GitHub repo) git-clone 2026-09-23 link 25e84ddbb67fe029… MIT rjkdc-analysis R+Python pipeline (_targets.R, R/*.R, py/*.py): NPV_data/monte_carlo_sim.R (Pareto fit + income-tax MC, Table 8), NPV_data/Raw_Data_Collection.xl…
original-materials/jaros-rauh-nber-c15504-2026-09.pdf Jaros, Rauh (NBER) public-download 2026-09-23 link 2b427d6b205c84c4… do-not-redistribute reference only; not read by the pipeline; headline: ~$30B wealth tax revenue (range $28-36B) and NPV -$38.9B, both stated on p.1 (abstract) and restated p.3; co…

What the checks found

Nine mismatches between the authors’ text, their READMEs and their code. The pipeline reproduces the code as shipped and records each one here.

Mismatches 1, 2, 7, 8 and 9 concern only the authors’ first version (Rauh, Jaros, Kearney, Doran and Cosso, Hoover Institution, SSRN 6340778, March 2026), which the pipeline also reproduces. Mismatches 3 to 6 concern the NBER chapter.

One numbered item per code/paper or doc/code discrepancy found while reproducing Jaros and Rauh, The Net Present Value of the California Billionaire Tax Act (NBER chapter c15504, Tax Policy and the Economy vol. 41, September 2026; first version by Rauh, Jaros, Kearney, Doran and Cosso, March 2026). By design, we do not “fix” the authors: we reproduce their code as run, record the discrepancy, and where it matters expose the alternative reading as a parameter with both readings under test.

1. First version: the printed WT upper bound (67.1) vs. the code’s literal (67.51)

First-version paper, p.20, eq. 22: WT ~ U[$35B, $67.1B]. Code, NPV_data/NPV_dist.R:31: wt_max <- 67.51.

$67.51B is the confirmed-6-departures revenue ceiling used everywhere else in that version (p.11, Table 9’s “Only 6 Confirmed Departures” column); $67.1B appears nowhere else. We treat 67.51 as the intended value (it is what the shipped, runnable code uses and what every other reference to the ceiling states) and reproduce the code, not the typo.

2. First version: growing perpetuity in the text (r-g) vs. plain-r code

First-version paper, p.18, eq. 20: NPV = WT - f * C / (r - g) (eq. 18 on the same page is the plain-r form, NPV = WT - f * C / r), introduced with an explicit growth-rate adjustment (Gordon Growth Model discussion, p.18-19). First-version paper, p.20, eq. 24 and surrounding prose: switches notation to plain r ~ U[0.015, 0.045] and states “NPV = WT - f * C / r” for the Sec 5.5 Monte Carlo. Code, NPV_data/NPV_dist.R:60: pv_lost <- annual_loss / r_discount (plain r, no growth term).

That version’s prose is internally inconsistent (Sec 5.2 derives a growing-perpetuity formula, Sec 5.5 silently drops the growth term); the code matches the later (Sec 5.5) plain-r reading. R/npv_mc.R:compute_npv_mc_ssrn() reproduces the code (plain r).

3. One 1.5% to 4.5% interval for two discount concepts, (r-g) and plain r

Code, NBER_2026_litigation_weighted/NPV_data/NPV_dist_v8.R:22,28: draws rg_discount <- runif(n_sims, 0.015, 0.045), i.e. explicitly labelled (r-g). Paper, PDF p.35, eq. 18 draws (r - g) ~ U[0.015, 0.045]; the sentence after eq. 19 computes NPV = WT - f * C / r, with the plain rate. NBER README assumptions table lists the same [1.5%, 4.5%] range for (r - g) under the litigation-weighted approach, and the identical range for plain r under the first version’s approach (see mismatch #2). The same interval stands for two different discount concepts (one nets out growth, one doesn’t); neither the README nor the text says why the same calibration should apply to both.

4. NBER ceiling: hard-coded 72 vs. 72.05/72.06 derivable from final.csv

Code, NBER_2026_litigation_weighted/NPV_data/NPV_dist_v8.R:19: wt_max <- 72 (a literal). NBER README assumptions table: “$72.06B” (text). NBER chapter, PDF p.22 (eq. 4): the same 72.06 in the semi-elasticity formula. Our recomputation from final.csv (R/nber_final.R:compute_nber_ceiling(), py/nber_final.py:compute_nber_ceiling()): domestic total at 7% growth (sum(face_tax_5pct[panel=="domestic"]) = $100.9056B) minus the 7 removed_departed rows ($28.8541B) = $72.0515B.

Three different numbers for the same quantity: the script’s literal (72), the README’s prose (72.06), and the value the underlying person-level data actually sums to (72.05). The 72.06 is the difference of the two figures as rounded to two decimals (100.91 − 28.85); the person-level sum is 72.0515. We rebuild the ceiling from final.csv by design rather than trusting either the script’s literal or the README’s rounding, and flag that the shipped NPV_dist_v8.R does not itself perform this rebuild - it uses the hard-coded 72.

5. q = 0.50 litigation-survival weight has no explicit multiplier in the code

NBER README assumptions table: “Litigation risk: Probability the Act survives constitutional challenge, q = 0.50 (ASC 740-10 ‘more likely than not’), applied to the recognized base” - listed as a defining feature of the litigation-weighted approach. Code, NPV_dist_v8.R:19: wt_min <- 0; wt_max <- 72. No q variable and no * 0.5 anywhere in the file.

The only place a 0.5 could be hiding is that E[U(0, 72)] = 36 = 0.5 * 72, i.e. drawing WT uniformly from 0 to the ceiling has the same expected value as multiplying the ceiling by 0.5. But that is not what the README describes (“applied to the recognized base,” i.e. a discrete survive/strike-down weighting of a fixed recognized amount), and a uniform draw over the full range is a materially different distributional assumption from a Bernoulli(q) weight on a point estimate (the uniform draw also puts positive probability on values between 0 and 72 that a true two-state litigation model would not produce). We reproduce the code as shipped (wt ~ U[0, 72]) and flag that it does not implement the README’s own stated mechanism.

6. Baseline revenue: the workbook’s precise 94.30 vs. the printed 94.20

Workbook, CA_Billionaires_Revenues_and_Migration_final.xlsx!Summary_Preferred!C5 (=SUM(Calculations_Preferred!O3:O214)/$M$2) = 94.30426206. First-version paper, p.10 (Sec 4.1): “the wealth tax would generate $94.20 billion in revenue.” NBER chapter, PDF pp.18-19 (Sec 4.1, eq. 2): the same $94.20B. Its own person-level file at the grown vintage, final.csv, sums to $100.906B, which is exactly 1.07 x 94.304: the chapter’s data also give 94.30. Workbook, Summary_Preferred!C8 = 94.2 (typed literal, feeds F8’s literature-calibrated formula, p.14 eq.13) and NPV_calculations_5.2.xlsx!B5 = 94.2 (typed literal, feeds every Table 9/10 scenario’s f = 1 - WT/baseline, p.19-20).

The first version’s prose rounds 94.30 to 94.20 in one place (p.10) and then every downstream calculation (literature calibration, Table 9, Table 10) is built from the rounded 94.2 literal, not from the workbook’s own more precise C5. R/revenue_chain.R:compute_revenue_chain() reports both baseline_precise (94.30, from the per-row sum) and baseline_literal (94.2, the value actually used downstream) so the discrepancy is visible rather than silently propagated.

7. First version: Summary_Expanded!D23:D24 reads from the wrong sheet (stale copy-paste)

Workbook, Summary_Expanded!D23: =SUMIF(Calculations_Preferred!F3:F214,"N",Calculations_Preferred!D3:D214) - and D24 the same pattern with column L. Both reference Calculations_Preferred (the 6-departure scenario) even though every other formula on this sheet (including the revenue figure that actually matters, F5) correctly references Calculations_Expanded (the 10-departure scenario). This looks like a copy-paste artifact from building Summary_Expanded off a copy of Summary_Preferred. It does not affect any number this pipeline reproduces (F5 = $55.10B is correctly sourced), only the “Stayers” net-worth memo cells (C22:D25), which we do not use.

8. First version: the Table 9 central scenario (WT=$42.0B) has no derivation

Workbook, NPV_calculations_5.2.xlsx!B12: 42 (typed literal, no formula). First-version paper, p.19: “The Central Scenario uses a revenue estimate of $42.0 billion and a midpoint income tax estimate (C = $4.55 billion).”

Every other WT figure in that version traces to a computation (94.20 -> 67.51 -> 55.10 baseline chain, or 45.59 via the literature semi-elasticity). $42.0B does not: it is not the average of any two chain values (e.g. (67.51+35)/2 = 51.3, not 42), not the preferred-estimate midpoint stated on p.14 ((35+46)/2 = 40.5, close but not equal), and not linked to any semi-elasticity via the eq. 12-13 formula used elsewhere. We treat it as a freestanding scenario input (labelled scenario in export/{r,py}/inputs.csv, not data or research) and do not attempt to re-derive it.

9. First version: the printed Monte Carlo summary (mean -$24.7B, sd $38.4B) does not exactly reproduce

Running the authors’ own unmodified NPV_data/NPV_dist.R from a scratch copy (seed 2026, n=100,000, wt_max <- 67.51 as shipped) gives mean -$24.8B, median -$19.1B, sd $38.6B, 71.0% negative on this machine/R version - matching our R port to 1e-9 (tests/testthat/test-npv-mc.R) but off by ~$0.1-0.2B from the paper’s printed mean/sd (median and %negative match exactly). We checked whether the eq. 22 typo (mismatch #1, 67.1 vs 67.51) explains the gap by re-running with wt_max <- 67.1: it does not - that moves the mean further from the printed value (to -$25.3B). The gap is within Monte Carlo error for one run (our tolerance: 4 * sd / sqrt(n) =~ $0.5B). A fixed seed gives the same draw on every run, so repeated runs cannot show whether the gap is systematic. What does explain the printed mean: the draws are independent uniforms, so the expectation has a closed form, E[WT] - (1 - E[WT]/94.2) x E[C] x E[1/r], with E[1/r] = ln(0.045/0.015)/0.03. That gives -$24.707B, which rounds to the printed -$24.7B (re-verified by the main session, 2026-09-23). So the printed mean matches the model’s exact expectation, while the shipped seed-2026 run gives -$24.77B. The printed sd ($38.4B vs $38.6B) remains unexplained; a different run or code state is the likeliest source. The NBER version’s printed summary (mean -$38.9B, median -$35.3B, 85.2% negative), by contrast, reproduces from NPV_dist_v8.R to within Monte Carlo noise with no directional gap.

Footnotes

  1. The export CSVs, headlines.csv and printed-tables.csv also carry rows for the authors’ first version (Rauh, Jaros, Kearney, Doran and Cosso, Hoover Institution, SSRN 6340778, March 2026), which the pipeline reproduces too. In the export CSVs and headlines.csv they are marked ssrn in the version column. In printed-tables.csv they are the tables table9 and table10; the label table8 is the chapter’s Table 9. The report’s appendix uses them.↩︎

  2. The manifest also lists the PDF of the authors’ first version (March 2026), which the pipeline reproduces as well.↩︎