Skip to content

Commit

Permalink
Edit docs for metrics, partition, and proposals modules
Browse files Browse the repository at this point in the history
  • Loading branch information
peterrrock2 committed Jan 13, 2024
1 parent 8a15cb9 commit 6ee772b
Show file tree
Hide file tree
Showing 8 changed files with 409 additions and 86 deletions.
27 changes: 24 additions & 3 deletions gerrychain/metrics/compactness.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import math
from typing import Dict


def compute_polsby_popper(area, perimeter):
def compute_polsby_popper(area: float, perimeter: float) -> float:
"""
Computes the Polsby-Popper score for a single district.
:param area: The area of the district
:type area: float
:param perimeter: The perimeter of the district
:type perimeter: float
:returns: The Polsby-Popper score for the district
:rtype: float
"""
try:
return 4 * math.pi * area / perimeter ** 2
except ZeroDivisionError:
return math.nan


def polsby_popper(partition):
"""Computes Polsby-Popper compactness scores for each district in the partition.
# Partition type hint left out due to circular import
# def polsby_popper(partition: Partition) -> Dict[int, float]:
def polsby_popper(partition) -> Dict[int, float]:
"""
Computes Polsby-Popper compactness scores for each district in the partition.
:param partition: The partition to compute scores for
:type partition: Partition
:returns: A dictionary mapping each district ID to its Polsby-Popper score
:rtype: Dict[int, float]
"""
return {
part: compute_polsby_popper(
Expand Down
83 changes: 59 additions & 24 deletions gerrychain/metrics/partisan.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
"""
The partisan metrics in this file are later used in the module
gerrychain.updaters.election.py. Thus, all of the election
results objects here are implicilty typed as ElectionResults,
but cannot be given an explicit type annotation due to problems
with circular imports.
"""

import numpy
from typing import Tuple


def mean_median(election_results):
def mean_median(election_results) -> float:
"""
Computes the Mean-Median score for the given ElectionResults.
A positive value indicates an advantage for the first party listed
in the Election's parties_to_columns dictionary.
:param election_results: An ElectionResults object
:type election_results: ElectionResults
:returns: The Mean-Median score for the given ElectionResults
:rtype: float
"""
first_party = election_results.election.parties[0]
data = election_results.percents(first_party)

return numpy.median(data) - numpy.mean(data)


def mean_thirdian(election_results):
def mean_thirdian(election_results) -> float:
"""
Computes the Mean-Median score for the given ElectionResults.
A positive value indicates an advantage for the first party listed
in the Election's parties_to_columns dictionary.
The motivation for this score is that the minority party in many
states struggles to win even a third of the seats.
:param election_results: An ElectionResults object
:type election_results: ElectionResults
:returns: The Mean-Thirdian score for the given ElectionResults
:rtype: float
"""
first_party = election_results.election.parties[0]
data = election_results.percents(first_party)
Expand All @@ -31,24 +52,35 @@ def mean_thirdian(election_results):
return thirdian - numpy.mean(data)


def efficiency_gap(results):
def efficiency_gap(election_results) -> float:
"""
Computes the efficiency gap for the given ElectionResults.
A positive value indicates an advantage for the first party listed
in the Election's parties_to_columns dictionary.
:param election_results: An ElectionResults object
:type election_results: ElectionResults
:returns: The efficiency gap for the given ElectionResults
:rtype: float
"""
party1, party2 = [results.counts(party) for party in results.election.parties]
party1, party2 = [election_results.counts(party) for party in election_results.election.parties]
wasted_votes_by_part = map(wasted_votes, party1, party2)
total_votes = results.total_votes()
total_votes = election_results.total_votes()
numerator = sum(waste2 - waste1 for waste1, waste2 in wasted_votes_by_part)
return numerator / total_votes


def wasted_votes(party1_votes, party2_votes):
def wasted_votes(party1_votes: int, party2_votes: int) -> Tuple[int, int]:
"""
Computes the wasted votes for each party in the given race.
:party1_votes: the number of votes party1 received in the race
:party2_votes: the number of votes party2 received in the race
:param party1_votes: the number of votes party1 received in the race
:type party1_votes: int
:param party2_votes: the number of votes party2 received in the race
:type party2_votes: int
:returns: a tuple of the wasted votes for each party
:rtype: Tuple[int, int]
"""
total_votes = party1_votes + party2_votes
if party1_votes > party2_votes:
Expand All @@ -60,12 +92,18 @@ def wasted_votes(party1_votes, party2_votes):
return party1_waste, party2_waste


def partisan_bias(election_results):
def partisan_bias(election_results) -> float:
"""
Computes the partisan bias for the given ElectionResults.
The partisan bias is defined as the number of districts with above-mean
vote share by the first party divided by the total number of districts,
minus 1/2.
:param election_results: An ElectionResults object
:type election_results: ElectionResults
:returns: The partisan bias for the given ElectionResults
:rtype: float
"""
first_party = election_results.election.parties[0]
party_shares = numpy.array(election_results.percents(first_party))
Expand All @@ -74,37 +112,34 @@ def partisan_bias(election_results):
return (above_mean_districts / len(party_shares)) - 0.5


def partisan_gini(election_results):
def partisan_gini(election_results) -> float:
"""
Computes the partisan Gini score for the given ElectionResults.
The partisan Gini score is defined as the area between the seats-votes
curve and its reflection about (.5, .5).
For more information on the computation, see Definition 1 in:
https://arxiv.org/pdf/2008.06930.pdf
:param election_results: An ElectionResults object
:type election_results: ElectionResults
:returns: The partisan Gini score for the given ElectionResults
:rtype: float
"""
# For two parties, the Gini score is symmetric--it does not vary by party.
party = election_results.election.parties[0]

# To find seats as a function of votes, we assume uniform partisan swing.
# That is, if the statewide popular vote share for a party swings by some
# delta, the vote share for that party swings by that delta in each
# district.
# We calculate the necessary delta to shift the district with the highest
# vote share for the party to a vote share of 0.5. This delta, subtracted
# from the original popular vote share, gives the minimum popular vote
# share that yields 1 seat to the party.
# We repeat this process for the district with the second-highest vote
# share, which gives the minimum popular vote share yielding 2 seats,
# and so on.
overall_result = election_results.percent(party)
race_results = sorted(election_results.percents(party), reverse=True)
seats_votes = [overall_result - r + 0.5 for r in race_results]

# Apply reflection of seats-votes curve about (.5, .5)
reflected_sv = reversed([1 - s for s in seats_votes])
# Calculate the unscaled, unsigned area between the seats-votes curve
# and its reflection. For each possible number of seats attained, we find
# the area of a rectangle of unit height, with a width determined by the
# horizontal distance between the curves at that number of seats.
# and its reflection.
unscaled_area = sum(abs(s - r) for s, r in zip(seats_votes, reflected_sv))

# We divide by area by the number of seats to obtain a partisan Gini score
# between 0 and 1.
return unscaled_area / len(race_results)
Loading

1 comment on commit 6ee772b

@cdonnay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read!

Please sign in to comment.