Maine IRV Primary Analysis

Maine has recently released the full results for all of the races in their July 14th IRV primary that required more then one round of tabulation. In total there were five races (out of a total of 189, though many of those were uncontested) in which no candidate received a majority of 1st preferences (as well as one race where they did the additional round anyways because one of the candidate’s majority was probably within the margin of error). @Brian_Olson do you mind running these through the same code you ran to analyze the last Maine IRV election?

2 Likes

It looks like the most interesting of these races are rep 47 and 49. Though in every single one of these races the candidate with the most initial 1st place preferences went on to win the election.

FWIW the rankings I got for Rep 47 were

(): 122,

(‘Abbott’): 160
(‘Abbott’, ‘Bell’, ‘Fromuth’): 410,
(‘Abbott’, ‘Bell’): 45
(‘Abbott’, ‘Fromuth’, ‘Bell’): 236
(‘Abbott’, ‘Fromuth’): 22

(‘Bell’,): 178,
(‘Bell’, ‘Abbott’, ‘Fromuth’): 398,
(‘Bell’, ‘Abbott’): 38,
(‘Bell’, ‘Fromuth’, ‘Abbott’): 304,
(‘Bell’, ‘Fromuth’): 52,

(‘Fromuth’,): 71
(‘Fromuth’, ‘Bell’, ‘Abbott’): 238,
(‘Fromuth’, ‘Bell’): 45,
(‘Fromuth’, ‘Abbott’, ‘Bell’): 199
(‘Fromuth’, ‘Abbott’): 29,

Which seems to agree with the official round by round count.

2 Likes

This is good stuff. What did you use to convert it into ballot data? I’d love to play around with it (in Codepen of course :slight_smile: ). It would be nice it it was in JSON.

I used Python code on the ballot data files on the Maine election results website.

Oh ok I was looking at the wrong file (I was thinking you magically converted the more condensed version). I ran it through some javascript to produce score ballots that can be pasted into my codepen at https://codepen.io/karmatics/pen/jOWQgBJ

I did it as score ballots because I don’t yet have the ability for it to work with ranked ballots. Since there are 3 candidates, I have it do scores of 5, 3 and 0 if they rank 3 of them. (maybe I could give intermediate middle ranks a random score from 1 to 4?)

My numbers are close but not exact to yours. (I have 409 rather than 410 of Abbott, Bell, Fromuth ballots, for instance). Hmmm.

I’ll work more on this and provide all the code eventually, but this may be interesting data to play with as a start. How it did on a few methods (STAR etc) is shown at bottom.

var candidates = {
a: “Abbott, Heather”,
b: “Bell, Arthur L.”,
c: “Fromuth, Peter J.”
};

409: a[5] b[3] c[0]
395: a[3] b[5] c[0]
303: a[0] b[5] c[3]
235: a[0] b[3] c[5]
230: a[5] b[0] c[3]
197: a[3] b[0] c[5]
179: a[0] b[5] c[0]
158: a[5] b[0] c[0]
98: a[0] b[5] c[5]
82: a[5] b[5] c[0]
69: a[0] b[0] c[5]
45: a[5] b[0] c[5]
4: a[4] b[0] c[5]
3: a[5] b[0] c[4]
a[5] b[4] c[0]
a[0] b[5] c[4]

----- processed 2409 ballots -----
(2409 explicit and 0 blurred)

----- Pairwise wins -----
b: 2
a: 1
c: 0
----- Score -----
b: 7226
a: 6432
c: 4855
----- Interpolated Median -----
b: 3.2740683229813663
a: 3.039695945945946
c: 0.48406862745098034
----- STAR -----
b: 1211
a: 1047
----- STLR -----
b: 7696
a: 6830

1 Like

Finally ran all the Maine 2020 elections and nothing had any differences Condorcet vs IRV. No drama this time.

Did you use all of the ballot data files? There’s 3 of them.

Anyway, the Python code I used was

from typing import List, TextIO

def tsv_to_list(tsv_file: TextIO) -> List[List[str]]:

# Read and discard header.
tsv_file.readline()

data = []
for line in tsv_file:
    data.append(line.strip().split('\t'))
return data

def multilist_read_ballots(tsv_file_names: List[str]) -> dict:
“”“Read a file listing the different ballot rankings people
indicated and output the number of permutations of votes cast”""
tsv_files = []
for filename in tsv_file_names:
tsv_files.append(open(filename, ‘r’))
list_of_ballots = []
for ballots in tsv_files:
list_of_ballots.extend(tsv_to_list(ballots))
permutation_count = {}
for ballot in list_of_ballots:
ranking = ballot[3:]
for i in range(0, len(ranking)):
if ‘Abbott’ in ranking[i]:
ranking[i] = ‘Abbott’
elif ‘Bell’ in ranking[i]:
ranking[i] = ‘Bell’
elif ‘Fromuth’ in ranking[i]:
ranking[i] = ‘Fromuth’
elif ranking[i] != ‘undervote’:
ranking[i] = ranking[i]
vote_as_tuple = (ranking[0], ranking[1], ranking[2])
if vote_as_tuple in permutation_count:
permutation_count[vote_as_tuple] += 1
else:
permutation_count[vote_as_tuple] = 1
return permutation_count

def multilist_effective_rankings(tsv_file_names: List[str]) -> dict:
tsv_files = []
for filename in tsv_file_names:
tsv_files.append(open(filename, ‘r’))
list_of_ballots = []
for ballots in tsv_files:
list_of_ballots.extend(tsv_to_list(ballots))
print(len(list_of_ballots))
permutation_count = {}
for ballot in list_of_ballots:
ranking = ballot[3:]
effective_ranking = []
overvote_found = False
follows_undervote = False
i=0
while i < len(ranking) and not overvote_found:
overvote_found = False
if ‘Abbott’ in ranking[i]:
follows_undervote = False
if not ‘Abbott’ in effective_ranking:
effective_ranking.append(‘Abbott’)
elif ‘Bell’ in ranking[i]:
follows_undervote = False
if not ‘Bell’ in effective_ranking:
effective_ranking.append(‘Bell’)
elif ‘Fromuth’ in ranking[i]:
follows_undervote = False
if not ‘Fromuth’ in effective_ranking:
effective_ranking.append(‘Fromuth’)
elif ‘overvote’ in ranking[i]:
follows_undervote = False
overvote_found = True
elif ranking[i] != ‘undervote’:
follows_undervote = False
effective_ranking.append(ranking[i])
else:
if follows_undervote:
overvote_found = True
# Treat double undervotes like overvotes; as a terminator
else:
follows_undervote = True
i+=1
vote_as_tuple = tuple(effective_ranking)
if vote_as_tuple in permutation_count:
permutation_count[vote_as_tuple] += 1
else:
permutation_count[vote_as_tuple] = 1
return permutation_count

def get_pairwise_table(votes: dict) -> List[List]:
cand_names = [‘Abbott’, ‘Bell’, ‘Fromuth’]
cand_id_lookup = {‘Abbott’:0, ‘Bell’:1, ‘Fromuth’:2}
pairwise_table = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
for vote, qty in dict.items(votes):
for i in range(0, len(vote)):
for x in range(0, 3):
if not cand_names[x] in vote[0:i+1]:
pairwise_table[cand_id_lookup[vote[i]]][x] += qty
return pairwise_table

if __name__ == ‘__main__’:
ballots = multilist_effective_rankings([‘sr47-1.tsv’, ‘sr47-2.tsv’, ‘sr47-3.tsv’])
print(ballots)

(Unfortunately Discourse won’t keep all the whitespace.

Originally I wrote it for the ME-2 race in 2018. I should probably generalize it so that you don’t have to change the candidate names each time you want to run it on a new race.

BTW I ran my code on the larger race, the 2nd congressional district with ~42,000 ballots.

Interestingly, some of the ballots had someone voted for twice. Those show up as score ballots with a 0, 4 and 5 instead of 0 3 and 5 (due to my counting and normalization logic). I just left them in there.

Candidates:

var candidates = {
a: “Crafts, Dale John”,
b: “Bennett, Adrienne”,
c: “Brakey, Eric L.”
};

Ballots:

7430: a[5] b[0] c[0]
5270: a[5] b[3] c[0]
4721: a[3] b[5] c[0]
4257: a[5] b[0] c[3]
3980: a[0] b[5] c[0]
3295: a[0] b[5] c[3]
3246: a[0] b[0] c[5]
2908: a[3] b[0] c[5]
2449: a[0] b[3] c[5]
1338: a[5] b[4] c[0]
1099: a[4] b[5] c[0]
733: a[5] b[0] c[4]
626: a[4] b[0] c[5]
322: a[0] b[5] c[4]
275: a[0] b[4] c[5]

Results:

----- processed 41949 ballots -----
----- Pairwise wins -----
a: 2
b: 1
c: 0
----- Score -----
a: 124927 (2.9780686071181672)
b: 96694 (2.3050370688216644)
c: 74396 (1.7734868530835062)
----- Interpolated Median -----
a: 3.4709660505964086
b: 2.7298872910998835
c: 0.3798766675056632
----- STAR -----
a: 22562
b: 16141
----- STLR -----
a: 131369
b: 101867

In the Python code I posted, multilist_read_ballots shows that, but multilist_effective_rankings filters it out. There’s many odd combinations and if you list them all it’s hard for a human to tell what’s going on.

Yeah I think the only ones I threw away were the ones where all three were “undercount”.

It’s pretty bizarre to me that they don’t show the ballots and results in an easier to view way. Just a count of each kind of ballot as I posted above (where there were 42000 ballots) can be expressed in a grand total of about 400 characters of text. Instead they give you xls files that are well over a meg.

(I know, first world problems… )

Rep 49

(): 189,

(‘Arford’,): 178,
(‘Arford’, ‘Perreault’, ‘Wilson’): 346,
(‘Arford’, ‘Perreault’): 61,
(‘Arford’, ‘Wilson’, ‘Perreault’): 267,
(‘Arford’, ‘Wilson’): 47

(‘Perreault’,): 94,
(‘Perreault’, ‘Arford’, ‘Wilson’): 237,
(‘Perreault’, ‘Arford’): 53,
(‘Perreault’, ‘Wilson’, ‘Arford’): 194,
(‘Perreault’, ‘Wilson’): 35,

(‘Wilson’,): 139,
(‘Wilson’, ‘Arford’, ‘Perreault’): 219,
(‘Wilson’, ‘Arford’): 42,
(‘Wilson’, ‘Perreault’, ‘Arford’): 230,
(‘Wilson’, ‘Perreault’): 27,

Does their IRV logic treat these as any different? I considered them the same.

There’s no difference, but in elections that allow write-ins there could be, so I think distinguishing between the two is the best default option.

I cleaned up my code and put it in a Codepen: https://codepen.io/karmatics/pen/eYJxoMa

You just have to export to CSV (I used Numbers on a Mac, not sure if it will be the same as if you use Excel) and paste it in. You also have to put the candidates’ names in another text box, and give them abbreviations.

Looks like this:

The data that’s in there by default is just a dozen lines or so, but it takes about half a second to crunch 40,000 ballots.

1 Like

My code is here:


xlsxtocsv.py to unpack the downloaded data
maine.py to convert that into the url-formated Alice=1&Bob=2 format I like
countvotes.py to run that through Condorcet and IRV

2 Likes

Nice. I scanned your repo a bit and see you also have a good bit of voting related stuff in C++ and Go as well. Cool stuff.

I started with C and C++ ages ago, but mostly use JS now, especially since I tend to do graphical stuff and the browser is such a rich environment.

I’d love it if your stuff got ported to JS so they can be included in Codepens… as you can tell, I am pretty gung ho on Codepen for making stuff low barrier to entry for the forum. (I don’t expect very many to be forking repos and such, and it sure is nice to be able to say to non-coders “go to this url and there is a working utility you can directly use”) I plan to have a permanent page at the same domain as the new forum, with links to Codepens, github repos such as yours, web pages with functioning apps, etc.