Source code for gerrychain.optimization.gingleator

from .optimization import SingleMetricOptimizer

from functools import partial
import numpy as np
import warnings
from typing import Callable, Iterable, Optional, Union
from gerrychain.partition import Partition
from gerrychain.constraints import Validator, Bounds


[docs]class Gingleator(SingleMetricOptimizer): """ `Gingleator` is a child class of `SingleMetricOptimizer` which can be used to search for plans with increased numbers of Gingles' districts. A gingles district (named for the Supreme Court case Thornburg v. Gingles) is a district that is majority-minority. aka 50% + 1 of some population subgroup. Demonstrating additional Gingles districts is one of the litmus tests used in bringing forth a VRA case. """ def __init__( self, proposal: Callable, constraints: Union[Iterable[Callable], Validator, Iterable[Bounds], Callable], initial_state: Partition, minority_perc_col: Optional[str] = None, threshold: float = 0.5, score_function: Optional[Callable] = None, minority_pop_col: Optional[str] = None, total_pop_col: str = "TOTPOP", min_perc_column_name: str = "_gingleator_auxiliary_helper_updater_min_perc_col", ): """ :param proposal: Function proposing the next state from the current state. :type proposal: Callable :param constraints: A function with signature ``Partition -> bool`` determining whether the proposed next state is valid (passes all binary constraints). Usually this is a :class:`~gerrychain.constraints.Validator` class instance. :type constraints: Union[Iterable[Callable], Validator, Iterable[Bounds], Callable] :param initial_state: Initial :class:`gerrychain.partition.Partition` class. :type initial_state: Partition :param minority_perc_col: The name of the updater mapping of district ids to the fraction of minority population within that district. If no updater is specified, one is made according to the ``min_perc_column_name`` parameter. Defaults to None. :type minority_perc_col: Optional[str] :param threshold: Fraction beyond which to consider something a "Gingles" (or opportunity) district. Defaults to 0.5. :type threshold: float, optional :param score_function: The function to use during optimization. Should have the signature ``Partition * str (minority_perc_col) * float (threshold) -> 'a where 'a is Comparable``. This class implements a few potential choices as class methods. Defaults to None. :type score_function: Optional[Callable] :param minority_pop_col: If minority_perc_col is not defined, the minority population column with which to compute percentage. Defaults to None. :type minority_pop_col: Optional[str] :param total_pop_col: If minority_perc_col is defined, the total population column with which to compute percentage. Defaults to "TOTPOP". :type total_pop_col: str, optional :param min_perc_column_name: If minority_perc_col is not defined, the name to give the created percentage updater. Defaults to "_gingleator_auxiliary_helper_updater_min_perc_col". :type min_perc_column_name: str, optional """ if minority_perc_col is None and minority_pop_col is None: raise ValueError( "`minority_perc_col` and `minority_pop_col` cannot both be `None`. \ Unclear how to compute gingles district." ) elif minority_perc_col is not None and minority_pop_col is not None: warnings.warn( "`minority_perc_col` and `minority_pop_col` are both specified. By \ default `minority_perc_col` will be used." ) score_function = ( self.num_opportunity_dists if score_function is None else score_function ) if minority_perc_col is None: perc_up = { min_perc_column_name: lambda part: { k: part[minority_pop_col][k] / part[total_pop_col][k] for k in part.parts.keys() } } initial_state.updaters.update(perc_up) minority_perc_col = min_perc_column_name score = partial( score_function, minority_perc_col=minority_perc_col, threshold=threshold ) super().__init__(proposal, constraints, initial_state, score, maximize=True) # --------------------- # Score functions # ---------------------
[docs] @classmethod def num_opportunity_dists( cls, part: Partition, minority_perc_col: str, threshold: float ) -> int: """ Given a partition, returns the number of opportunity districts. :param part: Partition to score. :type part: Partition :param minority_perc_col: The name of the updater mapping of district ids to the fraction of minority population within that district. :type minority_perc_col: str :param threshold: Fraction beyond which to consider something a "Gingles" (or opportunity) district. :type threshold: float :returns: Number of opportunity districts. :rtype: int """ dist_percs = part[minority_perc_col].values() return sum(list(map(lambda v: v >= threshold, dist_percs)))
[docs] @classmethod def reward_partial_dist( cls, part: Partition, minority_perc_col: str, threshold: float ) -> float: """ Given a partition, returns the number of opportunity districts + the percentage of the next highest district. :param part: Partition to score. :type part: Partition :param minority_perc_col: The name of the updater mapping of district ids to the fraction of minority population within that district. :type minority_perc_col: str :param threshold: Fraction beyond which to consider something a "Gingles" (or opportunity) district. :type threshold: float :returns: Number of opportunity districts + the percentage of the next highest district. :rtype: float """ dist_percs = part[minority_perc_col].values() num_opport_dists = sum(list(map(lambda v: v >= threshold, dist_percs))) next_dist = max(i for i in dist_percs if i < threshold) return num_opport_dists + next_dist
[docs] @classmethod def reward_next_highest_close( cls, part: Partition, minority_perc_col: str, threshold: float ): """ Given a partition, returns the number of opportunity districts, if no additional district is within 10% of reaching the threshold. If one is, the distance that district is from the threshold is scaled between 0 and 1 and added to the count of opportunity districts. :param part: Partition to score. :type part: Partition :param minority_perc_col: The name of the updater mapping of district ids to the fraction of minority population within that district. :type minority_perc_col: str :param threshold: Fraction beyond which to consider something a "Gingles" (or opportunity) district. :type threshold: float :returns: Number of opportunity districts + (next highest district - (threshold - 0.1)) * 10 :rtype: float """ dist_percs = part[minority_perc_col].values() num_opport_dists = sum(list(map(lambda v: v >= threshold, dist_percs))) next_dist = max(i for i in dist_percs if i < threshold) if next_dist < threshold - 0.1: return num_opport_dists else: return num_opport_dists + (next_dist - threshold + 0.1) * 10
[docs] @classmethod def penalize_maximum_over( cls, part: Partition, minority_perc_col: str, threshold: float ): """ Given a partition, returns the number of opportunity districts + (1 - the maximum excess) scaled to between 0 and 1. :param part: Partition to score. :type part: Partition :param minority_perc_col: The name of the updater mapping of district ids to the fraction of minority population within that district. :type minority_perc_col: str :param threshold: Fraction beyond which to consider something a "Gingles" (or opportunity) district. :type threshold: float :returns: Number of opportunity districts + (1 - the maximum excess) / (1 - threshold) :rtype: float """ dist_percs = part[minority_perc_col].values() num_opportunity_dists = sum(list(map(lambda v: v >= threshold, dist_percs))) if num_opportunity_dists == 0: return 0 else: max_dist = max(dist_percs) return num_opportunity_dists + (1 - max_dist) / (1 - threshold)
[docs] @classmethod def penalize_avg_over( cls, part: Partition, minority_perc_col: str, threshold: float ): """ Given a partition, returns the number of opportunity districts + (1 - the average excess) scaled to between 0 and 1. :param part: Partition to score. :type part: Partition :param minority_perc_col: The name of the updater mapping of district ids to the fraction of minority population within that district. :type minority_perc_col: str :param threshold: Fraction beyond which to consider something a "Gingles" (or opportunity) district. :type threshold: float :returns: Number of opportunity districts + (1 - the average excess) :rtype: float """ dist_percs = part[minority_perc_col].values() opport_dists = list(filter(lambda v: v >= threshold, dist_percs)) if opport_dists == []: return 0 else: num_opportunity_dists = len(opport_dists) avg_opportunity_dist = np.mean(opport_dists) return num_opportunity_dists + (1 - avg_opportunity_dist) / (1 - threshold)