Analyzing the 2025 SC election

"Happiness”, round 4

Back to the measure the bot and I co-created. It you don’t like the name “happiness”, that’s fine. It’s related in some way to satisfaction with the order in which the election method picked winners, but I wouldn’t claim it measures “actual human happiness” to even one significant digit.

[EDIT: rewrote for more rigor]
What it does is give one clean way of measuring how close the structure a ballot expresses is to the outcome actually seen. Where C is the number of candidates, and W the number of winners, it’s the L^1 (“Manhattan” or “taxicab”) distance between a point in C-dimensional “outcome space” to the closest point in a hyperrectangle (“box” for short) in C-dimensional “ballot space”, provided the point isn’t within the box. Coordinates in ballot space span 1 through C. Outcome space assigns ranks 1..W to the winners, and rank W+1 to all non‑winners. So it’s usually rare that an outcome is within a ballot’s ideal box.

As explained briefly before, a ballot defines an “ideal interval” structure, which is independent of W. For each candidate, this gives the range of result ranks the ballot is perfectly happy with.

Let’s pick a random ballot with 3 equivalence classes, and see what this structure is:

>>> random.seed(789)
>>> while len(distinct(e := random.choice(ballots))) != 3:
...     pass
...
>>> pprint(e)
{'Barry Warsaw': 5,
 'Donghee Na': 3,
 'Gregory P. Smith': 5,
 'Pablo Galindo Salgado': 4,
 'Savannah Ostrowski': 3,
 'Thomas Wouters': 5}
>>> w = Ballot(e)
>>> pprint(w.ideal_interval)
{'Barry Warsaw': (1, 3),
 'Donghee Na': (5, 6),
 'Gregory P. Smith': (1, 3),
 'Pablo Galindo Salgado': (4, 4),
 'Savannah Ostrowski': (5, 6),
 'Thomas Wouters': (1, 3)}

The ballot gave 5 stars to 3 candidates, so it’s saying it’s happiest if they appear at ranks in (1, 3) inclusive, but has no preference among their order, Its sole 4-star rating then belongs at rank 4. The remaining 2, with 3 stars each, are best at ranks 5 and 6. For any ballot, any two ideal intervals are either the same, or disjoint, and all together span all values in 1..C. The ideal box is then just the Cartesian product of the candidates’ ideal intervals.

The ideal intervals define the ballot’s ideal C-dimensional box. A ballot is “perfectly happy” if and only if an outcome is within its ideal box (every candidate’s actual rank is within the ballot’s ideal interval for that candidate).

What does an outcome deliver? A total ranking from 1 through W, and effectively an equivalence class containing all those who didn’t win a seat. For convenience and simplicity, the code just calls that (the fictional) rank W+1. “The math” stays simple then, and the distance measure quite smooth and gradual across small changes in outcome orders. It’s a point in C-dimensional space, but with coordinates from a smaller range than in a ballot’s ideal box.

The heart of the “happiness measure” then is actually an “unhappiness” measure: how far removed from the outcome is the ballot’s ideal interval structure?

def winner_displacement(winner_seq, ideal_interval):
    total = 0
    cand2rank = {c : i for i, c in enumerate(winner_seq, start=1)}
    last_rank = len(cand2rank) + 1
    for c in scands:
        if c not in cand2rank:
            cand2rank[c] = last_rank
    for c, actual_rank in cand2rank.items():
        s, e = ideal_interval[c]
        if actual_rank < s:
            total += s - actual_rank
        elif actual_rank > e:
            total += actual_rank - e
    return total

It’s just summing the distances of the actual outcome’s ranks from the closest endpoint of a candidate’s ideal interval (or adding nothing if the actual rank is in a candidate’s ideal interval).

A very desirable property of this measure is that a ballot’s maximum possible displacement can be computed in advance quickly, linear in C. Some earlier abandoned measures required enumerating all possible perm(C, W) outcomes to find the max. For our example:

>>> w.max_disp
14

So it just remains to divide the actual displacement by 14, and subtract that from 1.0.

Ha! It took a long time to realize this: it’s purely an accident of C=6 and W=5 that a displacement of 0 is always possible. In general, there’s also a minimum possible displacement.

>>> w.min_disp
0

[EDIT: repaired bad example]
For example, consider a 10-candidate election with 2 winners. If a ballot gives its highest rating to 3 candidates, it’s not possible for any outcome to have total displacement 0. The ideal interval for the three favorites is (1, 3)., and for all the other 7 candidates their ideal interval starts with at least rank 4. Each is displaced by at least one from the outcome’s catch-all “didn’t win” sentinel rank of 2+1 = 3.

But it’s also possible to compute the smallest possible displacement cheaply in advance, and rescaling to fit the actual possible displacement range is straightforward:

    def happiness(self, winners):
        if len(winners) != self.nseats:
            raise ValueError(
                f"Expected {self.nseats} winners, got {len(winners)}")
        actual_disp = winner_displacement(winners, self.ideal_interval)
        self.actual_disp = actual_disp
        lo, hi = self.min_disp, self.max_disp
        assert lo <= actual_disp <= hi
        span = hi - lo
        result = (1.0 - (actual_disp - lo) / span) if span else 1.0
        assert 0.0 <= result <= 1.0
        return result

These min/max bounds are sharp: achieved by some (at least one) possible outcome.

I won’t show more code unless there’s demand for that.

While the bot is a whiz at deducing things from decile accounts, I’m not, and doubt many people are. So instead here’s just a plot of the value distribution (after multiplying by 100.

It’s possible again to compute total happiness across all ballots across all 720 possible outcomes. In a small fraction of a second, in fact. The outcome STAR delivered came in 4th place then, although there’s very slight difference in rearranging the order of the last few candidates picked.

Note that this is akin to the “parallel” versions of Jefferson and Webster proportional Block Approval: don’t do “one at a time”, optimize a function over all outcomes. Ties are almost impossible then.

The measure developed here could also be used to check results from Condorcet-method elections (whether or not they allow expressing equal preferences), IRV/“Ranked Choice” elections, and even pure-score elections.

And more! The bot and I just played with that idea. It even applies to plain old miserable plurality (“pick one!”) elections. In those, the ideal interval ranges are (1,1) for the candidate you voted for, and (2, number_of_candidates) for everyone else. Minimum possible displacement is 0 (if who you voted for won), and maximum possible is 2 (if who you voted for didn’t win). So the outcome that maximizes total happiness is simply to pick whoever got the most votes.

It’s a Good Sign™ when a measure works gracefully for kinds of degeneracy that weren’t anticipated. Means it most likely hit on something truly fundamental about structures ballots can express (in various systems) and how they relate to outcomes.

Without details, I’ll also note that it makes short work of exposing Fishburn’s infamous “Condorcet killer” contrived election:

https://rangevoting.org/FishburnAntiC.html

Condorcet picks X there, despite that it’s all but dead obvious to everyone that Y should have won. Alas, STAR also picks X. Y swamps X by score, but they both make it into the runoff, where X ekes out a tiny 51-50 preference win. Well, talking about an extension to STAR that allows 8 stars, to capture the total ordering among the 9 candidates there.

By the measure here, “Y first, then A” stands alone as the clearly best possible 2-winner outcome. Because this measure isn’t “a sequential election rule”, it’s a neutral measure of how well the geometries of ballots and outcomes align. The best outcome picking X first just barely makes it into the top 20 (of 9*8 = 72 possible) outcomes.

I was asked:

What happens if a ballot gives everyone the same rank? Doesn't "the math" break down then?

It’s good to question! Happily, L^1 norms are easy to reason about, and nothing goes wrong here.

Intuitively, such a ballot expresses no preferences, so should be perfectly happy with any outcome.

And “the math” models that. Geometrically, the ideal box holds all points in 1..C across all C dimensions. No outcome can live outside that box. No matter who wins, or how many winners there are, they’ll each get some rank in 1..W, and W<C in any real election, so the “didn’t win” W+1 pseudo-rank also lives in each candidate’s ideal interval.

Total displacement is 0 regardless of who wins, or how many winners there are.

The only subtle point is avoiding division by 0 when normalizing the result to live in 0..1 That’s why the “happiness()” method makes a special case of zero “span”. An all-the-same-rank ballot is the only kind whose min and max displacements are the same, and so gives a 0 denominator. There were none in our election, but, of course, code I wrote anticipated them anyway :wink:.

Would results have changed under Approval?

I avoided addressing this because there’s no way to know for sure. We didn’t use Approval ballots, and any way of trying to pretend that we did needs to inject dubious guesses about what our ballots “really said”.

That said, my best guess is “almost certainly not”, but I won’t give details.

The fundamental problem: under Approval, people construct an order in their head, and pick their own idea of where best to cut it off. That idea varies across people. I can say, .e.g., let’s say they would have approved if and only if they gave a candidate at least 4 stars. Or 5, or 3, or 2, or 1. They’re all arbitrary, though, and any choice is sure to misjudge what a significant number of voters would have done under Approval.

So I tried all 5 of those cutoffs. They all delivered that same final 5 winners, but with small changes in ordering. - but even among the top 3 picks.

Another idea is to say they would have approved of all and only their 5 most-preferred candidates. Which is what I would have done [1]. Makes scant difference, though: same winners in the same order as STAR gave.

Then I remembered the ballot that gave 5 stars to one candidate and 0 to all the rest. It’s no stretch to guess they would have approved of only one under Approval. So “would have approved their 5 most favorite” is also making stuff up not justified by the data.

In any case, there’s no reason to believe results would have changed under Approval, and various poke-and-hope experiments didn’t find any material differences.


  1. in this specific election - in other elections I typically approve of more candidates than open seats, but my preference structure in this one had a clear bottom tier ↩︎