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() function. As parameters, it expects a place name and, optionally, a network type.

import osmnx

PLACE_NAME = "Kamppi, Helsinki, Finland"
graph = osmnx.graph_from_place(
    PLACE_NAME,
    network_type="bike"
)
figure, ax = osmnx.plot_graph(graph)
../../_images/c2b5da15dd6d8ed7d4ac62737955cd88399ce988d8746e870a6922360d7e7925.png

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.

# 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)
../../_images/4e3172a1a2d6a2a71e0ef2938499834d681903febd959d32ba2394b500f18147.png

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):

# Retrieve only edges from the graph
edges = osmnx.graph_to_gdfs(graph, nodes=False, edges=True)
edges.head()
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.

Columns in edges#

Column

Description

Data type

bridge

Bridge feature

boolean

geometry

Geometry of the feature

Shapely.geometry

highway

Tag for roads (road type)

str / list

lanes

Number of lanes

int (or nan)

length

Length of feature (meters)

float

maxspeed

maximum legal speed limit

int /list

name

Name of the (street) element

str (or nan)

oneway

One way road

boolean

osmid

Unique ids for the element

list

u

The start node of edge

int

v

The end node of edge

int

What types of streets does our network comprise of?

edges["highway"].value_counts()
service                      912
residential                  478
cycleway                     465
pedestrian                   372
tertiary                     218
primary                      167
secondary                    121
unclassified                  42
living_street                 16
[pedestrian, service]         14
[residential, cycleway]        6
[pedestrian, cycleway]         4
[service, living_street]       3
[living_street, service]       3
[residential, pedestrian]      2
[pedestrian, residential]      2
tertiary_link                  2
primary_link                   1
[unclassified, service]        1
Name: highway, 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() 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.

# Transform the graph to UTM
graph = osmnx.project_graph(graph) 

# Extract reprojected nodes and edges
nodes, edges = osmnx.graph_to_gdfs(graph)

nodes.crs
<Derived Projected CRS: +proj=utm +zone=35 +ellps=WGS84 +datum=WGS84 +unit ...>
Name: unknown
Axis Info [cartesian]:
- E[east]: Easting (metre)
- N[north]: Northing (metre)
Area of Use:
- undefined
Coordinate Operation:
- name: UTM zone 35N
- method: Transverse Mercator
Datum: World Geodetic System 1984
- 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():

# Calculate network statistics
osmnx.basic_stats(graph)
{'n': 1332,
 'm': 2829,
 'k_avg': 4.247747747747748,
 'edge_length_total': 91859.41999999988,
 'edge_length_avg': 32.47063273241424,
 'streets_per_node_avg': 2.623123123123123,
 'streets_per_node_counts': {0: 0, 1: 392, 2: 17, 3: 642, 4: 263, 5: 18},
 'streets_per_node_proportions': {0: 0.0,
  1: 0.29429429429429427,
  2: 0.012762762762762763,
  3: 0.481981981981982,
  4: 0.19744744744744744,
  5: 0.013513513513513514},
 'intersection_count': 940,
 'street_length_total': 57356.40399999986,
 'street_segment_count': 1706,
 'street_length_avg': 33.62040093786627,
 'circuity_avg': 1.0372724102098172,
 'self_loop_proportion': 0.0011723329425556857}

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.

convex_hull = edges.unary_union.convex_hull
convex_hull
../../_images/7d7c4e6653213f32a29865697df23db5c98f2c2e64cce285dbadb40ffd39f794.svg
stats = osmnx.basic_stats(graph, area=convex_hull.area)
stats
{'n': 1332,
 'm': 2829,
 'k_avg': 4.247747747747748,
 'edge_length_total': 91859.41999999988,
 'edge_length_avg': 32.47063273241424,
 'streets_per_node_avg': 2.623123123123123,
 'streets_per_node_counts': {0: 0, 1: 392, 2: 17, 3: 642, 4: 263, 5: 18},
 'streets_per_node_proportions': {0: 0.0,
  1: 0.29429429429429427,
  2: 0.012762762762762763,
  3: 0.481981981981982,
  4: 0.19744744744744744,
  5: 0.013513513513513514},
 'intersection_count': 940,
 'street_length_total': 57356.40399999986,
 'street_segment_count': 1706,
 'street_length_avg': 33.62040093786627,
 'circuity_avg': 1.0372724102098172,
 'self_loop_proportion': 0.0011723329425556857,
 'node_density_km': 772.426100508414,
 'intersection_density_km': 545.1055063647967,
 'edge_density_km': 53269.22941859199,
 'street_density_km': 33260.94855923806}

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 772.4 nodes/km and that the total edge length of our network is more than 91 kilometers.

Centrality measures

In 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 according OSMnx notebook for an example.


Shortest path analysis#

Let’s now calculate the shortest path between two points using osmnx.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": a startup hub in a former hospital area.

  • "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.Points 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.

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.Points 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:

origin_node_id = osmnx.nearest_nodes(graph, origin.x, origin.y)
origin_node_id
319719983
destination_node_id = osmnx.nearest_nodes(graph, destination.x, destination.y)
destination_node_id
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().

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.

# Find the shortest path between origin and destination
route = osmnx.shortest_path(graph, origin_node_id, destination_node_id)
route
[319719983,
 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:

# Plot the shortest path
fig, ax = osmnx.plot_graph_route(graph, route)
../../_images/b08276a8895126e52acce85d0e5b275a26d460af342fe34f875025624a6906e9.png

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:

# Get the nodes along the shortest path
route_nodes = nodes.loc[route]
route_nodes
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)
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:

import shapely.geometry

# Create a geometry for the shortest path
route_line = shapely.geometry.LineString(
    list(route_nodes.geometry.values)
)
route_line
../../_images/491e620621ef1177c2608484188ca3da0e5ee0916f8c929b05454a68548e5ae9.svg

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.

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()
geometry osm_nodes length_m
0 LINESTRING (384706.296 6671815.989, 384709.579... [319719983, 1382316822, 1382316829, 1382316852... 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:

buildings = osmnx.geometries_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:

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)
../../_images/1bcd2aab60fb2935f63f85423c8dc5f0e261ca227156b22cb1255259344245c4.png

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).

edges.head()
osmid oneway lanes name highway maxspeed reversed length geometry junction bridge width tunnel access service
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 25290781 0 29191566 False NaN NaN cycleway NaN True 228.587 LINESTRING (384620.884 6671579.965, 384621.184... NaN NaN NaN NaN NaN NaN
60070671 0 [930820929, 930820930, 654270815] False NaN NaN cycleway NaN False 41.006 LINESTRING (384620.884 6671579.965, 384620.720... NaN NaN NaN NaN NaN NaN
1372425714 25238874 0 [78537378, 8169098, 29081876, 78537375] True [3, 2] Mechelininkatu primary 40 False 85.540 LINESTRING (384624.179 6671539.986, 384623.768... NaN NaN NaN NaN NaN NaN
# 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)
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:#

import pathlib
NOTEBOOK_PATH = pathlib.Path().resolve()
DATA_DIRECTORY = NOTEBOOK_PATH / "data"
# 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 ... drive_through ice_cream lippakioski covered area leisure ways type electrified nohousenumber
count 1 452 15 2 13 18 6 173 173 21 ... 1 2 1 1 1 1 36 36 1 2
unique 1 452 12 2 3 4 1 66 36 21 ... 1 1 1 1 1 1 36 1 1 1
top 5 POLYGON ((385459.64989424404 6672184.469037577... place_of_worship City of Helsinki yes survey private 2-10 Lapinlahdenkatu K ... no yes yes yes yes sauna [22665612, 22664922] multipolygon yes yes
freq 1 1 3 1 10 9 6 14 19 1 ... 1 2 1 1 1 1 1 36 1 2

4 rows × 115 columns

ele geometry amenity operator wheelchair source access addr:housenumber addr:street addr:unit ... drive_through ice_cream lippakioski covered area leisure ways type electrified nohousenumber
element_type osmid
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
14797172 NaN POLYGON ((384938.763 6671804.334, 384939.456 6... NaN NaN NaN survey NaN 2 Lapinrinne 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

452 rows × 115 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.