Working with GeoDataFrames
Goal of this page
Build a FalcomChain Graph from a shapefile, GeoPackage, or GeoJSON —
column mapping, candidate marking, CRS handling, and the errors you
will actually hit.
Most users will start from a shapefile, GeoPackage, or GeoJSON of geographic
units. This guide shows how to build a FalcomChain Graph from such data.
Quick reference
from falcomchain import Graph
# From a file
graph = Graph.from_file(
"districts.shp",
demand_col="POP", # your population column
candidate_col="is_clinic", # your candidate flag column
)
# From a GeoDataFrame already loaded
import geopandas as gpd
gdf = gpd.read_file("districts.shp")
graph = Graph.from_geodataframe(
gdf,
demand_col="POP",
candidate_col="is_clinic",
)
The library will:
Compute rook adjacency from polygon boundaries
Add
areafrom each polygon’s geometryAdd
shared_perimto each edgeAdd
boundary_nodeandboundary_perimfor outer-boundary unitsCopy
demand_col→demandandcandidate_col→candidateValidate the schema
Specifying columns
If your demand column isn’t named demand:
graph = Graph.from_geodataframe(gdf, demand_col="population_2020")
The column is renamed to demand on the resulting graph (the canonical name
the algorithms use).
Same for candidates:
graph = Graph.from_geodataframe(gdf, candidate_col="has_clinic_site")
Marking facility candidates
The candidate column should be 0/1 or True/False per row. If you don’t have
this column, create it before building the graph:
# Example: any unit within 500m of a hospital is a candidate
gdf["candidate"] = (gdf.distance_to_hospital < 500).astype(int)
graph = Graph.from_geodataframe(gdf)
Adding extra columns
Pass cols_to_add to copy additional columns as node attributes:
graph = Graph.from_geodataframe(
gdf,
demand_col="POP",
candidate_col="has_clinic",
cols_to_add=["median_income", "vulnerability_score", "GEOID"],
)
# Now accessible as:
graph.nodes[0]["median_income"]
CRS and reprojection
The library reads gdf.crs and stores it on graph.graph["crs"]. If your
data is in degrees (longitude/latitude), area and perimeter will be in
square degrees — usually wrong.
# Reproject to a UTM projection automatically (good for area calculations)
graph = Graph.from_file("districts.shp", reproject=True)
# Or override the CRS if your file is missing it
graph = Graph.from_file("districts.shp", crs_override="EPSG:4326")
Common errors
SchemaValidationError: Missing node attribute 'demand'
You need a demand column in your GeoDataFrame, OR pass demand_col="..."
with the actual column name.
SchemaValidationError: No nodes are marked as facility candidates
Your candidate column has all zeros (or is missing). At least one node
must be a candidate for the algorithms to produce valid districts.
GeometryError: Invalid geometries at rows ...
Some polygons have self-intersections or other geometry issues. Either:
Fix them:
gdf.geometry = gdf.geometry.buffer(0)Or skip validation:
Graph.from_geodataframe(gdf, ignore_errors=True)
Areas/perimeters look weird
You’re probably in lat/lon coordinates. Use reproject=True.
Small example
import geopandas as gpd
from shapely.geometry import box
from falcomchain import Graph
# 2x2 grid of unit squares
gdf = gpd.GeoDataFrame(
{
"POP": [100, 200, 150, 175],
"is_clinic": [1, 0, 1, 0],
"name": ["A", "B", "C", "D"],
},
geometry=[box(0,0,1,1), box(1,0,2,1), box(0,1,1,2), box(1,1,2,2)],
crs="EPSG:32616", # UTM zone — uses meters
)
graph = Graph.from_geodataframe(
gdf,
demand_col="POP",
candidate_col="is_clinic",
cols_to_add=["name"],
)
print(graph.nodes[0])
# {'area': 1.0, 'demand': 100, 'candidate': 1, 'name': 'A',
# 'boundary_node': True, 'boundary_perim': 4.0}