EdvardM
4/14/2019 - 12:18 PM

Toy program for playing with D'Hondt system

Toy program for playing with D'Hondt system

import argparse
import logging
import random
import string

from itertools import groupby
from operator import itemgetter

PARTIES = ["Kesk", "Kok", "SDP", "Sin", "PS", "Vihr", "Vas", "RKP", "KD", "TL", "EOP", "FP", "IPU", "KP", "KTP", "Lib",
           "PP", "SKE", "SKP"]

logging.basicConfig(level=logging.INFO)


def main():
    def split_at(lst, index):
        return lst[:index + 1], lst[index + 1:]

    def chunks(l, n):
        for i in range(0, len(l), n):
            yield l[i:i + n]

    def print_votes(lst, max_rows=None):
        for i, chunk in enumerate(chunks(lst, 5), start=1):
            if max_rows and i > max_rows:
                break

            for party, idx, votes in chunk:
                print('{}{}: {:6d}'.format(party, idx, int(votes)), end="\t")
            print()
        print()

    def rank_order(elect_count, votes):
        def gen_weighted_votes(label, votes, max_count):
            return [(label, i + 1, votes / float(i + 1)) for i in range(max_count)]

        weighted_votes = []
        for k, v in votes.items():
            weighted_votes.extend(gen_weighted_votes(k, v, 10 * elect_count))

        return weighted_votes

    parser = argparse.ArgumentParser(description='Show D\'Hondt vote distribution')
    parser.add_argument('votes', metavar='N', type=int, nargs='*',
                        help='Votes a party gets. First for A, second for B, third for C etc')
    parser.add_argument('--seats', '-s', type=int, help='Number of seats to elect in total')
    parser.add_argument('--party-count', '-p', type=int, default=4)
    parser.add_argument('--num-voters', '-v', type=int, default=int(3E6), help='Total number of voters (use with -f)')
    parser.add_argument('--max-party-votes', '-m', type=float, default=30.0,
                        help='Max number of votes for party (percentage)')
    parser.add_argument('--fun-mode', '-f', action='store_true',
                        help='Map abstract party symbols to real parties, implies -c 19')
    args = parser.parse_args()

    zipper = lambda i: dict(zip(range(len(i)), i))

    pos_to_party = zipper(string.ascii_uppercase)
    if args.fun_mode:
        logging.info("Funny mode enabled")
        args.party_count = len(PARTIES)
        args.seats = args.seats or 200
        random.shuffle(PARTIES)
        pos_to_party = zipper(PARTIES)
    else:
        args.seats = args.seats or 3

    vote_list = args.votes
    if not vote_list:
        print(f'Using randomized votes for {args.party_count} parties')
        vote_list = []

        voters_left = args.num_voters
        for _ in range(args.party_count):
            vote_max_limit = min((args.max_party_votes / 100.0) * args.num_voters, voters_left)
            votes = random.randint(0, vote_max_limit)
            vote_list.append(votes)
            voters_left -= votes

        vote_list = sorted(vote_list, reverse=True)

        for i, chunk in enumerate(chunks(vote_list, 5)):
            print(', '.join(['{}: {}'.format(pos_to_party[5 * i + j], v) for j, v in enumerate(chunk)]))

    vote_map = {}
    for i, num_votes in enumerate(vote_list):
        label = pos_to_party[i]
        vote_map[label] = num_votes

    total_votes = sum(vote_list)
    elected, rest = split_at(sorted(rank_order(args.seats, vote_map), key=lambda x: -x[-1]), args.seats - 1)

    print(f'Total votes: {total_votes}')
    print()

    print(f'Elected {args.seats} people, ordered by relative votes:')
    print_votes(elected)

    sorted_by_party = sorted(elected, key=itemgetter(0))

    print('Seats and proportionate seats:')
    by_seats = []
    for party, entries in groupby(sorted_by_party, key=itemgetter(0)):
        values = list(entries)
        seats = len(values)
        top_seats = values[0][-1]
        prop = args.seats * top_seats / total_votes
        by_seats.append((seats, party, prop))

    for seats, party, prop in sorted(by_seats, reverse=True):
        print('{:>6s}: {:<6d} / {:.2f}'.format(party, seats, prop))

    print()
    print("Top 3 rows for not chosen, ordered by relative votes:")
    print_votes(rest, max_rows=3)


main()