Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/mggg/gerrychain
Browse files Browse the repository at this point in the history
  • Loading branch information
maxhully committed Apr 17, 2019
2 parents 5c023e0 + bb32899 commit c07cae5
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 63 deletions.
13 changes: 10 additions & 3 deletions gerrychain/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
import networkx

from gerrychain.partition import Partition
from gerrychain.updaters import (Tally, boundary_nodes, cut_edges,
cut_edges_by_part, exterior_boundaries,
interior_boundaries, perimeter, polsby_popper)
from gerrychain.updaters import (
Tally,
boundary_nodes,
cut_edges,
cut_edges_by_part,
exterior_boundaries,
interior_boundaries,
perimeter,
)
from gerrychain.metrics import polsby_popper


class Grid(Partition):
Expand Down
2 changes: 2 additions & 0 deletions gerrychain/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .compactness import polsby_popper
from .partisan import *
17 changes: 17 additions & 0 deletions gerrychain/metrics/compactness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import math


def compute_polsby_popper(area, perimeter):
try:
return 4 * math.pi * area / perimeter ** 2
except ZeroDivisionError:
return math.nan


def polsby_popper(partition):
return {
part: compute_polsby_popper(
partition["area"][part], partition["perimeter"][part]
)
for part in partition.parts
}
File renamed without changes.
12 changes: 8 additions & 4 deletions gerrychain/updaters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from .compactness import (boundary_nodes, exterior_boundaries,
exterior_boundaries_as_a_set, flips,
interior_boundaries, perimeter, polsby_popper)
from .compactness import (
boundary_nodes,
exterior_boundaries,
exterior_boundaries_as_a_set,
flips,
interior_boundaries,
perimeter,
)
from .county_splits import CountySplit, county_splits
from .cut_edges import cut_edges, cut_edges_by_part
from .election import Election
Expand All @@ -9,7 +14,6 @@

__all__ = [
"flows_from_changes",
"polsby_popper",
"county_splits",
"cut_edges",
"cut_edges_by_part",
Expand Down
71 changes: 37 additions & 34 deletions gerrychain/updaters/compactness.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,73 @@
import collections
import math

from .flows import on_flow
from .cut_edges import on_edge_flow


def compute_polsby_popper(area, perimeter):
try:
return 4 * math.pi * area / perimeter**2
except ZeroDivisionError:
return math.nan


def polsby_popper(partition):
return {part: compute_polsby_popper(partition['area'][part], partition['perimeter'][part])
for part in partition.parts}


def boundary_nodes(partition, alias='boundary_nodes'):
def boundary_nodes(partition, alias="boundary_nodes"):
if partition.parent:
return partition.parent[alias]
return {node for node in partition.graph.nodes if partition.graph.nodes[node]['boundary_node']}
return {
node
for node in partition.graph.nodes
if partition.graph.nodes[node]["boundary_node"]
}


def initialize_exterior_boundaries_as_a_set(partition):
part_boundaries = collections.defaultdict(set)
for node in partition['boundary_nodes']:
for node in partition["boundary_nodes"]:
part_boundaries[partition.assignment[node]].add(node)
return part_boundaries


@on_flow(initialize_exterior_boundaries_as_a_set, alias='exterior_boundaries_as_a_set')
@on_flow(initialize_exterior_boundaries_as_a_set, alias="exterior_boundaries_as_a_set")
def exterior_boundaries_as_a_set(partition, previous, inflow, outflow):
graph_boundary = partition['boundary_nodes']
graph_boundary = partition["boundary_nodes"]
return (previous | (inflow & graph_boundary)) - outflow


def initialize_exterior_boundaries(partition):
graph_boundary = partition['boundary_nodes']
graph_boundary = partition["boundary_nodes"]
boundaries = collections.defaultdict(lambda: 0)
for node in graph_boundary:
part = partition.assignment[node]
boundaries[part] += partition.graph.nodes[node]['boundary_perim']
boundaries[part] += partition.graph.nodes[node]["boundary_perim"]
return boundaries


@on_flow(initialize_exterior_boundaries, alias='exterior_boundaries')
@on_flow(initialize_exterior_boundaries, alias="exterior_boundaries")
def exterior_boundaries(partition, previous, inflow, outflow):
graph_boundary = partition['boundary_nodes']
added_perimeter = sum(partition.graph.nodes[node]['boundary_perim']
for node in inflow & graph_boundary)
removed_perimeter = sum(partition.graph.nodes[node]['boundary_perim']
for node in outflow & graph_boundary)
graph_boundary = partition["boundary_nodes"]
added_perimeter = sum(
partition.graph.nodes[node]["boundary_perim"]
for node in inflow & graph_boundary
)
removed_perimeter = sum(
partition.graph.nodes[node]["boundary_perim"]
for node in outflow & graph_boundary
)
return previous + added_perimeter - removed_perimeter


def initialize_interior_boundaries(partition):
return {part: sum(partition.graph.edges[edge]['shared_perim']
for edge in partition['cut_edges_by_part'][part])
for part in partition.parts}
return {
part: sum(
partition.graph.edges[edge]["shared_perim"]
for edge in partition["cut_edges_by_part"][part]
)
for part in partition.parts
}


@on_edge_flow(initialize_interior_boundaries, alias='interior_boundaries')
@on_edge_flow(initialize_interior_boundaries, alias="interior_boundaries")
def interior_boundaries(partition, previous, new_edges, old_edges):
added_perimeter = sum(partition.graph.edges[edge]['shared_perim'] for edge in new_edges)
removed_perimeter = sum(partition.graph.edges[edge]['shared_perim'] for edge in old_edges)
added_perimeter = sum(
partition.graph.edges[edge]["shared_perim"] for edge in new_edges
)
removed_perimeter = sum(
partition.graph.edges[edge]["shared_perim"] for edge in old_edges
)
return previous + added_perimeter - removed_perimeter


Expand All @@ -78,8 +81,8 @@ def perimeter_of_part(partition, part):
Requires that 'boundary_perim' be a node attribute, 'shared_perim' be an edge
attribute, 'cut_edges' be an updater, and 'exterior_boundaries' be an updater.
"""
exterior_perimeter = partition['exterior_boundaries'][part]
interior_perimeter = partition['interior_boundaries'][part]
exterior_perimeter = partition["exterior_boundaries"][part]
interior_perimeter = partition["interior_boundaries"][part]

return exterior_perimeter + interior_perimeter

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions tests/metrics/test_compactness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import math
from gerrychain.metrics.compactness import compute_polsby_popper


def test_polsby_popper_returns_nan_when_perimeter_is_0():
area = 10
perimeter = 0
assert compute_polsby_popper(area, perimeter) is math.nan
40 changes: 27 additions & 13 deletions tests/test_scores.py → tests/metrics/test_partisan.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import pytest
from unittest.mock import MagicMock
from gerrychain.scores import (efficiency_gap, wasted_votes, mean_median,
partisan_bias, partisan_gini)
from gerrychain.metrics import (
efficiency_gap,
wasted_votes,
mean_median,
partisan_bias,
partisan_gini,
)
from gerrychain.updaters.election import ElectionResults


@pytest.fixture
def mock_election():
election = MagicMock()
election.parties = ['B', 'A']
election.parties = ["B", "A"]

return ElectionResults(election,
{
'B': {1: 5, 2: 60, 3: 25, 4: 55, 5: 55},
'A': {1: 95, 2: 40, 3: 75, 4: 45, 5: 45}
},
[1, 2, 3, 4, 5])
return ElectionResults(
election,
{
"B": {1: 5, 2: 60, 3: 25, 4: 55, 5: 55},
"A": {1: 95, 2: 40, 3: 75, 4: 45, 5: 45},
},
[1, 2, 3, 4, 5],
)


def test_efficiency_gap(mock_election):
Expand All @@ -24,8 +31,13 @@ def test_efficiency_gap(mock_election):


def test_wasted_votes(mock_election):
result = [wasted_votes(mock_election.totals_for_party['A'][i],
mock_election.totals_for_party['B'][i]) for i in range(1, 6)]
result = [
wasted_votes(
mock_election.totals_for_party["A"][i],
mock_election.totals_for_party["B"][i],
)
for i in range(1, 6)
]
assert result == [(45, 5), (40, 10), (25, 25), (45, 5), (45, 5)]


Expand All @@ -43,12 +55,14 @@ def test_mean_median_has_right_value(mock_election):
assert abs(mm - 0.15) < 0.00001


def test_signed_partisan_scores_are_positive_if_first_party_has_advantage(mock_election):
def test_signed_partisan_scores_are_positive_if_first_party_has_advantage(
mock_election
):
eg = efficiency_gap(mock_election)
mm = mean_median(mock_election)
pb = partisan_bias(mock_election)

assert (eg > 0 and mm > 0 and pb > 0)
assert eg > 0 and mm > 0 and pb > 0


def test_partisan_bias_has_right_value(mock_election):
Expand Down
10 changes: 1 addition & 9 deletions tests/updaters/test_perimeters.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import math
from collections import defaultdict

from gerrychain import MarkovChain
from gerrychain.accept import always_accept
from gerrychain.constraints import (no_vanishing_districts,
single_flip_contiguous)
from gerrychain.constraints import no_vanishing_districts, single_flip_contiguous
from gerrychain.grid import Grid
from gerrychain.proposals import propose_random_flip
from gerrychain.updaters.compactness import compute_polsby_popper


def setup():
Expand Down Expand Up @@ -132,8 +129,3 @@ def expected_perimeter(partition):
expected = expected_perimeter(state)
assert expected == state["perimeter"]


def test_polsby_popper_returns_nan_when_perimeter_is_0():
area = 10
perimeter = 0
assert compute_polsby_popper(area, perimeter) is math.nan

0 comments on commit c07cae5

Please sign in to comment.