Prop 40, the comparison: the materials

Every script, test, data file and source behind the explorer and the report

Published

October 14, 2026

This page is a guide to the Open Materials layer (the repository folder) of the comparison, the reconciliation of the BSZ analysis and the JR analysis, each in its latest version (BSZ, August 2026; Jaros and Rauh, September 2026). It says what to run, what the tests check, what each file is, and what the checks found. Everything lives in one repository, fhoces/opa-prop40, under comparison/. The tables on this page are built from the files when the page is rendered.

Rebuild everything

git clone https://github.com/fhoces/opa-prop40 && cd opa-prop40
pip install scipy                         # the Python twin's one dependency
Rscript comparison/run.R                  # export/r, export/py, index.html, site/explorer/, assets/comparison-data.js
Rscript comparison/tests/testthat.R       # the test suite
python3 comparison/py/bridge.py           # the Python twin alone, writes export/py/
cd comparison/site && quarto render repro.qmd && quarto render materials.qmd

The comparison needs nothing downloaded. It reads each analysis’s export contract: the two files export/r/inputs.csv and export/r/outputs.csv that each analysis’s own pipeline writes for others to read. Both are committed, so the comparison rebuilds without either analysis’s sources. It also reads the values transcribed from the PDFs in comparison/data/. Set OPA_PROP40_ROOT if the scripts cannot find the repository root, and PYTHON to choose the interpreter run.R calls for the Python twin, py/bridge.py (the same computation written a second time in Python and checked against the R result).

Tests

file blocks passed failed skipped
test-endpoints.R 8 33 0 0
test-parity.R 3 102 0 0
test-shapley.R 7 55 0 0
test-site.R 11 260 0 0
test-sources.R 4 13 0 0
total 33 463 0 0

Run while this page was rendered: 463 expectations passed, 0 failed, in 33 test blocks. The files check:

  • test-endpoints.R: each end of the bridge against the number its authors print (BSZ Table 5 row 1, the NBER mean NPV; NBER is the National Bureau of Economic Research, which circulates working papers), plus the earlier figures the code still checks: the headline of GGSS (the Galle, Gamage, Saez and Shanske expert report of July 2026, written in support of the measure), behind the BSZ analysis, and the March 2026 version of the JR analysis, where the bridge’s disputed-input steps end.
  • test-shapley.R: the Shapley steps sum exactly to the gap in every setting, and match a brute-force average over orders (a Shapley value: a step’s effect averaged over every order the inputs could be switched in, so the steps add up to the gap).
  • test-parity.R: the R exports are current, and the Python twin’s agree with them to 1e-9.
  • test-sources.R: every transcribed input has a label, a source and a page.
  • test-site.R: the overview page and the explorer are exactly the generator’s output, have no unfilled token or em-dash, show the key numbers and the two main estimates, the explorer’s data file carries the same two values, the switchboard’s all-off and all-on settings equal the two ends in every horizon setting, the explorer’s slider panels start and end at the two analyses’ estimates, site/explorer/model.js (run with node) returns the R value on every mix in slider_sample.csv to 1e-9, the explorer shows no gross revenue number, and every relative link resolves.

GitHub Actions rebuilds the exports in R and Python on a clean runner and runs the suite on every push that touches comparison/ or either analysis’s export contract (.github/workflows/comparison-ci.yml). It does not render the Quarto pages.

Scripts

file lines what it does
R/contract.R 48 reads the two export contracts and the document inputs, and nothing else
R/model.R 235 score_common(), the one estimating function; exact Shapley over all orders; one sequential order; Rauh’s Monte Carlo expectations
R/build.R 270 every number the page shows, written to export/r/*.csv
R/site.R 518 renders the overview page (index.html), the explorer (site/explorer/index.html) and their data file from site/index.template.html and site/explorer.template.html
py/bridge.py 405 the Python twin of contract.R, model.R and build.R; writes export/py/*.csv
run.R 16 builds all three: the R exports, the Python exports and the two pages
tools/digitize_hoopes_fig2.py 114 reads Hoopes (2026) Figure 2 off the chart into data/hoopes-fig2.csv
site/explorer/model.js 113 score_common() and the slider rule ported to JavaScript; the explorer’s panels run it in the browser (written by hand, checked against slider_sample.csv)
site/repro.qmd 673 the reproduction report
site/slides/slides.Rmd 700 the slide deck: what is in the report and where the main differences come from (xaringan; every number read from export/r/)
site/materials.qmd 258 this page

Data files the site publishes

file rows columns contents
inputs.csv 14 7 the disputed inputs: one row per bridge step, both analyses’ values and sources
bridge.csv 140 9 every step’s Shapley value and one-order value, for each output and horizon setting
ends.csv 14 6 the two ends of every bridge (BSZ, Jaros and Rauh NBER) for each output and horizon setting, and the model difference at the JR end (zero by construction, kept as a check)
endpoints.csv 6 7 each end against the number its authors print
anchors.csv 2 6 the two revenue numbers of the JR analysis’s September 2026 version, as candidates for where the steps end on output (a) (an anchor: the published number an end of the bridge is checked against)
loss.csv 6 8 every annual income tax loss on one scale, with present values
hoopes.csv 9 5 Hoopes (2026) Figure 2 beside the bridge read in his order
meta.csv 9 2 intermediate values the page quotes (annuity factors, the BSZ net terms)
switchboard.csv 8192 4 the common function’s value for every mix of switches (bit i of mask = disputed input i at the JR value), for each output and horizon setting
slider_sample.csv 600 13 300 slider mixes per explorer panel (positions 0 to 4 per input, 0 = the panel’s own analysis) and the panel’s value, the sample the tests run model.js on

loss.csv’s last column, net_at_nber_revenue_perpetuity, nets each loss in perpetuity against the JR analysis’s expected revenue ($36.0B, in ends.csv and anchors.csv); it is the last column of the comparison overview’s table of yearly income tax losses.

comparison/export/py/ holds the same files from the Python twin (without inputs.csv). The explorer reads assets/comparison-data.js, written by R/site.R from these files. Its two slider panels read the two input vectors from the same data file and compute each panel’s estimate in the browser with site/explorer/model.js, which follows slider_inputs() in R/model.R: positions 1 to 3 are evenly spaced between the two values, and the horizon steps through 5, 10, 20 and 50 years and a perpetuity. The inputs the comparison transcribes itself are in comparison/data/document-inputs.csv (18 rows, each with an origin label, a source and a page) and comparison/data/hoopes-fig2.csv (Hoopes’s Figure 2, read off the chart). None is edited by hand except these two transcriptions.

Sources

The comparison reads no third-party file at run time. Each analysis’s sources are listed on its own materials page (BSZ, JR). The two documents only the comparison uses are recorded in the manifest with where to get them and their SHA-256:

file provider obtained SHA-256 (first 12) terms
ssrn-6428578.pdf Jeffrey L. Hoopes (UNC Chapel Hill), “Galle v Rauh: Comparing Two Revenue Estimates for California Billionaire Taxation” public-download 2026-09-23 abbdf7e70192 do-not-redistribute
lao-prop40-110326.pdf California Legislative Analyst’s Office, Proposition 40 ballot analysis (November 3, 2026 ballot) public-download 2026-09-23 e6034bbba3e0 public-government-document (not committed per repo policy)

Full rows: comparison/DATA-SOURCES.csv. Neither PDF is redistributed.

The GGSS expert report (20 July 2026) and the GGSS response to Rauh (17 March 2026), which the bridge cites for the non-citizen residents and for the BSZ value “none applied” of the step “One-time tax estimated as a permanent 5 pp rate”, are listed in the BSZ analysis’s manifest, bsz-analysis/DATA-SOURCES.csv, with their URLs on Saez’s Berkeley page.

What the checks found

The full crosswalk of disputed inputs, with every page reference checked against the PDFs, is comparison/DISPUTES.md. The checks behind the bridge found:

  • The September 2026 ceiling has three values. The script draws revenue up to the literal 72; the README prints 72.06; the person-level file sums to 72.05 (the JR analysis’s mismatch 4). The comparison uses the script’s 72, so the common function returns the authors’ own mean exactly and the bridge has no model-difference step.
  • The litigation weight has no multiplier in the code. The README applies q = 0.50 to the recognized base; the script instead draws revenue uniformly between 0 and the ceiling, whose mean is the same half (mismatch 5). The comparison reads that mean as the weight q.
  • The printed NPV is one run, not the expectation. The exact expectation of the NBER Monte Carlo, the bridge’s JR end, is −$38.98B against a printed −$38.9B. The JR pipeline reproduces the seeded run as −$38.886B, the JR main estimate.
  • Both analyses’ discount factors are right. One reading magnifies the loss by a factor 67 (1/1.5%), BSZ say 33 (1/3%). The first is the rate the JR text anchors its range on, the second the mean rate of its Monte Carlo (DISPUTES.md, row 2).
  • Two March disputes are closed in the BSZ analysis by August: Larry Ellison’s exclusion and the non-US-citizen residents (rows 7 and 8).
  • One Hoopes bar could not be traced. His confirmed-departures bar matches no number in the March 2026 version of the JR analysis (see the overview page).