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:
- 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.
emitalways writes a leadingBallotIDcolumn; the site then reports "In CVR but not in config: BallotID", which is the sign it parsed correctly. - The filename must contain
cvr, or the site raises aconfirm()that an automated browser suppresses, silently rejecting the file. - 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.
Up: tabulation engines · 07_Concepts · related: RCV Lab · RCTab · cross-checking with pref_voting.