Skip to content

Commit

Permalink
Policy and Preselection Meeting 2013 counters
Browse files Browse the repository at this point in the history
  • Loading branch information
bbqsrc committed May 5, 2013
1 parent 3a38127 commit 68313f4
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 49 deletions.
103 changes: 103 additions & 0 deletions ppm2013_elections.py
@@ -0,0 +1,103 @@
from __future__ import division
from collections import defaultdict, Counter
from itertools import chain
from schulze import run_election

import argparse
import json
import sys


if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('--slugs', nargs='*',
help="Elections to be calculated (Default: all)")
p.add_argument('--approval', action="store_true",
help="Approval phase only")
p.add_argument('--ranking', action="store_true",
help="Ranking phase only")
p.add_argument("--show-errors", action="store_true",
help="Show erroneous ballots")
p.add_argument("--html", action="store_true",
help="Output HTML")
p.add_argument('ballots', type=argparse.FileType('r'))
args = p.parse_args()

if not args.approval and not args.ranking:
args.approval = True
args.ranking = True

ballots = json.load(args.ballots)
parsed_approval = defaultdict(list)
parsed_ranking = defaultdict(list)
count = Counter()
errors = 0

# Phase 0: Parse ballots
if args.slugs:
keys = args.slugs
else:
keys = list(ballots.keys())
# This key has no elections
keys.remove('ppm2013')

for key in keys:
ballot_list = ballots[key]
for ballot in ballot_list:
if ballot.get('ballot') is None or\
ballot['ballot'].get('election') is None:
if args.show_errors:
print("Erroneous ballot: %s" % json.dumps(ballot))
errors += 1
continue

election = ballot['ballot']['election']
for state, o in election.items():
count[state] += 1
for phase, values in o.items():
if phase == "Approval":
parsed_approval[state].append(values)
elif phase == "Ranking":
parsed_ranking[state].append(values)
print("There were %d erroneous ballots.\n" % errors)

# Phase 1: Approval
if args.approval:
print("# Approval")
for state, ballots in parsed_approval.items():
print("\n## %s" % state)

counter = Counter()
for ballot in ballots:
for candidate, value in ballot.items():
# 'on' is default HTML value for a checked checkbox.
if value == "on":
counter[candidate] += 1

for candidate, value in counter.items():
print("%s: Y:%s N:%s [%s%%]" % (candidate, value,
count[state] - value, "%.2f" % (value / count[state] * 100)))
print()

# Phase 2: Ranking
if args.ranking:
print ("# Ranking")
# Convert ballots to CSV for schulze.py counting
csvs = defaultdict(list)
for state, ballots in parsed_ranking.items():
candidates = list(sorted(parsed_ranking[state][0].keys()))
csvs[state].append(",".join(candidates))
for ballot in ballots:
csvs[state].append(",".join([ballot[candidate] for candidate in candidates]))

parsed_csvs = {}
for state, csv in csvs.items():
parsed_csvs[state] = "\n".join(csv)

for state, csv in parsed_csvs.items():
print("\n## %s" % state)
run_election(csv, *[], **{
"show_errors": args.show_errors,
"html": args.html,
"urlencode": True
})
37 changes: 37 additions & 0 deletions ppm2013_motions.py
@@ -0,0 +1,37 @@
from collections import defaultdict, Counter
from itertools import chain

import argparse
import json
import sys

if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('ballots', type=argparse.FileType('r'),
help="Ballot JSON file")
p.add_argument('--show-errors', action='store_true',
help="Show erroneous ballots")
args = p.parse_args()

# As the ballot json file is a list of dicts, we only care about the values
# of each dict, so we're going to flatten it.
ballots = chain(*json.load(args.ballots).values())
motions = defaultdict(Counter)
errors = 0

for ballot in ballots:
if ballot['ballot'].get('motions') is None:
if args.show_errors:
print("Errorneous ballot: " + json.dumps(ballot['ballot']))
errors += 1
continue
for k, v in ballot['ballot']['motions'].items():
motions[int(k)][v] += 1


print("There were %d erroneous ballots.\n" % errors)
for n in sorted(motions.keys()):
c = motions[n]
percent = "%.2f" % (c["Yes"] / (c["Yes"] + c["No"]) * 100)
print("Motion %s: Y:%s N:%s A:%s [%s%%]" % (n, c["Yes"], c['No'],
c['Abstain'], percent))
117 changes: 68 additions & 49 deletions schulze.py
@@ -1,6 +1,10 @@
from collections import defaultdict, Counter
from urllib.parse import quote
from io import StringIO

import json
import sys
import argparse

"""
Ballots are csv's.
Expand All @@ -16,8 +20,8 @@
,4,4
"""

def load_ballots(fn):
lines = open(fn, 'r').readlines()
def load_ballots(f):
lines = f.readlines()
header = lines[0].strip().split(',')
ballots = [line.strip().split(',') for line in lines[1:]]
return header, ballots
Expand Down Expand Up @@ -104,12 +108,13 @@ def print_first_prefs(candidates, ballots):
))


def count_ballots(candidates, ballots):
def count_ballots(candidates, ballots, show_errors=False):
count = build_matrix(len(candidates))

for ballot in ballots:
if not check_ballot(candidates, ballot):
print("Invalid ballot: %s" % ballot)
if show_errors:
print("Invalid ballot: %s" % ballot)
continue

for a in range(len(ballot)):
Expand Down Expand Up @@ -191,32 +196,15 @@ def print_rankings(candidates, rankings, winner_only=False):
return

c = 0
print("Rankings:\n")
for k, v in count:
c += 1
print("(%s) %s" % (c, candidates[k]))


def run_election(fn, *withdraws, winner_only=False, hide_grids=False, first_prefs=False, html=False):
candidates, ballots = load_ballots(fn)
for w in withdraws:
withdraw_candidate(w, candidates, ballots)

if first_prefs:
print_first_prefs(candidates, ballots)
return

count = count_ballots(candidates, ballots)
paths = calculate_strongest_paths(count)
rankings = determine_rankings(paths)

# check to see if highest rank is shared
tie = False
if rankings.count(max(rankings)) > 1:
tie = True
rankings = break_ties(count, rankings)

def output(candidates, paths, rankings, count, tie, winner_only=False, html=False, urlencode=False, hide_grids=False):
if html:
print(convert_matrix_to_html_table(candidates, count))
print(convert_matrix_to_html_table(candidates, count, urlencode))
#print(strongest_path_html(candidates, paths))
print("<pre>")
if tie:
Expand Down Expand Up @@ -246,6 +234,30 @@ def run_election(fn, *withdraws, winner_only=False, hide_grids=False, first_pref
print_rankings(candidates, rankings, winner_only)


def run_election(f, *withdraws, winner_only=False, hide_grids=False, first_prefs=False, html=False, urlencode=False, show_errors=False):
if isinstance(f, str):
f = StringIO(f)
candidates, ballots = load_ballots(f)
for w in withdraws:
withdraw_candidate(w, candidates, ballots)

if first_prefs:
print_first_prefs(candidates, ballots)
return

count = count_ballots(candidates, ballots, show_errors)
paths = calculate_strongest_paths(count)
rankings = determine_rankings(paths)

# check to see if highest rank is shared
tie = False
if rankings.count(max(rankings)) > 1:
tie = True
rankings = break_ties(count, rankings)

output(candidates, paths, rankings, count, tie, winner_only, html, urlencode, hide_grids)


def strongest_path_html(candidates, matrix):
cross = "&times;"
info = "These are the strongest paths. This is required to determine a winner in the case of no obvious majority pairwise winner."
Expand Down Expand Up @@ -276,11 +288,13 @@ def strongest_path_html(candidates, matrix):

return ("<table><caption>%s</caption><thead>%s</thead><tbody>" % (info, headers)) + "".join(x) + "</tbody></table>"

def convert_matrix_to_html_table(candidates, matrix):

def convert_matrix_to_html_table(candidates, matrix, urlencode):
info = "The horizontal axis is compared with the vertical axis to see who is the most preferred candidate. The ranking order is determined by the candidates with the most green squares."
headers = '<tr><th class="empty"></th><th><div>' +\
"</div></th><th><div>".join(candidates) + "</div></th></tr>"
x = []
data = {}

for i in range(len(matrix)):
row = []
Expand All @@ -302,36 +316,41 @@ def convert_matrix_to_html_table(candidates, matrix):
'''</div>''') % (matrix[i][j], candidates[i], candidates[j],
success, "%s (%s%%)" % (matrix[i][j], success),
fail, "%s (%s%%)" % (matrix[j][i], fail))
data[title] = content
if matrix[i][j] < matrix[j][i]:
row.append('<td title="%s" data-content="%s" class="red">%s</td>' % (title, content, matrix[i][j]))
row.append('<td title="%s" class="red">%s</td>' % (title, matrix[i][j]))
elif matrix[i][j] > matrix[j][i]:
row.append('<td title="%s" data-content="%s" class="green">%s</td>' % (title, content, matrix[i][j]))
row.append('<td title="%s" class="green">%s</td>' % (title, matrix[i][j]))
elif matrix[i][j] == matrix[j][i]:
row.append('<td title="%s" data-content="%s" class="yellow">%s</td>' % (title, content, matrix[i][j]))
row.append('<td title="%s" class="yellow">%s</td>' % (title, matrix[i][j]))
x.append("<tr><th>%s</th>%s</tr>" % (candidates[i], "".join(row)))

return ('<table class="ranking"><caption>%s</caption><thead>%s</thead><tbody>' % (info, headers)) + "".join(x) + "</tbody></table>"
return ('<script>var electionData = \n%s\n</script><table class="ranking"><caption>%s</caption><thead>%s</thead><tbody>' % (script_data(data, urlencode), info, headers)) + "".join(x) + "</tbody></table>"


if __name__ == "__main__":
args = {}

# TODO: make this sane, obviously. This is just derpy.
def script_data(data, urlencode):
if not urlencode:
return json.dumps(data, indent=2)
else:
return 'JSON.parse(decodeURIComponent("%s"))' % quote(json.dumps(data))

if '-h' in sys.argv:
args['html'] = True
del sys.argv[sys.argv.index('-h')]

if '-w' in sys.argv:
args['winner_only'] = True
del sys.argv[sys.argv.index('-w')]

if '-s' in sys.argv:
args['hide_grids'] = True
del sys.argv[sys.argv.index('-s')]

if '-f' in sys.argv:
args['first_prefs'] = True
del sys.argv[sys.argv.index('-f')]

run_election(sys.argv[1], *sys.argv[2:], **args)
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('-H', '--html', action='store_true',
help="Output HTML")
p.add_argument('-U', '--urlencode', action='store_true')
p.add_argument('-w', '--winner-only', action='store_true')
p.add_argument('-s', '--hide-grids', action='store_true')
p.add_argument('-f', '--first-prefs', action='store_true')
p.add_argument('--show-errors', action='store_true')
p.add_argument('--withdraw', nargs='*',
help="Candidates to withdraw from election")
p.add_argument('ballots', type=argparse.FileType('r'))
args = dict(p.parse_args()._get_kwargs())
ballots = args.get('ballots')
withdraw = args.get('withdraw') or []

del args['withdraw']
del args['ballots']
run_election(ballots, *withdraw, **args)

0 comments on commit 68313f4

Please sign in to comment.