implement single transferable vote
This commit is contained in:
parent
b1dfabd515
commit
705df55ef9
|
@ -0,0 +1,2 @@
|
||||||
|
secrets.env
|
||||||
|
venv
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
echo "BEARER=???" > secrets.env
|
||||||
|
echo "API_VOTE=https://voting.2021.egoi.ch/api/votes/{question_id}" >> secrets.env
|
|
@ -0,0 +1,2 @@
|
||||||
|
python-dotenv
|
||||||
|
requests
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
def get_votes(ids):
|
||||||
|
votes = {}
|
||||||
|
for id in ids:
|
||||||
|
res = requests.get(os.environ['API_VOTE'].format(question_id=id), headers={'Authorization': 'Bearer ' + os.environ['BEARER']})
|
||||||
|
if not res.ok:
|
||||||
|
print('Could not get votes for id', id)
|
||||||
|
continue
|
||||||
|
f = StringIO(res.text)
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
votes.setdefault(row['Delegation'], []).append(row['Option'])
|
||||||
|
return list(votes.values())
|
||||||
|
|
||||||
|
def get_candidates_from_votes(votes):
|
||||||
|
candidates = set()
|
||||||
|
for vote in votes:
|
||||||
|
for option in vote:
|
||||||
|
candidates.add(option)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
def determine_winners(people, votes):
|
||||||
|
"""Single Transferable Vote (https://en.wikipedia.org/wiki/Single_transferable_vote)"""
|
||||||
|
elected = []
|
||||||
|
elected_set = set()
|
||||||
|
candidates = get_candidates_from_votes(votes)
|
||||||
|
surplus_votes = {x: 0 for x in candidates}
|
||||||
|
quota = len(votes) / people + 1
|
||||||
|
|
||||||
|
print('Votes:', len(votes))
|
||||||
|
print('Quota:', quota)
|
||||||
|
print('Candidates:', len(candidates))
|
||||||
|
|
||||||
|
def one_round():
|
||||||
|
print('-----------')
|
||||||
|
print('Start Round')
|
||||||
|
print('-----------')
|
||||||
|
round_votes = {x: 0 for x in candidates}
|
||||||
|
for vote in votes:
|
||||||
|
vote_weight = 1
|
||||||
|
for option in vote:
|
||||||
|
if option in elected_set:
|
||||||
|
vote_weight *= surplus_votes[option]
|
||||||
|
if option in candidates and option not in elected_set:
|
||||||
|
round_votes[option] += vote_weight
|
||||||
|
break
|
||||||
|
vote_counts = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate in elected_set:
|
||||||
|
continue
|
||||||
|
vote_counts.append((round_votes[candidate], candidate))
|
||||||
|
vote_counts.sort()
|
||||||
|
print('Vote counts:')
|
||||||
|
for (count, candidate) in reversed(vote_counts):
|
||||||
|
print(' {}: {}'.format(candidate, count))
|
||||||
|
if vote_counts[-1][0] >= quota or len(candidates) == people:
|
||||||
|
elected.append((vote_counts[-1][1], vote_counts[-1][0]))
|
||||||
|
elected_set.add(vote_counts[-1][1])
|
||||||
|
|
||||||
|
surplus = vote_counts[-1][0] - quota
|
||||||
|
if surplus > 0:
|
||||||
|
surplus_votes[vote_counts[-1][1]] = surplus / vote_counts[-1][0]
|
||||||
|
|
||||||
|
print('Elect', vote_counts[-1][1])
|
||||||
|
elif len(candidates) > people:
|
||||||
|
candidates.remove(vote_counts[0][1])
|
||||||
|
print('Remove', vote_counts[0][1])
|
||||||
|
|
||||||
|
while len(elected_set) < people:
|
||||||
|
one_round()
|
||||||
|
|
||||||
|
return elected
|
||||||
|
|
||||||
|
def test_votes():
|
||||||
|
return [
|
||||||
|
['O'],
|
||||||
|
['O'],
|
||||||
|
['O'],
|
||||||
|
['O'],
|
||||||
|
['P', 'O'],
|
||||||
|
['P', 'O'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'S'],
|
||||||
|
['C', 'H'],
|
||||||
|
['C', 'H'],
|
||||||
|
['C', 'H'],
|
||||||
|
['C', 'H'],
|
||||||
|
['S'],
|
||||||
|
['H'],
|
||||||
|
]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
load_dotenv('secrets.env')
|
||||||
|
parser = argparse.ArgumentParser(description='Determine voted persons from list of votes.')
|
||||||
|
parser.add_argument('--test', action='store_true', help='Use test values for the votes')
|
||||||
|
parser.add_argument('people', type=int, help='Number of people who need to be voted.')
|
||||||
|
parser.add_argument('questions', type=int, nargs='+', help='IDs of the questions.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.test:
|
||||||
|
votes = test_votes()
|
||||||
|
else:
|
||||||
|
votes = get_votes(args.questions)
|
||||||
|
winners = determine_winners(args.people, votes)
|
||||||
|
|
||||||
|
print('-------------')
|
||||||
|
print('Final results')
|
||||||
|
print('-------------')
|
||||||
|
for i, (winner, votes) in enumerate(winners):
|
||||||
|
print('{:2} with {:2} votes: {}'.format(i+1, int(votes), winner))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue