network
High-level CityNetwork API for urban network analysis. The CityNetwork class wraps network construction, centrality computation, land-use analysis, and export into a single interface. It builds dual graphs (where street segments become nodes rather than intersections) directly from LineString geometries, enabling both shortest-path (metric distance) and simplest-path (angular change) analysis. See the Guide for concepts, conventions, and worked examples.
CityNetwork
High-level interface for urban network analysis. Wraps network construction, centrality computation, and land-use analysis into a single object that manages graph topology, node attributes, and coordinate reference systems. The network is built as a dual graph where street segments become nodes and intersections become edges, enabling both shortest-path (metric) and simplest-path (angular) centrality analysis.
Construct instances via the class methods rather than calling __init__ directly:
from_geopandas– from a GeoDataFrame of LineString geometriesfrom_wkts– from a dictionary of WKT strings or Shapely geometriesfrom_nx– from a cityseer-compatible NetworkX MultiGraphfrom_osm– from OpenStreetMap via a bounding polygonload– from a previously saved parquet/pickle pair
Most methods return self to support method chaining:
cn = (
CityNetwork.from_geopandas(edges_gdf, crs=32632)
.set_boundary(boundary_polygon)
.centrality_shortest(distances=[500, 1000, 2000])
)
The underlying graph construction automatically cleans input geometries by removing short self-loops, near-duplicate
edges, and short danglers. Use the feature_status property to inspect which input features were
filtered and why.
Dual graph architecture
CityNetwork always constructs a dual graph internally. In the dual representation, each street segment becomes a node (positioned at the segment midpoint) and edges connect segments that share a common intersection. This enables both shortest-path and simplest-path (angular) analysis from a single topology:
- Shortest-path analysis uses metric distances along street segments.
- Simplest-path analysis uses cumulative angular change along streets and at intersections as the routing cost.
Because the dual is built automatically, there is no need to call nx_to_dual when using CityNetwork. Although the topology is dual internally, results are visualised and exported as the original street segment geometries via to_geopandas, so each row in the output corresponds to one input street.
Working with results
All computed metrics are written to the internal nodes_gdf GeoDataFrame. Since CityNetwork uses a dual graph, each row in nodes_gdf represents a street segment, with a Point geometry at the segment midpoint.
To retrieve results with the original LineString geometries, use to_geopandas:
cn = CityNetwork.from_geopandas(edges_gdf, crs=32632)
cn.centrality_shortest(distances=[800])
# Midpoint geometries (internal representation)
cn.nodes_gdf["cc_harmonic_800"]
# Original LineString geometries with the same computed columns
result_gdf = cn.to_geopandas()
result_gdf["cc_harmonic_800"]
Column names follow the cc_{metric}_{distance} convention described in the Column Naming Conventions section.
Feature cleaning
Input geometries are automatically cleaned during construction. Short self-loops, near-duplicate edges, and short danglers are removed. The feature_status property returns a Series indicating the status of each input feature:
cn = CityNetwork.from_geopandas(edges_gdf, crs=32632)
print(cn.feature_status.value_counts())
# active 142
# short_dangler 3
# duplicate 1
Saving and loading
Networks can be serialised to disk and restored later, preserving all computed metrics:
cn.save("my_network")
# Creates: my_network.nodes.parquet, my_network.state.pkl
cn_restored = CityNetwork.load("my_network")
Incremental updates
The update method performs an incremental topology diff: unchanged features keep their node indices, added features are inserted, and removed features are deleted. Previously computed centrality columns are cleared since they are invalidated by topology changes.
# Initial build
cn = CityNetwork.from_geopandas(edges_gdf, crs=32632)
cn.centrality_shortest(distances=[800])
# Update with modified geometries
cn.update(updated_edges_gdf)
cn.centrality_shortest(distances=[800])
Typical workflow
import geopandas as gpd
from cityseer.network import CityNetwork
from cityseer import decay
# 1. Load street network edges
edges_gdf = gpd.read_file("streets.gpkg")
# 2. Build the network
cn = CityNetwork.from_geopandas(edges_gdf, crs="EPSG:32632")
# 3. Compute centrality at multiple scales
cn.centrality_shortest(distances=[400, 800, 1600])
cn.centrality_simplest(distances=[400, 800, 1600])
# 4. Compute land-use accessibility
landuses_gdf = gpd.read_file("landuses.gpkg")
cn, landuses_gdf = cn.compute_accessibilities(
data_gdf=landuses_gdf,
landuse_column_label="category",
accessibility_keys=["retail", "park"],
distances=[400, 800],
decay_fn=decay.exponential(),
)
# 5. Compute statistical aggregations
prices_gdf = gpd.read_file("property_prices.gpkg")
cn, prices_gdf = cn.compute_stats(
data_gdf=prices_gdf,
stats_column_labels=["price"],
distances=[800, 1600],
decay_fn=decay.gaussian(peak=400, cutoff=1600),
)
# 6. Export results with original LineString geometries
result_gdf = cn.to_geopandas()
result_gdf.to_file("results.gpkg")
For end-to-end worked examples with real-world data, see the Cityseer Examples site, including the Quickstart notebook.
CityNetwork
network_structure: NetworkStructure
nodes_gdf: geopandas.geodataframe.GeoDataFrame
to_geopandas
Return a GeoDataFrame with the original input LineString geometries. The returned GeoDataFrame contains all computed columns (centrality metrics, layer results, etc.) joined to the original edge geometries rather than the midpoint representations used internally.
Returns
A new GeoDataFrame indexed by feature id with LineString geometries.
Notes
cn.centrality_shortest(distances=[800])
result_gdf = cn.to_geopandas()
# result_gdf has LineString geometries (not midpoint Points)
print(result_gdf.geometry.geom_type.unique()) # ['LineString']
# All computed columns are present
print(result_gdf["cc_harmonic_800"])
# Export to file
result_gdf.to_file("centrality_results.gpkg")
is_dual: bool
is_directed: bool
crs: pyproj.crs.crs.CRS | None
node_count: int
feature_status: pandas.Series
from_wkts
Construct a CityNetwork from a dictionary of WKT strings or Shapely geometries.
Parameters
A mapping from feature identifiers to WKT strings or Shapely LineString geometries. Input geometries may include z (elevation) coordinates, which are preserved and used for slope-based walking impedance calculations.
A projected coordinate reference system (EPSG code, CRS object, or proj string).
Optional polygon in the same projected CRS; nodes inside are marked as live, nodes outside as dead.
If True, build a directed network. Requires oneway_fids. Features in oneway_fids are one-way (in LineString coordinate order); all other features are bidirectional.
Feature IDs that are one-way when directed=True. Ignored if directed=False.
Optional mapping from primal feature ID to its impedance factor. Each dual edge’s imp_factor becomes the length-weighted mean of its two adjacent primal segments’ impedances; missing entries default to 1.0. See the Edge Impedance section of the guide.
Returns
A new CityNetwork instance.
Raises
If directed=True but oneway_fids is not provided.
Notes
from shapely.geometry import LineString
from cityseer.network import CityNetwork
wkts = {
"street_a": LineString([(0, 0), (100, 0)]),
"street_b": LineString([(100, 0), (100, 100)]),
"street_c": LineString([(100, 0), (200, 0)]),
}
cn = CityNetwork.from_wkts(wkts, crs=32632)
cn.centrality_shortest(distances=[200])
from_geopandas
Construct a CityNetwork from a GeoDataFrame of LineString geometries. Extra columns from the input GeoDataFrame are carried through to the internal nodes GeoDataFrame. The CRS is read from the GeoDataFrame unless explicitly overridden.
Parameters
A GeoDataFrame with LineString or MultiLineString geometries. The index must be unique. Input geometries may include z (elevation) coordinates, which are preserved and used for slope-based walking impedance calculations.
Optional projected CRS override. If None, uses the GeoDataFrame’s CRS.
Optional polygon in the same projected CRS; nodes inside are marked as live, nodes outside as dead.
If True, build a directed network. Requires a boolean oneway column in the GeoDataFrame. Features with oneway=True are one-way in LineString coordinate order; features with oneway=False are bidirectional.
Returns
A new CityNetwork instance.
Raises
If directed=True but the GeoDataFrame has no oneway column.
Notes
An optional imp_factor column on the input GeoDataFrame is propagated to each dual edge as the length-weighted mean of the two adjacent primal segments’ impedances; omit it to leave every dual edge at the default 1.0. See the Edge Impedance section of the guide.
import geopandas as gpd
from shapely.geometry import LineString
from cityseer.network import CityNetwork
gdf = gpd.GeoDataFrame(
{
"geometry": [
LineString([(0, 0), (100, 0)]),
LineString([(100, 0), (100, 100)]),
LineString([(100, 0), (200, 0)]),
]
},
crs="EPSG:32632",
)
cn = CityNetwork.from_geopandas(gdf)
cn.centrality_shortest(distances=[200])
print(cn.nodes_gdf[["cc_harmonic_200", "cc_betweenness_200"]])
With directed one-way streets:
gdf = gpd.GeoDataFrame(
{
"geometry": [
LineString([(0, 0), (100, 0)]),
LineString([(100, 0), (200, 0)]),
],
"oneway": [True, False],
},
crs="EPSG:32632",
)
cn = CityNetwork.from_geopandas(gdf, directed=True)
from_nx
Construct a CityNetwork from a cityseer-compatible NetworkX graph. The input graph must be a primal edge graph (not a dual graph) with geom attributes on edges and a crs attribute on the graph. Node live attributes are preserved.
When a MultiDiGraph is passed, directed mode is enabled automatically: each directed edge becomes its own one-way dual node (in the coordinate order of the directed edge). Two-way streets should be represented as two reciprocal edges (A to B and B to A), which produce two separate dual nodes.
Parameters
A cityseer-compatible primal NetworkX graph. MultiDiGraph enables directed routing. Any imp_factor edge attribute is propagated to each dual edge as the length-weighted mean of the two adjacent primal segments’ impedances (default 1.0 if absent). See the Edge Impedance section of the guide.
Optional polygon in the same projected CRS; nodes inside are marked as live, nodes outside as dead.
Returns
A new CityNetwork instance.
Raises
If the input graph is a dual graph.
Notes
From an undirected graph:
import networkx as nx
from shapely.geometry import LineString
from cityseer.network import CityNetwork
G = nx.MultiGraph(crs="EPSG:32632")
G.add_node("a", x=0.0, y=0.0)
G.add_node("b", x=100.0, y=0.0)
G.add_node("c", x=200.0, y=0.0)
G.add_edge("a", "b", geom=LineString([(0, 0), (100, 0)]))
G.add_edge("b", "c", geom=LineString([(100, 0), (200, 0)]))
cn = CityNetwork.from_nx(G)
From a directed MultiDiGraph (e.g. via OSMnx):
G = nx.MultiDiGraph(crs="EPSG:32632")
G.add_node("a", x=0.0, y=0.0)
G.add_node("b", x=100.0, y=0.0)
# One-way: a -> b only
G.add_edge("a", "b", key=0, geom=LineString([(0, 0), (100, 0)]))
cn = CityNetwork.from_nx(G)
assert cn.is_directed
from_osm
Construct a CityNetwork from OpenStreetMap data within a bounding polygon. Downloads the road network and converts it to a dual CityNetwork.
For directed (one-way) routing with OSM data, fetch a directed graph via OSMnx <https://osmnx.readthedocs.io/>_ and pass it to :meth:from_nx or convert it with :func:io.nx_from_osm_nx(directed=True) <cityseer.tools.io.nx_from_osm_nx>.
Parameters
A Shapely polygon defining the area of interest.
EPSG code for poly_geom. Defaults to 4326 (WGS84).
Target projected EPSG code. If None, an appropriate UTM zone is inferred.
Whether to simplify the OSM graph topology. Defaults to True.
Optional polygon for live/dead node assignment (in the target projected CRS).
Additional keyword arguments passed to io.osm_graph_from_poly.
Returns
A new CityNetwork instance.
Notes
from shapely.geometry import box
from cityseer.network import CityNetwork
# Bounding box in WGS84 (lon/lat)
polygon = box(-0.13, 51.51, -0.12, 51.52)
cn = CityNetwork.from_osm(polygon, to_crs_code=32630)
cn.centrality_shortest(distances=[400, 800])
update
Update the network topology with new or modified geometries. Performs an incremental diff against the current state: unchanged features retain their node indices, added features are inserted, and removed features are deleted. Previously computed centrality columns are cleared since they are invalidated by topology changes.
For directed networks built via from_geopandas(directed=True), the incoming GeoDataFrame must include the oneway column. Direction changes (even without geometry changes) trigger a rebuild.
Parameters
The complete updated set of geometries (not just the diff).
Returns
Returns self for method chaining.
set_boundary
Set live/dead node status based on a boundary polygon. Nodes whose midpoints fall inside the polygon are marked live; others are marked dead. Dead nodes participate in traversal but their own values are not reported. Closeness skips dead sources in exact mode (a cost saving); betweenness sources from every node so that routes through the study area — including those between dead nodes — credit the live nodes they traverse.
Parameters
A Shapely polygon in the same projected CRS as the network.
Returns
Returns self for method chaining.
set_all_live
Mark all nodes as live, clearing any boundary restriction.
Returns
Returns self for method chaining.
save
Save the network to disk as a parquet/pickle pair. Creates two files: <path>.nodes.parquet (the nodes GeoDataFrame with all computed columns) and <path>.state.pkl (source WKTs, boundary, and feature status). Use load to restore.
Parameters
Base file path. File extensions are replaced automatically.
Notes
cn.centrality_shortest(distances=[800])
cn.save("my_network")
# Creates: my_network.nodes.parquet, my_network.state.pkl
# Later, restore the full network with all metrics
cn_restored = CityNetwork.load("my_network")
print(cn_restored.nodes_gdf["cc_harmonic_800"])
load
Load a previously saved CityNetwork from disk. Rebuilds the full graph topology from the saved source WKTs and merges any previously computed columns (centrality metrics, layer results) from the saved nodes GeoDataFrame.
Parameters
Base file path (same as was passed to save).
Returns
The restored CityNetwork instance.
centrality_shortest
Compute shortest-path (metric) centrality. Wraps centrality_shortest. All keyword arguments are forwarded; see that function for the full parameter list including distances, minutes, closeness, betweenness, cycles, postprocess, segment_weighted, sample, and epsilon.
Returns
Returns self for method chaining. Results are written to nodes_gdf.
Notes
# Multiple distance thresholds
cn.centrality_shortest(distances=[400, 800, 1600])
# With segment-length weighting
cn.centrality_shortest(distances=[400, 800], segment_weighted=True)
# With sampling for large networks
cn.centrality_shortest(
distances=[800, 2000, 5000],
sample=True,
epsilon=0.05,
)
# Custom closeness metric
cn.centrality_shortest(
distances=[800],
closeness={"harmonic": "1/c", "gravity": "exp(-0.005 * c)"},
betweenness={},
)
By default this emits just cc_harmonic_{d} (closeness) and cc_betweenness_{d}, with cycles off. Pass closeness / betweenness expression dicts (and cycles=True) to compute any of the metrics below (see Column Naming Conventions):
| Column | Description |
|---|---|
cc_density_{d} | Count of reachable nodes (or total reachable street length if segment_weighted). |
cc_harmonic_{d} | Harmonic closeness: sum of inverse distances to reachable nodes (default). |
cc_farness_{d} | Sum of distances to reachable nodes. |
cc_hillier_{d} | Hillier normalisation (density² / farness). |
cc_cycles_{d} | Circuit rank: count of independent loops in the reachable subgraph. |
cc_decay_{d} | Decay-weighted closeness (e.g. exp(-4 * p)). |
cc_betweenness_{d} | Betweenness: count of shortest paths passing through each node (default). |
cc_betweenness_decay_{d} | Decay-weighted betweenness (e.g. exp(-4 * p)). |
centrality_simplest
Compute simplest-path (angular) centrality. Wraps centrality_simplest. All keyword arguments are forwarded; see that function for the full parameter list.
Returns
Returns self for method chaining. Results are written to nodes_gdf.
Notes
cn.centrality_simplest(distances=[400, 800, 1600])
By default this emits just cc_harmonic_{d}_ang (closeness) and cc_betweenness_{d}_ang. Pass closeness / betweenness expression dicts to compute any of the metrics below (note the _ang suffix):
| Column | Description |
|---|---|
cc_density_{d}_ang | Count of reachable nodes (or total reachable street length if segment_weighted). |
cc_harmonic_{d}_ang | Harmonic closeness using angular cost as impedance (default). |
cc_farness_{d}_ang | Sum of angular costs to reachable nodes. |
cc_hillier_{d}_ang | Hillier normalisation (density² / farness). |
cc_betweenness_{d}_ang | Betweenness via simplest angular paths (angular choice; default). |
build_od_matrix
Build an origin-destination (OD) matrix for OD-weighted betweenness. In standard betweenness, every pair of nodes contributes equally. OD-weighted betweenness instead weights each pair by observed trip counts between their respective zones (e.g. commuter cycling counts), so streets carrying more actual traffic receive higher scores.
Wraps build_od_matrix. See that function for the full parameter list.
Parameters
Origin-destination flow data with columns for origin zone, destination zone, and trip weight.
Zone boundaries (polygons) or centroids (points) corresponding to the OD matrix.
Returns
An OD matrix for use with betweenness_od.
betweenness_od
Compute OD-weighted betweenness centrality. Weights betweenness by actual origin-destination trip counts rather than treating all paths equally. Only source nodes with outbound trips are traversed, and each shortest-path contribution is scaled by the corresponding OD weight.
Wraps betweenness_od. See that function for the full parameter list.
Parameters
An OD matrix from build_od_matrix.
Returns
Returns self for method chaining. Results are written to nodes_gdf.
compute_accessibilities
Compute land-use accessibility metrics. Counts how many instances of each specified land-use category (e.g. retail, parks) are reachable within each distance threshold, and records the nearest distance to each category.
Wraps compute_accessibilities. All additional keyword arguments are forwarded; see that function for the full parameter list including landuse_column_label, accessibility_keys, distances, minutes, decay_fn, and angular. decay_fn accepts a single expression or a {label: expression} dict that computes several decay variants in one network traversal, each label suffixing its output columns.
Parameters
A GeoDataFrame of land-use points with categorical columns.
Returns
Returns self with accessibility columns added to nodes_gdf.
The input data GeoDataFrame with nearest network assignments.
Notes
from cityseer import decay
cn, landuses_gdf = cn.compute_accessibilities(
data_gdf=landuses_gdf,
landuse_column_label="category",
accessibility_keys=["retail", "cafe", "park"],
distances=[400, 800],
decay_fn=decay.exponential(),
)
# Count of reachable "retail" within 800m
print(cn.nodes_gdf["cc_retail_800"])
# Nearest distance to "park" at the maximum threshold
print(cn.nodes_gdf["cc_park_nearest_max_800"])
compute_mixed_uses
Compute mixed-use diversity metrics. Measures the diversity of land-use categories reachable from each node using Hill numbers: q=0 counts how many distinct types are present, q=1 measures diversity accounting for how evenly types are represented, and q=2 gives greater weight to the most common types.
Wraps compute_mixed_uses. All additional keyword arguments are forwarded; see that function for the full parameter list including landuse_column_label, distances, minutes, compute_hill, compute_shannon, compute_gini, decay_fn, and angular. decay_fn accepts a single expression or a {label: expression} dict that computes several decay variants in one network traversal, each label suffixing its output columns (only the Hill measures vary with decay).
Parameters
A GeoDataFrame of land-use points with categorical columns.
Returns
Returns self with mixed-use columns added to nodes_gdf.
The input data GeoDataFrame with nearest network assignments.
Notes
cn, landuses_gdf = cn.compute_mixed_uses(
data_gdf=landuses_gdf,
landuse_column_label="category",
distances=[400, 800],
)
# Hill q=0 (count of distinct land-use types) at 800m
print(cn.nodes_gdf["cc_hill_q0_800"])
compute_stats
Compute statistical aggregations of numerical data over the network. Aggregates numerical attributes (e.g. property prices, floor areas) within network-distance thresholds, computing weighted statistics (mean, sum, min, max, variance, etc.) at each node.
Wraps compute_stats. All additional keyword arguments are forwarded; see that function for the full parameter list including stats_column_labels, distances, minutes, decay_fn, and angular. decay_fn accepts a single expression or a {label: expression} dict that computes several decay variants in one network traversal, each label suffixing its output columns.
Parameters
A GeoDataFrame of data points with numerical columns.
Returns
Returns self with statistical columns added to nodes_gdf.
The input data GeoDataFrame with nearest network assignments.
Notes
from cityseer import decay
cn, prices_gdf = cn.compute_stats(
data_gdf=prices_gdf,
stats_column_labels=["price", "floor_area"],
distances=[800, 1600],
decay_fn=decay.exponential(),
)
# Weighted mean of "price" at 800m
print(cn.nodes_gdf["cc_price_mean_800"])
# Weighted sum of "floor_area" at 1600m
print(cn.nodes_gdf["cc_floor_area_sum_1600"])
add_gtfs
Add GTFS (General Transit Feed Specification) public transport data to the network. Integrates transit stops and routes so that centrality and accessibility analyses account for public transport connections.
Wraps io.add_transport_gtfs.
Parameters
Path to a GTFS zip file or directory.
Optional CRS override for the GTFS data.
Maximum distance (metres) for snapping transit stops to the nearest network node. Stops beyond this distance are excluded. Defaults to 400.
Returns
Returns self for method chaining.
to_nx
Convert the network to a NetworkX MultiGraph (or MultiDiGraph if directed). If the network was built with from_nx, returns a copy of the original graph with computed centrality and layer columns added to each edge’s data dictionary. Otherwise builds a new cityseer-compatible undirected graph from the internal GeoDataFrame.
Returns
A primal edge graph with computed metrics added to edge data.
Raises
If the network is directed but was not built via from_nx (no source graph to export).
Notes
cn = CityNetwork.from_nx(G)
cn.centrality_shortest(distances=[800])
# Round-trip: get back a NetworkX graph with metrics on edges
G_with_metrics = cn.to_nx()
u, v, k, data = list(G_with_metrics.edges(keys=True, data=True))[0]
print(data["cc_harmonic_800"])