Network analysis in Python#
Finding a shortest path using a specific street network is a common GIS problem that has many practical applications. For example, navigation, one of those ‘every-day’ applications for which routing algorithms are used to find the optimal route between two or more points.
Of course, the Python ecosystem has produced packages that can be used to conduct network analyses, such as routing. The NetworkX package provides various tools to analyse networks, and implements several different routing algorithms, such as the Dijkstra’s or the A* algorithms. Both are commonly used to find shortest paths along transport networks.
To be able to conduct network analysis, it is, of course, necessary to have a network that is used for the analyses. The OSMnx package enables us to retrieve routable networks from OpenStreetMap for various transport modes (walking, cycling and driving). OSMnx also wraps some of NetworkX’s functionality in a convenient way for using it on OpenStreetMap data.
In the following section, we will use OSMnx to find the shortest path between two points based on cyclable roads. With only the tiniest modifications, we can then repeat the analysis for the walkable street network.
Obtain a routable network#
To download OpenStreetMap data that represents the street network, we can use it’s `graph_from_place()
<https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.graph.graph_from_place>`__ function. As parameters, it expects a place name and, optionally, a network type.
[1]:
import osmnx
PLACE_NAME = "Kamppi, Helsinki, Finland"
graph = osmnx.graph_from_place(
PLACE_NAME,
network_type="bike"
)
figure, ax = osmnx.plot_graph(graph)
Pro tip!Sometimes the shortest path might go slightly outside the defined area of interest. To account for this, we can fetch the network for a bit larger area than the district of Kamppi, in case the shortest path is not completely inside its boundaries.
[2]:
# Get the area of interest polygon
place_polygon = osmnx.geocode_to_gdf(PLACE_NAME)
# Re-project the polygon to a local projected CRS (so that the CRS unit is meters)
place_polygon = place_polygon.to_crs("EPSG:3067")
# Buffer by 200 meters
place_polygon["geometry"] = place_polygon.buffer(200)
# Re-project the polygon back to WGS84 (required by OSMnx)
place_polygon = place_polygon.to_crs("EPSG:4326")
# Retrieve the network graph
graph = osmnx.graph_from_polygon(
place_polygon.at[0, "geometry"],
network_type="bike"
)
fig, ax = osmnx.plot_graph(graph)
Data overview#
Now that we obtained a complete network graph for the travel mode we specified (cycling), we can take a closer look at which attributes are assigned to the nodes and edges of the network. It is probably easiest to first convert the network into a geo-data frame on which we can then use the tools we learnt in earlier lessons.
To convert a graph into a geo-data frame, we can use osmnx.graph_to_gdfs()
(see previous section). Here, we can make use of the function’s parameters nodes
and edges
to select whether we want only nodes, only edges, or both (the default):
[3]:
# Retrieve only edges from the graph
edges = osmnx.graph_to_gdfs(graph, nodes=False, edges=True)
edges.head()
[3]:
osmid | oneway | lanes | name | highway | maxspeed | reversed | length | geometry | junction | width | access | bridge | service | tunnel | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
u | v | key | |||||||||||||||
25216594 | 1372425721 | 0 | 23717777 | True | 2 | Porkkalankatu | primary | 40 | False | 10.404 | LINESTRING (24.92106 60.16479, 24.92087 60.16479) | NaN | NaN | NaN | NaN | NaN | NaN |
1372425714 | 0 | 23856784 | True | 2 | Mechelininkatu | primary | 40 | False | 40.885 | LINESTRING (24.92106 60.16479, 24.92095 60.164... | NaN | NaN | NaN | NaN | NaN | NaN | |
25238865 | 146447626 | 0 | [59355210, 4229487] | False | 2 | Santakatu | residential | 30 | False | 44.303 | LINESTRING (24.91994 60.16279, 24.91932 60.162... | NaN | NaN | NaN | NaN | NaN | NaN |
57661989 | 0 | 7842621 | False | NaN | Sinikaislankuja | residential | 30 | True | 76.704 | LINESTRING (24.91994 60.16279, 24.91995 60.162... | NaN | NaN | NaN | NaN | NaN | NaN | |
314767800 | 0 | 231643806 | False | NaN | NaN | cycleway | NaN | False | 60.066 | LINESTRING (24.91994 60.16279, 24.92014 60.162... | NaN | NaN | NaN | NaN | NaN | NaN |
The resulting geo-data frame comprises of a long list of columns. Most of them relate to OpenStreetMap tags, and their names are rather self-explanatory. the columns u
and v
describe the topological relationship within the network: they denote the start and end node of each edge.
Column |
Description |
Data type |
---|---|---|
Bridge feature |
boolean |
|
geometry |
Geometry of the feature |
Shapely.geometry |
Tag for roads (road type) |
str / list |
|
Number of lanes |
int (or nan) |
|
Length of feature (meters) |
float |
|
Maximum legal speed limit |
int / list |
|
Name of the (street) element |
str (or nan) |
|
One way road |
boolean |
|
Unique ids for the element |
list |
|
The start node of edge |
int |
|
The end node of edge |
int |
What types of streets does our network comprise of?
[4]:
edges["highway"].value_counts()
[4]:
highway
service 1048
pedestrian 564
cycleway 491
residential 483
tertiary 214
primary 165
secondary 124
unclassified 42
path 24
[pedestrian, service] 16
living_street 8
[living_street, service] 4
[cycleway, residential] 4
[pedestrian, residential] 4
[cycleway, pedestrian] 3
[pedestrian, cycleway] 3
tertiary_link 2
[tertiary, unclassified] 2
[cycleway, service] 2
[residential, service] 2
primary_link 1
[unclassified, service] 1
Name: count, dtype: int64
Transform to projected reference system#
The network data’s cartographic reference system (CRS) is WGS84 (EPSG:4326), a geographic reference system. That means, distances are recorded and expressed in degrees, areas in square-degrees. This is not convenient for network analyses, such as finding a shortest path.
Again, OSMnx’s graph objects do not offer a method to transform their geodata, but OSMnx comes with a separate function: `osmnx.project_graph()
<https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.projection.project_graph>`__ accepts an input graph and a CRS as parameters, and returns a new, transformed, graph. If crs
is omitted, the transformation defaults to the locally most appropriate UTM zone.
[5]:
# Transform the graph to UTM
graph = osmnx.project_graph(graph)
# Extract reprojected nodes and edges
nodes, edges = osmnx.graph_to_gdfs(graph)
nodes.crs
[5]:
<Projected CRS: EPSG:32635>
Name: WGS 84 / UTM zone 35N
Axis Info [cartesian]:
- E[east]: Easting (metre)
- N[north]: Northing (metre)
Area of Use:
- name: Between 24°E and 30°E, northern hemisphere between equator and 84°N, onshore and offshore. Belarus. Bulgaria. Central African Republic. Democratic Republic of the Congo (Zaire). Egypt. Estonia. Finland. Greece. Latvia. Lesotho. Libya. Lithuania. Moldova. Norway. Poland. Romania. Russian Federation. Sudan. Svalbard. Türkiye (Turkey). Uganda. Ukraine.
- bounds: (24.0, 0.0, 30.0, 84.0)
Coordinate Operation:
- name: UTM zone 35N
- method: Transverse Mercator
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
Analysing network properties#
Now that we have prepared a routable network graph, we can turn to the more analytical features of OSMnx, and extract information about the network. To compute basic network characteristics, use `osmnx.basic_stats()
<https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.stats.basic_stats>`__:
[6]:
# Calculate network statistics
osmnx.basic_stats(graph)
[6]:
{'n': 1486,
'm': 3207,
'k_avg': 4.31628532974428,
'edge_length_total': 102804.43999999992,
'edge_length_avg': 32.05626442157777,
'streets_per_node_avg': 2.623822341857335,
'streets_per_node_counts': {0: 0, 1: 442, 2: 15, 3: 709, 4: 300, 5: 20},
'streets_per_node_proportions': {0: 0.0,
1: 0.297442799461642,
2: 0.010094212651413189,
3: 0.4771197846567968,
4: 0.2018842530282638,
5: 0.013458950201884253},
'intersection_count': 1044,
'street_length_total': 63645.17499999979,
'street_segment_count': 1907,
'street_length_avg': 33.37450183534336,
'circuity_avg': 1.0458720155076198,
'self_loop_proportion': 0.0015731515469323545}
This does not yet yield all interesting characteristics of our network, as OSMnx does not automatically take the area covered by the network into consideration. We can do that manually, by, first, delineating the complex hull of the network (of an ’unary’ union of all its features), and then, second, computing the area of this hull.
[7]:
convex_hull = edges.geometry.union_all().convex_hull
convex_hull
[7]:
[8]:
stats = osmnx.basic_stats(graph, area=convex_hull.area)
stats
[8]:
{'n': 1486,
'm': 3207,
'k_avg': 4.31628532974428,
'edge_length_total': 102804.43999999992,
'edge_length_avg': 32.05626442157777,
'streets_per_node_avg': 2.623822341857335,
'streets_per_node_counts': {0: 0, 1: 442, 2: 15, 3: 709, 4: 300, 5: 20},
'streets_per_node_proportions': {0: 0.0,
1: 0.297442799461642,
2: 0.010094212651413189,
3: 0.4771197846567968,
4: 0.2018842530282638,
5: 0.013458950201884253},
'intersection_count': 1044,
'street_length_total': 63645.17499999979,
'street_segment_count': 1907,
'street_length_avg': 33.37450183534336,
'circuity_avg': 1.0458720155076198,
'self_loop_proportion': 0.0015731515469323545,
'node_density_km': 857.9273994157286,
'intersection_density_km': 602.7430719986679,
'edge_density_km': 59353.126418297594,
'street_density_km': 36744.91216225354}
[9]:
import math
import myst_nb
myst_nb.glue("node_density_km", round(stats["node_density_km"], 1))
myst_nb.glue("edge_length_total", math.floor(stats["edge_length_total"] / 1000))
857.9
102
As we can see, now we have a lot of information about our street network that can be used to understand its structure. We can for example see that the average node density in our network is {glue:}node_density_km
nodes/km and that the total edge length of our network is more than {glue:}edge_length_total
kilometers.
Note: Centrality measuresIn earlier years, this course also discussed degree centrality. Computing network centrality has changed in OSMnx: going in-depth would be beyond the scope of this course. Please see the OSMnx notebook for an example.
Shortest path analysis#
Let’s now calculate the shortest path between two points using `osmnx.shortest_path()
<https://osmnx.readthedocs.io/en/stable/osmnx.html?highlight=get_nearest_node#osmnx.distance.shortest_path>`__.
Origin and destination points#
First we need to specify the source and target locations for our route. If you are familiar with the Kamppi area, you can specify a custom placename as a source location. Or, you can follow along and choose these points as the origin and destination in the analysis: - `"Maria 01, Helsinki"
<https://nominatim.openstreetmap.org/ui/search.html?q=Maria+01>`__: a startup hub in a former hospital area. - `"ruttopuisto"
<https://nominatim.openstreetmap.org/ui/search.html?q=ruttopuisto>`__, a
park. The park’s official name is ’Vanha kirkkopuisto’, but Nominatim is also able to geocode the nickname.
We could figure out the coordinates for these locations manually, and create shapely.geometry.Point
s based on the coordinates. However, if we would have more than just two points, that would quickly become a chore. Instead, we can use OSMnx to geocode the locations.
Remember to transform the origin and destination points to the same reference system as the network data.
[10]:
origin = (
osmnx.geocode_to_gdf("Maria 01, Helsinki") # fetch geolocation
.to_crs(edges.crs) # transform to UTM
.at[0, "geometry"] # pick geometry of first row
.centroid # use the centre point
)
destination = (
osmnx.geocode_to_gdf("ruttopuisto")
.to_crs(edges.crs)
.at[0, "geometry"]
.centroid
)
We now have shapely.geometry.Point
s representing the origin and destination locations for our network analysis. In a next step, we need find these points on the routable network before the final routing.
Nearest node#
To route on the network, we first have to find a starting point and endpoint that is part of the network. Use [osmnx.distance.nearest_nodes()
](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.distance.nearest_nodes) to return the nearest node’s ID:
[11]:
origin_node_id = osmnx.nearest_nodes(graph, origin.x, origin.y)
origin_node_id
[11]:
319719983
[12]:
destination_node_id = osmnx.nearest_nodes(graph, destination.x, destination.y)
destination_node_id
[12]:
2195109761
Routing#
Now we are ready for routing and to find the shortest path between the origin and target locations. We will use `osmnx.shortest_path()
<https://osmnx.readthedocs.io/en/stable/osmnx.html?highlight=get_nearest_node#osmnx.distance.shortest_path>`__.
The function accepts three mandatory parameters: a graph, an origin node id, and a destination node id, and two optional parameters: weight
can be set to consider a different cost impedance than the length of the route, and cpus
controls parallel computation of many routes.
[13]:
# Find the shortest path between origin and destination
route = osmnx.shortest_path(graph, origin_node_id, destination_node_id)
route
[13]:
[319719983,
11702007434,
1382316822,
1382316829,
1382316852,
5464887863,
1382320461,
5154747161,
1378064352,
1372461709,
1372441203,
3205236795,
3205236793,
8244768393,
60278325,
56115897,
60072524,
7699019923,
7699019916,
7699019908,
7699019903,
267117319,
1897461604,
724233143,
724233128,
267117317,
846597945,
846597947,
2037356632,
1547012339,
569742461,
1372441189,
4524927399,
298372061,
7702074840,
7702074833,
60170471,
8856704555,
3227176325,
7676757030,
8856704573,
7676756995,
8856704588,
1377211668,
7676890497,
1377211666,
292859324,
25291565,
2195109761]
As a result we get a list of all the nodes that are along the shortest path.
We could extract the locations of those nodes from the nodes
GeoDataFrame and create a LineString presentation of the points, but luckily, OSMnx can do that for us and we can plot shortest path by using plot_graph_route()
function:
[14]:
# Plot the shortest path
fig, ax = osmnx.plot_graph_route(graph, route)
Nice! Now we have the shortest path between our origin and target locations. Being able to analyze shortest paths between locations can be valuable information for many applications. Here, we only analyzed the shortest paths based on distance but quite often it is more useful to find the optimal routes between locations based on the travelled time. Here, for example we could calculate the time that it takes to cross each road segment by dividing the length of the road segment with the speed limit and calculate the optimal routes by taking into account the speed limits as well that might alter the result especially on longer trips than here.
Saving shortest paths to disk#
Quite often you need to save the route into a file for further analysis and visualization purposes, or at least have it as a GeoDataFrame object in Python. Hence, let’s continue still a bit and see how we can turn the route into a linestring and save the shortest path geometry and related attributes into a geopackage file.
First we need to get the nodes that belong to the shortest path:
[15]:
# Get the nodes along the shortest path
route_nodes = nodes.loc[route]
route_nodes
[15]:
y | x | street_count | lon | lat | highway | ref | geometry | |
---|---|---|---|---|---|---|---|---|
osmid | ||||||||
319719983 | 6.671816e+06 | 384706.296089 | 3 | 24.922273 | 60.166932 | NaN | NaN | POINT (384706.296 6671815.989) |
11702007434 | 6.671820e+06 | 384706.910549 | 3 | 24.922282 | 60.166971 | NaN | NaN | POINT (384706.911 6671820.338) |
1382316822 | 6.671839e+06 | 384709.579017 | 4 | 24.922319 | 60.167142 | NaN | NaN | POINT (384709.579 6671839.311) |
1382316829 | 6.671850e+06 | 384711.044607 | 3 | 24.922339 | 60.167236 | NaN | NaN | POINT (384711.045 6671849.707) |
1382316852 | 6.671861e+06 | 384712.504583 | 3 | 24.922359 | 60.167338 | NaN | NaN | POINT (384712.505 6671860.984) |
5464887863 | 6.671865e+06 | 384713.220293 | 3 | 24.922370 | 60.167377 | NaN | NaN | POINT (384713.220 6671865.374) |
1382320461 | 6.671887e+06 | 384719.671826 | 3 | 24.922473 | 60.167575 | NaN | NaN | POINT (384719.672 6671887.215) |
5154747161 | 6.671874e+06 | 384758.946564 | 3 | 24.923188 | 60.167471 | NaN | NaN | POINT (384758.947 6671874.411) |
1378064352 | 6.671869e+06 | 384776.322613 | 3 | 24.923504 | 60.167428 | NaN | NaN | POINT (384776.323 6671869.117) |
1372461709 | 6.671853e+06 | 384830.142058 | 3 | 24.924482 | 60.167300 | NaN | NaN | POINT (384830.142 6671853.149) |
1372441203 | 6.671833e+06 | 384899.781649 | 3 | 24.925748 | 60.167135 | NaN | NaN | POINT (384899.782 6671832.539) |
3205236795 | 6.671821e+06 | 384940.404180 | 3 | 24.926486 | 60.167040 | NaN | NaN | POINT (384940.404 6671820.709) |
3205236793 | 6.671819e+06 | 384945.256772 | 3 | 24.926574 | 60.167029 | NaN | NaN | POINT (384945.257 6671819.264) |
8244768393 | 6.671819e+06 | 384946.335048 | 3 | 24.926594 | 60.167026 | NaN | NaN | POINT (384946.335 6671818.940) |
60278325 | 6.671811e+06 | 384973.265328 | 3 | 24.927083 | 60.166961 | crossing | NaN | POINT (384973.265 6671810.884) |
56115897 | 6.671809e+06 | 384981.149619 | 3 | 24.927226 | 60.166943 | traffic_signals | NaN | POINT (384981.150 6671808.541) |
60072524 | 6.671806e+06 | 384989.084898 | 4 | 24.927371 | 60.166924 | crossing | NaN | POINT (384989.085 6671806.231) |
7699019923 | 6.671802e+06 | 385004.619120 | 3 | 24.927653 | 60.166886 | NaN | NaN | POINT (385004.619 6671801.508) |
7699019916 | 6.671787e+06 | 385053.438353 | 3 | 24.928540 | 60.166771 | NaN | NaN | POINT (385053.438 6671787.182) |
7699019908 | 6.671778e+06 | 385082.455525 | 3 | 24.929068 | 60.166697 | NaN | NaN | POINT (385082.456 6671778.014) |
7699019903 | 6.671770e+06 | 385107.865205 | 3 | 24.929530 | 60.166632 | NaN | NaN | POINT (385107.865 6671769.973) |
267117319 | 6.671765e+06 | 385122.997863 | 3 | 24.929805 | 60.166595 | NaN | NaN | POINT (385122.998 6671765.364) |
1897461604 | 6.671758e+06 | 385124.328076 | 4 | 24.929834 | 60.166527 | crossing | NaN | POINT (385124.328 6671757.666) |
724233143 | 6.671751e+06 | 385129.242187 | 4 | 24.929926 | 60.166471 | NaN | NaN | POINT (385129.242 6671751.271) |
724233128 | 6.671747e+06 | 385134.459142 | 4 | 24.930022 | 60.166435 | NaN | NaN | POINT (385134.459 6671747.096) |
267117317 | 6.671744e+06 | 385154.958400 | 3 | 24.930393 | 60.166409 | NaN | NaN | POINT (385154.958 6671743.611) |
846597945 | 6.671741e+06 | 385157.556336 | 3 | 24.930441 | 60.166384 | NaN | NaN | POINT (385157.556 6671740.744) |
846597947 | 6.671741e+06 | 385168.666425 | 3 | 24.930642 | 60.166387 | NaN | NaN | POINT (385168.666 6671740.674) |
2037356632 | 6.671745e+06 | 385172.125438 | 3 | 24.930701 | 60.166429 | NaN | NaN | POINT (385172.125 6671745.257) |
1547012339 | 6.671745e+06 | 385191.531754 | 3 | 24.931051 | 60.166433 | NaN | NaN | POINT (385191.532 6671745.173) |
569742461 | 6.671734e+06 | 385229.630826 | 3 | 24.931743 | 60.166343 | NaN | NaN | POINT (385229.631 6671733.927) |
1372441189 | 6.671719e+06 | 385268.523844 | 4 | 24.932452 | 60.166218 | NaN | NaN | POINT (385268.524 6671718.767) |
4524927399 | 6.671723e+06 | 385274.647756 | 3 | 24.932560 | 60.166256 | NaN | NaN | POINT (385274.648 6671722.799) |
298372061 | 6.671664e+06 | 385321.073501 | 3 | 24.933429 | 60.165737 | NaN | NaN | POINT (385321.074 6671663.529) |
7702074840 | 6.671673e+06 | 385336.128259 | 3 | 24.933695 | 60.165830 | NaN | NaN | POINT (385336.128 6671673.444) |
7702074833 | 6.671687e+06 | 385356.847665 | 3 | 24.934060 | 60.165959 | NaN | NaN | POINT (385356.848 6671687.094) |
60170471 | 6.671704e+06 | 385382.616738 | 4 | 24.934515 | 60.166117 | NaN | NaN | POINT (385382.617 6671703.996) |
8856704555 | 6.671723e+06 | 385411.444462 | 3 | 24.935023 | 60.166293 | NaN | NaN | POINT (385411.444 6671722.641) |
3227176325 | 6.671725e+06 | 385414.610525 | 3 | 24.935079 | 60.166311 | NaN | NaN | POINT (385414.611 6671724.615) |
7676757030 | 6.671750e+06 | 385453.325086 | 3 | 24.935762 | 60.166546 | NaN | NaN | POINT (385453.325 6671749.549) |
8856704573 | 6.671755e+06 | 385462.274213 | 3 | 24.935920 | 60.166600 | NaN | NaN | POINT (385462.274 6671755.321) |
7676756995 | 6.671765e+06 | 385477.769273 | 3 | 24.936194 | 60.166694 | NaN | NaN | POINT (385477.769 6671765.312) |
8856704588 | 6.671777e+06 | 385495.230414 | 3 | 24.936502 | 60.166800 | NaN | NaN | POINT (385495.230 6671776.557) |
1377211668 | 6.671789e+06 | 385514.573340 | 4 | 24.936843 | 60.166917 | NaN | NaN | POINT (385514.573 6671789.024) |
7676890497 | 6.671771e+06 | 385526.530522 | 3 | 24.937069 | 60.166755 | NaN | NaN | POINT (385526.531 6671770.575) |
1377211666 | 6.671703e+06 | 385570.886277 | 4 | 24.937906 | 60.166160 | NaN | NaN | POINT (385570.886 6671702.892) |
292859324 | 6.671593e+06 | 385642.586987 | 4 | 24.939258 | 60.165196 | crossing | NaN | POINT (385642.587 6671593.155) |
25291565 | 6.671586e+06 | 385647.124210 | 4 | 24.939344 | 60.165135 | traffic_signals | NaN | POINT (385647.124 6671586.216) |
2195109761 | 6.671628e+06 | 385711.398411 | 3 | 24.940478 | 60.165531 | NaN | NaN | POINT (385711.398 6671628.330) |
As we can see, now we have all the nodes that were part of the shortest path as a GeoDataFrame.
Now we can create a LineString out of the Point geometries of the nodes:
[16]:
import shapely.geometry
# Create a geometry for the shortest path
route_line = shapely.geometry.LineString(
list(route_nodes.geometry.values)
)
route_line
[16]:
Now we have the route as a LineString geometry.
Let’s make a GeoDataFrame out of it having some useful information about our route such as a list of the osmids that are part of the route and the length of the route.
[17]:
import geopandas
route_geom = geopandas.GeoDataFrame(
{
"geometry": [route_line],
"osm_nodes": [route],
},
crs=edges.crs
)
# Calculate the route length
route_geom["length_m"] = route_geom.length
route_geom.head()
[17]:
geometry | osm_nodes | length_m | |
---|---|---|---|
0 | LINESTRING (384706.296 6671815.989, 384706.911... | [319719983, 11702007434, 1382316822, 138231682... | 1291.324641 |
Now we have a GeoDataFrame that we can save to disk. Let’s still confirm that everything is ok by plotting our route on top of our street network and some buildings, and plot also the origin and target points on top of our map.
Download buildings:
[25]:
buildings = osmnx.features_from_place(
PLACE_NAME,
{
"building" : True
}
).to_crs(edges.crs)
Let’s now plot the route and the street network elements to verify that everything is as it should:
[19]:
import contextily
import matplotlib.pyplot
fig, ax = matplotlib.pyplot.subplots(figsize=(12,8))
# Plot edges and nodes
edges.plot(ax=ax, linewidth=0.75, color='gray')
nodes.plot(ax=ax, markersize=2, color='gray')
# Add buildings
ax = buildings.plot(ax=ax, facecolor='lightgray', alpha=0.7)
# Add the route
ax = route_geom.plot(ax=ax, linewidth=2, linestyle='--', color='red')
# Add basemap
contextily.add_basemap(ax, crs=buildings.crs, source=contextily.providers.CartoDB.Positron)
Great everything seems to be in order! As you can see, now we have a full control of all the elements of our map and we can use all the aesthetic properties that matplotlib provides to modify how our map will look like. Now we are almost ready to save our data into disk.
Prepare data for saving to file#
The data contain certain data types (such as list
) that should be converted into character strings prior to saving the data to file (an alternative would be to drop invalid columns).
[20]:
edges.head()
[20]:
osmid | oneway | lanes | name | highway | maxspeed | reversed | length | geometry | junction | bridge | width | service | tunnel | access | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
u | v | key | |||||||||||||||
25216594 | 1372425721 | 0 | 23717777 | True | 2 | Porkkalankatu | primary | 40 | False | 10.404 | LINESTRING (384631.322 6671580.071, 384620.884... | NaN | NaN | NaN | NaN | NaN | NaN |
1372425714 | 0 | 23856784 | True | 2 | Mechelininkatu | primary | 40 | False | 40.885 | LINESTRING (384631.322 6671580.071, 384624.750... | NaN | NaN | NaN | NaN | NaN | NaN | |
1372425721 | 60070671 | 0 | [930820929, 1296002562, 654270815] | False | NaN | NaN | cycleway | NaN | False | 41.002 | LINESTRING (384620.884 6671579.965, 384620.406... | NaN | NaN | NaN | NaN | NaN | NaN |
25290781 | 0 | [1296002562, 29191566] | False | NaN | NaN | cycleway | NaN | True | 228.587 | LINESTRING (384620.884 6671579.965, 384621.184... | NaN | NaN | NaN | NaN | NaN | NaN | |
1372425714 | 25238874 | 0 | [78537378, 8169098, 29081876, 78537375] | True | [3, 2] | Mechelininkatu | primary | 40 | False | 85.741 | LINESTRING (384624.179 6671539.986, 384623.768... | NaN | NaN | NaN | NaN | NaN | NaN |
[21]:
# Columns with invalid values
problematic_columns = [
"osmid",
"lanes",
"name",
"highway",
"width",
"maxspeed",
"reversed",
"junction",
"bridge",
"tunnel",
"access",
"service",
]
# convert selected columns to string format
edges[problematic_columns] = edges[problematic_columns].astype(str)
[22]:
route_geom["osm_nodes"] = route_geom["osm_nodes"].astype(str)
Now we can see that most of the attributes are of type object
that quite often (such as ours here) refers to a string type of data.
Save the data:#
[23]:
import pathlib
NOTEBOOK_PATH = pathlib.Path().resolve()
DATA_DIRECTORY = NOTEBOOK_PATH / "data"
[24]:
# Save one layer after another
output_gpkg = DATA_DIRECTORY / "OSM_Kamppi.gpkg"
edges.to_file(output_gpkg, layer="streets")
route_geom.to_file(output_gpkg, layer="route")
nodes.to_file(output_gpkg, layer="nodes")
#buildings[['geometry', 'name', 'addr:street']].to_file(output_gpkg, layer="buildings")
display(buildings.describe())
display(buildings)
ele | geometry | amenity | operator | wheelchair | source | access | addr:housenumber | addr:street | addr:unit | ... | lippakioski | toilets:disposal | unisex | covered | area | leisure | ways | type | electrified | nohousenumber | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 1 | 454 | 17 | 3 | 14 | 18 | 7 | 193 | 195 | 20 | ... | 1 | 1 | 1 | 1 | 1 | 1 | 36 | 36 | 1 | 2 |
unique | 1 | 454 | 12 | 3 | 3 | 4 | 2 | 65 | 35 | 20 | ... | 1 | 1 | 1 | 1 | 1 | 1 | 36 | 1 | 1 | 1 |
top | 5 | POINT (384966.6609617978 6671503.785973044) | place_of_worship | Nice Bike Oy | yes | survey | private | 2-10 | Kalevankatu | K | ... | yes | flush | yes | yes | yes | sauna | [22665612, 22664922] | multipolygon | yes | yes |
freq | 1 | 1 | 4 | 1 | 10 | 9 | 6 | 14 | 26 | 1 | ... | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 36 | 1 | 2 |
4 rows × 121 columns
ele | geometry | amenity | operator | wheelchair | source | access | addr:housenumber | addr:street | addr:unit | ... | lippakioski | toilets:disposal | unisex | covered | area | leisure | ways | type | electrified | nohousenumber | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
element_type | osmid | |||||||||||||||||||||
node | 11711721042 | NaN | POINT (384966.661 6671503.786) | NaN | Nice Bike Oy | NaN | NaN | NaN | 46 | Eerikinkatu | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
way | 8035238 | NaN | POLYGON ((385459.650 6672184.469, 385456.356 6... | NaN | NaN | NaN | NaN | NaN | 22-24 | Mannerheimintie | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
8042297 | NaN | POLYGON ((385104.154 6671916.693, 385101.584 6... | NaN | NaN | NaN | NaN | NaN | 2 | Runeberginkatu | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
14797170 | NaN | POLYGON ((384815.326 6671762.710, 384815.792 6... | NaN | City of Helsinki | NaN | survey | NaN | 10 | Lapinlahdenkatu | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
14797171 | NaN | POLYGON ((384797.759 6671853.253, 384798.253 6... | NaN | NaN | NaN | survey | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
relation | 8092998 | NaN | POLYGON ((384747.465 6671811.996, 384744.270 6... | NaN | NaN | NaN | NaN | NaN | 16 | Lapinlahdenkatu | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | [567890372, 567890373, 567890374] | multipolygon | NaN | NaN |
8280536 | NaN | POLYGON ((384839.007 6671934.815, 384839.485 6... | NaN | NaN | NaN | NaN | NaN | 38 | Malminkatu | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | [586432486, 586432485, 586432484, 123653652, 5... | multipolygon | NaN | NaN | |
8525159 | NaN | POLYGON ((385494.804 6672166.709, 385494.902 6... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | [616111835, 616258502, 616258501, 654959157] | multipolygon | NaN | NaN | |
8525161 | NaN | POLYGON ((385486.225 6672173.653, 385486.717 6... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | [616111832, 616258504] | multipolygon | NaN | yes | |
8535506 | NaN | POLYGON ((385481.130 6672167.861, 385482.372 6... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | [617247984, 617247983] | multipolygon | NaN | yes |
454 rows × 121 columns
Great, now we have saved all the data that was used to produce the maps into a geopackage.
Advanced reading#
Here we learned how to solve a simple routing task between origin and destination points. What about if we have hundreads or thousands of origins? This is the case if you want to explore the travel distances to a spesific location across the whole city, for example, when analyzing the accessibility of jobs and services (like in the Travel Time Matrix dataset used in previous sections).
Check out pyrosm documentation on working with graphs for more advanced examples of network analysis in python. For example, pandana is a fast and efficient python library for creating aggretated network analysis in no time across large networks, and pyrosm can be used for preparing the input data for such analysis.