Module spatial_inequality.optimization.run_metrics
Structured information container, to track specified metrics over a single run of our algorithm.
Expand source code
"""
Structured information container, to track specified metrics over a single run
of our algorithm.
"""
import json
import copy
from time import time
class RunMetrics:
"""
This class is used to track metrics over a single run of the redistricting
algorithm (done over a single state). Most of its methods are intended to be
used as callbacks at specific points of its iterations.
Attributes:
__per_student_funding_whole_state (float): Per-student funding across
the state.
__spatial_inequality_values (list of float): List of inequality values,
registered throughout the algorithm's run.
__percentage_of_schools_redistricted (list of float): List of
percentages of schools redistricted at each iteration of the
algorithm, compared to their initial assignment.
__number_of_districts (list of int): List of absolute number of existing
district at each iteration.
__move_history (list of tuple): List of all redistricting moves
performed throughout the algorithm's run, containing a school's
standardized NCES ID, a source district's standardized NCES ID, and
a destination district's standardized NCES ID.
__district_assignment_by_school_id (dict of str: dict): Mapping between
a checkpoint label (i.e., 'before' or 'after') and the corresponding
school/district assignment.
__per_student_funding_by_district_id (dict of str: dict): Mapping
between a checkpoint label (i.e., 'before' or 'after') and the
corresponding per-student funding.
"""
# One time measurements
__per_student_funding_whole_state = None
# Lists of overtime measurements
__spatial_inequality_values = None
__percentage_of_schools_redistricted = None
__number_of_districts = None
__move_history = None
# Before/after comparison measurements
__district_assignment_by_school_id = None
__per_student_funding_by_district_id = None
# One time metrics
__start_timestamp = None
__end_timestamp = None
def __init__(self):
# Overtime measurement initialization
self.__spatial_inequality_values = []
self.__percentage_of_schools_redistricted = []
self.__number_of_districts = []
self.__move_history = []
# Before/after measurement initialization
self.__district_assignment_by_school_id = {}
self.__per_student_funding_by_district_id = {}
def on_init(self, schools, districts, lookup):
"""
Initializes necessary class' attributes and stores initial metrics'
values (prior to the algorithm's iterations).
This method should be called immediately after school/district
assignment is finalized.
Args:
schools (list of optimization.entity_nodes.School): List of all
initialized School instances.
districts (list of optimization.entity_nodes.District): List of all
initialized District instances.
lookup (optimization.lookup.Lookup): Lookup instance.
"""
# Calculate average funding per student
total_funding_in_state = sum(map(lambda x: x.get_total_funding(), districts))
total_students_in_state = sum(map(lambda x: x.get_total_students(), districts))
# Update variables
self.__per_student_funding_whole_state = total_funding_in_state / total_students_in_state
self.__checkpoint_before_and_after_measurements(schools, districts, lookup, "before")
self.__start_timestamp = time()
def on_end(self, schools, districts, lookup):
"""
Initializes necessary class' attributes and stores final metrics'
values (after the algorithm concludes its run).
This method should be called at the end of the algorithm's last
iteration.
Args:
schools (list of optimization.entity_nodes.School): List of all
initialized School instances.
districts (list of optimization.entity_nodes.District): List of all
initialized District instances.
lookup (optimization.lookup.Lookup): Lookup instance.
"""
self.__checkpoint_before_and_after_measurements(schools, districts, lookup, "after")
self.on_update(schools, districts, lookup)
self.__end_timestamp = time()
def on_update(self, schools, districts, lookup):
"""
Updates necessary class' attributes and calculates runtime metrics'
values (during the algorithm's run).
This method should be called at the end of each of the algorithm's
iterations.
Args:
schools (list of optimization.entity_nodes.School): List of all
initialized School instances.
districts (list of optimization.entity_nodes.District): List of all
initialized District instances.
lookup (optimization.lookup.Lookup): Lookup instance.
"""
# Calculate inequality
cur_inequality = self.__calculate_inequality(districts, lookup)
self.__spatial_inequality_values.append(cur_inequality)
# Calculate percentage of redistricted schools
cur_percentage = self.__calculate_percentage_of_schools_redistricted(schools, lookup)
self.__percentage_of_schools_redistricted.append(cur_percentage)
# Calculate number of districts
self.__number_of_districts.append(len(districts))
def on_move(self, iteration_idx, moves):
"""
Registers all redistricting moves performed during one of the
algorithm's iterations.
This method should be called during any/all of the algorithm's
iterations, immediately after schools are effectively redistricted. If
schools are not redistricted, this method needs not be called.
Args:
iteration_idx (int): Current iteration's index (needs not be a
continuous variable).
moves (list of tuple): List of all moves performed during the
current iteration of the algorithm (i.e., tuples comprised of a
redistricted school standardized NCES ID, its source district
standardized NCES ID and its destination district standardized
NCES ID).
"""
for move in moves:
# Get configuration
school_id = move[0]
from_district_id = move[1]
to_district_id = move[2]
# Add move to moves' history
self.__move_history.append((
iteration_idx,
school_id,
from_district_id,
to_district_id
))
def as_dict(self):
"""
Creates a dictionary containing all metrics tracked.
Returns:
dict: Resulting dictionary with tracked metrics.
"""
return {
# Overtime measurements
"spatial_inequality": copy.copy(self.__spatial_inequality_values),
"percentage_of_schools_redistricted": copy.copy(self.__percentage_of_schools_redistricted),
"number_of_districts": copy.copy(self.__number_of_districts),
"move_history": copy.copy(self.__move_history),
# Before/after measurements
"district_assignment_by_school_id": copy.copy(self.__district_assignment_by_school_id),
"per_student_funding_by_district_id": copy.copy(self.__per_student_funding_by_district_id),
# One time measurements
"time_elapsed": self.__end_timestamp - self.__start_timestamp,
"per_student_funding_whole_state": self.__per_student_funding_whole_state
}
def to_file(self, filepath):
"""
Writes all tracked metrics to a JSON-formatted file.
Args:
filepath (str): Full path for output file, including filename and
extension.
"""
with open(filepath, "w") as file:
json.dump(self.as_dict(), file)
def __len__(self):
return len(self.__spatial_inequality_values)
def __checkpoint_before_and_after_measurements(self, schools, districts, lookup, label):
"""
Auxiliary method to handle class' attributes update/initialization upon
the algorithm's start or end.
Args:
schools (list of optimization.entity_nodes.School): List of all
initialized School instances.
districts (list of optimization.entity_nodes.District): List of all
initialized District instances.
lookup (optimization.lookup.Lookup): Lookup instance.
label (str): Should be 'before' or 'after'.
Raises:
AssertionError: Whenever there is an invalid school/district
assignment, missing information on number of students or overall
funding, or when an invalid label is provided.
"""
assert(self.__district_assignment_by_school_id is not None)
assert(self.__per_student_funding_by_district_id is not None)
assert(label == "before" or label == "after")
# Initialize school assignment
get_district_id = lambda school: lookup.get_district_by_school_id(school.get_id()).get_id()
self.__district_assignment_by_school_id[label] = dict(map(
lambda school: (school.get_id(), get_district_id(school)),
schools
))
# Initialize district funding
get_per_student_funding = lambda district: district.get_total_funding() / district.get_total_students()
self.__per_student_funding_by_district_id[label] = dict(map(
lambda district: (district.get_id(), get_per_student_funding(district)),
districts
))
def __calculate_inequality(self, districts, lookup):
"""
Auxiliary method to calculate spatial inequality for current
school/district assignment.
Args:
districts (list of optimization.entity_nodes.District): List of all
initialized District instances.
lookup (optimization.lookup.Lookup): Lookup instance.
Returns:
float: Spatial inequality index.
"""
get_per_student_funding = lambda district: district.get_total_funding() / district.get_total_students()
abs_funding_diff = lambda x,y: abs(get_per_student_funding(x) - get_per_student_funding(y))
overall_inequality = 0
normalization_factor = 0
for district in districts:
neighboring_districts = lookup.get_neighboor_districts_by_district_id(district.get_id())
full_neighborhood = [*neighboring_districts, district]
ineq_contribution = sum(map(
lambda x: abs_funding_diff(district, x),
full_neighborhood
))
overall_inequality += ineq_contribution / len(full_neighborhood)
normalization_factor += get_per_student_funding(district)
return overall_inequality / normalization_factor
def __calculate_percentage_of_schools_redistricted(self, schools, lookup):
"""
Auxiliary method to calculate the percentage of schools that are
currently redistricted (compared to their initial assignment).
Args:
districts (list of optimization.entity_nodes.School): List of all
initialized School instances.
lookup (optimization.lookup.Lookup): Lookup instance.
Returns:
float: Percentage of currently redistricted schools.
"""
# Get initial assignment
initial_district_assignment = self.__district_assignment_by_school_id["before"]
# Extract current assignment
get_district_id = lambda school: lookup.get_district_by_school_id(school.get_id()).get_id()
cur_district_assignment = dict(map(
lambda school: (school.get_id(), get_district_id(school)),
schools
))
# Filter schools that were redistricted
is_redistricted = lambda school: initial_district_assignment[school.get_id()] != cur_district_assignment[school.get_id()]
schools_redistricted = list(filter(
is_redistricted,
schools
))
# Return final percentage
return 100 * len(schools_redistricted) / len(schools)
Classes
class RunMetrics
-
This class is used to track metrics over a single run of the redistricting algorithm (done over a single state). Most of its methods are intended to be used as callbacks at specific points of its iterations.
Attributes
__per_student_funding_whole_state
:float
- Per-student funding across the state.
__spatial_inequality_values
:list
offloat
- List of inequality values, registered throughout the algorithm's run.
__percentage_of_schools_redistricted
:list
offloat
- List of percentages of schools redistricted at each iteration of the algorithm, compared to their initial assignment.
__number_of_districts
:list
ofint
- List of absolute number of existing district at each iteration.
__move_history
:list
oftuple
- List of all redistricting moves performed throughout the algorithm's run, containing a school's standardized NCES ID, a source district's standardized NCES ID, and a destination district's standardized NCES ID.
__district_assignment_by_school_id (dict of str: dict): Mapping between a checkpoint label (i.e., 'before' or 'after') and the corresponding school/district assignment. __per_student_funding_by_district_id (dict of str: dict): Mapping between a checkpoint label (i.e., 'before' or 'after') and the corresponding per-student funding.
Expand source code
class RunMetrics: """ This class is used to track metrics over a single run of the redistricting algorithm (done over a single state). Most of its methods are intended to be used as callbacks at specific points of its iterations. Attributes: __per_student_funding_whole_state (float): Per-student funding across the state. __spatial_inequality_values (list of float): List of inequality values, registered throughout the algorithm's run. __percentage_of_schools_redistricted (list of float): List of percentages of schools redistricted at each iteration of the algorithm, compared to their initial assignment. __number_of_districts (list of int): List of absolute number of existing district at each iteration. __move_history (list of tuple): List of all redistricting moves performed throughout the algorithm's run, containing a school's standardized NCES ID, a source district's standardized NCES ID, and a destination district's standardized NCES ID. __district_assignment_by_school_id (dict of str: dict): Mapping between a checkpoint label (i.e., 'before' or 'after') and the corresponding school/district assignment. __per_student_funding_by_district_id (dict of str: dict): Mapping between a checkpoint label (i.e., 'before' or 'after') and the corresponding per-student funding. """ # One time measurements __per_student_funding_whole_state = None # Lists of overtime measurements __spatial_inequality_values = None __percentage_of_schools_redistricted = None __number_of_districts = None __move_history = None # Before/after comparison measurements __district_assignment_by_school_id = None __per_student_funding_by_district_id = None # One time metrics __start_timestamp = None __end_timestamp = None def __init__(self): # Overtime measurement initialization self.__spatial_inequality_values = [] self.__percentage_of_schools_redistricted = [] self.__number_of_districts = [] self.__move_history = [] # Before/after measurement initialization self.__district_assignment_by_school_id = {} self.__per_student_funding_by_district_id = {} def on_init(self, schools, districts, lookup): """ Initializes necessary class' attributes and stores initial metrics' values (prior to the algorithm's iterations). This method should be called immediately after school/district assignment is finalized. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. """ # Calculate average funding per student total_funding_in_state = sum(map(lambda x: x.get_total_funding(), districts)) total_students_in_state = sum(map(lambda x: x.get_total_students(), districts)) # Update variables self.__per_student_funding_whole_state = total_funding_in_state / total_students_in_state self.__checkpoint_before_and_after_measurements(schools, districts, lookup, "before") self.__start_timestamp = time() def on_end(self, schools, districts, lookup): """ Initializes necessary class' attributes and stores final metrics' values (after the algorithm concludes its run). This method should be called at the end of the algorithm's last iteration. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. """ self.__checkpoint_before_and_after_measurements(schools, districts, lookup, "after") self.on_update(schools, districts, lookup) self.__end_timestamp = time() def on_update(self, schools, districts, lookup): """ Updates necessary class' attributes and calculates runtime metrics' values (during the algorithm's run). This method should be called at the end of each of the algorithm's iterations. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. """ # Calculate inequality cur_inequality = self.__calculate_inequality(districts, lookup) self.__spatial_inequality_values.append(cur_inequality) # Calculate percentage of redistricted schools cur_percentage = self.__calculate_percentage_of_schools_redistricted(schools, lookup) self.__percentage_of_schools_redistricted.append(cur_percentage) # Calculate number of districts self.__number_of_districts.append(len(districts)) def on_move(self, iteration_idx, moves): """ Registers all redistricting moves performed during one of the algorithm's iterations. This method should be called during any/all of the algorithm's iterations, immediately after schools are effectively redistricted. If schools are not redistricted, this method needs not be called. Args: iteration_idx (int): Current iteration's index (needs not be a continuous variable). moves (list of tuple): List of all moves performed during the current iteration of the algorithm (i.e., tuples comprised of a redistricted school standardized NCES ID, its source district standardized NCES ID and its destination district standardized NCES ID). """ for move in moves: # Get configuration school_id = move[0] from_district_id = move[1] to_district_id = move[2] # Add move to moves' history self.__move_history.append(( iteration_idx, school_id, from_district_id, to_district_id )) def as_dict(self): """ Creates a dictionary containing all metrics tracked. Returns: dict: Resulting dictionary with tracked metrics. """ return { # Overtime measurements "spatial_inequality": copy.copy(self.__spatial_inequality_values), "percentage_of_schools_redistricted": copy.copy(self.__percentage_of_schools_redistricted), "number_of_districts": copy.copy(self.__number_of_districts), "move_history": copy.copy(self.__move_history), # Before/after measurements "district_assignment_by_school_id": copy.copy(self.__district_assignment_by_school_id), "per_student_funding_by_district_id": copy.copy(self.__per_student_funding_by_district_id), # One time measurements "time_elapsed": self.__end_timestamp - self.__start_timestamp, "per_student_funding_whole_state": self.__per_student_funding_whole_state } def to_file(self, filepath): """ Writes all tracked metrics to a JSON-formatted file. Args: filepath (str): Full path for output file, including filename and extension. """ with open(filepath, "w") as file: json.dump(self.as_dict(), file) def __len__(self): return len(self.__spatial_inequality_values) def __checkpoint_before_and_after_measurements(self, schools, districts, lookup, label): """ Auxiliary method to handle class' attributes update/initialization upon the algorithm's start or end. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. label (str): Should be 'before' or 'after'. Raises: AssertionError: Whenever there is an invalid school/district assignment, missing information on number of students or overall funding, or when an invalid label is provided. """ assert(self.__district_assignment_by_school_id is not None) assert(self.__per_student_funding_by_district_id is not None) assert(label == "before" or label == "after") # Initialize school assignment get_district_id = lambda school: lookup.get_district_by_school_id(school.get_id()).get_id() self.__district_assignment_by_school_id[label] = dict(map( lambda school: (school.get_id(), get_district_id(school)), schools )) # Initialize district funding get_per_student_funding = lambda district: district.get_total_funding() / district.get_total_students() self.__per_student_funding_by_district_id[label] = dict(map( lambda district: (district.get_id(), get_per_student_funding(district)), districts )) def __calculate_inequality(self, districts, lookup): """ Auxiliary method to calculate spatial inequality for current school/district assignment. Args: districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. Returns: float: Spatial inequality index. """ get_per_student_funding = lambda district: district.get_total_funding() / district.get_total_students() abs_funding_diff = lambda x,y: abs(get_per_student_funding(x) - get_per_student_funding(y)) overall_inequality = 0 normalization_factor = 0 for district in districts: neighboring_districts = lookup.get_neighboor_districts_by_district_id(district.get_id()) full_neighborhood = [*neighboring_districts, district] ineq_contribution = sum(map( lambda x: abs_funding_diff(district, x), full_neighborhood )) overall_inequality += ineq_contribution / len(full_neighborhood) normalization_factor += get_per_student_funding(district) return overall_inequality / normalization_factor def __calculate_percentage_of_schools_redistricted(self, schools, lookup): """ Auxiliary method to calculate the percentage of schools that are currently redistricted (compared to their initial assignment). Args: districts (list of optimization.entity_nodes.School): List of all initialized School instances. lookup (optimization.lookup.Lookup): Lookup instance. Returns: float: Percentage of currently redistricted schools. """ # Get initial assignment initial_district_assignment = self.__district_assignment_by_school_id["before"] # Extract current assignment get_district_id = lambda school: lookup.get_district_by_school_id(school.get_id()).get_id() cur_district_assignment = dict(map( lambda school: (school.get_id(), get_district_id(school)), schools )) # Filter schools that were redistricted is_redistricted = lambda school: initial_district_assignment[school.get_id()] != cur_district_assignment[school.get_id()] schools_redistricted = list(filter( is_redistricted, schools )) # Return final percentage return 100 * len(schools_redistricted) / len(schools)
Methods
def as_dict(self)
-
Creates a dictionary containing all metrics tracked.
Returns
dict
- Resulting dictionary with tracked metrics.
Expand source code
def as_dict(self): """ Creates a dictionary containing all metrics tracked. Returns: dict: Resulting dictionary with tracked metrics. """ return { # Overtime measurements "spatial_inequality": copy.copy(self.__spatial_inequality_values), "percentage_of_schools_redistricted": copy.copy(self.__percentage_of_schools_redistricted), "number_of_districts": copy.copy(self.__number_of_districts), "move_history": copy.copy(self.__move_history), # Before/after measurements "district_assignment_by_school_id": copy.copy(self.__district_assignment_by_school_id), "per_student_funding_by_district_id": copy.copy(self.__per_student_funding_by_district_id), # One time measurements "time_elapsed": self.__end_timestamp - self.__start_timestamp, "per_student_funding_whole_state": self.__per_student_funding_whole_state }
def on_end(self, schools, districts, lookup)
-
Initializes necessary class' attributes and stores final metrics' values (after the algorithm concludes its run).
This method should be called at the end of the algorithm's last iteration.
Args
schools
:list
ofoptimization.entity_nodes.School
- List of all initialized School instances.
districts
:list
ofoptimization.entity_nodes.District
- List of all initialized District instances.
lookup
:optimization.lookup.Lookup
- Lookup instance.
Expand source code
def on_end(self, schools, districts, lookup): """ Initializes necessary class' attributes and stores final metrics' values (after the algorithm concludes its run). This method should be called at the end of the algorithm's last iteration. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. """ self.__checkpoint_before_and_after_measurements(schools, districts, lookup, "after") self.on_update(schools, districts, lookup) self.__end_timestamp = time()
def on_init(self, schools, districts, lookup)
-
Initializes necessary class' attributes and stores initial metrics' values (prior to the algorithm's iterations).
This method should be called immediately after school/district assignment is finalized.
Args
schools
:list
ofoptimization.entity_nodes.School
- List of all initialized School instances.
districts
:list
ofoptimization.entity_nodes.District
- List of all initialized District instances.
lookup
:optimization.lookup.Lookup
- Lookup instance.
Expand source code
def on_init(self, schools, districts, lookup): """ Initializes necessary class' attributes and stores initial metrics' values (prior to the algorithm's iterations). This method should be called immediately after school/district assignment is finalized. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. """ # Calculate average funding per student total_funding_in_state = sum(map(lambda x: x.get_total_funding(), districts)) total_students_in_state = sum(map(lambda x: x.get_total_students(), districts)) # Update variables self.__per_student_funding_whole_state = total_funding_in_state / total_students_in_state self.__checkpoint_before_and_after_measurements(schools, districts, lookup, "before") self.__start_timestamp = time()
def on_move(self, iteration_idx, moves)
-
Registers all redistricting moves performed during one of the algorithm's iterations.
This method should be called during any/all of the algorithm's iterations, immediately after schools are effectively redistricted. If schools are not redistricted, this method needs not be called.
Args
iteration_idx
:int
- Current iteration's index (needs not be a continuous variable).
moves
:list
oftuple
- List of all moves performed during the current iteration of the algorithm (i.e., tuples comprised of a redistricted school standardized NCES ID, its source district standardized NCES ID and its destination district standardized NCES ID).
Expand source code
def on_move(self, iteration_idx, moves): """ Registers all redistricting moves performed during one of the algorithm's iterations. This method should be called during any/all of the algorithm's iterations, immediately after schools are effectively redistricted. If schools are not redistricted, this method needs not be called. Args: iteration_idx (int): Current iteration's index (needs not be a continuous variable). moves (list of tuple): List of all moves performed during the current iteration of the algorithm (i.e., tuples comprised of a redistricted school standardized NCES ID, its source district standardized NCES ID and its destination district standardized NCES ID). """ for move in moves: # Get configuration school_id = move[0] from_district_id = move[1] to_district_id = move[2] # Add move to moves' history self.__move_history.append(( iteration_idx, school_id, from_district_id, to_district_id ))
def on_update(self, schools, districts, lookup)
-
Updates necessary class' attributes and calculates runtime metrics' values (during the algorithm's run).
This method should be called at the end of each of the algorithm's iterations.
Args
schools
:list
ofoptimization.entity_nodes.School
- List of all initialized School instances.
districts
:list
ofoptimization.entity_nodes.District
- List of all initialized District instances.
lookup
:optimization.lookup.Lookup
- Lookup instance.
Expand source code
def on_update(self, schools, districts, lookup): """ Updates necessary class' attributes and calculates runtime metrics' values (during the algorithm's run). This method should be called at the end of each of the algorithm's iterations. Args: schools (list of optimization.entity_nodes.School): List of all initialized School instances. districts (list of optimization.entity_nodes.District): List of all initialized District instances. lookup (optimization.lookup.Lookup): Lookup instance. """ # Calculate inequality cur_inequality = self.__calculate_inequality(districts, lookup) self.__spatial_inequality_values.append(cur_inequality) # Calculate percentage of redistricted schools cur_percentage = self.__calculate_percentage_of_schools_redistricted(schools, lookup) self.__percentage_of_schools_redistricted.append(cur_percentage) # Calculate number of districts self.__number_of_districts.append(len(districts))
def to_file(self, filepath)
-
Writes all tracked metrics to a JSON-formatted file.
Args
filepath
:str
- Full path for output file, including filename and extension.
Expand source code
def to_file(self, filepath): """ Writes all tracked metrics to a JSON-formatted file. Args: filepath (str): Full path for output file, including filename and extension. """ with open(filepath, "w") as file: json.dump(self.as_dict(), file)