Spatial networks¶
A SpatialNetwork holds places as nodes with coordinates, joined by edges with a length in meters or another weight. A road network from OSMRoadsFetcher is one kind. The network is two tables, and every edit returns a new network. The road networks guide shows a full workflow.
to_pandana and backend="pandana" need pip install "giga-spatial[pandana]". Why the network is held as tables, and how a saved network is laid out, are in ADR 0004 and ADR 0005.
The methods below are in this order: building, saving, and converting; reading the tables; editing; inserting points; algorithms; and reducing.
Bases: EditingMixin, AlgorithmsMixin, ReductionMixin, InsertionMixin
A spatial network held as a nodes table and an edges table.
Nodes carry coordinates in EPSG:4326, and edges carry a length in meters or another weight. A road network fetched by OSMRoadsFetcher is one kind; points joined to their nearest neighbours are another.
The nodes table is indexed by node_id and has the columns x (longitude), y (latitude), and kind, plus any attributes. The edges table has u, v, key, length (meters), kind, an optional geometry column of LineStrings, and any attributes. An edge is identified by (u, v, key).
A network never changes after it is built: methods that edit it return a new network. The tables returned by nodes and edges are shallow copies; adding columns to them does not change the network, but do not modify their values in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes | DataFrame | Node table, indexed by node ID or with a | required |
edges | DataFrame | Edge table with | required |
directed | bool | Whether edges run from | True |
crs | Any | CRS of the input coordinates. They are converted to EPSG:4326. The CRS of a GeoDataFrame takes precedence for its geometries. | WGS84 |
weight | str | Default edge column for algorithms. | 'length' |
metadata | Optional[Dict[str, Any]] | Provenance, such as the fetch parameters. | None |
Raises:
| Type | Description |
|---|---|
ValueError | If node IDs repeat, an edge references an unknown node, an edge |
nodes property ¶
The nodes table, indexed by node ID.
edges property ¶
The edges table.
nodes_gdf cached property ¶
Nodes as a GeoDataFrame of Points in EPSG:4326.
edges_gdf cached property ¶
Edges as a GeoDataFrame in EPSG:4326, with straight lines where geometry is missing.
node_index property ¶
Node IDs by position: node_index[i] is the ID at position i, and node_index.get_indexer(ids) gives positions.
bounds property ¶
(minx, miny, maxx, maxy) of the nodes and edge geometries, in EPSG:4326.
from_networkx(G, weight='length') classmethod ¶
Build a network from a networkx graph whose nodes have x and y.
osmnx graphs work directly. Whether the network is directed comes from the graph, and the CRS from G.graph["crs"] (EPSG:4326 if absent). Graphs without edge keys get key 0. The other graph attributes become the network's metadata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
G | Graph | Any networkx graph. | required |
weight | str | Default edge column for algorithms. | 'length' |
Returns:
| Type | Description |
|---|---|
SpatialNetwork | The network. |
from_points(points, connect='knn', k=5, max_distance=None, id_column=None, weight='length', weight_func=None) classmethod ¶
Build an undirected network whose nodes are the points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points | Any | A GeoDataFrame of points, a DataFrame with latitude and longitude columns, or a list of | required |
connect | str |
| 'knn' |
k | int | Number of neighbours for | 5 |
max_distance | Optional[float] | Longest edge in meters. Required for | None |
id_column | Optional[str] | Column holding node IDs. The frame's index otherwise. | None |
weight | str | Default weight column. Anything but | 'length' |
weight_func | Optional[Callable[[DataFrame, DataFrame, ndarray], Any]] | Computes the | None |
Returns:
| Type | Description |
|---|---|
SpatialNetwork | The network. Nodes have kind |
load(path, data_store=None, columns=None) classmethod ¶
Load a network saved with :meth:save.
List attributes such as osmid come back as one numpy array per edge, and text attributes as categoricals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path | Any | Folder the network was saved to. | required |
data_store | Optional[Any] | Data store to read from. The local file system otherwise. | None |
columns | Optional[Iterable[str]] | Edge attribute columns to read, in addition to | None |
Raises:
| Type | Description |
|---|---|
FileNotFoundError | If there is no saved network at |
ValueError | If the folder was saved by a newer format version, or |
save(path, data_store=None) ¶
Save the network as a folder: nodes.parquet, edges.parquet, and network.json.
Edges are GeoParquet with WKB geometry, readable by geopandas, DuckDB, and QGIS; without edge geometry, edges.parquet is plain Parquet. Attributes that mix lists and single values, as osmnx produces, are normalised first (see normalize_attributes); the network itself is not changed. Existing files in the folder are overwritten.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path | Any | Folder to write. | required |
data_store | Optional[Any] | Data store to write to. The local file system otherwise. | None |
Raises:
| Type | Description |
|---|---|
ValueError | If node IDs are neither integers nor text, or a column cannot be stored. |
TypeError | If the metadata is not JSON serializable. |
to_networkx(directed=None) ¶
A new networkx graph with every node and edge attribute.
Missing values are left out of the attribute dictionaries, so straight edges have no geometry, as in osmnx. G.graph holds the metadata and crs. The graph is built on every call and not cached.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directed | Optional[bool] |
| None |
Returns:
| Type | Description |
|---|---|
MultiGraph |
|
to_pandana(weight=None) ¶
A pandana network indexed by node ID, with twoway set to whether the network is undirected.
Built once per weight and cached. Needs pip install 'giga-spatial[pandana]'. backend="pandana" does not need it: it works on any network.
Raises:
| Type | Description |
|---|---|
ValueError | If node IDs mix integers and text, which pandana cannot index. |
to_geodataframes(crs=None) ¶
Nodes and edges as GeoDataFrames, projected to crs if given.
node_coordinates(ids=None, crs=None) ¶
Node coordinates as an array of (y, x) rows.
In EPSG:4326 that is (lat, lon). With crs, the coordinates are projected and keep the (y, x) order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ids | Any | Node IDs, in the order wanted. All nodes otherwise. | None |
crs | Any | CRS to project to. | None |
Raises:
| Type | Description |
|---|---|
KeyError | If an ID is not in the network. |
degree(directed=None) ¶
Number of edges at each node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directed | Optional[bool] | With | None |
Returns:
| Type | Description |
|---|---|
Series | Degrees indexed by node ID. A self-loop counts twice. |
total_length(by=None) ¶
Sum of edge lengths in meters, optionally grouped by an edge column.
In a directed network a two-way road is two rows, so it counts twice.
info() ¶
Summary of the network.
nearest_nodes(points, max_distance=None, return_distance=False) ¶
The node closest to each point, by great-circle distance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points | Any | A GeoDataFrame of points, a DataFrame with latitude and longitude columns, or a list of | required |
max_distance | Optional[float] | Meters. Points with no node this close get a missing ID. | None |
return_distance | bool | Also return the distance in meters. | False |
Returns:
| Type | Description |
|---|---|
Any | A Series of node IDs indexed like the points, or with |
Any |
|
filter_nodes(ids=None, mask=None, geometry=None, predicate='intersects') ¶
Keep the selected nodes and the edges between them.
Pass exactly one of ids, mask, or geometry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ids | Any | Node IDs to keep. | None |
mask | Any | Booleans in node order, or a boolean Series indexed by node ID. | None |
geometry | Any | Area in EPSG:4326, a shapely geometry or a GeoDataFrame or GeoSeries in any CRS. | None |
predicate | str |
| 'intersects' |
remove_nodes(ids=None, points=None, geometry=None, max_distance=None, predicate='intersects') ¶
Remove nodes and the edges attached to them.
Pass exactly one of ids, points, or geometry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ids | Any | Node IDs to remove. | None |
points | Any | Remove the node nearest each point, within | None |
geometry | Any | Remove the nodes in this area (see :meth: | None |
max_distance | Optional[float] | Meters, for | None |
predicate | str |
| 'intersects' |
set_node_attribute(name, values) ¶
Set one node attribute.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name | str | Column to set or create. | required |
values | Any | A single value for every node, values in node order, or a Series or dict keyed by node ID. With a Series or dict, other nodes keep their value, or get a missing value in a new column. | required |
Raises:
| Type | Description |
|---|---|
KeyError | If a key is not a node ID. |
update_nodes(frame, by='id', max_distance=None) ¶
Update several node attributes from a table.
Values that are present overwrite; missing values leave the node's value as it is. New columns are created, missing for other nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame | DataFrame | With | required |
by | str |
| 'id' |
max_distance | Optional[float] | Meters, for | None |
Raises:
| Type | Description |
|---|---|
KeyError | If |
add_nodes(frame, id_column=None, kind='point') ¶
Add nodes from a points input with IDs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame | Any | A GeoDataFrame of points, a DataFrame with latitude and longitude columns, or a list of | required |
id_column | Optional[str] | Column holding the node IDs. The frame's index otherwise. | None |
kind | str | Kind of the new nodes. | 'point' |
Raises:
| Type | Description |
|---|---|
ValueError | If an ID is already a node of the network. |
filter_edges(mask=None, geometry=None, predicate='intersects') ¶
Keep the selected edges. All nodes are kept, so some may be left without edges.
Pass exactly one of mask or geometry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask | Any | Booleans in edge order. | None |
geometry | Any | Area in EPSG:4326, a shapely geometry or a GeoDataFrame or GeoSeries in any CRS. Straight edges are tested as lines between their nodes. | None |
predicate | str | The edge's relation to the area: | 'intersects' |
remove_edges(edges=None, mask=None) ¶
Remove edges. Nodes are kept.
Pass exactly one of edges or mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
edges | Any |
| None |
mask | Any | Booleans in edge order. | None |
Raises:
| Type | Description |
|---|---|
KeyError | If an edge is not in the network. |
set_edge_attribute(name, values) ¶
Set one edge attribute.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name | str | Column to set or create. | required |
values | Any | A single value for every edge, values in edge order, or a Series indexed by | required |
Raises:
| Type | Description |
|---|---|
KeyError | If a key is not an edge of the network. |
update_edges(frame) ¶
Update several edge attributes from a table with u, v, and key columns.
Values that are present overwrite; missing values leave the edge's value as it is. New columns are created, missing for other edges. In an undirected network, (v, u, key) matches (u, v, key).
Raises:
| Type | Description |
|---|---|
KeyError | If a row is not an edge of the network. |
add_weighted_edges(u, v, weights, weight='length', kind='added', **attributes) ¶
Add edges with a weight, like networkx's add_weighted_edges_from but with arrays.
Keys continue from the edges already between each pair of nodes. When weight is not "length" and no length is given, the length is computed along the given geometry, or between the endpoints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u | Sequence[Any] | Start node IDs. | required |
v | Sequence[Any] | End node IDs. | required |
weights | Sequence[float] | The weight of each edge, stored in the | required |
weight | str | Column for | 'length' |
kind | str | Kind of the new edges. | 'added' |
**attributes | Any | More columns, each a single value or one value per edge. | {} |
Raises:
| Type | Description |
|---|---|
KeyError | If an endpoint is not a node of the network. |
clip(geometry, predicate='intersects') ¶
The edges matching predicate with the area, and their endpoints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry | Any | Area in EPSG:4326, a shapely geometry or a GeoDataFrame or GeoSeries in any CRS. | required |
predicate | str |
| 'intersects' |
drop_geometry() ¶
The network without edge geometry, to save memory when only the topology matters.
to_undirected(weight=None) ¶
An undirected network with one edge per pair of nodes.
Reverse and parallel edges are merged into the one with the smallest weight, which keeps its attributes and geometry; every key becomes 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weight | Optional[str] | Edge column to compare. The network's weight otherwise. | None |
concat(networks) classmethod ¶
Combine networks into one.
Nodes with the same ID are the same node, as where separately fetched areas meet; the first network's row wins. Edges with the same (u, v, key) are kept once. The weight is the first network's; the metadata is shared metadata, or the list of each network's metadata under "parts".
Raises:
| Type | Description |
|---|---|
ValueError | If the networks are not all directed or all undirected. |
insert_points(points, id_column=None, max_distance=None, snap_to_node=5.0, chain=False, return_skipped=False) ¶
Insert points of interest as nodes, each joined to its nearest road.
Each point is joined to the nearest road edge at the foot of the perpendicular: the edge is split there, a node of kind split is added, and a connection edge joins the point (kind point) to it. When the foot is within snap_to_node meters of the edge's end, the point connects to that node instead. Points whose feet fall on the same spot share one split node. Each road edge is split once, at all of its points, and the pieces' lengths add up exactly to the original length. In a directed network, both directions of a two-way road are split at the same nodes, and connections run both ways.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points | Any | A GeoDataFrame of points, a DataFrame with latitude and longitude columns, or a list of | required |
id_column | Optional[str] | Column holding the point IDs. The frame's index otherwise. | None |
max_distance | Optional[float] | Meters. Points farther from any road are skipped, with a warning. | None |
snap_to_node | float | Meters. Connect to the edge's end node when the foot is this close to it. | 5.0 |
chain | bool | Handle points nearest to the road first, and join a point to an already inserted point (a | False |
return_skipped | bool | Also return the points that were skipped. | False |
Returns:
| Type | Description |
|---|---|
Any | The new network, or |
Any | negative integer IDs, continuing below the lowest existing one. |
Any | When the network's weight is not |
Any | get that weight equal to their length, and road pieces get their |
Any | share of the road's weight. |
Raises:
| Type | Description |
|---|---|
ValueError | If point IDs repeat or are already node IDs, or a point column is named |
adjacency(weight=None, directed=None) ¶
Sparse adjacency matrix by node position (see node_index).
Parallel edges keep the smallest weight. An undirected network, or directed=False, gives a symmetric matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weight | Optional[str] | Edge column. The network's weight otherwise. | None |
directed | Optional[bool] |
| None |
Raises:
| Type | Description |
|---|---|
ValueError | If the weight is missing or negative on some edge. |
shortest_path_lengths(origins, destinations=None, pairwise=False, weight=None, backend='scipy', directed=None) ¶
Shortest path lengths between nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origins | Any | Origin node IDs. | required |
destinations | Any | Destination node IDs. All nodes otherwise. | None |
pairwise | bool | Pair | False |
weight | Optional[str] | Edge column to add up. The network's weight otherwise. | None |
backend | str |
| 'scipy' |
directed | Optional[bool] |
| None |
Returns:
| Type | Description |
|---|---|
Any | With |
Any | DataFrame indexed by origin with one column per destination. |
Any | Unreachable pairs are |
Raises:
| Type | Description |
|---|---|
ValueError | If the matrix would exceed |
KeyError | If a node ID is not in the network. |
shortest_paths(origins, destinations, pairwise=True, weight=None, geometry=False, directed=None) ¶
Shortest paths between nodes, as node sequences or lines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origins | Any | Origin node IDs. | required |
destinations | Any | Destination node IDs. With | required |
pairwise | bool | Pair origins and destinations by position. | True |
weight | Optional[str] | Edge column to minimise. The network's weight otherwise. | None |
geometry | bool | Return routes as lines with their lengths. | False |
directed | Optional[bool] |
| None |
Returns:
| Type | Description |
|---|---|
Any | A list of node ID lists, empty where there is no path. With |
Any |
|
Any |
|
Any | there is no path or the origin is the destination). |
reachable(origins, max_distance, weight=None, directed=None) ¶
Every node within a network distance of each origin.
Returns:
| Type | Description |
|---|---|
DataFrame | A long table with |
DataFrame | by origin and distance; each distinct origin appears once, reaching |
DataFrame | itself at distance 0. |
nearest_by_network(origins, targets, k=1, max_distance=None, weight=None, directed=None) ¶
The nearest targets to each origin by network distance, such as each school's nearest fiber node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origins | Any | Origin node IDs. | required |
targets | Any | Target node IDs. | required |
k | int | Number of targets per origin. | 1 |
max_distance | Optional[float] | Ignore targets farther than this. | None |
Returns:
| Type | Description |
|---|---|
DataFrame | A table with |
DataFrame | origin for |
DataFrame | none is reachable), and up to |
DataFrame | nearest first. |
count_within(origins, targets, max_distance, weight=None, directed=None) ¶
How many targets each origin reaches within a network distance, such as clinics within 5 km by road.
Targets are counted once each; an origin that is also a target counts itself.
Returns:
| Type | Description |
|---|---|
Series | Counts indexed by origin ID, in the order given. |
connected_components(strong=False) ¶
Component label of each node; label 0 is the largest component, then by size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strong | bool | In a directed network, components in which every node can reach every other along one-way roads. Weak components ignore direction. The same for undirected networks. | False |
largest_component(strong=False) ¶
The network's largest connected component.
connect_components(max_distance=None) ¶
Join disconnected parts with component_link edges between their nearest nodes.
The links form a minimum spanning tree over the components by great-circle distance, found with Borůvka's method. In a directed network each link is added in both directions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_distance | Optional[float] | Meters. Parts farther apart than this stay apart. | None |
Returns:
| Type | Description |
|---|---|
SpatialNetwork | The network with the links added, or the same network when it is |
SpatialNetwork | already connected. |
minimum_spanning_tree(weight=None) ¶
An undirected network of the edges that join every component at the smallest total weight.
Directions are ignored, all nodes are kept (a forest when the network is disconnected), and keys become 0.
simplify(keep=None, sum_columns=('length',)) ¶
Remove nodes that only continue a road, joining the edges on either side.
A node is removed when it has exactly two neighbours and its edges continue through it: one edge in and one out of a one-way road, or both directions of a two-way road (in an undirected network, exactly two edges). This is the rule osmnx uses, except that osmnx also removes nodes between duplicated parallel ways and drops one of the ways, and deletes rings with no junction at all; here those nodes stay, and a ring keeps one node. Nodes of kind point or split and nodes in keep are never removed.
Joined edges get:
- the sum of
sum_columnsand of the network's weight column; - one geometry along the joined edges;
- text joined with
;where values differ, without repeats; osmidand other list columns concatenated, without repeats;onewaytrue only if every part is one-way,reversedtrue if any part is, other booleans true only if all parts are;- the length-weighted mean of other numeric columns;
- the kind of their longest part.
Each direction of a two-way road is joined separately, so it stays two-way when both directions can be joined. Joined edges always get a geometry, as in osmnx, so a network without edge geometry gains a geometry column that is missing for the edges left as they were.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keep | Any | Node IDs to keep. | None |
sum_columns | Iterable[str] | Edge columns to add up. | ('length',) |
Returns:
| Type | Description |
|---|---|
SpatialNetwork | The simplified network, or the same network when no node can be removed. |
connecting_subnetwork(terminals, roots=None, method='shortest_path', weight=None) ¶
The part of the network that joins a set of points, contracted to the points and branching nodes.
The network of :meth:ConnectionPlanner.plan. Directions are ignored. The result is undirected; it keeps the terminals, the roots, and the nodes where paths branch, with joined edges as :meth:simplify makes them, so lengths are added up and geometries joined. Use :class:ConnectionPlanner for each terminal's status, its distance to a root, and the plan's cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
terminals | Any | Node IDs to join, such as schools. | required |
roots | Any | Node IDs already connected to each other, such as fibre nodes. Reaching any root is enough. | None |
method | str |
| 'shortest_path' |
weight | Optional[str] | Edge column to minimise. The network's weight otherwise. | None |
Returns:
| Type | Description |
|---|---|
SpatialNetwork | An undirected network. Terminals that cannot be connected are kept |
SpatialNetwork | without edges. |
Connection planning¶
ConnectionPlanner plans how to connect terminals, such as schools, to roots, such as fibre nodes, along a network at the smallest shared cost, and returns a ConnectionPlan. Why it is a rooted Steiner tree and a separate object is in ADR 0008.
from gigaspatial import ConnectionPlanner
plan = ConnectionPlanner(network, school_ids, roots=fibre_ids, weight="cost").plan()
Plans how to connect terminals to roots along a spatial network at the smallest shared cost.
Terminals are the points to connect, such as schools. Roots are nodes already connected to each other through an existing network, such as fibre nodes, a power grid's substations, or water mains; reaching any root is enough. A terminal may reach a root directly or through other terminals, and an edge used by several terminals is paid once. Directions are ignored, since a cable or a pipe does not follow one-way streets.
With roots, this is the Steiner tree problem once every root is joined to one virtual root at weight 0: the plan is a forest whose every tree reaches a root. Without roots, it is the Steiner tree joining the terminals. Both are NP-hard; the planner returns approximations.
Insert the terminals and roots into the network first, with :meth:SpatialNetwork.insert_points, and pass their node IDs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network | SpatialNetwork | The network to plan along. | required |
terminals | Any | Node IDs to connect. | required |
roots | Any | Node IDs already connected to each other. | () |
weight | Optional[str] | Edge column to minimise, such as a cost. The network's weight otherwise. Zero is allowed. | None |
bridge_cost | Optional[float] | Weight per metre of a straight-line link between parts of the network that no road joins, such as a microwave link or a planned road, in the units of | None |
bridge_max_distance | float | Longest link, in metres. | 5000.0 |
Raises:
| Type | Description |
|---|---|
KeyError | If a terminal or root is not a node of the network. |
ValueError | If the weight is missing or negative on some edge, or the bridge options are negative. |
plan(method='shortest_path') ¶
Plan the connections.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method | str |
| 'shortest_path' |
Returns:
| Type | Description |
|---|---|
ConnectionPlan | The plan. With roots, terminals that cannot reach any root, even |
ConnectionPlan | through bridges, are unreachable. Without roots, the plan joins the |
ConnectionPlan | terminals of the network's part that holds the most of them, and |
ConnectionPlan | the others are unreachable. |
Raises:
| Type | Description |
|---|---|
ValueError | If the method is unknown, or is |
The result of a connection planner.
Attributes:
| Name | Type | Description |
|---|---|---|
network | SpatialNetwork | The connecting subnetwork, undirected, contracted to the terminals, the roots, the ends of bridges, and the nodes where paths branch, with lengths added up and geometries joined as :meth: |
edges | GeoDataFrame | The chosen edges, uncontracted, one row per pair of nodes with the attributes of its cheapest edge and a geometry (a straight line where the network has none). Bridges have kind |
terminals | DataFrame | One row per terminal, indexed by node ID: |
cost | float | The total weight of |
metadata | Dict[str, Any] | The method, the weight, |
bound | Optional[float] | A lower bound on the cost of any plan connecting the same terminals, set by :meth: |
gap | Optional[float] |
|