def centroid(self): """The average centroid of the community's precincts. """ precinct_X = [] precinct_Y = [] for precinct in self.precincts.values(): precinct_X.append(precinct.centroid[0]) precinct_Y.append(precinct.centroid[1]) return [average(precinct_X), average(precinct_Y)]
def get_quantification_values(table): """Gets the statewide 0 to 1 and -1 to 1 scores from a data table. :param table: 2d list of a quantification output from the c++ algorithm implementation. :type table: list :return: 0 to 1 quantification value, and -1 to 1 quantification value. :rtype: float, float """ gerry_score = average(table[0]) avg_rep = average([d["REP"] for d in table[-1]]) partisanship_score = 2 * (0.5 - avg_rep) * gerry_score return gerry_score, partisanship_score
def update_partisanship(self): """Updates the partisanship attribute for this community. """ self.partisanship = [ average([eval(f"p.{attr}") for p in self.precincts.values()]) for attr in self.parties ]
def update_partisanship_stdev(self): """Updates the partisanship_stdev attribute for this community. """ party_stdevs = [] for party in self.parties: percentage_func = eval(f"lambda p: p.{party}") precinct_percentages = [] for precinct in self.precincts.values(): precinct_percentages.append(percentage_func(precinct)) party_stdevs.append(standard_deviation(precinct_percentages)) self.partisanship_stdev = average(party_stdevs)
def _check_communities_complete(communities, percentage): """Checks if communities have satifsfactory populations. :param communities: A list of communities. :type communities: list of `hacking_the_election.utils.community.Community` :param percentage: The maximum tolerated percentage difference between two communities. :type percentage: float :return: Whether or not the communities fulfil population requirements. :rtype: bool """ ideal_pop = average([c.population for c in communities]) for community in communities: if abs(community.population - ideal_pop) / ideal_pop > (percentage / 200): return False return True
def optimize_population_stdev(communities, graph, animation_dir=None): """Takes a set of communities and exchanges precincts such that the standard deviation of the populations of their precincts are as low as possible. :param communities: The current state of the communities within a state. :type communities: list of `hacking_the_election.utils.community.Community` :param graph: A graph containing precinct data within a state. :type graph: `pygraph.classes.graph.graph` :param animation_dir: Path to the directory where animation files should be saved, defaults to None :type animation_dir: str or NoneType """ for community in communities: community.update_population_stdev() if animation_dir is not None: draw_state(graph, animation_dir) best_communities = [copy.copy(c) for c in communities] last_communities = [] while True: # Find most inconsistent population community. community = max(communities, key=lambda c: c.population_stdev) iteration_stdevs = [c.population_stdev for c in communities] avg_stdev = average(iteration_stdevs) rounded_stdevs = [round(stdev, 3) for stdev in iteration_stdevs] print(rounded_stdevs, round(avg_stdev, 3)) if avg_stdev > average([c.population_stdev for c in best_communities]): best_communities = [copy.copy(c) for c in communities] if last_communities != []: # Check if anything changed. If not, exit the function. changed = False try: for c in communities: for c2 in last_communities: if c.id == c2.id: if c.precincts != c2.precincts: changed = True raise LoopBreakException except LoopBreakException: pass if not changed: # Revert to best community state. for c in best_communities: for c2 in communities: if c.id == c2.id: c2 = c for c in communities: for precinct in c.precincts.values(): precinct.community = c.id rounded_stdevs = [ round(c.population_stdev, 3) for c in communities ] avg_stdev = average([c.population_stdev for c in communities]) print(rounded_stdevs, round(avg_stdev, 3)) if animation_dir is not None: draw_state(graph, animation_dir) return # Not deepcopy so that precinct objects are not copied (saves memory). last_communities = [copy.copy(c) for c in communities] giveable_precincts = get_giveable_precincts(graph, communities, community.id) for precinct, other_community in giveable_precincts: starting_stdev = average([c.population_stdev for c in communities]) community.give_precinct(other_community, precinct.id, update={"population_stdev"}) # Check if exchange made `community` non-contiguous. # Or if it hurt the previous population stdev. if (len(get_components(community.induced_subgraph)) > 1 or community.population_stdev > starting_stdev): other_community.give_precinct(community, precinct.id, update={"population_stdev"}) else: average_stdev = average( [c.population_stdev for c in communities]) if average_stdev > starting_stdev: other_community.give_precinct(community, precinct.id, update={"population_stdev"}) else: if animation_dir is not None: draw_state(graph, animation_dir) takeable_precincts = get_takeable_precincts(graph, communities, community.id) for precinct, other_community in takeable_precincts: starting_stdev = average([c.population_stdev for c in communities]) other_community.give_precinct(community, precinct.id, update={"population_stdev"}) # Check if exchange made `community` non-contiguous. # Or if it hurt the previous population stdev. if len(get_components(other_community.induced_subgraph)) > 1: community.give_precinct(other_community, precinct.id, update={"population_stdev"}) else: average_stdev = average( [c.population_stdev for c in communities]) if average_stdev > starting_stdev: community.give_precinct(other_community, precinct.id, update={"population_stdev"}) else: if animation_dir is not None: draw_state(graph, animation_dir)
def optimize_compactness(communities, graph, animation_dir=None): """Takes a set of communities and exchanges precincts in a way that maximizes compactness. :param communities: The current state of the communities within a state. :type communities: list of `hacking_the_election.utils.community.Community` :param graph: A graph containing precinct data within a state. :type graph: `pygraph.classes.graph.graph` :param animation_dir: Path to the directory where animation files should be saved, defaults to None :type animation_dir: str or NoneType """ for community in communities: community.update_imprecise_compactness() if animation_dir is not None: draw_state(graph, animation_dir) best_communities = [copy.copy(c) for c in communities] last_communities = [] iterations_since_best = 0 n = 0 while True: # Stop if number of iterations since the best # communities so far is more than N. if min([c.imprecise_compactness for c in communities]) \ > min([c.imprecise_compactness for c in best_communities]): # Current communities are new best. best_communities = [copy.copy(c) for c in communities] iterations_since_best = 0 else: iterations_since_best += 1 if iterations_since_best > N: # Revert to best and exit function. for c in communities: for bc in best_communities: if c.id == bc.id: c = bc rounded_compactnesses = [round(c.imprecise_compactness, 3) for c in communities] print(rounded_compactnesses, min(rounded_compactnesses)) return # Stop if there has been no change in communities this iteration. if last_communities != []: try: for c in communities: for lc in last_communities: if c.id == lc.id: if c.precincts != lc.precincts: changed = True raise LoopBreakException # Revert to best and exit function. # (LoopBreakException was not raised, # so communities did not change). for c in communities: for bc in best_communities: if c.id == bc.id: c = bc rounded_compactnesses = [round(c.imprecise_compactness, 3) for c in communities] print(rounded_compactnesses, min(rounded_compactnesses)) return except LoopBreakException: pass community = communities[n] # Precinct centroid coords X = [] Y = [] precinct_areas = [] for p in community.precincts.values(): X.append(p.centroid[0]) Y.append(p.centroid[1]) precinct_areas.append(p.coords.area) center = [average(X), average(Y)] radius = math.sqrt(sum(precinct_areas) / math.pi) for _ in range(M): # Communities that have exchanged precincts with `community` other_communities = set() giveable_precincts = get_giveable_precincts( graph, communities, community.id) for precinct, other_community in giveable_precincts: if get_distance(precinct.centroid, center) > radius: community.give_precinct( other_community, precinct.id) if len(get_components(community.induced_subgraph)) > 1: # Giving precinct made `community` non contiguous. other_community.give_precinct( community, precinct.id) else: other_communities.add(other_community) takeable_precincts = get_takeable_precincts( graph, communities, community.id) for precinct, other_community in takeable_precincts: if get_distance(precinct.centroid, center) <= radius: other_community.give_precinct( community, precinct.id) if len(get_components(other_community.induced_subgraph)) > 1: # Giving precicnt made `other_community` non contiguous. community.give_precinct( other_community, precinct.id) else: other_communities.add(other_community) for other_community in other_communities: other_community.update_imprecise_compactness() other_community.update_imprecise_compactness() rounded_compactnesses = [round(c.imprecise_compactness, 3) for c in communities] print(rounded_compactnesses, min(rounded_compactnesses)) n += 1 if n == len(communities): n = 0
def optimize_population(communities, graph, percentage, animation_dir=None): """Takes a set of communities and exchanges precincts so that the population is as evenly distributed as possible. :param communities: The current state of the communities within a state. :type communities: list of `hacking_the_election.utils.community.Community` :param graph: A graph containing precinct data within a state. :type graph: `pygraph.classes.graph.graph` :param percentage: By how much the populations are able to deviate from one another. A value of 1% means that all the communities must be within 0.5% of the ideal population, so that they are guaranteed to be within 1% of each other. :type percentage: float between 0 and 1 :param animation_dir: Path to the directory where animation files should be saved, defaults to None :type animation_dir: str or NoneType """ for community in communities: community.update_population() if animation_dir is not None: draw_state(graph, animation_dir) ideal_population = average([c.population for c in communities]) print(f"{ideal_population=}") while not _check_communities_complete(communities, percentage): community = max(communities, key=lambda c: abs(c.population - ideal_population)) if community.population > ideal_population: giveable_precincts = get_giveable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid giveable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in giveable_precincts ] max_distance = max(giveable_precinct_weights) giveable_precinct_weights = \ [max_distance - weight for weight in giveable_precinct_weights] while community.population > ideal_population: if giveable_precincts == []: giveable_precincts = get_giveable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid giveable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in giveable_precincts ] max_distance = max(giveable_precinct_weights) giveable_precinct_weights = \ [max_distance - weight for weight in giveable_precinct_weights] # Choose random precinct and community and remove from lists. precinct, other_community = random.choices( giveable_precincts, weights=giveable_precinct_weights)[0] giveable_precinct_weights.pop( giveable_precincts.index((precinct, other_community))) giveable_precincts.remove((precinct, other_community)) community.give_precinct(other_community, precinct.id, update={"population"}) if len(get_components(community.induced_subgraph)) > 1: # Give back precinct. other_community.give_precinct(community, precinct.id, update={"population"}) elif community.population < ideal_population: takeable_precincts = get_takeable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid takeable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in takeable_precincts ] while community.population < ideal_population: if takeable_precincts == []: takeable_precincts = get_takeable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid takeable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in takeable_precincts ] precinct, other_community = random.choices( takeable_precincts, weights=takeable_precinct_weights)[0] takeable_precinct_weights.pop( takeable_precincts.index((precinct, other_community))) takeable_precincts.remove((precinct, other_community)) other_community.give_precinct(community, precinct.id, update={"population"}) if len(get_components(other_community.induced_subgraph)) > 1: # Give back precinct. community.give_precinct(other_community, precinct.id, update={"population"}) if animation_dir is not None: draw_state(graph, animation_dir) print([c.population for c in communities], community.population)