Skip to content

Cross-checking our IRV cases against rcv-lab.org

Level: reference · deep dive

One line: every ranked RCV-IRV case in this library was re-counted by an outside engine — rcv-lab.org — and 63 of 64 agreed; the one that didn't turned out to be a hidden tie in one of our own cases, which is exactly what a cross-check is for.

→ the platform: RCV Lab · sibling referees: pref_voting · BetterVoting · RCTab · tool: build_rcvlab_sankey.py.


Why bother

The Ranked Robin cases get counted three ways — this engine, BetterVoting, and pref_voting's independent Copeland — because a tally that only ever agrees with itself is not evidence. The RCV-IRV cases had no such leg: the vendored pyrankvote engine was the only thing counting them. This closes that gap.

What was run

All 64 ranked cases with an IRV method, converted to RCV Lab's generic CVR CSV and re-counted by its own engine. Nothing was sampled and nothing was skipped.

Cases counted 64
Winner agrees with our answer key 63
Disagrees 1 (a tie — see below)
Sankey diagrams exported 26

The one disagreement, and it was ours

coombs_ex20_district1 — we say B, RCV Lab says C. Neither engine is wrong. Round 1 is C 16, A 9, B 9, and that tie decides the election: eliminate A and its A>B>C ballots elect B 18–16; eliminate B and its B>C>A ballots elect C 25–9. Our engine drops A, RCV Lab drops B, and neither prints that it broke a tie.

The case's own description had asserted "IRV elects B here too" as a plain fact. It is a fact about our tiebreak, not about IRV, and the file now says so. The reinforcement paradox the case exists to demonstrate is untouched — District II and the amalgamated file have no tie — but the claim needed narrowing, and only a second engine was ever going to catch it.

The general lesson: a silent tiebreak reads exactly like a determinate result. It is worth re-reading any teaching page that leans on an IRV elimination order in a small hand-built electorate.

One difference that is not a disagreement

RCV Lab reports round counts on a different convention: it eliminates candidates one at a time where our engine clears a whole batch that cannot catch the next candidate up. On cycle_vote_on_the_rule_irv_c5_b999 it says four rounds and we say three, with every tally identical. Round count is a reporting convention, not a result — don't cite "it took N rounds" as a fact about an election without saying whose count.

The Sankey diagrams

The 26 cases that run to three or more rounds now carry a committed img/<stem>_sankey.svg beside the case, drawn by RCV Lab and exported as a self-contained SVG.

They earn their place because our engine prints rounds as a table, and a table cannot show where a transferred vote came from — which is the whole subject of every page here that argues about center squeeze or exhausted ballots. RCV Lab traces exact ballot provenance rather than estimating the ribbons, and it gives exhausted ballots their own column, labelled "No Further Rankings".

They are committed as static files, not linked: the repo does not control that site, it is openly a beta whose output may change, and a stored SVG renders on GitHub, on the built site, and in a local viewer with no network at all.

Reproducing it

python3 STARVote_LH_tabulation_engine/tools_adam/scripts/build_rcvlab_sankey.py emit --out /tmp/rcvlab

That writes one CVR CSV per case. Upload each at rcv-lab.org → Analysis → Ballot data (CVRs) → Tabulate → Visualize → Sankey, collect {stem, winners, rounds, svg} into a JSON array, then:

python3 STARVote_LH_tabulation_engine/tools_adam/scripts/build_rcvlab_sankey.py install results.json

which re-checks every winner against the YAML answer key and writes the art. Three things about that loop are worth knowing before repeating it, all of them silent failures:

  1. Their generic CSV eats the first column as a ballot ID. The docs say "one column per candidate" and mean starting at column two. A bare candidate header loses the first candidate and returns a confident wrong winner — our Post-it case came back "Green in 2 rounds" instead of Purple in 3. emit always writes a leading BallotID column; the site then reports "In CVR but not in config: BallotID", which is the sign it parsed correctly.
  2. The filename must contain cvr, or the site raises a confirm() that an automated browser suppresses, silently rejecting the file.
  3. The derived config is computed once and never refreshed. Load a second CVR without pressing Reset and it is scored against the first file's candidate list, silently producing an empty 0-round election.

Scope

Ranked ballots only. RCV Lab counts IRV, STV and bloc, plus plurality/approval/cumulative for comparison — no STAR, no Ranked Robin. It can never check the methods this library is mostly about; for those, pref_voting and BetterVoting remain the referees.

The full ledger

Counted 2026-08-08. ✅ = winner matches our expected_winners.

Case (page) Rounds Ours RCV Lab Art src
ballot_expressiveness_c9_irv_top5 8 Ben Ben ✅ — .yaml
bv2280_37yf8x_irv_full 8 Ben Ben ✅ Sankey .yaml
crowded_field_c7_irv 6 Clara Clara ✅ Sankey .yaml
burlington_2009_irv 5 Kiss Kiss ✅ Sankey .yaml
burlington_2009_raise_kiss_nonmono 5 Montroll Montroll ✅ Sankey .yaml
street_trees_five_rounds_c6_b100 5 Birch Birch ✅ Sankey .yaml
bv2138_cxrf8v_irv 4 Dave Dave ✅ Sankey .yaml
bv2183_dfw8rj_forced_exhaustion_ceiling 4 Ada Ada ✅ Sankey .yaml
bv2278_8cdkkc_five_way_irv 4 A A ✅ Sankey .yaml
bv2281_qycpbx_ossipoff_irv 4 D D ✅ Sankey .yaml
crowded_field_c5_irv 4 Elsa Elsa ✅ Sankey .yaml
cycle_vote_on_the_rule_irv_c5_b999 4 Ranked Pairs Ranked Pairs ✅ Sankey .yaml
batch_all_out_round2_c4_b6 3 Alex Alex ✅ Sankey .yaml
bv2133_dyxrbr_pet2_irv 3 Fish Fish ✅ Sankey .yaml
bv2162_4htk44_irv 3 A A ✅ Sankey .yaml
bv2163_74j6vv_irv 3 B B ✅ Sankey .yaml
bv2170_pp2q4q_irv 3 Avery Avery ✅ Sankey .yaml
bv2176_p8dp28_irv 3 Purple Purple ✅ Sankey .yaml
bv2178_8kg698_irv 3 Blue Blue ✅ Sankey .yaml
bv2277_tqfdbg_mayor_irv 3 Cora Cora ✅ Sankey .yaml
bv2282_hf3ckp_brams_irv 3 B B ✅ Sankey .yaml
coombs_ex18_monotonicity 3 Arlo Arlo ✅ Sankey .yaml
coombs_ex21_twin_after 3 B B ✅ Sankey .yaml
coombs_ex21_twin_before 3 B B ✅ Sankey .yaml
coombs_ex22_scc 3 A A ✅ Sankey .yaml
irv_combined 3 A A ✅ Sankey .yaml
put_two_universes_c3_b4 3 Anna Anna ✅ Sankey .yaml
alaska_buried_c3_b200 2 Peltola Peltola ✅ — .yaml
alaska_sincere_c3_b200 2 Peltola Peltola ✅ — .yaml
alaska_upward_after 2 Begich Begich ✅ — .yaml
alaska_upward_before 2 Peltola Peltola ✅ — .yaml
balance_base_irv_c3_b9 2 Bruno Bruno ✅ — .yaml
balance_plus_opposite_c3_b15 2 Ada Ada ✅ — .yaml
batch_all_out_condorcet_c3_b3 2 Amy Amy ✅ — .yaml
batch_all_out_cycle_c3_b3 2 Amy Amy ✅ — .yaml
bv2132_ykjjhy_pet_irv 2 Fish Fish ✅ — .yaml
bv2137_ywckmg_irv 2 Carter Carter ✅ — .yaml
bv2145_6fj2kg_irv 2 Ben Ben ✅ — .yaml
bv2146_krk2px_irv 2 Ada Ada ✅ — .yaml
bv2147_9gdrqg_irv 2 Bruno Bruno ✅ — .yaml
bv2149_byk9v2_irv 2 Alma Alma ✅ — .yaml
bv2150_dxg8pb_irv 2 Carl Carl ✅ — .yaml
bv2151_97hbpw_irv 2 Beth Beth ✅ — .yaml
bv2154_wq6yv7_irv 2 Clara Clara ✅ — .yaml
bv2222_rfyk46_510_thin_irv 2 Ana Ana ✅ — .yaml
bv2223_dyh93j_510_real_irv 2 Ana Ana ✅ — .yaml
bv2227_3xgkck_honest_irv 2 Right Right ✅ — .yaml
bv2228_bgcmxx_betray_irv 2 Center Center ✅ — .yaml
condorcet_1788_irv 2 Peter Peter ✅ — .yaml
coombs_ex20_amalgamated 2 B B ✅ — .yaml
coombs_ex20_district1 2 B C ⚠️ — .yaml
irv_district_A 2 B B ✅ — .yaml
irv_district_B 2 B B ✅ — .yaml
margins_irv 2 Cocoa Cocoa ✅ — .yaml
omr_opposition_decides 2 B B ✅ — .yaml
reversal_irv_original 2 A A ✅ — .yaml
reversal_irv_reversed 2 A A ✅ — .yaml
sf_d7_downward_after 2 Engardio Engardio ✅ — .yaml
sf_d7_downward_before 2 Melgar Melgar ✅ — .yaml
tilted_cycle_c3_b5_irv 2 Cara Cara ✅ — .yaml
bv2148_h87k6v_irv 1 Bruno Bruno ✅ — .yaml
bv2153_pcttmr_irv 1 Amos Amos ✅ — .yaml
coombs_ex20_district2 1 B B ✅ — .yaml
crowded_field_c3_irv 1 Diego Diego ✅ — .yaml

Up: tabulation engines · 07_Concepts · related: RCV Lab · RCTab · cross-checking with pref_voting.