Skip to content

06_Other/ballot_style_lab — random-but-human electorates (the style lab)

What happens when you fill a whole election with the ballot styles real people actually use — bullet votes, partisan slates, harsh graders who top out at 2, gentle souls who never score below 3, cliff voters who jump straight from 0 to "one of mine"? This folder is a seeded generator plus ten frozen elections it found — six single-winner STAR, plus a multi-winner wing (Bloc STAR and STAR-PR, up to 7 candidates and 4 seats): random enough to surprise, human enough to mean something, deterministic enough to be test cases.

→ The style taxonomy these voters follow: Filling Out the 5-Star Ballot — Voting Styles · the methodology this generator obeys: Simulate utilities, not ballots · one voter's opinion across ballot formats: Alternate ballot styles


The idea — opinion first, expression second

Pure-uniform random ballots correspond to no electorate on Earth, and the methodology page explains why they rig method comparisons. So this generator injects randomness in two human-shaped layers:

  1. Opinion (layer 1): each voter's underlying 0–1 utilities are sampled from a faction model — slanted camps with a shared lean, personal noise on top, plus a few genuinely noisy voters.
  2. Expression (layer 2): those utilities are then rendered through the voter's ballot style — the legal styles from the style gallery (bullet, backups, slate, ranked-style, nuanced, anyone-but, protest) plus deliberately compressed scale habits (0–2 harsh, 3–5 gentle, 0-or-3–5 cliff, 3–4 sliver, flat lines, pure noise).

Same opinion, different rendering — the same-opinion line-up idea, scaled up to a whole electorate. Per the methodology page, this sits in the honest "stress-test the tabulator" job (not a method-welfare comparison): the ballots exist to exercise the count, and the human shaping makes the exercises look like elections instead of dice.

Full disclosure — the seeds are hunted. The generator tabulates hundreds of seeds with the real LH engine and keeps the interesting draws: runoff reversals, Condorcet cycles, tie rungs, photo finishes, Equal-Support blowouts. A typical random seed is boring; these six are champions selected across 250 seeds each, then frozen. That selection bias is the point of a test fixture — and it's disclosed here and inside every file.

The frozen elections — single-winner wing

Every file records its scenario + seed and regenerates byte-identically (--emit <scenario>); expected_winners is embedded, and the single-winner files run in the repo's positive test suite (the multi-winner files are engine-verified on emission — the suite is single-winner-scoped by design). All cases use 24–47 individual ballots and fresh casts per the naming rules. Files 1–6 are single-winner STAR; the multi-winner wing below covers Bloc STAR and Allocated Score / STAR-PR at three field sizes (5c/3 seats, 6c/2 seats, 7c/4 seats).

# Case The electorate What the frozen seed produced Source
1 The Graders' Divide 15 harsh graders (0–2) vs 16 gentle souls (3–5) — two grading cultures, zero overlap Totals bunch at 80–80–78; the top two tie dead even and Clara wins the runoff 13–11. No Condorcet winner; Plurality & RCV-IRV say Bruno, Approval-thinking says Abby — four counting philosophies, three different "winners" from 31 ballots yaml · report
2 Cliff City 40 food-truck voters, all cliff/slate ballots — the 1–2 middle of the scale is a ghost town Scoring 100–99; Churro beats Bao 15–14 in the runoff (11 Equal Support) and is the Condorcet winner; Plurality and RCV-IRV both pick Bao — the engine flags the center-squeeze signature yaml · report
3 Bullet Storm Three bullet-voting brigades (many leave blanks, as real bulleters do) + a thoughtful few who spread scores The runoff ties 13–13 (7 Equal Support — a whole brigade went silent) and is broken by STAR's official next tiebreaker: higher scoring total → Carla. Plurality & RCV-IRV say Astrid yaml · report
4 Noise Soup 47 messy ballots: weak leans, cross-winds, flat-liners, pure noise, a race abstention ~, a spoiled ?, stray blanks A genuine Condorcet cycle out of noise; scoring leader Beth loses the runoff to Caleb 18–16 (13 Equal Support); Plurality says Aaron, RCV-IRV says Beth. Markers all tabulate as 0 and are reported honestly yaml · report
5 Does the squeeze survive noise? The center-squeeze profile rebuilt from noisy utilities + mixed styles (nuanced, ranked-style, backups, harsh, slate, gentle) Yes. Consensus-Ben trails the scoring round, wins the runoff 19–18, and is the Condorcet winner; RCV-IRV eliminates him and elects Cora — the tidy classroom demos aren't cherry-picked, the squeeze survives real-world mess yaml · report
6 Narrow Bands 24 paint-swatch voters and nobody uses the whole ballot: 0–2 camp, 3–5 camp, cliff & sliver voters, flat-liners, one protest Compressed totals produce a three-way scoring tie 61–61–61; the official head-to-head tiebreaker picks the finalists, the runoff lands 9–8, and there's no Condorcet winner. Plurality, RCV-IRV and Approval-thinking all say Azure; STAR says Beige, Ranked Robin says Coral yaml · report

Voter counts here (24–47) deliberately sit above the repo's keep-it-small default (choosing voter counts): statistical style patterns need a crowd, and 20–50 is still small enough to eyeball every row.

The frozen elections — multi-winner wing

Bloc STAR (single-winner STAR run once per seat — a majority can sweep) and Allocated Score / STAR-PR (each Hare quota fills one seat, spent ballots reweighted), at deliberately different field sizes. The headline is the twin pair: files 07a/07b tabulate the identical 36 ballots under both philosophies and seat different councils.

# Case Field What the frozen seed produced Source
7a Herb Garden Council — Bloc 5 cand · 3 seats · 36 ballots The kitchen bloc sweeps all three seats — and the sweep hangs by a thread: the final seat's runoff ties, and the official tiebreaker settles it on total scores Dill 83 vs Anise 82. One star of score decides seat #3 yaml · report
7b Herb Garden Council — STAR-PR same 5 cand · 3 seats · the identical 36 ballots The tea caucus's quota seats Anise; kitchen keeps two (final reweighted round Chive 35.58 vs Dill 33.38). Same rows, different philosophy, different council — the majoritarian-vs-proportional lesson as a live pair yaml · report
8 Quota Circus 6 cand · 2 seats · 29 ballots The provocation: under proportional STAR-PR the north side sweeps both seats (final round Bree 39.25 vs Cato 36.94) — because the south's gentle 3–5 ballots donate floor-3s to their rivals. Grade inflation is unilateral disarmament, quota edition yaml · report
9 Replant the Park 7 cand · 4 seats · 44 ballots Four consecutive STAR counts (margins 4, 7, 4, 0): the shade trio sweeps seats 1–3, then the evergreen duo tie for the last chair — Cedar beats Fir on the score rung 110–101. Deepest Bloc exercise in the repo yaml · report

The multi-winner files need no display options — the engine switches the finalists matrix off for multi-winner races automatically (a "Top 2 Finalist" matrix is a single-winner concept). The hand-built companion set for the sweep-vs-share lesson is 03_STAR_PR/_main/02a–02c; the style lab adds realistic style mixing, individual rows, and the tie drama.

A taste — the Graders' Divide on screen

Two camps that never touch each other's half of the scale. Look at the Score Distribution: Abby's column is nothing but 2s and 3s (all 31 voters!), Bruno's is nothing but 5s and 0s — and the totals land 80–80–78:

[Divergence from STAR]
  STAR                   = Clara
  Choose-One (Plurality) = Bruno   (differs from STAR)
  RCV-IRV                = Bruno   (differs from STAR)
  Approval               = Abby   (differs from STAR)
  Note: 9 of 31 ballots (29%) had equal non-zero scores, so their ranks were
        decided by candidate priority order. The RCV-IRV result may be an
        artifact of score-to-rank tie-breaking rather than a deep
        difference.
  Note: Ranked Robin (RCV-RR) agrees with STAR, so RCV-IRV is the lone
        outlier — the classic center-squeeze signature.
  Full round-by-round reports (generated for review):
  RCV-IRV rounds: cases_tabulated/01_c3_b31_graders-divide_RCV-IRV_tabulated.txt

--- STAR Voting Method (single winner) ---

[STAR Voting]
 Tabulating 31 ballots.
Count × Abby,Bruno,Clara
   12 ×    2,    0,    1
    8 ×    3,    5,    4
    5 ×    3,    5,    5
    3 ×    3,    5,    3
    2 ×    2,    0,    0
    1 ×    2,    0,    2

[STAR Voting: Scoring Round]
 The two highest-scoring candidates advance to the next round.
   Bruno         -- 80 -- First place
   Clara         -- 80 -- Second place
   Abby          -- 78
 Bruno and Clara advance.

[STAR Voting: Automatic Runoff Round]
 The candidate preferred in the most head-to-head matchups wins.
   Clara         -- 13 -- First place
   Bruno         -- 11
   Equal Support --  7
 Clara wins.
   Runoff math:
     31  ballots cast
   −  7  Equal Support (no preference between the two finalists)
     ──
     24  voters with a preference  (majority = 13)
           Clara 13 (54%)  ·  Bruno 11 (46%)

[STAR Voting: Winner — STAR Voting Method (single winner)]
 Clara

The Scoring Round can't tell a harsh 2 from a gentle enthusiasm — but the Automatic Runoff only asks which finalist each ballot scored higher, and on that question the grading cultures cancel out. Full detail (matrix, divergence block, the Condorcet wobble): the _tabulated report.

The style menu

Renderings implemented by the generator (utilities → 0–5 scores). Gallery styles carry the style-gallery names; band styles are the lab's additions.

Style Renders as From
nuanced honest min-max onto the full 0–5, ties kept gallery
bullet favorite 5, everyone else 0/blank gallery (Traditional)
strong_backup / weak_backup 5 plus a 4 (or a grudging 1) for the runner-up gallery
slate equal 5s for the in-group, 0 outside gallery (Partisan slate)
ranked_style each score used once, 5 downward, like a ranking gallery
anyone_but 5 for everyone except the villain (0) gallery
protest all zeros plus a lone least-bad 1 gallery
harsh everything squeezed into 0–2 band
gentle everything squeezed into 3–5, zeros never band
cliff35 / cliff34 0 or 3–5 (or 3–4) — nothing in between band
flat the same score for everyone (legal no-preference ballot) band
chaos uniform noise — the fuzzing voter the methodology page blesses band

Realism garnishes: bullet/backup/slate voters flip a coin between writing 0s and leaving real-world blanks (-); Noise Soup adds a race abstention row (~), a spoiled row (?), and stray blank cells — all tabulate as 0 and are reported honestly.

Running the lab

python generate_ballot_styles.py --list                 # scenario menu + frozen seeds
python generate_ballot_styles.py --emit all             # regenerate all ten files byte-identically
python generate_ballot_styles.py --hunt cliff_city --seeds 250   # go gem-hunting yourself

The hunter tabulates every seed with the real LH engine and scores it for: runoff reversals, no-Condorcet/cycles, RCV-IRV divergence (and the engine's center-squeeze note), Equal-Support share, runoff margins and tie rungs, finalist-line photo finishes, and lot-decided anything. Multi-winner scenarios add slate-aware signals — sweeps, shutouts, dark-horse seats, per-seat margins, final-seat ties, reweighted seat-round gaps — and a twin-pair mode that tabulates each seed under two methods (Bloc vs PR) and scores the seat difference. Champions must also satisfy the scenario's target predicate and resolve deterministically (a lot-decided result can't be a frozen test case; the hunter rejects those seeds). To add a scenario: give it factions, a style mix, a fresh cast (naming rules), optional slates/variants, hunt, freeze the seed, --emit.

Stdlib-only; runs with the repo .venv (or any Python 3) and calls starvote_larry_hastings.py for every tabulation — the engine is the oracle, the generator never reimplements the count.

file: README.md