or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

acceptance-functions.mdconstraints.mddata-aggregation.mdevaluation-metrics.mdgraph-operations.mdindex.mdmarkov-chain-analysis.mdoptimization.mdpartition-management.mdproposal-algorithms.md

evaluation-metrics.mddocs/

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

```