Created
August 10, 2011 16:20
-
-
Save ghing/1137317 to your computer and use it in GitHub Desktop.
Custom calculators for Ohio District Builder instance
This file contains hidden or 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 sys | |
from django.conf import settings | |
from publicmapping.redistricting.calculators import CalculatorBase, SplitCounter, Roeck | |
from decimal import Decimal | |
class ValueRange(CalculatorBase): | |
""" | |
Determine a value, and indicate if it falls within a range | |
This calculator differs from the default Range calculator because it | |
checks the value argument (usually passed from another calculator) | |
rather than operating on a district or plan. | |
It also differs in that it checks if the value is >= min and <= max | |
rather than > min and < max | |
""" | |
def compute(self, **kwargs): | |
""" | |
Calculate and determine if a value lies within a range. | |
""" | |
self.result = 0 | |
val = self.get_value('value') | |
minval = self.get_value('min',district) | |
maxval = self.get_value('max',district) | |
if val is None or minval is None or maxval is None: | |
return | |
if float(val) >= float(minval) and float(val) <= float(maxval): | |
self.result += 1 | |
class Difference(CalculatorBase): | |
""" | |
Return the difference of two values. | |
For districts or numbers, this calculator will return the difference of two | |
arguments. | |
The first argument should be assigned the argument name "value1" and | |
the second argument should be assigned the argument name "value2". The | |
difference will then be calculated as value1 - value2. | |
""" | |
def compute(self, **kwargs): | |
""" | |
Calculate the sum of a series of values. | |
Keywords: | |
district -- A district whose values should be subtracted. | |
version -- Optional. The version of the plan, defaults to the | |
most recent version. | |
list -- A list of values to sum, when summing a set of | |
ScoreArguments. | |
""" | |
if settings.DEBUG: | |
sys.stderr.write('Entering Difference.compute()') | |
self.result = 0 | |
if 'value1' in self.arg_dict and 'value2' in self.arg_dict: | |
value1 = self.get_value('value1') | |
value2 = self.get_value('value2') | |
self.result = value1 - value2 | |
if settings.DEBUG: | |
sys.stderr.write('value1 = %d, value2 = %d, self.result = %d\n' % (value1, value2, self.result)) | |
def html(self): | |
""" | |
Generate an HTML representation of the difference score. This | |
is represented as a decimal formatted with commas or "n/a". | |
Returns: | |
The result wrapped in an HTML SPAN element: "<span>1</span>". | |
""" | |
if isinstance(self.result, Decimal): | |
result = locale.format("%d", self.result, grouping=True) | |
return '<span>%s</span>' % result | |
elif not self.result is None: | |
return '<span>%s</span>' % self.result | |
else: | |
return '<span>N/A</span>' | |
class MultiplyValues(CalculatorBase): | |
""" | |
Multiply all values. | |
For districts, this calculator will multiply a series of arguments. | |
For plans, this calculator will multiply a series of arguments across | |
all districts. If a literal value is included in a plan calculation, | |
that literal value is combined with the subject value for each | |
district. | |
For lists of numbers, this calculator will return the product of the list. | |
Each argument should be assigned the argument name "valueN", where N | |
is a positive integer. The summation will add all arguments, starting | |
at position 1, until an argument is not found. | |
This calculator takes an optional "target" argument. If passed this | |
argument, the calculator will return a string suitable for display in | |
a plan summary | |
""" | |
def compute(self, **kwargs): | |
""" | |
Calculate the product of a series of values. | |
Keywords: | |
district -- A district whose values should be summed. | |
plan -- A plan whose district values should be summed. | |
version -- Optional. The version of the plan, defaults to the | |
most recent version. | |
list -- A list of values to multiplying, when multiplying a set of | |
ScoreArguments. | |
""" | |
if settings.DEBUG: | |
sys.stderr.write("Entering MultiplyValues.compute()\n") | |
self.result = 0 | |
if settings.DEBUG: | |
sys.stderr.write("self.arg_dict = %s\n" % (self.arg_dict)) | |
argnum = 1 | |
while ('value%d'%argnum) in self.arg_dict: | |
number = self.get_value('value%d'%argnum) | |
if settings.DEBUG: | |
sys.stderr.write("value%d found in arg_dict. value%d = %d\n" % (argnum, argnum, number)) | |
if not number is None: | |
if argnum == 1: | |
self.result = number | |
else: | |
self.result *= number | |
argnum += 1 | |
if settings.DEBUG: | |
sys.stderr.write("Leaving MultiplyValues.compute()\n") | |
def html(self): | |
""" | |
Generate an HTML representation of the summation score. This | |
is represented as a decimal formatted with commas or "n/a". | |
Returns: | |
The result wrapped in an HTML SPAN element: "<span>1</span>". | |
""" | |
if isinstance(self.result, Decimal): | |
result = locale.format("%d", self.result, grouping=True) | |
return '<span>%s</span>' % result | |
elif not self.result is None: | |
return '<span>%s</span>' % self.result | |
else: | |
return '<span>N/A</span>' | |
class AveragePartisanIndex(CalculatorBase): | |
""" | |
Calculate the partisan index across a number of elections. | |
""" | |
def compute(self, **kwargs): | |
""" | |
Compute the representational fairness. | |
Keywords: | |
plan -- A plan whose set of districts should be evaluated for | |
representational fairness. | |
version -- Optional. The version of the plan, defaults to the | |
most recent version. | |
""" | |
if settings.DEBUG: | |
sys.stderr.write("Leaving AveragePartisanIndex.compute()\n") | |
districts = [] | |
if 'plan' in kwargs: | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
districts = plan.get_districts_at_version(version, include_geom=False) | |
elif 'district' in kwargs : | |
districts.append(kwargs['district']) | |
else: | |
return | |
district_count = 0 | |
plan_dem_pi_sum = 0.0 | |
plan_rep_pi_sum = 0.0 | |
for district in districts: | |
if district.district_id == 0: | |
# do nothing with the unassigned districts | |
continue | |
district_count += 1 | |
if settings.DEBUG: | |
sys.stderr.write("district_count = %d\n" % (district_count)) | |
argnum = 0 | |
dem_pi_sum = 0.0 | |
rep_pi_sum = 0.0 | |
avg_dem_pi = 0.0 | |
avg_rep_pi = 0.0 | |
while (('democratic%d' % (argnum+1,)) in self.arg_dict and | |
('republican%d' % (argnum+1,)) in self.arg_dict): | |
argnum += 1 | |
dem = self.get_value('democratic%d'%argnum, district) | |
rep = self.get_value('republican%d'%argnum, district) | |
if dem is None or rep is None: | |
continue | |
dem = float(dem) | |
rep = float(rep) | |
if dem == 0.0 and rep == 0.0: | |
continue | |
dem_pi = dem / (rep + dem) | |
rep_pi = rep / (rep + dem) | |
dem_pi_sum += dem_pi | |
rep_pi_sum += rep_pi | |
avg_dem_pi = dem_pi_sum / argnum | |
avg_rep_pi = rep_pi_sum / argnum | |
plan_dem_pi_sum += avg_dem_pi | |
plan_rep_pi_sum += avg_rep_pi | |
plan_avg_dem_pi = plan_dem_pi_sum / district_count | |
plan_avg_rep_pi = plan_rep_pi_sum / district_count | |
self.result = (plan_avg_dem_pi, plan_avg_rep_pi) | |
if settings.DEBUG: | |
sys.stderr.write("self.result = %s\n" % (str(self.result))) | |
sys.stderr.write("Leaving AveragePartisanIndex.compute()\n") | |
class OhioPartisanBalance(CalculatorBase): | |
""" | |
This calculator is similar to the RepresentationalFairness calculator but it | |
uses the average of a number of election results to calculate the partisan | |
indices and returns a single score as described in the following rubric: | |
In the competition, each newly-created district will be rated as follows: | |
Strong Republican: Republican index in excess of 55% | |
Lean Republican: Republican index between 51 - 55% | |
Even District: Republican or Democratic index less than 51% | |
Lean Democrat: Democratic index between 51 - 55% | |
Strong Democrat: Democratic index in excess of 55% | |
To determine the overall partisan balance for a plan, the following | |
calculation will be used: | |
1.Multiply the number of Strong Republican Districts by 1.5. Add this | |
figure to the number of Lean Republican Districts and the number of Even | |
Districts. | |
2.Multiply the number of districts which are Strong Republican or | |
Democratic by 1.5. Add this to the number of districts which Lean | |
Republican or Democratic plus 2 times the number of even districts. | |
3.Divide the number of arrived at in step one with the number arrived at in | |
step 2. Convert to a percentage (rounded to 1/10th of one percent) to | |
arrive at the Republican balance for the plan. | |
""" | |
def compute(self, **kwargs): | |
""" | |
Compute the representational fairness. | |
Keywords: | |
plan -- A plan whose set of districts should be evaluated for | |
representational fairness. | |
version -- Optional. The version of the plan, defaults to the | |
most recent version. | |
Returns: | |
Does not return a value, but sets self.result to be a dictionary | |
with a number of key, value pairs: | |
value -- The Republican balance for the plan. | |
strong_rep -- Number of strong Republican districts | |
lean_rep -- Number of lean Republican districts | |
even -- Number of even districts | |
lean_dem -- Number of lean Democratic districts | |
strong_dem -- Number of strong Democratic districts | |
""" | |
if settings.DEBUG: | |
sys.stderr.write('Entering OhioPartisanBalance.compute()\n') | |
self.result = {} | |
self.result['value'] = None | |
self.result['strong_rep'] = None | |
self.result['lean_rep'] = None | |
self.result['even'] = None | |
self.result['lean_dem'] = None | |
self.result['strong_dem'] = None | |
if 'plan' in kwargs: | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
districts = plan.get_districts_at_version(version, include_geom=False) | |
else: | |
return | |
rep_strong = 0 | |
rep_lean = 0 | |
even = 0 | |
dem_strong = 0 | |
dem_lean = 0 | |
for district in districts: | |
if settings.DEBUG: | |
sys.stderr.write("district = %s\n" % (district)) | |
if district.district_id == 0: | |
# do nothing with the unassigned districts | |
continue | |
partisan_idx = AveragePartisanIndex() | |
partisan_idx.arg_dict = self.arg_dict | |
partisan_idx.compute(district=district) | |
(dem_pi, rep_pi) = partisan_idx.result | |
if settings.DEBUG: | |
sys.stderr.write("District %d: dem_pi = %f, rep_pi = %f\n" % (district.district_id, dem_pi, rep_pi)) | |
if rep_pi > 0.55: | |
rep_strong += 1 | |
if settings.DEBUG: | |
sys.stderr.write("District %d is strong Republican\n" % (district.district_id)) | |
elif rep_pi >= 0.51 and rep_pi <= 0.55: | |
rep_lean += 1 | |
if settings.DEBUG: | |
sys.stderr.write("District %d is lean Republican\n" % (district.district_id)) | |
elif (rep_pi >= 0.5 and rep_pi < 0.51) or (dem_pi >= 0.5 and dem_pi < 0.51): | |
even += 1 | |
if settings.DEBUG: | |
sys.stderr.write("District %d is even\n" % (district.district_id)) | |
elif dem_pi >= 0.51 and dem_pi <= 0.55: | |
dem_lean += 1 | |
if settings.DEBUG: | |
sys.stderr.write("District %d is lean Democrat\n" % (district.district_id)) | |
elif dem_pi > 0.55: | |
dem_strong += 1 | |
if settings.DEBUG: | |
sys.stderr.write("District %d is strong Democrat\n" % (district.district_id)) | |
if settings.DEBUG: | |
sys.stderr.write("Strong Republican: %d, Lean Republican: %d, Strong Democrat: %d, Lean Democrat: %d, Even: %d\n" % (rep_strong, rep_lean, dem_strong, dem_lean, even)) | |
self.result['value'] = (((rep_strong * 1.5) + rep_lean + even) / | |
(((rep_strong + dem_strong) * 1.5) + (rep_lean + dem_lean) + (even * 2)) | |
) | |
self.result['strong_rep'] = rep_strong | |
self.result['lean_rep'] = rep_lean | |
self.result['even'] = even | |
self.result['lean_dem'] = dem_lean | |
self.result['strong_dem'] = dem_strong | |
if settings.DEBUG: | |
sys.stderr.write("self.result['value'] = %f\n" % (self.result['value'])) | |
if settings.DEBUG: | |
sys.stderr.write('Leaving OhioPartisanBalance.compute()\n') | |
def html(self): | |
""" | |
Generate an HTML representation of the score. This | |
is represented as a percentage or "n/a" | |
@return: A number formatted similar to "1.00%", or "n/a" | |
""" | |
if not self.result['value'] is None: | |
return ("<span>%0.2f%%</span>" % (self.result['value'] * 100)) | |
else: | |
return "<span>n/a</span>" | |
class OhioElectoralDisproportionality(CalculatorBase): | |
def compute(self, **kwargs): | |
""" | |
Generate the Electoral Disproportionality for an Ohio plan. | |
Parameters: | |
plan -- Plan to score. | |
version -- Version of plan to score. Defaults to most recent version. | |
Returns: | |
Does not return a value, but sets self.result to be a dictionary | |
with a number of key, value pairs: | |
value -- The electoral disproportionality score for the plan (see below). | |
strong_rep -- Number of strong Republican districts | |
lean_rep -- Number of lean Republican districts | |
even -- Number of even districts | |
lean_dem -- Number of lean Democratic districts | |
strong_dem -- Number of strong Democratic districts | |
The electoral disproportionality is calculated by calculating the partisan | |
balance for a plan, converting it from a decimal to a percentage | |
(by multiplying by 100) and then subtracting the state political index. | |
""" | |
self.result = {} | |
self.result['value'] = None | |
self.result['strong_rep'] = None | |
self.result['lean_rep'] = None | |
self.result['even'] = None | |
self.result['lean_dem'] = None | |
self.result['strong_dem'] = None | |
if 'plan' in kwargs: | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
partisan_balance_calc = OhioPartisanBalance() | |
partisan_balance_calc.arg_dict = self.arg_dict | |
partisan_balance_calc.compute(**kwargs) | |
partisan_balance = partisan_balance_calc.result['value'] * 100 | |
state_political_index = float(self.get_value('state_political_index')) | |
self.result['value'] = partisan_balance - state_political_index | |
self.result['strong_rep'] = partisan_balance_calc.result['strong_rep'] | |
self.result['lean_rep'] = partisan_balance_calc.result['lean_rep'] | |
self.result['even'] = partisan_balance_calc.result['even'] | |
self.result['lean_dem'] = partisan_balance_calc.result['lean_dem'] | |
self.result['strong_dem'] = partisan_balance_calc.result['strong_dem'] | |
def html(self): | |
""" | |
Generate an HTML representation of the score. This | |
is represented as a percentage or "n/a" | |
@return: A number formatted similar to "1.00%", or "n/a" | |
""" | |
if not self.result['value'] is None: | |
return ("<span>%0.2f%%</span>" % (self.result['value'])) | |
else: | |
return "<span>n/a</span>" | |
class OhioRepresentationalFairnessScore(CalculatorBase): | |
def compute(self, **kwargs): | |
""" | |
Generate the Representational Fairness score for an Ohio plan. | |
Parameters: | |
plan -- Plan to score. | |
version -- Version of plan to score. Defaults to most recent version. | |
Returns: | |
Does not return a value, but sets self.result to be a dictionary | |
with a number of key, value pairs: | |
value -- The representational fairness score for the plan (see below). | |
strong_rep -- Number of strong Republican districts | |
lean_rep -- Number of lean Republican districts | |
even -- Number of even districts | |
lean_dem -- Number of lean Democratic districts | |
strong_dem -- Number of strong Democratic districts | |
The representational fairness score is determined by the following | |
algorithm depending on the legislative body of the plan. | |
For Congressional plans: | |
(25 - electoral disproportionality) * 4 | |
For State House/State Senate plans: | |
(25 - electoral disproportionality) * 2 | |
For State Senate plans: | |
25 - (num_splits / 2) | |
""" | |
self.result = {} | |
self.result['value'] = None | |
self.result['strong_rep'] = None | |
self.result['lean_rep'] = None | |
self.result['even'] = None | |
self.result['lean_dem'] = None | |
self.result['strong_dem'] = None | |
if 'plan' in kwargs: | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
calc = OhioElectoralDisproportionality() | |
argnum = 0 | |
while (('democratic%d' % (argnum+1,)) in self.arg_dict and | |
('republican%d' % (argnum+1,)) in self.arg_dict): | |
argnum += 1 | |
calc.arg_dict['democratic%d' % (argnum)] = self.arg_dict['democratic%d' % (argnum)] | |
calc.arg_dict['republican%d' % (argnum)] = self.arg_dict['republican%d' % (argnum)] | |
calc.arg_dict['state_political_index'] = self.arg_dict['state_political_index'] | |
calc.compute(**kwargs) | |
electoral_disproportionality = calc.result['value'] | |
self.result['strong_rep'] = calc.result['strong_rep'] | |
self.result['lean_rep'] = calc.result['lean_rep'] | |
self.result['even'] = calc.result['even'] | |
self.result['lean_dem'] = calc.result['lean_dem'] | |
self.result['strong_dem'] = calc.result['strong_dem'] | |
congressional_legislative_body_id = self.get_value('congressional_legislative_body_id') | |
state_house_legislative_body_id = self.get_value('state_house_legislative_body_id') | |
state_senate_legislative_body_id = self.get_value('state_senate_legislative_body_id') | |
if (plan.legislative_body.id == congressional_legislative_body_id): | |
self.result['value'] = (25 - electoral_disproportionality) * 4 | |
elif (plan.legislative_body.id == state_house_legislative_body_id or | |
plan.legislative_body.id == state_senate_legislative_body_id): | |
self.result['value'] = (25 - electoral_disproportionality) * 2 | |
else: | |
# Error. TODO: Do something here. | |
pass | |
def html(self): | |
""" | |
Generate an HTML representation of the score. This | |
is represented as a percentage or "n/a" | |
@return: A number formatted similar to "1.00%", or "n/a" | |
""" | |
if not self.result['value'] is None: | |
return ("<span>%0.2f%%</span>" % (self.result['value'])) | |
else: | |
return "<span>n/a</span>" | |
class OhioSplitCounter(CalculatorBase): | |
def compute(self, **kwargs): | |
if settings.DEBUG: | |
sys.stderr.write('Entering OhioSplitCounter.compute()\n') | |
sys.stderr.write('kwargs = %s\n' % (kwargs)) | |
self.result = 0 | |
if 'plan' in kwargs: | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
else: | |
raise UnboundLocalError | |
# HACK ALERT! Parse out the geolevel_id and turn it into a boundary_id | |
# NOTE: geolevel_id should be the id of the geolevel in the database, not | |
# the id in the configuration file. In the test instance at least | |
# the database geolevel ids are: | |
# | |
# 3 - county | |
# 2 - mcdplace | |
# 1 - block | |
geolevel_id = self.get_value('geolevel_id') | |
# The way Jim defined splits to be counted in the Ohio Scoring Rubric is to count all | |
# the fragments created by a district and to ignore cases where a district is entirely | |
# contained in a county. | |
# | |
# So, we'll find all the counties whose boundaries are crossed by a district. This will | |
# find districts that create county fragments and rule out districts that are entirely | |
# contained in a county. However, this set will also contain counties that are entirely | |
# contained in a district. So, we'll also need to get the set of counties that are entirely | |
# contained in a district. | |
# Find all geolevel boundaries that districts in this plan cross | |
crosses_set = plan.find_geolevel_relationships(geolevel_id, version=version, de_9im='T*T******') | |
# Find all geolevel boundaries that districts in this plan contain | |
contains_set = plan.find_geolevel_relationships(geolevel_id, version=version, de_9im='T*****FF*') | |
if settings.DEBUG: | |
sys.stderr.write('crosses_set = %s\n' % (crosses_set)) | |
sys.stderr.write('contains_set = %s\n' % (contains_set)) | |
if not crosses_set is None: | |
for crossed_relationship in crosses_set: | |
# TODO: Add logic to rule out certain counties. | |
if crossed_relationship not in contains_set: | |
if settings.DEBUG: | |
district_name = crossed_relationship[0] | |
geounit_name = crossed_relationship[3] | |
sys.stderr.write('Geounit %s is split by district %s\n' % | |
(geounit_name, district_name)) | |
self.result += 1 | |
else: | |
if settings.DEBUG: | |
district_name = crossed_relationship[0] | |
geounit_name = crossed_relationship[3] | |
sys.stderr.write('Geounit %s is entirely contained in district %s, not counting as split\n' % | |
(geounit_name, district_name)) | |
else: | |
if settings.DEBUG: | |
sys.stderr.write('ERROR: crosses_set is None\n') | |
if settings.DEBUG: | |
sys.stderr.write('self.result = %d\n' % self.result) | |
sys.stderr.write('Leaving OhioSplitCounter.compute()\n') | |
class OhioSplitScore(CalculatorBase): | |
def compute(self, **kwargs): | |
""" | |
Compute the split score for Ohio plans. | |
Parameters: | |
plan -- Plan to score. | |
version -- Version of plan to score. Defaults to most recent version. | |
Returns: | |
Dictionary with two key, value pairs: | |
value -- The split score for the plan (see below). | |
num_splits -- The number of splits detected. | |
The split score is determined by the following algorithm depending on the | |
legislative body of the plan. | |
For Congressional plans: | |
50 - num_splits | |
For State House plans: | |
25 - (num_splits / 4) | |
For State Senate plans: | |
25 - (num_splits / 2) | |
""" | |
self.result = {} | |
self.result['value'] = 0 | |
self.result['num_splits'] = 0 | |
if 'plan' in kwargs: | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
calc = OhioSplitCounter() | |
calc.arg_dict['geolevel_id'] = self.arg_dict['geolevel_id'] | |
calc.compute(**kwargs) | |
num_splits = calc.result | |
self.result['num_splits'] = num_splits | |
if settings.DEBUG: | |
sys.stderr.write('num_splits = %d\n' % (num_splits)) | |
congressional_legislative_body_id = self.get_value('congressional_legislative_body_id') | |
state_house_legislative_body_id = self.get_value('state_house_legislative_body_id') | |
state_senate_legislative_body_id = self.get_value('state_senate_legislative_body_id') | |
if (plan.legislative_body.id == congressional_legislative_body_id): | |
self.result['value'] = 50.0 - num_splits | |
elif (plan.legislative_body.id == state_house_legislative_body_id): | |
self.result['value'] = 25.0 - (num_splits / 4.0) | |
elif (plan.legislative_body.id == state_senate_legislative_body_id): | |
self.result['value'] = 25.0 - (num_splits / 2.0) | |
else: | |
raise ValueError("Plan's legislative body ID of %d is not one of the congressional, state house or state senate IDs (%d, %d, %d" % | |
(plan.legislative_body.id, congressional_legislative_body_id, state_house_legislative_body_id, state_senate_legislative_body_id)) | |
def html(self): | |
""" | |
Generate an HTML representation of the score. This | |
is represented as a percentage or "n/a" | |
@return: A number formatted similar to "1.00%", or "n/a" | |
""" | |
if not self.result['value'] is None: | |
return ("<span>%0.2f%%</span>" % (self.result['value'])) | |
else: | |
return "<span>n/a</span>" | |
class SumPlanScores(CalculatorBase): | |
"""SumValues will try to get an argument for all districts in a plan, even | |
if the argument is a plan score. This works around this.""" | |
def compute(self, **kwargs): | |
self.result = 0 | |
argnum = 1 | |
while ('value%d'%argnum) in self.arg_dict: | |
number = self.get_value('value%d'%argnum) | |
# HACK ALERT!: Workaround to CalculatorBase.get_value not converting | |
# floats to Decimals. [email protected] 2011-08-04 | |
if isinstance(number, float): | |
number = Decimal('%f' % number) | |
if not number is None: | |
self.result += number | |
argnum += 1 | |
def html(self): | |
""" | |
Generate an HTML representation of the summation score. This | |
is represented as a decimal formatted with commas or "n/a". | |
Returns: | |
The result wrapped in an HTML SPAN element: "<span>1</span>". | |
""" | |
if isinstance(self.result, Decimal): | |
result = locale.format("%f", self.result, grouping=True) | |
return '<span>%s</span>' % result | |
elif not self.result is None: | |
return '<span>%s</span>' % self.result | |
else: | |
return '<span>N/A</span>' | |
pass | |
# TODO: Implement this | |
class Message(CalculatorBase): | |
def compute(self, **kwargs): | |
self.result = '' | |
if 'message' in self.arg_dict: | |
self.result = self.get_value('message') | |
def html(self): | |
return self.result | |
class PartisanDifferential(CalculatorBase): | |
""" | |
Compute the plan's partisan Differential. | |
This calculator only operates on Plans. | |
This calculator requires three arguments: 'democratic', 'republican', | |
and 'range' | |
""" | |
def compute(self, **kwargs): | |
""" | |
Compute the partisan differential. | |
@keyword plan: A L{Plan} whose set of districts should be | |
evaluated for competitiveness. | |
@keyword version: Optional. The version of the plan, defaults to | |
the most recent version. | |
""" | |
if not 'plan' in kwargs: | |
return | |
plan = kwargs['plan'] | |
version = kwargs['version'] if 'version' in kwargs else plan.version | |
districts = plan.get_districts_at_version(version, include_geom=False) | |
try: | |
partisan_differential_range = float(self.get_value('range')) | |
except: | |
partisan_differential_range = .05 | |
fair = 0 | |
for district in districts: | |
if district.district_id == 0: | |
continue | |
tmpdem = self.get_value('democratic',district) | |
tmprep = self.get_value('republican',district) | |
if tmpdem is None or tmprep is None: | |
continue | |
dem = float(tmpdem) | |
rep = float(tmprep) | |
if dem == 0.0 and rep == 0.0: | |
continue | |
pidx_rep = rep / (dem + rep) | |
pidx_dem = dem / (dem + rep) | |
partisan_differential = abs(pidx_rep - pidx_dem) | |
if partisan_differential <= partisan_differential_range: | |
fair += 1 | |
self.result = { 'value': fair } | |
class OhioCompetitiveness(CalculatorBase): | |
ranges = ('0.05', '0.10', '0.15', '0.5') | |
def compute(self, **kwargs): | |
if settings.DEBUG: | |
sys.stderr.write('Entering OhioCompetitiveness.compute()\n') | |
sys.stderr.write('self.arg_dict = %s\n' % (self.arg_dict)) | |
self.result = {} | |
self.result['value'] = None | |
self.result['highly_competitive'] = None | |
self.result['generally_competitive'] = None | |
self.result['generally_noncompetitive'] = None | |
self.result['highly_noncompetitive'] = None | |
counts_in_range = {} | |
for cptv_range in self.ranges: | |
partisan_differential_calc = PartisanDifferential() | |
partisan_differential_calc.arg_dict['democratic'] = self.arg_dict['democratic'] | |
partisan_differential_calc.arg_dict['republican'] = self.arg_dict['republican'] | |
partisan_differential_calc.arg_dict['range'] = ('literal', cptv_range) | |
partisan_differential_calc.compute(**kwargs) | |
counts_in_range[cptv_range] = int(partisan_differential_calc.result['value']) | |
if settings.DEBUG: | |
sys.stderr.write('counts_in_range = %s\n' % (counts_in_range)) | |
self.result['highly_competitive'] = counts_in_range['0.05'] | |
self.result['generally_competitive'] = counts_in_range['0.10'] - counts_in_range['0.05'] | |
self.result['generally_noncompetitive'] = counts_in_range['0.15'] - counts_in_range['0.10'] | |
self.result['highly_noncompetitive'] = counts_in_range['0.5'] - counts_in_range['0.15'] | |
if settings.DEBUG: | |
sys.stderr.write('Highly competitive districts: %d\n' % (self.result['highly_competitive'])) | |
sys.stderr.write('Generally competitive districts: %d\n' % (self.result['generally_competitive'])) | |
sys.stderr.write('Generally non-competitive districts: %d\n' % (self.result['generally_noncompetitive'])) | |
sys.stderr.write('Highly non-competitive districts: %d\n' % (self.result['highly_noncompetitive'])) | |
self.result['value'] = ( | |
self.result['highly_competitive'] * 3 + | |
self.result['generally_competitive'] * 2 + | |
self.result['generally_noncompetitive'] * 1 | |
) | |
if settings.DEBUG: | |
sys.stderr.write('Leaving OhioCompetitiveness.compute()\n') | |
def html(self): | |
""" | |
Generate an HTML representation of the summation score. This | |
is represented as a decimal formatted with commas or "n/a". | |
Returns: | |
The result wrapped in an HTML SPAN element: "<span>1</span>". | |
""" | |
if isinstance(self.result['value'], Decimal): | |
result = locale.format("%d", self.result['value'], grouping=True) | |
return '<span>%s</span>' % result | |
elif not self.result['value'] is None: | |
return '<span>%s</span>' % self.result['value'] | |
else: | |
return '<span>N/A</span>' | |
class OhioCompactnessScore(CalculatorBase): | |
"""Wrapper for compactness score.""" | |
def compute(self, **kwargs): | |
self.result = 0 | |
calc = Roeck() | |
calc.compute(**kwargs) | |
self.result = calc.result['value'] * 100 | |
def html(self): | |
""" | |
Generate an HTML representation of the summation score. This | |
is represented as a decimal formatted with commas or "n/a". | |
Returns: | |
The result wrapped in an HTML SPAN element: "<span>1</span>". | |
""" | |
if isinstance(self.result, Decimal): | |
result = locale.format("%f", self.result, grouping=True) | |
return '<span>%s</span>' % result | |
elif not self.result is None: | |
return '<span>%s</span>' % self.result | |
else: | |
return '<span>N/A</span>' | |
This file contains hidden or 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 sys | |
import csv | |
from optparse import make_option | |
from datetime import datetime, timedelta | |
from redistricting.models import * | |
from redistricting.ohcalculators import OhioSplitCounter, OhioCompetitiveness, OhioRepresentationalFairnessScore, OhioSplitScore, OhioCompactnessScore | |
import re | |
# Add this to the option list of your management command and use get_plans() | |
# to retrieve plans to be able to filter | |
plan_filtering_option_list = ( | |
make_option('-p', '--plan', dest='plan_id', default=None, action='store', help='Choose a single plan to list.'), | |
make_option('-s', '--shared', dest='is_shared', default=False, action='store_true', help='Only list shared plans'), | |
make_option('-e', '--last-edited', dest='last_edited', default=False, action='store', help='Only list plans edited before or after a certain time period.'), | |
make_option('-V', '--versions', dest='versions', default=False, action='store', help='Only list plans with a minimum or maximum number of versions.'), | |
make_option('-i', '--ids-only', dest='ids_only', default=False, action='store_true', help='Only display plans IDs.'), | |
make_option('-u', '--users', dest='users', default=None, action='store', help="Only list plans by these users"), | |
make_option('-U', '--exclude-users', dest='exclude_users', default=None, action='store', help="Don't list plans by these users"), | |
) | |
def get_plans(options): | |
"""Helper for management commands that filter plans with command line options.""" | |
# Grab all of the plans from the database | |
plan_id = options.get('plan_id') | |
if plan_id: | |
plan_ids = re.split('\s*,\s*', plan_id) | |
plans = Plan.objects.filter(pk__in=plan_ids) | |
else: | |
plans = Plan.objects.all() | |
last_edited = options.get("last_edited") | |
if last_edited: | |
if last_edited[0] in ('-', '+') and last_edited[-1:] in ('w', 'd'): | |
last_edited_days = int(last_edited[1:-1]) | |
if last_edited[-1:] == 'w': | |
last_edited_days *= 7 | |
td = timedelta(days=last_edited_days) | |
if last_edited[0] == '-': | |
plans = plans.filter(edited__gt=(datetime.now() - td)) | |
else: | |
# last_edited[0] == '+' | |
plans = plans.filter(edited__lt=(datetime.now() - td)) | |
else: | |
sys.stderr.write('Invalid format for last-edited value. It must be in the format +Xd, -Xd, +Xw, -Xw where X is an integer.') | |
versions = options.get("versions") | |
if versions: | |
if versions[0] in ('-', '+'): | |
version_limit = versions[1:] | |
if versions[0] == '-': | |
plans = plans.filter(version__lt=version_limit) | |
else: | |
# versions[0] == '+' | |
plans = plans.filter(version__gt=version_limit) | |
users = options.get("users") | |
if users: | |
user_list = re.split('[\s,]+', users) | |
plans = [p for p in plans if p.owner.username in (user_list)] | |
exclude_users = options.get("exclude_users") | |
if exclude_users: | |
exclude_user_list = re.split('[\s,]+', exclude_users) | |
plans = [p for p in plans if p.owner.username not in (exclude_user_list)] | |
# Filter out all non-shared plans if specified | |
if options.get("is_shared"): | |
plans = [p for p in plans if p.is_shared] | |
return plans | |
def score_plan(plan): | |
""" | |
Score a plan according to the Ohio scoring rubric. | |
This chains together a bunch of custom calculators for Ohio. | |
It's a quick and dirty helper to facilitate export of a score | |
CSV either through a view or management command. | |
""" | |
total_score = 0 | |
calc = OhioSplitScore() | |
calc.arg_dict['geolevel_id'] = ('literal', '3') | |
calc.arg_dict['congressional_legislative_body_id'] = ('literal', '1') | |
calc.arg_dict['state_house_legislative_body_id'] = ('literal', '2') | |
calc.arg_dict['state_senate_legislative_body_id'] = ('literal', '3') | |
calc.compute(plan=plan) | |
split_score = calc.result['value'] | |
num_splits = calc.result['num_splits'] | |
if settings.DEBUG: | |
sys.stderr.write("Split Score: %f\n" % (split_score)) | |
compactness_calc = OhioCompactnessScore() | |
compactness_calc.compute(plan=plan) | |
compactness_score = compactness_calc.result | |
if settings.DEBUG: | |
sys.stderr.write("Compactness Score: %f\n" % (compactness_score)) | |
competitiveness_calc = OhioCompetitiveness() | |
competitiveness_calc.arg_dict['democratic'] = ('subject', 'demtot') | |
competitiveness_calc.arg_dict['republican'] = ('subject', 'reptot') | |
competitiveness_calc.compute(plan=plan) | |
competitiveness_score = competitiveness_calc.result['value'] | |
if settings.DEBUG: | |
sys.stderr.write("Competitiveness Score: %d\n" % (competitiveness_score)) | |
rep_fairness_calc = OhioRepresentationalFairnessScore() | |
rep_fairness_calc.arg_dict['democratic1'] = ('subject', 'demtot') | |
rep_fairness_calc.arg_dict['republican1'] = ('subject', 'reptot') | |
rep_fairness_calc.arg_dict['state_political_index'] = ('literal', '51.4') | |
rep_fairness_calc.arg_dict['congressional_legislative_body_id'] = ('literal', '1') | |
rep_fairness_calc.arg_dict['state_house_legislative_body_id'] = ('literal', '2') | |
rep_fairness_calc.arg_dict['state_senate_legislative_body_id'] = ('literal', '3') | |
rep_fairness_calc.compute(plan=plan) | |
rep_fairness_score = rep_fairness_calc.result['value'] | |
if settings.DEBUG: | |
sys.stderr.write("Representational Fairness Score: %f\n" % (rep_fairness_score)) | |
total_score = split_score + compactness_score + competitiveness_score + rep_fairness_score | |
if settings.DEBUG: | |
sys.stderr.write("Total Score: %f\n" % (total_score)) | |
return { | |
'num_splits': num_splits, | |
'split_score': split_score, | |
'compactness': compactness_score, | |
'highly_competitive': competitiveness_calc.result['highly_competitive'], | |
'generally_competitive': competitiveness_calc.result['generally_competitive'], | |
'generally_noncompetitive': competitiveness_calc.result['generally_noncompetitive'], | |
'highly_noncompetitive': competitiveness_calc.result['highly_noncompetitive'], | |
'competitiveness': competitiveness_score, | |
'strong_rep': rep_fairness_calc.result['strong_rep'], | |
'lean_rep': rep_fairness_calc.result['lean_rep'], | |
'even': rep_fairness_calc.result['even'], | |
'lean_dem': rep_fairness_calc.result['lean_dem'], | |
'strong_dem': rep_fairness_calc.result['strong_dem'], | |
'rep_fairness': rep_fairness_score, | |
'total': total_score } | |
def plan_score_csv(plans, csv_file): | |
""" | |
Output plan scores in a CSV file. | |
Parameters: | |
plans -- Queryset of redistricting.models.Plan objects | |
csv_file -- File object to write CSV to | |
""" | |
csv_fields = ( | |
'id', | |
'name', | |
'username', | |
'legislative_body', | |
'edited', | |
'split_score', | |
'compactness', | |
'competitiveness', | |
'rep_fairness', | |
# Jim asked to suppress the total score since it doesn't make | |
# much sense because of the way scores for State Senate and | |
# State House plans are combined. | |
# [email protected] 2011-08-10 | |
# 'total' | |
) | |
# We probably want to wrap this in an if settings.DEBUG once we | |
# work out the scoring issues. [email protected] 2011-08-10 | |
csv_fields = csv_fields + ( | |
'num_splits', | |
'highly_competitive', | |
'generally_competitive', | |
'generally_noncompetitive', | |
'highly_noncompetitive', | |
'strong_rep', | |
'lean_rep', | |
'even', | |
'lean_dem', | |
'strong_dem', | |
) | |
score_writer = csv.DictWriter(csv_file, csv_fields, dialect='excel') | |
csv_field_values = { | |
'id': 'ID', | |
'name': 'NAME', | |
'username': 'USERNAME', | |
'legislative_body': 'LEGISLATIVE_BODY', | |
'edited': 'EDITED', | |
'split_score': 'SPLIT_SCORE', | |
'compactness': 'COMPACTNESS_SCORE', | |
'competitiveness': 'COMPETITIVENESS_SCORE', | |
'rep_fairness': 'REPRESENATIONAL_FAIRNESS_SCORE', | |
# Jim asked to suppress the total score since it doesn't make | |
# much sense because of the way scores for State Senate and | |
# State House plans are combined. | |
# [email protected] 2011-08-10 | |
#'total': 'TOTAL_SCORE' | |
} | |
# We probably want to wrap this in an if settings.DEBUG once we | |
# work out the scoring issues. [email protected] 2011-08-10 | |
csv_field_values['num_splits'] = 'NUM_SPLITS' | |
csv_field_values['highly_competitive'] = 'highly_competitive'.upper() | |
csv_field_values['generally_competitive'] = 'generally_competitive'.upper() | |
csv_field_values['generally_noncompetitive'] = 'generally_noncompetitive'.upper() | |
csv_field_values['highly_noncompetitive'] = 'highly_noncompetitive'.upper() | |
csv_field_values['strong_rep'] = 'strong_rep'.upper() | |
csv_field_values['lean_rep'] = 'lean_rep'.upper() | |
csv_field_values['even'] = 'even'.upper() | |
csv_field_values['lean_dem'] = 'lean_dem'.upper() | |
csv_field_values['strong_dem'] = 'strong_dem'.upper() | |
score_writer.writerow(csv_field_values) | |
for plan in plans: | |
if settings.DEBUG: | |
sys.stderr.write("Scoring plan '%s' with id %d - started at %s\n" % ( | |
plan.name, plan.id, datetime.now())) | |
scores = score_plan(plan) | |
scores['id'] = plan.id | |
scores['name'] = plan.name | |
scores['username'] = plan.owner.username | |
scores['legislative_body'] = plan.legislative_body.name | |
scores['edited'] = plan.edited | |
# Suppress total score. DictWriter.writerow() complains if there are values in | |
# the dict that are not specified when initializing the writer. | |
# [email protected] 2011-08-10 | |
del(scores['total']) | |
score_writer.writerow(scores) | |
if settings.DEBUG: | |
sys.stderr.write("Scoring plan '%s' with id %d - ended at %s\n" % ( | |
plan.name, plan.id, datetime.now())) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment