0
# Evaluation Metrics
1
2
Analyze districting plans using partisan metrics, compactness measures, and demographic analysis. Metrics provide quantitative tools for comparing redistricting plans and identifying outliers.
3
4
## Capabilities
5
6
### Partisan Metrics
7
8
Measure partisan bias and electoral competitiveness using established political science metrics.
9
10
```python { .api }
11
def mean_median(election_results: Dict[str, Dict[str, int]]) -> float:
12
"""
13
Calculate mean-median difference for partisan bias analysis.
14
15
Compares the median district vote share to the statewide vote share.
16
Positive values indicate bias toward the party with >50% statewide vote.
17
18
Parameters:
19
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
20
21
Returns:
22
float: Mean-median difference as decimal (-1 to 1)
23
"""
24
25
def mean_thirdian(election_results: Dict[str, Dict[str, int]]) -> float:
26
"""
27
Calculate mean-thirdian difference (more robust than mean-median).
28
29
Parameters:
30
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
31
32
Returns:
33
float: Mean-thirdian difference as decimal
34
"""
35
36
def partisan_bias(election_results: Dict[str, Dict[str, int]]) -> float:
37
"""
38
Calculate partisan bias using the standard seats-votes relationship.
39
40
Parameters:
41
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
42
43
Returns:
44
float: Partisan bias measure
45
"""
46
47
def partisan_gini(election_results: Dict[str, Dict[str, int]]) -> float:
48
"""
49
Calculate partisan Gini coefficient measuring vote distribution inequality.
50
51
Parameters:
52
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
53
54
Returns:
55
float: Gini coefficient (0 = perfectly equal, 1 = maximally unequal)
56
"""
57
58
def efficiency_gap(election_results: Dict[str, Dict[str, int]]) -> float:
59
"""
60
Calculate efficiency gap measuring wasted votes.
61
62
Standard metric for measuring partisan gerrymandering. Values >7% or <-7%
63
are often considered evidence of significant partisan bias.
64
65
Parameters:
66
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
67
68
Returns:
69
float: Efficiency gap as decimal (-1 to 1)
70
"""
71
72
def wasted_votes(election_results: Dict[str, Dict[str, int]]) -> Dict[str, Dict[str, int]]:
73
"""
74
Calculate wasted votes by party and district.
75
76
Wasted votes are votes that don't contribute to winning:
77
- All votes for losing candidate
78
- Surplus votes for winning candidate (beyond 50% + 1)
79
80
Parameters:
81
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
82
83
Returns:
84
Dict[str, Dict[str, int]]: District -> party -> wasted votes
85
"""
86
87
def seats_won(election_results: Dict[str, Dict[str, int]]) -> Dict[str, int]:
88
"""
89
Count seats won by each party.
90
91
Parameters:
92
- election_results (Dict[str, Dict[str, int]]): District -> party -> votes
93
94
Returns:
95
Dict[str, int]: Party -> number of seats won
96
"""
97
```
98
99
Usage example:
100
```python
101
from gerrychain.metrics import mean_median, efficiency_gap, seats_won
102
from gerrychain.updaters import Election
103
104
# Set up election tracking
105
partition = GeographicPartition(
106
graph,
107
assignment="district",
108
updaters={"SEN18": Election("SEN18", ["SEN18D", "SEN18R"])}
109
)
110
111
# Calculate metrics
112
mm = mean_median(partition["SEN18"])
113
eg = efficiency_gap(partition["SEN18"])
114
seats = seats_won(partition["SEN18"])
115
116
print(f"Mean-Median: {mm:.3f}")
117
print(f"Efficiency Gap: {eg:.3f}")
118
print(f"Dem seats: {seats['SEN18D']}, Rep seats: {seats['SEN18R']}")
119
```
120
121
### Compactness Metrics
122
123
Measure district shape compactness using geometric and graph-theoretic measures.
124
125
```python { .api }
126
def polsby_popper(partition: Partition) -> Dict[DistrictId, float]:
127
"""
128
Calculate Polsby-Popper compactness score for each district.
129
130
Measures ratio of district area to area of circle with same perimeter.
131
Higher values (closer to 1.0) indicate more compact districts.
132
133
Parameters:
134
- partition (Partition): Partition with geometric data
135
136
Returns:
137
Dict[DistrictId, float]: Polsby-Popper scores by district
138
"""
139
140
def schwartzberg(partition: Partition) -> Dict[DistrictId, float]:
141
"""
142
Calculate Schwartzberg compactness ratio for each district.
143
144
Measures ratio of district perimeter to perimeter of circle with same area.
145
Lower values (closer to 1.0) indicate more compact districts.
146
147
Parameters:
148
- partition (Partition): Partition with geometric data
149
150
Returns:
151
Dict[DistrictId, float]: Schwartzberg ratios by district
152
"""
153
154
def convex_hull_ratio(partition: Partition) -> Dict[DistrictId, float]:
155
"""
156
Calculate ratio of district area to its convex hull area.
157
158
Parameters:
159
- partition (Partition): Partition with geometric data
160
161
Returns:
162
Dict[DistrictId, float]: Convex hull ratios by district
163
"""
164
165
def reock(partition: Partition) -> Dict[DistrictId, float]:
166
"""
167
Calculate Reock compactness score (area to minimum bounding circle).
168
169
Parameters:
170
- partition (Partition): Partition with geometric data
171
172
Returns:
173
Dict[DistrictId, float]: Reock scores by district
174
"""
175
176
def boundary_node_ratio(partition: Partition) -> Dict[DistrictId, float]:
177
"""
178
Calculate ratio of boundary nodes to total nodes per district.
179
180
Graph-theoretic compactness measure not requiring geometric data.
181
182
Parameters:
183
- partition (Partition): Any partition
184
185
Returns:
186
Dict[DistrictId, float]: Boundary node ratios by district
187
"""
188
```
189
190
Usage example:
191
```python
192
from gerrychain.metrics import polsby_popper, schwartzberg, boundary_node_ratio
193
194
# Calculate compactness for current partition
195
pp_scores = polsby_popper(partition)
196
schw_scores = schwartzberg(partition)
197
boundary_ratios = boundary_node_ratio(partition)
198
199
# Report compactness
200
for district in partition.parts:
201
print(f"District {district}:")
202
print(f" Polsby-Popper: {pp_scores[district]:.3f}")
203
print(f" Schwartzberg: {schw_scores[district]:.3f}")
204
print(f" Boundary ratio: {boundary_ratios[district]:.3f}")
205
```
206
207
### Complete Metrics Analysis
208
209
Example of comprehensive metric analysis across a Markov chain:
210
211
```python
212
from gerrychain import MarkovChain, GeographicPartition
213
from gerrychain.metrics import (
214
mean_median, efficiency_gap, partisan_bias,
215
polsby_popper, schwartzberg, seats_won
216
)
217
from gerrychain.updaters import Election
218
import numpy as np
219
220
# Set up partition with election data
221
partition = GeographicPartition(
222
graph,
223
assignment="district",
224
updaters={
225
"SEN18": Election("SEN18", ["SEN18D", "SEN18R"]),
226
"GOV18": Election("GOV18", ["GOV18D", "GOV18R"])
227
}
228
)
229
230
# Set up chain
231
chain = MarkovChain(
232
proposal=recom,
233
constraints=constraints,
234
accept=always_accept,
235
initial_state=partition,
236
total_steps=10000
237
)
238
239
# Collect metrics
240
metrics_data = {
241
"mean_median_sen": [],
242
"efficiency_gap_sen": [],
243
"mean_median_gov": [],
244
"efficiency_gap_gov": [],
245
"dem_seats_sen": [],
246
"rep_seats_sen": [],
247
"avg_polsby_popper": [],
248
"avg_schwartzberg": []
249
}
250
251
for state in chain.with_progress_bar():
252
# Partisan metrics
253
metrics_data["mean_median_sen"].append(mean_median(state["SEN18"]))
254
metrics_data["efficiency_gap_sen"].append(efficiency_gap(state["SEN18"]))
255
metrics_data["mean_median_gov"].append(mean_median(state["GOV18"]))
256
metrics_data["efficiency_gap_gov"].append(efficiency_gap(state["GOV18"]))
257
258
# Seat counts
259
seats_sen = seats_won(state["SEN18"])
260
metrics_data["dem_seats_sen"].append(seats_sen["SEN18D"])
261
metrics_data["rep_seats_sen"].append(seats_sen["SEN18R"])
262
263
# Compactness (average across districts)
264
pp_scores = polsby_popper(state)
265
schw_scores = schwartzberg(state)
266
metrics_data["avg_polsby_popper"].append(np.mean(list(pp_scores.values())))
267
metrics_data["avg_schwartzberg"].append(np.mean(list(schw_scores.values())))
268
269
# Analyze distributions
270
print("Metric Analysis Results:")
271
print("=" * 50)
272
273
for metric, values in metrics_data.items():
274
mean_val = np.mean(values)
275
std_val = np.std(values)
276
print(f"{metric:20}: {mean_val:7.3f} ± {std_val:.3f}")
277
278
# Compare initial plan to ensemble
279
initial_mm = mean_median(partition["SEN18"])
280
ensemble_mm = metrics_data["mean_median_sen"]
281
percentile = 100 * np.mean(np.array(ensemble_mm) <= initial_mm)
282
283
print(f"\nInitial plan mean-median: {initial_mm:.3f}")
284
print(f"Percentile in ensemble: {percentile:.1f}%")
285
286
# Outlier analysis
287
outlier_threshold = 2.0 # 2 standard deviations
288
outliers = []
289
for i, mm in enumerate(ensemble_mm):
290
z_score = abs(mm - np.mean(ensemble_mm)) / np.std(ensemble_mm)
291
if z_score > outlier_threshold:
292
outliers.append((i, mm, z_score))
293
294
print(f"\nFound {len(outliers)} outlier plans (>2σ)")
295
```
296
297
### Custom Metric Functions
298
299
Examples of creating custom evaluation metrics:
300
301
```python
302
def competitiveness_index(election_results):
303
"""Measure electoral competitiveness across districts."""
304
competitive_districts = 0
305
total_districts = len(election_results)
306
307
for district, votes in election_results.items():
308
total_votes = sum(votes.values())
309
if total_votes > 0:
310
parties = list(votes.keys())
311
if len(parties) >= 2:
312
# Sort by vote count
313
sorted_votes = sorted(votes.values(), reverse=True)
314
margin = (sorted_votes[0] - sorted_votes[1]) / total_votes
315
if margin < 0.10: # Within 10 percentage points
316
competitive_districts += 1
317
318
return competitive_districts / total_districts if total_districts > 0 else 0
319
320
def minority_opportunity_districts(partition, minority_column="BVAP", threshold=0.4):
321
"""Count districts with minority opportunity (>40% minority VAP)."""
322
count = 0
323
for district in partition.parts:
324
total_vap = partition["vap"][district]
325
minority_vap = partition[minority_column][district]
326
if total_vap > 0 and minority_vap / total_vap >= threshold:
327
count += 1
328
return count
329
330
# Use in analysis
331
competitiveness_scores = []
332
minority_districts = []
333
334
for state in chain:
335
comp_score = competitiveness_index(state["SEN18"])
336
minority_count = minority_opportunity_districts(state)
337
338
competitiveness_scores.append(comp_score)
339
minority_districts.append(minority_count)
340
```
341
342
## Types
343
344
```python { .api }
345
ElectionResults = Dict[str, Dict[str, int]] # District -> Party -> Votes
346
DistrictId = int
347
CompactnessScores = Dict[DistrictId, float]
348
```