Last updated: 2026-03-04 01:42:26
Routing
Introduction
In this chapter we introduce the central and most common task in spatial network analysis: finding the optinal route between two given points, also known as routing:
First, we show the basic usage of
nx.shortest_pathto find the optinal route between two points (Finding optimal route), and develop helper functions to calculate (Basic routing function) and graphically display (Graphics: Route) the calculated route.We move on to examine the implications of choosing to minimize different types of weights—typically either distance or travel time in spatial networks (Choice of weights to minimize and Sum of route weights).
Next we demonstrate the effect of spatial network directions, namely the distinction between one-way or two-way road segments and its effects on routing (Effect of directions).
Finally, we cover the techniques to extract route edges along with their associated attributes (Route edges) and to transform them to a
GeoSeries(Route to GeoDataFrame), which is useful when we need to apply further (non-network) spatial analysis techniques on the resulting route (such as evaluating which land use types it goes through, calculating an elevation profiles, etc.).
Packages
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import shapely
import geopandas as gpd
import networkx as nx
import osmnx as ox
import net2Sample data
To demonstrate routing, let’s start with the small road network in output/roads2.xml:
G = nx.read_graphml('output/roads2.xml')
G = net2.prepare(G)
G<networkx.classes.digraph.DiGraph at 0x7698584e03e0>
To remind, we have 17 nodes:
G.nodesNodeView((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16))
And the edges are associated with 'geometry', 'length', 'speed', and 'time' attributes:
list(G.edges.data())[0](0,
1,
{'geometry': <LINESTRING (2000 0, 2000 1000)>,
'length': 1000.0,
'speed': 90,
'time': 40.0})
Finding optimal route
The nx.shortest_path function can be used to calculate the shortest path between the specified nodes, given the attribute to minimize (weight). The result is a list of the nodes along the calculated path. The default optimal path search method of nx.shortest_path is Dijkstra’s algorithm.
For example, the following expression calculates the shortest route—i.e., the one with the minimum length—from node 3 to node 1:
route = nx.shortest_path(G, 3, 1, weight='length')
route[3, 2, 1]
Graphics: Route
The nx.draw_networkx_edges function can be used to draw individual edges. That way, we can style some of the edges in a different way, e.g., when drawing a route along a network. Importantly, the nx.draw_networkx_edges function accepts an edgelist argument, which is a list of edges to draw.
For example, in Figure 8.1 we draw two specific edges—(4,5) and (12,13)—in red color (edge_color='r') and increased line width (width=3):
fig, ax = plt.subplots()
nx.draw(G, with_labels=True, pos=net2.pos(G), edge_color='grey', width=0.1, ax=ax)
nx.draw_networkx_edges(G, pos=net2.pos(G), edgelist=[(4,5),(12,13)], edge_color='r', width=3);
We are going to use the nx.draw_networkx_edges function to highlight the calculated route along the network. However, the route is defined through the nodes it goes through, rather than the edges:
route[3, 2, 1]
To go from the nodes to edges, we use zip and indexing (zip):
list(zip(route, route[1:]))[(3, 2), (2, 1)]
For example, Figure 8.2 highlights the route:
fig, ax = plt.subplots()
nx.draw(G, with_labels=True, pos=net2.pos(G), edge_color='grey', width=0.1, ax=ax)
nx.draw_networkx_edges(G, pos=net2.pos(G), edgelist=list(zip(route, route[1:])), edge_color='r', width=3);
3 to node 1 in the roads network
Choice of weights to minimize
The weight parameter of the nx.shortest_path function lets us choose which edge weight to minimize when searching for the optimal route. For example, instead of the shortest route (weight='length'), we may want to search for the fastest route (weight='time'):
route2 = nx.shortest_path(G, 3, 1, weight='time')
route2[3, 8, 5, 4, 1]
The fastest route (Figure 8.3) is not the same as the shortest route (Figure 8.2):
fig, ax = plt.subplots()
nx.draw(G, with_labels=True, pos=net2.pos(G), edge_color='grey', width=0.1, ax=ax)
nx.draw_networkx_edges(G, pos=net2.pos(G), edgelist=list(zip(route2, route2[1:])), edge_color='r', width=3);
3 to node 1 in the roads network
Sum of route weights
The nx.path_weight function is used to calculate the sum of edge weights along the given route. For example, here we calculate the total length of route along G:
nx.path_weight(G, route, weight='length')2000.0
In terms of length, we can see that route is shorter than route2:
nx.path_weight(G, route, weight='length'), nx.path_weight(G, route2, weight='length')(2000.0, 4000.0)
However, in terms of time, we can see that route2 is faster:
nx.path_weight(G, route, weight='time'), nx.path_weight(G, route2, weight='time')(240.00000000000003, 192.0)
Effect of directions
Note that routes can only follow edge directions. For example, the route from node 1 to adjacent node 2 has to go around because there is no direct 1→2 edge:
route = nx.shortest_path(G, 1, 2, weight='length')
route[1, 4, 5, 8, 3, 2]
- What happens if you omit the argument
2from the above result? - Try to guess based on the result, and then check the documentation
A graphical illustration makes this more clear:
nx.draw(G, with_labels=True, pos=net2.pos(G), edge_color='grey', width=0.1)
nx.draw_networkx_edges(G, pos=net2.pos(G), edgelist=list(zip(route, route[1:])), edge_color='r', width=3);
1 to node 2 in the roads network
Basic routing function
Let’s define a minimal routing function, named net2.route1:
def route1(network, node_start, node_end, weight):
try:
route = nx.shortest_path(network, node_start, node_end, weight)
weight_sum = nx.path_weight(network, route, weight=weight)
return {'route': route, 'weight': weight_sum}
except:
return {'route': np.nan, 'weight': np.nan}
The function accepts the network, start node, and end node. The additional parameter is the weight. The function then returns the route nodes, and the summed weight, calculated using nx.shortest_path and nx.path_weight, respectively, as shown above. For example:
net2.route1(G, 1, 2, 'length'){'route': [1, 4, 5, 8, 3, 2], 'weight': 5000.0}
Note that the function has try/except code blocks. The interpreter tries to run the try block, and if an error is raised—then it runs the except block, where the function returns np.nan for the route and summed weight. For example, try finding the optimal route between nodes 14 and 1 with nx.shortest_path. You should see an error, because the nodes are disconnected:
nx.shortest_path(G, 14, 1, weight='length')NetworkXNoPath: No path between 14 and 1.
Our net2.route1 function, however, does work:
net2.route1(G, 14, 2, 'length'){'route': nan, 'weight': nan}
Route edges
The nx.path_graph function can be used to create a new network, consisting of consecutive edges along the list elements:
route_G = nx.path_graph(route)
route_G<networkx.classes.graph.Graph at 0x7698581a6d80>
Figure 8.5 shows the result:
nx.draw(route_G, with_labels=True)
1 and 2, extracted to a sub-network object
Unfortunately, the data are lost:
route_edges = route_G.edges
dict(route_edges){(1, 4): {}, (4, 5): {}, (5, 8): {}, (8, 3): {}, (3, 2): {}}
In necessary, we can go over the respective edges in the original network G to get the attribute data, as follows:
for u,v in route_edges:
x = G.edges[u,v]
print(x){'geometry': <LINESTRING (2000 1000, 2000 2000)>, 'length': 1000.0, 'speed': 90, 'time': 40.0}
{'geometry': <LINESTRING (2000 2000, 3000 2000)>, 'length': 1000.0, 'speed': 90, 'time': 40.0}
{'geometry': <LINESTRING (3000 2000, 4000 2000)>, 'length': 1000.0, 'speed': 90, 'time': 40.0}
{'geometry': <LINESTRING (4000 1000, 4000 2000)>, 'length': 1000.0, 'speed': 50, 'time': 72.0}
{'geometry': <LINESTRING (3000 1000, 4000 1000)>, 'length': 1000.0, 'speed': 30, 'time': 120.00000000000001}
Route to GeoDataFrame
To create a line layer of the edges along a route, we can go over the edge properties and extract the 'geometry' attributes into a list:
geoms = [G.edges[u,v]['geometry'] for u,v in route_edges]
geoms[<LINESTRING (2000 1000, 2000 2000)>,
<LINESTRING (2000 2000, 3000 2000)>,
<LINESTRING (3000 2000, 4000 2000)>,
<LINESTRING (4000 1000, 4000 2000)>,
<LINESTRING (3000 1000, 4000 1000)>]
Then, we can transform the list into a GeoSeries:
geom = gpd.GeoSeries(geoms)
geom0 LINESTRING (2000 1000, 2000 2000)
1 LINESTRING (2000 2000, 3000 2000)
2 LINESTRING (3000 2000, 4000 2000)
3 LINESTRING (4000 1000, 4000 2000)
4 LINESTRING (3000 1000, 4000 1000)
dtype: geometry
Figure 8.6 shows the resulting line layer:
geom.plot(linewidth=5);
GeoDataFrame representing a route through the network
When writing a function for the “route to GeoDataFrame” workflow, we must try to consider the possible edge cases, where the function may fail. For example, let’s see what happens when we try to find the route between an origin and a destination which are the same node?
route = nx.shortest_path(G, 1, 1, weight='length')
route[1]
route_edges = nx.path_graph(route).edges
route_edgesEdgeView([])
The current workflow returns a \(0 \times 0\) GeoDataFrame:
result = []
for u,v in route_edges:
x = G.edges[u,v]['geometry']
result.append({'from': u, 'to': v, 'geometry': x})
result = gpd.GeoDataFrame(result)
resultresult.shape(0, 0)
It can be argued that it would make more sense returning a \(1 \times 3\) GeoDataFrame, with a “line” that goes from the origin to itself. That way, chances are improved that downstream processing of the result (such as displaying it on a map) will work:
result = []
pnt = G.nodes[route[0]]['geometry']
line = shapely.LineString([pnt, pnt])
print(line)LINESTRING (2000 1000, 2000 1000)
result = gpd.GeoDataFrame([{'from': route[0], 'to': route[0], 'geometry': line}])
result| from | to | geometry | |
|---|---|---|---|
| 0 | 1 | 1 | LINESTRING (2000 1000, 2000 1000) |
Incorporating both possible cases, the route_to_gdf function can be defined as follows:
def route_to_gdf(network, route, crs=None):
route_edges = nx.path_graph(route).edges
if len(route) == 1:
result = []
pnt = network.nodes[route[0]]['geometry']
line = shapely.LineString([pnt, pnt])
result = gpd.GeoDataFrame([{'from': route[0], 'to': route[0], 'geometry': line}], crs=crs)
if len(route) > 1:
result = []
for u,v in route_edges:
x = network.edges[u,v]['geometry']
result.append({'from': u, 'to': v, 'geometry': x})
result = gpd.GeoDataFrame(result, crs=crs)
return result
Here is a demonstration of the function:
net2.route_to_gdf(G, route2)| from | to | geometry | |
|---|---|---|---|
| 0 | 3 | 8 | LINESTRING (4000 1000, 4000 2000) |
| 1 | 8 | 5 | LINESTRING (3000 2000, 4000 2000) |
| 2 | 5 | 4 | LINESTRING (2000 2000, 3000 2000) |
| 3 | 4 | 1 | LINESTRING (2000 1000, 2000 2000) |
And here is a demonstration of the edge case:
net2.route_to_gdf(G, route)| from | to | geometry | |
|---|---|---|---|
| 0 | 1 | 1 | LINESTRING (2000 1000, 2000 1000) |
Routing in a road network can be further improved: * Adding penalty for junctions, assuming that there are traffic lights, or at least a “stop” or “give way” sign * Taking into account traffic, possibly from real-time data
Exercises
Exercise 07-01
- Modify the roads network so that the
14,15component is connected to the rest of the network in both directions. You can choose any way you like to connect the components: adding a new edge, adding new nodes, modifying existing edges, etc. - Make sure that the new edges and nodes (if any) contain all of the “standard” attributes that the other nodes and edges have (you can use any travel speed you like for the new edges)
- Calculate and plot the fastest route from node
15to node7(Figure 19.13)
Exercise 07-02
- Re-create the directed railway network with time weights from Exercise 06-02
- Calculate and plot the fastest route between Haifa and Jerusalem (Figure 19.14)
- Calculate the total travel time along the route, and compare it to a manual sum of the segment travel times
Exercise 07-03
- Calculate travel times from node
0to all other nodes in the roads network. Mark unreachable nodes with-1. - Plot the network, displaying travel times on the edges, and the calculated travel times from node
0for each node. Make node sizes proportional to the travel times from node0(Figure 19.15)