implement single transferable vote

This commit is contained in:
Benjamin Schmid 2021-06-01 15:55:17 +02:00
parent b1dfabd515
commit 705df55ef9
4 changed files with 135 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
secrets.env
venv

3
create_secrets.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
echo "BEARER=???" > secrets.env
echo "API_VOTE=https://voting.2021.egoi.ch/api/votes/{question_id}" >> secrets.env

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
python-dotenv
requests

128
vote.py Executable file
View File

@ -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()