Last active
May 20, 2019 20:59
-
-
Save fredkingham/8fe1506c6393a6919baa98789b159856 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import copy | |
from collections import defaultdict | |
def calculate_seats(_party_dict, seat_number): | |
""" | |
takes in the parties as a dictionary to % of vote | |
returns a dictionary of the parties to seat numbers | |
""" | |
party_dict = copy.copy(_party_dict) | |
original_party_dict = copy.copy(_party_dict) | |
result = defaultdict(int) | |
for amount in range(seat_number): | |
winner = calculate_step(party_dict) | |
result[winner] += 1 | |
party_dict[winner] = original_party_dict[winner]/(result[winner]+1) | |
return result | |
def calculate_step(party_dict): | |
""" | |
Takes in a dictionary of party to number | |
returns the party with the highest number | |
""" | |
winner = "" | |
highest = 0 | |
for i, v in party_dict.items(): | |
if v > highest: | |
highest = v | |
winner = i | |
return winner | |
def remain_total(result_dict, region): | |
""" | |
The total votes in a regions remain parties | |
""" | |
return sum([result_dict[i] for i in region.remain_parties]) | |
def remain_weight(result_dict, region): | |
""" | |
In terms of recommendations its easier to say | |
vote for a single party then a group of partes. | |
As such if to maximise a remain vote we have | |
an option of 900 Lib Dem and 100 Change | |
vs 500 Lib Dem and 500 Change. The former is | |
a more interesting result. | |
The former would yield 900100 and the latter 500500 | |
""" | |
if not result_dict: | |
return 0 | |
votes = sorted(list(result_dict.values())) | |
votes.reverse() | |
weight_string = "" | |
for vote in votes: | |
weight_string = weight_string + ('%03d' % vote) | |
return int(weight_string) | |
def get_combinations(region): | |
""" | |
All possible combinations of voting patterns for | |
remain parties within the region | |
""" | |
change_total = sum([region.party_dict[i] for i in region.remain_parties]) | |
cache = set() | |
if len(region.remain_parties) == 3: | |
for i in range(int(change_total)): | |
for x in range(int(change_total)): | |
if x + i > change_total: | |
continue | |
combo = (i, x, change_total - i - x,) | |
if combo not in cache: | |
cache.add(frozenset(combo)) | |
yield(combo) | |
if len(region.remain_parties) == 4: | |
for i in range(int(change_total)): | |
for x in range(int(change_total)): | |
for y in range(int(change_total)): | |
if x + i + y > change_total: | |
continue | |
combo = (i, x, y, change_total - i - x - y,) | |
if combo not in cache: | |
cache.add(frozenset(combo)) | |
yield(combo) | |
def translate_combination_to_party_dict(_party_dict, combination): | |
party_dict = copy.copy(_party_dict) | |
ordered_combination = sorted(list(combination)) | |
ordered_combination.reverse() | |
for i, v in enumerate(region.remain_parties): | |
party_dict[v] = int(ordered_combination[i]) | |
return party_dict | |
def calculate_min_max(region): | |
combinations = list(get_combinations(region)) | |
votes_for_min = None | |
min_combination_seat_count = None | |
min_combination_seats = 1000 | |
votes_for_max = None | |
max_combination_seats = 0 | |
max_combination_seat_count = None | |
for combination in combinations: | |
vote_combination = translate_combination_to_party_dict( | |
region.party_dict, combination | |
) | |
calculated_seats = calculate_seats(vote_combination, region.SEATS) | |
combination_result = remain_total( | |
calculated_seats, region | |
) | |
combination_weight = remain_weight(vote_combination, region) | |
if combination_result > max_combination_seats: | |
max_combination_seats = combination_result | |
max_combination_seat_count = calculated_seats | |
votes_for_max = vote_combination | |
if combination_result == max_combination_seats: | |
if combination_weight > remain_weight(votes_for_max, region): | |
max_combination_seats = combination_result | |
max_combination_seat_count = calculated_seats | |
votes_for_max = vote_combination | |
if combination_result < min_combination_seats: | |
min_combination_seats = combination_result | |
min_combination_seat_count = calculated_seats | |
votes_for_min = vote_combination | |
if combination_result == min_combination_seats: | |
if combination_weight > remain_weight(votes_for_min, region): | |
min_combination_seats = combination_result | |
min_combination_seat_count = calculated_seats | |
votes_for_min = vote_combination | |
return dict( | |
min_result=votes_for_min, | |
min_seats=min_combination_seat_count, | |
max_result=votes_for_max, | |
max_seats=max_combination_seat_count | |
) | |
class Region(): | |
PARTIES = [ | |
"SNP", | |
"PLAID", | |
"BREXIT_PARTY", | |
"LAB", | |
"LIB_DEM", | |
"CON", | |
"GREEN", | |
"CHANGE_UK", | |
"UKIP", | |
] | |
REMAIN_PARTIES = [ | |
"SNP", "PLAID", "LIB_DEM", "GREEN", "CHANGE_UK" | |
] | |
@property | |
def party_dict(self): | |
return {i: getattr(self, i) for i in self.PARTIES if hasattr(self, i)} | |
@property | |
def remain_parties(self): | |
return [i for i in self.REMAIN_PARTIES if hasattr(self, i)] | |
# Figures taken from (the excellent) https://www.telegraph.co.uk/politics/0/european-elections-2019-polls-latest-brexit-party/?li_source=LI&li_medium=li-recommendation-widget | |
class NorthEast(Region): | |
SEATS = 3 | |
BREXIT_PARTY = 368 | |
LAB = 300 | |
LIB_DEM = 113 | |
CON = 85 | |
GREEN = 50 | |
CHANGE_UK = 40 | |
UKIP = 33 | |
class NorthWest(Region): | |
SEATS = 8 | |
BREXIT_PARTY = 325 | |
LAB = 293 | |
LIB_DEM = 130 | |
CON = 90 | |
GREEN = 71 | |
CHANGE_UK = 35 | |
UKIP = 30 | |
class YorksAndHumber(Region): | |
SEATS = 6 | |
BREXIT_PARTY = 350 | |
LAB = 195 | |
LIB_DEM = 160 | |
CON = 90 | |
GREEN = 90 | |
CHANGE_UK = 58 | |
UKIP = 30 | |
class EastMids(Region): | |
SEATS = 5 | |
BREXIT_PARTY = 368 | |
LAB = 185 | |
LIB_DEM = 113 | |
CON = 140 | |
GREEN = 93 | |
CHANGE_UK = 63 | |
UKIP = 25 | |
class WestMids(Region): | |
SEATS = 7 | |
BREXIT_PARTY = 360 | |
LAB = 188 | |
LIB_DEM = 120 | |
CON = 136 | |
GREEN = 103 | |
CHANGE_UK = 63 | |
UKIP = 18 | |
class East(Region): | |
SEATS = 7 | |
BREXIT_PARTY = 380 | |
LAB = 165 | |
LIB_DEM = 188 | |
CON = 88 | |
GREEN = 96 | |
CHANGE_UK = 33 | |
UKIP = 38 | |
class SouthWest(Region): | |
SEATS = 6 | |
BREXIT_PARTY = 350 | |
LAB = 195 | |
LIB_DEM = 160 | |
CON = 90 | |
GREEN = 90 | |
CHANGE_UK = 58 | |
UKIP = 30 | |
class SouthEast(Region): | |
SEATS = 10 | |
BREXIT_PARTY = 355 | |
LAB = 145 | |
LIB_DEM = 193 | |
CON = 115 | |
GREEN = 106 | |
CHANGE_UK = 53 | |
UKIP = 25 | |
class London(Region): | |
SEATS = 8 | |
BREXIT_PARTY = 200 | |
LAB = 273 | |
LIB_DEM = 230 | |
CON = 105 | |
GREEN = 98 | |
CHANGE_UK = 58 | |
UKIP = 15 | |
class Scotland(Region): | |
SEATS = 6 | |
SNP = 363 | |
BREXIT_PARTY = 154 | |
LAB = 131 | |
LIB_DEM = 105 | |
CON = 103 | |
GREEN = 93 | |
CHANGE_UK = 28 | |
UKIP = 25 | |
class Wales(Region): | |
SEATS = 4 | |
PLAID = 203 | |
BREXIT_PARTY = 343 | |
LAB = 170 | |
LIB_DEM = 95 | |
CON = 48 | |
GREEN = 70 | |
CHANGE_UK = 25 | |
UKIP = 33 | |
def print_dict(party_dict, is_votes=False): | |
party_tuples = list(party_dict.items()) | |
party_tuples.sort(key=lambda x: x[0]) | |
party_tuples.sort(key=lambda x: -x[1]) | |
if is_votes: | |
party_tuples = [ | |
(i[0], "{}%".format(float(i[1])/10)) for i in party_tuples | |
] | |
result = ", ".join(f"{i}={v}" for i, v in party_tuples) | |
print(result) | |
if __name__ == "__main__": | |
for region_cls in Region.__subclasses__(): | |
region = region_cls() | |
print(region_cls.__name__) | |
print("current seats") | |
print_dict(calculate_seats(region.party_dict, region.SEATS)) | |
print() | |
print("current votes") | |
print_dict(region.party_dict, is_votes=True) | |
result = calculate_min_max(region) | |
print("min seats") | |
print_dict(result["min_seats"]) | |
print("min votes") | |
print_dict(result["min_result"], is_votes=True) | |
print() | |
print("max seats") | |
print_dict(result["max_seats"]) | |
print("max votes") | |
print_dict(result["max_result"], is_votes=True) | |
print("\n\n") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment