1
+ """
2
+ The partisan metrics in this file are later used in the module
3
+ gerrychain.updaters.election.py. Thus, all of the election
4
+ results objects here are implicilty typed as ElectionResults,
5
+ but cannot be given an explicit type annotation due to problems
6
+ with circular imports.
7
+ """
8
+
1
9
import numpy
10
+ from typing import Tuple
2
11
3
12
4
- def mean_median (election_results ):
13
+ def mean_median (election_results ) -> float :
5
14
"""
6
15
Computes the Mean-Median score for the given ElectionResults.
7
16
A positive value indicates an advantage for the first party listed
8
17
in the Election's parties_to_columns dictionary.
18
+
19
+ :param election_results: An ElectionResults object
20
+ :type election_results: ElectionResults
21
+
22
+ :returns: The Mean-Median score for the given ElectionResults
23
+ :rtype: float
9
24
"""
10
25
first_party = election_results .election .parties [0 ]
11
26
data = election_results .percents (first_party )
12
27
13
28
return numpy .median (data ) - numpy .mean (data )
14
29
15
30
16
- def mean_thirdian (election_results ):
31
+ def mean_thirdian (election_results ) -> float :
17
32
"""
18
33
Computes the Mean-Median score for the given ElectionResults.
19
34
A positive value indicates an advantage for the first party listed
20
35
in the Election's parties_to_columns dictionary.
21
36
22
37
The motivation for this score is that the minority party in many
23
38
states struggles to win even a third of the seats.
39
+
40
+ :param election_results: An ElectionResults object
41
+ :type election_results: ElectionResults
42
+
43
+ :returns: The Mean-Thirdian score for the given ElectionResults
44
+ :rtype: float
24
45
"""
25
46
first_party = election_results .election .parties [0 ]
26
47
data = election_results .percents (first_party )
@@ -31,24 +52,35 @@ def mean_thirdian(election_results):
31
52
return thirdian - numpy .mean (data )
32
53
33
54
34
- def efficiency_gap (results ) :
55
+ def efficiency_gap (election_results ) -> float :
35
56
"""
36
57
Computes the efficiency gap for the given ElectionResults.
37
58
A positive value indicates an advantage for the first party listed
38
59
in the Election's parties_to_columns dictionary.
60
+
61
+ :param election_results: An ElectionResults object
62
+ :type election_results: ElectionResults
63
+
64
+ :returns: The efficiency gap for the given ElectionResults
65
+ :rtype: float
39
66
"""
40
- party1 , party2 = [results .counts (party ) for party in results .election .parties ]
67
+ party1 , party2 = [election_results .counts (party ) for party in election_results .election .parties ]
41
68
wasted_votes_by_part = map (wasted_votes , party1 , party2 )
42
- total_votes = results .total_votes ()
69
+ total_votes = election_results .total_votes ()
43
70
numerator = sum (waste2 - waste1 for waste1 , waste2 in wasted_votes_by_part )
44
71
return numerator / total_votes
45
72
46
73
47
- def wasted_votes (party1_votes , party2_votes ) :
74
+ def wasted_votes (party1_votes : int , party2_votes : int ) -> Tuple [ int , int ] :
48
75
"""
49
76
Computes the wasted votes for each party in the given race.
50
- :party1_votes: the number of votes party1 received in the race
51
- :party2_votes: the number of votes party2 received in the race
77
+ :param party1_votes: the number of votes party1 received in the race
78
+ :type party1_votes: int
79
+ :param party2_votes: the number of votes party2 received in the race
80
+ :type party2_votes: int
81
+
82
+ :returns: a tuple of the wasted votes for each party
83
+ :rtype: Tuple[int, int]
52
84
"""
53
85
total_votes = party1_votes + party2_votes
54
86
if party1_votes > party2_votes :
@@ -60,12 +92,18 @@ def wasted_votes(party1_votes, party2_votes):
60
92
return party1_waste , party2_waste
61
93
62
94
63
- def partisan_bias (election_results ):
95
+ def partisan_bias (election_results ) -> float :
64
96
"""
65
97
Computes the partisan bias for the given ElectionResults.
66
98
The partisan bias is defined as the number of districts with above-mean
67
99
vote share by the first party divided by the total number of districts,
68
100
minus 1/2.
101
+
102
+ :param election_results: An ElectionResults object
103
+ :type election_results: ElectionResults
104
+
105
+ :returns: The partisan bias for the given ElectionResults
106
+ :rtype: float
69
107
"""
70
108
first_party = election_results .election .parties [0 ]
71
109
party_shares = numpy .array (election_results .percents (first_party ))
@@ -74,37 +112,34 @@ def partisan_bias(election_results):
74
112
return (above_mean_districts / len (party_shares )) - 0.5
75
113
76
114
77
- def partisan_gini (election_results ):
115
+ def partisan_gini (election_results ) -> float :
78
116
"""
79
117
Computes the partisan Gini score for the given ElectionResults.
80
118
The partisan Gini score is defined as the area between the seats-votes
81
119
curve and its reflection about (.5, .5).
120
+
121
+ For more information on the computation, see Definition 1 in:
122
+ https://arxiv.org/pdf/2008.06930.pdf
123
+
124
+ :param election_results: An ElectionResults object
125
+ :type election_results: ElectionResults
126
+
127
+ :returns: The partisan Gini score for the given ElectionResults
128
+ :rtype: float
82
129
"""
83
130
# For two parties, the Gini score is symmetric--it does not vary by party.
84
131
party = election_results .election .parties [0 ]
85
132
86
- # To find seats as a function of votes, we assume uniform partisan swing.
87
- # That is, if the statewide popular vote share for a party swings by some
88
- # delta, the vote share for that party swings by that delta in each
89
- # district.
90
- # We calculate the necessary delta to shift the district with the highest
91
- # vote share for the party to a vote share of 0.5. This delta, subtracted
92
- # from the original popular vote share, gives the minimum popular vote
93
- # share that yields 1 seat to the party.
94
- # We repeat this process for the district with the second-highest vote
95
- # share, which gives the minimum popular vote share yielding 2 seats,
96
- # and so on.
97
133
overall_result = election_results .percent (party )
98
134
race_results = sorted (election_results .percents (party ), reverse = True )
99
135
seats_votes = [overall_result - r + 0.5 for r in race_results ]
100
136
101
137
# Apply reflection of seats-votes curve about (.5, .5)
102
138
reflected_sv = reversed ([1 - s for s in seats_votes ])
103
139
# Calculate the unscaled, unsigned area between the seats-votes curve
104
- # and its reflection. For each possible number of seats attained, we find
105
- # the area of a rectangle of unit height, with a width determined by the
106
- # horizontal distance between the curves at that number of seats.
140
+ # and its reflection.
107
141
unscaled_area = sum (abs (s - r ) for s , r in zip (seats_votes , reflected_sv ))
142
+
108
143
# We divide by area by the number of seats to obtain a partisan Gini score
109
144
# between 0 and 1.
110
145
return unscaled_area / len (race_results )
0 commit comments