Methods
How the scores are built
Written so a technically literate skeptic can rerun the pipeline and match every published number. The estimator does not know who built the site.
Reproduce this
Three commands from a clean machine. data/votes.csv is the adjudication input; vact export-web recomputes scores, empirical Bayes hyperparameters, and this page's worked example. No number on the scorecard is stored in the warehouse.1
git clone https://github.com/EAName/Congressional-Tracker.git
cd Congressional-Tracker && uv sync
./bin/vact votes validate && ./bin/vact audit symmetry && ./bin/vact export-web1. Vote selection
The measured axis is small business climate: Positive = a YEA advances the small-business / affordability axis (lower input costs, easier access to capital, lighter compliance burden). Direction is not inferred from party. It is a per-vote valence: does a YEA advance that axis (+1) or oppose it (−1)?
A roll call enters a score only if all of the following hold:
vote_categoryis in AMENDMENT, PASSAGE. Currently excluded: CLOTURE, MOTION_TO_RECOMMIT, NOMINATION, PROCEDURAL, SUSPENSION.- An adjudicated valence of ±1 exists for that
(vote_id, impact_tag). Un-adjudicated pairs (no valence row, or valence 0) are dropped. RULE-proposed valence is a proposal until a human promotes it. - Special-rule resolutions (“Providing for consideration of …”) are excluded even when they land in PASSAGE. They set debate terms; they are not a policy position on the underlying bill.
“Procedural” here means the excluded categories plus those special-rule resolutions. It is a category filter, not a judgment that the vote was unimportant.
Adjudication lives in the public file data/votes.csv (unique key member_bioguide_id, rollcall_id, theme). Valence was exported from fact_vote_valence; the CSV is what scoring reads at runtime. Who adjudicated is the adjudicator column (HUMAN / RULE / LLM). Join key across the system is bioguide_id only. Corrections policy.
2. Scoring
Let n be the member's contested votes (YEA or NAY) on a theme, and k the count that advanced the axis. Absences (NOT_VOTING, PRESENT) do not enter k or n; they are reported separately as an absence rate. A cell is labeled sufficient when n ≥ 3.
Cosponsorship is a second signal, not a second vote. Among adjudicated bills a member sponsored or cosponsored, k/n is the share that advance the axis, shrunk with the same beta-binomial estimator. Silence is not a Nay. Cosponsorship is cheap talk relative to a floor vote, so it is drawn as a hollow marker on the forest plot and is never averaged into the headline score.
p_raw = k / n
raw_score = 2 p_raw − 1 ∈ [−1, +1]
Wilson band = 2 · Wilson(k, n; z=1.96) − 1Vote depths are small, so the published default is a beta-binomial empirical Bayes estimate, fit separately per (theme, party).2 Method: moments. Caucuses with fewer than 3 members who cast a contested vote use Beta(2, 2) and are flagged weakly_informative. Unanimous caucuses use a pooled prior with half-pseudocounts (degenerate) so the prior mean stays at the caucus edge instead of 0.5.
prior Beta(α, β) from the caucus (k_i, n_i)
posterior Beta(α + k, β + n − k)
eb_score = 2 · (α+k) / (α+β+n) − 1
cred band = 2 · Beta-quantile_{0.025, 0.975} − 1Hyperparameters in this build
These α, β values are written at export time from the live frame. They will change when data/votes.csv changes.
| Theme | Party | α | β | Source | EB center | Members |
|---|---|---|---|---|---|---|
| Access to capital | Democrat | 5.500 | 0.500 | degenerate | +0.83 | 5 |
| Access to capital | Republican | 0.500 | 5.500 | degenerate | -0.83 | 5 |
| Federal contracting | Democrat | 2.000 | 2.000 | weakly_informative | +0.00 | 6 |
| Federal contracting | Republican | 5.500 | 0.500 | degenerate | +0.83 | 5 |
| Health costs | Democrat | 6.500 | 0.500 | degenerate | +0.86 | 6 |
| Health costs | Republican | 0.500 | 5.500 | degenerate | -0.83 | 5 |
| Input costs | Democrat | 0.118 | 2.715 | moments | -0.92 | 6 |
| Input costs | Republican | 20.500 | 0.500 | degenerate | +0.95 | 5 |
| Compliance and reporting | Democrat | 0.411 | 6.007 | moments | -0.87 | 6 |
| Compliance and reporting | Republican | 22.702 | 1.098 | moments | +0.91 | 5 |
| Taxes and credits | Democrat | 1.260 | 0.540 | moments | +0.40 | 5 |
| Taxes and credits | Republican | 10.500 | 0.500 | degenerate | +0.91 | 5 |
| Workforce | Democrat | 0.500 | 12.500 | degenerate | -0.92 | 6 |
| Workforce | Republican | 10.500 | 0.500 | degenerate | +0.91 | 5 |
Worked example
Eugene Simon Vindman (Democrat, VA-7), theme Input costs. This cell had the largest |EB − raw| among sufficient cells in the current export.
- Counts: k = 1 advancing votes out of n = 4 contested, so p_raw = 1/4 = 0.2500.
- Raw signed score: 2 × 0.2500 − 1 = -0.50. Wilson 95% band [-0.91, +0.40].
- Caucus prior: Beta(0.1181, 2.7153), source
moments, mean 0.0417 (signed -0.92). - Posterior: Beta(1.1181, 5.7153) = Beta(0.1181+1, 2.7153+3). Mean 0.1636.
- Shrunk signed score: 2 × 0.1636 − 1 = -0.67. Credible 95% band [-0.99, -0.01]. Shrinkage -0.17 from the raw point.
3. Uncertainty
The 95% credible interval is the central 95% of the member's posterior Beta on the signed scale, given the fitted (theme, party) prior and that member's k, n. It is not a frequentist confidence interval, and it is not a statement about sampling from a superpopulation of bills. If the prior is wrong, the interval is wrong in the same direction.3
Interval overlap on the compare module is a visual heuristic. Non-overlap is not a hypothesis test, does not control Type I error, and is not adjusted for the number of pairwise looks on the page.
How much vote depth is needed to reliably separate two members 0.25 apart on the signed scale? Simulation, not a slogan: two independent binomials with true signed scores +0.00 and +0.25 (p = 0.5 and 0.6250), equal n, 95% credible intervals, 2000 draws per n, 80% power defined as P(intervals disjoint).
- Under the weakly-informative fallback Beta(2, 2): no n ≤ 400 reached 80% power (power at n=400 was 0.779; shrinkage toward a shared prior makes a 0.25 gap harder to declare than a Wilson calculation suggests).
- Under this build's worked-example prior Beta(0.118, 2.715),
moments: no n ≤ 400 reached 80% power (power at n=400 was 0.779). A peaked caucus prior can demand more votes to declare a 0.25 gap, because both members are pulled toward the same center.
Most published cells in this tracker have n in the single digits. A 0.25 gap is generally not identifiable at current depth. That is why the bands are the claim, not the point.
4. Symmetry audit and falsification
Downstream scoring is party-blind arithmetic. Bias can enter when roll calls are chosen or when axis direction is coded. This build pre-registers inclusion in VOTE_INCLUSION_SPEC.md and publishes every excluded pair in data/votes_excluded.csv.
Spec version vote-inclusion-v1.0. Blind-coded share: 100% of roll-call × theme units not blind-coded. Any tripped threshold is a signal to re-open adjudication, not proof of bias.
Excluded roll calls by reason
| Reason | Count |
|---|---|
NEAR_UNANIMOUS | 1 |
NO_IMPACT_TAG | 338 |
PROCEDURAL_CATEGORY | 1067 |
RULE_RESOLUTION | 63 |
UNADJUDICATED_DIRECTION | 33 |
Caucus majority advancing the coded axis
| Theme | Dem share | Rep share | Gap (pp) |
|---|---|---|---|
| Access to capital | 100% | 0% | 100 |
| Federal contracting | 0% | 100% | -100 |
| Health costs | 100% | 0% | 100 |
| Input costs | 0% | 100% | -100 |
| Compliance and reporting | 0% | 92% | -92.3 |
| Taxes and credits | 50% | 100% | -50 |
| Workforce | 0% | 100% | -100 |
Falsification thresholds
caucus_advancing_gap: >|threshold| percentage-point gap between parties' caucus-majority advancing share on the same theme — tripped in this build. Re-open adjudication for themes that trip; log reason in this spec's changelog.n_depth_median_gap(threshold 2): >|threshold| vote-depth median between parties — ok. Audit scoreable filter and theme assignment for thin caucus evidence.ci_width_gap_at_matched_n(threshold 0.08): >|threshold| credible-interval width gap at the same n — tripped in this build. Investigate estimator parity (should be party-blind arithmetic).exclusion_rate_gap_pp(threshold 10): >|threshold| pp gap in exclusion rate by sponsor party leadership — tripped in this build. Review votes_excluded.csv and inclusion keywords.coded_blind_false_share_pp(threshold 5): >|threshold| of adjudications not blind-coded — tripped in this build. Route new rows through valence_review_queue before commit.
5. Historical challenger records
Challengers with prior House service (Elaine Luria, 116th–117th; Tom Perriello, 111th) are scored with the same pipeline as incumbents: identical themes, adjudication schema, and empirical Bayes shrinkage. Shannon Taylor has no federal voting record; VA-1 shows incumbent-only comparison.
Historical rows carry a congress_era tag. Caucus priors for shrinkage are fit from the Democratic caucus in that Congress, not from the current Virginia delegation. Head-to-head modules always show this caption (it cannot be disabled):
Scored on votes from the [era] Congress; themes matched by adjudication, not identical bills. Cross-era comparison is indicative, not exact.
Review queues: data/historical_rollcall_review.csv (roll-call theme proposals) and data/votes_historical_candidates.csv (full caucus member rows). Run vact historical propose after backfilling 111th / 116th / 117th House roll calls.
6. Known limitations
- Small n. Sufficiency is n ≥ 3. Several themes still sit on the weakly-informative or degenerate prior because the caucus sample is thin or unanimous.
- Theme coverage. Only impact tags with adjudicated valence appear. A member can look extreme on a theme that has two votes and ordinary on one that has twelve.
- No consequence weights. A minibus and a narrow amendment count the same. Bill importance is not in the likelihood.
- Single-state scope. The scorecard is the Virginia delegation. Senate roll calls include 100 members in the warehouse; scoring still joins VA legislators only.
- Authorship. Operators build and maintain the pipeline; see the About page. Valence is a political judgment. The math above is the estimator, not a claim of neutrality about which bills belong on the axis.
- Map version. District attributes are keyed to map version 2021 unless an export says otherwise. Mixing 2021 voting geography with 2026 targeting attributes votes to the wrong electorate.
7. Changelog
Auto-generated from git log of data/votes.csv, src/vact/analysis/scoring.py, src/vact/analysis/estimators.py, and config/scoring.yaml.
| Date | Commit | Subject |
|---|---|---|
| 2026-08-19 | 828fd34 | Add challenger historical scoring and head-to-head race module. |
| 2026-08-19 | 1b3ba9c | Add methodology, time series, IRT, and cosponsorship surfaces. |
| 2026-08-19 | 343d3dd | Add per-caucus empirical Bayes shrinkage for signed scores. |
| 2026-08-19 | 348b066 | Add versioned votes.csv as the scoring adjudication layer. |
| 2026-08-04 | a8f82e8 | Add signed scoring frame and within-party deviation report. |
Notes
- Signed scores are never written to DuckDB. Only valence (the political input) is persisted. See AGENTS.md §8.
- A single national prior would shrink Democrats toward Republicans. The (theme, party) cell is the grouping on purpose.
- Equal-tailed posterior quantiles, mapped through 2x−1. They are not highest-density intervals, and they inherit whatever misfit the method-of-moments (or MLE) prior has.