From Road Network to Graph#
This section contains a non-exhaustive list of operations on geospatial data that you should familiarize yourself with. More information can be found by consulting the Tools and Python Libraries page or the respective libary’s API documentation.
Creating a Graph from a named place#
Mathematically speaking, a graph can be represented by \(G\), where
\(G=(V,E)\)
For a graph \(G\), vertices are represented by \(V\), and edges by \(E\).
Each edge is a tuple \((v,w)\), where
\(w\), \(v \in V\)
Weight can be added as a third component to the edge tuple.
In other words, graphs consist of 3 sets:
vertices/nodes
edges
a set representing relations between vertices and edges
The nodes represent intersections, and the edges represent the roads themselves. A route
is a sequence of edges connecting the origin node
to the destination node
.
osmnx
can convert a text descriptor of a place into a networkx
graph. Let’s use the University of Toronto as an example:
import osmnx
place_name = "University of Toronto"
# networkx graph of the named place
graph = osmnx.graph_from_address(place_name)
# Plot the graphs
osmnx.plot_graph(graph,figsize=(10,10))
(<Figure size 1000x1000 with 1 Axes>, <AxesSubplot:>)
The graph shows edges and nodes of the road network surrouding the University of Toronto’s St. George (downtown Toronto) campus. While it may look visually interesting, it extends a bit too far off campus, and lacks the context of the street names and other geographic features. Let’s restrict the scope of the network to 500 meters around the university, and use a folium
map as a baselayer. We will discuss more about folium
later in this section.
graph = osmnx.graph_from_address(place_name, dist=300)
osmnx.folium.plot_graph_folium(graph)
Suppose you want to get from one location to another on this campus.
Starting at the King Edward VII Equestrian Statue near Queen’s Park, you need to cut across the campus to attend a lecture at the Bahen Centre for Information Technology. Later in this section, you will see how you can calculate the shortest path between these two points.
For now, let’s plot these two locations on the map using ipyleaflet
:
from ipyleaflet import *
# UofT main building
center=(43.662643, -79.395689)
# King Edward VII Equestrian Statue
source_point = (43.664527, -79.392442)
# Bahen Centre for Information Technology at UofT
destination_point = (43.659659, -79.397669)
m = Map(center=center, zoom=15)
m.add_layer(Marker(location=source_point, icon=AwesomeIcon(name='camera', marker_color='red')))
m.add_layer(Marker(location=center, icon=AwesomeIcon(name='graduation-cap')))
m.add_layer(Marker(location=destination_point, icon=AwesomeIcon(name='university',marker_color='green')))
m
Notice that the way we create maps in ipyleaflet
is different from folium
. For the latter, the code is as follows:
import folium
m = folium.Map(location=center, zoom_start=15)
folium.Marker(location=source_point,icon=folium.Icon(color='red',icon='camera', prefix='fa')).add_to(m)
folium.Marker(location=center,icon=folium.Icon(color='blue',icon='graduation-cap', prefix='fa')).add_to(m)
folium.Marker(location=destination_point,icon=folium.Icon(color='green',icon='university', prefix='fa')).add_to(m)
m
Extracting Graph Information#
Various properties of the graph can be examined, such as the graph type, edge (road) types, CRS projection, etc.
Graph Type#
type(graph)
networkx.classes.multidigraph.MultiDiGraph
Note
You can read more about MultiDiGraph
here.
Edges and Nodes#
We can extract the nodes and edges of the graph as separate structures.
nodes, edges = osmnx.graph_to_gdfs(graph)
nodes.head(5)
y | x | highway | street_count | geometry | |
---|---|---|---|---|---|
osmid | |||||
21631731 | 43.664076 | -79.398440 | traffic_signals | 4 | POINT (-79.39844 43.66408) |
24959527 | 43.664436 | -79.396904 | NaN | 3 | POINT (-79.39690 43.66444) |
24959528 | 43.664687 | -79.395701 | NaN | 3 | POINT (-79.39570 43.66469) |
24959535 | 43.663497 | -79.400045 | traffic_signals | 4 | POINT (-79.40004 43.66350) |
24959542 | 43.665940 | -79.401019 | stop | 3 | POINT (-79.40102 43.66594) |
edges.head(5)
osmid | lanes | name | highway | maxspeed | oneway | reversed | length | geometry | access | service | width | tunnel | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
u | v | key | |||||||||||||
21631731 | 389678033 | 0 | 277878927 | 3 | St George Street | tertiary | 30 | False | True | 12.829 | LINESTRING (-79.39844 43.66408, -79.39840 43.6... | NaN | NaN | NaN | NaN |
389678008 | 0 | 327452916 | 3 | St George Street | tertiary | 30 | False | True | 12.667 | LINESTRING (-79.39844 43.66408, -79.39848 43.6... | NaN | NaN | NaN | NaN | |
389677903 | 0 | 10334990 | 3 | Hoskin Avenue | tertiary | NaN | False | True | 10.351 | LINESTRING (-79.39844 43.66408, -79.39839 43.6... | NaN | NaN | NaN | NaN | |
389677893 | 0 | 126742589 | 3 | Harbord Street | tertiary | 40 | False | True | 14.564 | LINESTRING (-79.39844 43.66408, -79.39847 43.6... | NaN | NaN | NaN | NaN | |
24959527 | 389678004 | 0 | 77720372 | 2 | Devonshire Place | residential | 30 | False | False | 8.867 | LINESTRING (-79.39690 43.66444, -79.39694 43.6... | NaN | NaN | NaN | NaN |
We can further drill down to examine each individual node or edge.
# Rendering the 2nd node
list(graph.nodes(data=True))[1]
(24959527, {'y': 43.6644357, 'x': -79.3969038, 'street_count': 3})
# Rendering the 1st edge
list(graph.edges(data=True))[0]
(21631731,
389678033,
{'osmid': 277878927,
'lanes': '3',
'name': 'St George Street',
'highway': 'tertiary',
'maxspeed': '30',
'oneway': False,
'reversed': True,
'length': 12.829})
Street Types#
Street types can also be retrieved for the graph:
print(edges['highway'].value_counts())
footway 898
service 90
residential 58
tertiary 54
[footway, steps] 21
unclassified 15
living_street 14
pedestrian 10
[footway, service] 4
steps 4
[steps, footway] 3
secondary 2
[footway, steps, corridor] 2
[footway, service, steps] 1
[steps, service, footway] 1
Name: highway, dtype: int64
GeoDataFrame to MultiDiGraph#
GeoDataFrames can be easily converted back to MultiDiGraphs by using osmnx.graph_from_gdfs
.
new_graph = osmnx.graph_from_gdfs(nodes,edges)
osmnx.plot_graph(new_graph,figsize=(10,10))
(<Figure size 1000x1000 with 1 Axes>, <AxesSubplot:>)
Calculating Network Statistics#
We can see some basic statistics of our graph using osmnx.basic_stats
.
osmnx.basic_stats(graph)
{'n': 421,
'm': 1177,
'k_avg': 5.59144893111639,
'edge_length_total': 32953.49499999998,
'edge_length_avg': 27.997871707731505,
'streets_per_node_avg': 3.02375296912114,
'streets_per_node_counts': {0: 0, 1: 62, 2: 1, 3: 228, 4: 126, 5: 3, 6: 1},
'streets_per_node_proportions': {0: 0.0,
1: 0.14726840855106887,
2: 0.0023752969121140144,
3: 0.5415676959619953,
4: 0.2992874109263658,
5: 0.007125890736342043,
6: 0.0023752969121140144},
'intersection_count': 359,
'street_length_total': 16663.342,
'street_segment_count': 594,
'street_length_avg': 28.05276430976431,
'circuity_avg': 1.0287183227563486,
'self_loop_proportion': 0.0}
We can also see the circuity average. Circuity average is the sum of edge lengths divided by the sum of straight line distances. It produces a metric > 1 that indicates how “direct” the network is (i.e. how much more distance is required when travelling via the graph as opposed to walking in a straight line).
# osmnx expects an undirected graph
undir = graph.to_undirected()
osmnx.stats.circuity_avg(undir)
1.0287183227563486
Extended and Density Stats#
Density-based statistics requires knowing the area of the graph. To do this, the convex hull of the graph is required.
convex_hull = edges.unary_union.convex_hull
convex_hull
import pandas
area = convex_hull.area
stats = osmnx.basic_stats(graph, area=area)
extended_stats = osmnx.extended_stats(graph,ecc=True,cc=True)
stats.update(extended_stats)
# Show the data in a pandas Series for better formatting
pandas.Series(stats)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Input In [15], in <cell line: 4>()
2 area = convex_hull.area
3 stats = osmnx.basic_stats(graph, area=area)
----> 4 extended_stats = osmnx.extended_stats(graph,ecc=True,cc=True)
5 stats.update(extended_stats)
7 # Show the data in a pandas Series for better formatting
AttributeError: module 'osmnx' has no attribute 'extended_stats'
Warning
extended_stats
has already been deprecated from osmnx
. In the absence of an alternative, you can access these metrics directly through networkx
.
CRS Projection#
You can also look at the projection of the graph. To find out more about projections, check out this section. Additionally, you can also reproject the graph to a different CRS.
edges.crs
<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
merc_edges = edges.to_crs(epsg=3857)
merc_edges.crs
<Projected CRS: EPSG:3857>
Name: WGS 84 / Pseudo-Mercator
Axis Info [cartesian]:
- X[east]: Easting (metre)
- Y[north]: Northing (metre)
Area of Use:
- name: World between 85.06°S and 85.06°N.
- bounds: (-180.0, -85.06, 180.0, 85.06)
Coordinate Operation:
- name: Popular Visualisation Pseudo-Mercator
- method: Popular Visualisation Pseudo Mercator
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
Shortest Path Analysis#
Let’s revisit our trip across campus from the statue in Queen’s Park to our lecture hall at the Bahen centre.
To calculate the shortest path, we first need to find the closest nodes on the network to our starting and ending locations.
import geopandas
X = [source_point[1], destination_point[1]]
Y = [source_point[0], destination_point[0]]
closest_nodes = osmnx.distance.nearest_nodes(graph,X,Y)
# Get the rows from the Node GeoDataFrame
closest_rows = nodes.loc[closest_nodes]
# Put the two nodes into a GeoDataFrame
od_nodes = geopandas.GeoDataFrame(closest_rows, geometry='geometry', crs=nodes.crs)
od_nodes
y | x | highway | street_count | geometry | |
---|---|---|---|---|---|
osmid | |||||
390545921 | 43.665132 | -79.394109 | NaN | 3 | POINT (-79.39411 43.66513) |
239055725 | 43.660783 | -79.397878 | NaN | 3 | POINT (-79.39788 43.66078) |
Let’s find and plot the shortest route now!
import networkx
shortest_route = networkx.shortest_path(G=graph,source=closest_nodes[0],target=closest_nodes[1], weight='length')
print(shortest_route)
[390545921, 60654129, 60654119, 389678001, 389678002, 2143434369, 390550470, 127289393, 8277128565, 8277128566, 4920594801, 3996671922, 80927418, 7967019552, 127275360, 80927426, 2143468197, 2143468182, 55808564, 55808527, 130170945, 389677905, 389678182, 389677906, 50885141, 389678180, 1258706668, 2143436407, 1258706673, 2143402269, 2143402268, 1258706670, 239055725]
osmnx.plot_graph_route(graph,shortest_route,figsize=(15,15))
(<Figure size 1080x1080 with 1 Axes>, <AxesSubplot:>)
Now let’s try visualizing this on a leaflet
map. We’ll use the ipyleaflet
library for this. Graphs rendered in ipyleaflet
tend to be very slow when there are many nodes. For this reason, the code will fall back to folium
if there are more than 1,500 nodes in the graph (this particular graph should have ~1,090 nodes).
Let’s make a map that shows the above route, with both starting and ending nodes shows as markers.
import ipyleaflet
import networkx
def draw_route(G, route, zoom=15):
if len(G)>=1500:
# Too many nodes to use ipyleaflet
return osmnx.plot_route_folium(G=G,route=route)
# Calculate the center node of the graph
# This used to be accessible from extended_stats, but now we have to do it manually with a undirected graph in networkx
undir = G.to_undirected()
length_func = networkx.single_source_dijkstra_path_length
sp = {source: dict(length_func(undir, source, weight="length")) for source in G.nodes}
eccentricity = networkx.eccentricity(undir,sp=sp)
center = networkx.center(undir,e=eccentricity)[0]
# Create the leaflet map and center it around the center of our graph
G_gdfs = osmnx.graph_to_gdfs(G)
nodes_frame = G_gdfs[0]
ways_frame = G_gdfs[1]
center_node = nodes_frame.loc[center]
location = [center_node.y,center_node.x]
m = ipyleaflet.Map(center=location, zoom=zoom)
# Get our starting and ending nodes
start_node = nodes_frame.loc[route[0]]
end_node = nodes_frame.loc[route[-1]]
start_xy = [start_node.y, start_node.x]
end_xy = [end_node.y,end_node.x]
# Add both nodes as markers
m.add_layer(ipyleaflet.Marker(location=start_xy, draggable=False))
m.add_layer(ipyleaflet.Marker(location=end_xy, draggable=False))
# Add edges of route
for u,v in zip(route[0:],route[1:]):
try:
x,y = (ways_frame.query(f'u == {u} and v == {v}').to_dict('list')['geometry'])[0].coords.xy
except:
x,y = (ways_frame.query(f'u == {v} and v == {u}').to_dict('list')['geometry'])[0].coords.xy
points = map(list, [*zip([*y],[*x])])
ant_path = ipyleaflet.AntPath(
locations = [*points],
dash_array=[1, 10],
delay=1000,
color='#7590ba',
pulse_color='#3f6fba'
)
m.add_layer(ant_path)
return m
draw_route(graph, shortest_route)
Retrieve buildings from named place#
Just like our graph above, we can also retrieve all the building footprints of a named place.
# Retrieve the building footprint, project it to match our previous graph, and plot it.
buildings = osmnx.geometries.geometries_from_address(place_name, tags={'building':True}, dist=300)
buildings = buildings.to_crs(edges.crs)
osmnx.plot_footprints(buildings)
(<Figure size 576x576 with 1 Axes>, <AxesSubplot:>)
Now that we have the building footprints for the campus, let’s plot that shortest route again.
First, get the nodes from the shortest route, create a geometry from it, and then visualize building footprints, street network, and shortest route all on one plot.
from shapely.geometry import LineString
# Nodes of our shortest path
route_nodes = nodes.loc[shortest_route]
# Convert the nodes into a line geometry
route_line = LineString(list(route_nodes.geometry.values))
# Create a GeoDataFrame from the line
route_geom = geopandas.GeoDataFrame([[route_line]], geometry='geometry', crs=edges.crs, columns=['geometry'])
# Plot edges and nodes
ax = edges.plot(linewidth=0.75, color='gray', figsize=(15,15))
ax = nodes.plot(ax=ax, markersize=2, color='gray')
# Add building footprints
ax = buildings.plot(ax=ax, facecolor='khaki', alpha=0.7)
# Add the shortest route
ax = route_geom.plot(ax=ax, linewidth=2, linestyle='--', color='red')
# Highlight the starting and ending nodes
ax = od_nodes.plot(ax=ax, markersize=100, color='green')