This notebook constructs a cleaned precinct geography for Arizona by combining 2020 Census blocks with 2024 election precincts.
The workflow performs several major tasks:
Load Census block and precinct shapefiles.
Standardize county names.
Reproject both datasets into a common projected coordinate system.
Assign every Census block to a voting precinct.
Clean small geometric discontinuities.
Merge disconnected precinct fragments.
Ensure every precinct is contiguous.
Detect and eliminate graph articulation points.
Attach 2024 presidential election results.
Export cleaned shapefiles
Required Local Libraries
Several custom libraries are used throughout the workflow.
These libraries provide functionality for:
county lookup tables
graph construction
shapefile I/O
hierarchy operations
topology cleaning
Most of the spatial processing relies on these modules rather than GeoPandas alone.
import geopandas as gpdimport numpy as npimport pandas as pdimport osimport shapelyimport syssys.path.append("/Users/Ranthony/Downloads/kmeans/data/lib")from tqdm import tqdm############ local libraries #################from importlib importreloadimport censusBlockFunctions as cbFimport countyFunctions as cnFimport hierarchyimport readerimport shapefileToGraphimport writerreload(cbF)reload(cnF)reload(hierarchy)reload(reader)reload(shapefileToGraph)reload(writer)##############################################
<module 'writer' from '/Users/Ranthony/Downloads/kmeans/data/lib/writer.py'>
Configuration
Specify the state abbreviation, FIPS code, and expected congressional district count.
These values are reused throughout the pipeline.
Changing them allows this workflow to be adapted for another state, provided the appropriate shapefiles are supplied.
stateAbv ="AZ"stateFIPS ="04"dists =9
Load GIS Data
Load
election precinct boundaries
Census block boundaries
The precinct shapefile contains election attributes, while the Census blocks provide the smallest geographic units that will ultimately be aggregated into clean precinct polygons.
You will need to obtain precinct shape file and census block shapes files for this, example links are provided below:
Spatial overlay operations require both datasets to share the same coordinate reference system (CRS), so we need the CRS of our two shape files to match!
Arizona State Plane (ESRI:103226) is used because it preserves distances and areas much better than geographic latitude/longitude coordinates.
You can find state CRS (or approximation) here: - https://spatialreference.org/ref/esri/
Note: Working in a projected CRS is particularly important for:
# check naming convensions -- should be fine since both were sourced from fp codes aboveprecinctShapefile.loc[0, "COUNTY"], cblockShapefile.loc[0, "COUNTY"]
Spatial overlay frequently introduces tiny disconnected polygons caused by
sliver polygons
precision errors
extremely small isolated fragments
These artifacts are generally not meaningful voting geography.
We want to remove these small geographic artifacts. The cleaning routine reassigns these fragments based on neighboring population, producing cleaner precinct geometries suitable for graph construction.
adjacency_check = [e for e in shapefileToGraph.findAdjacency(pctSF) if-1notin e]nbrList_check = shapefileToGraph.neighborList(pctSF, adjacency_check)articulationPoints = biconnectedComponents.getArticulationPoints_cdef(nbrList_check)assertlen(articulationPoints) ==0
Add Election Results
Election results are transferred from the original precinct shapefile to the cleaned precinct geometries.
Most precinct names match exactly.
However, some cleaned precincts are the result of merging multiple original precincts.
For merged precincts, vote totals are computed as the sum of the constituent precincts.
Merged precinct identifiers are stored as comma-separated precinct names.This helper function1. checks for an exact match,2. otherwise splits the merged name,3. sums vote totals across all original precincts.If necessary, missing precinct names are reported so they can be investigated manually.
def getv(name, votedict):# First try exact matchif name in votedict:return votedict[name]# Otherwise assume merged precincts split_name = [n.strip() for n in name.split(",")] total =0for n in split_name:if n notin votedict:print("Missing precinct:", repr(n))continue total += votedict[n]return total