Skip to content

Instantly share code, notes, and snippets.

@fredkingham
Last active May 20, 2019 20:59
Show Gist options
  • Save fredkingham/8fe1506c6393a6919baa98789b159856 to your computer and use it in GitHub Desktop.
Save fredkingham/8fe1506c6393a6919baa98789b159856 to your computer and use it in GitHub Desktop.
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