OpenStreetMap: OSMnx Introduction

Python
Pandas
Jupyter
Data Analytics
An introductory tutorial to the Python OSMnx package.
Author

Dennis Chua

Published

June 24, 2025

Open In Colab

Introduction to Open Street Maps Python Library (OSMnx)

Content Outline

  • Introduction
  • Relevant Python Packages
  • OpenstreetMap Data Structures and Constructs
  • Acquiring Points of Interest (POI) Data
  • Acquiring Polygonal Data
  • Acquiring Graph Data
  • Adding a Basemap

Introduction

OSMnx is a Python package used for modeling ubran geographic features and relationships. Drawing on volunteer-supplied data at OpenstreetMap, OSMnx facilitates the geoanalytics of various aspects of urban life such as travel networks and accesibility, zoning or public health. OSMnx is a useful tool for urban planning. The global data that this API opens up is publicly accessible through the OpenstreetMap portal.

In this notebook we’ll go over some of the basic features of OSMnx.

Relevant Python Packages

For starters we’ll need to load the OSMnx package together with Matplotlib for visualization. Under the hood, OSMnx extends the Overpass package, wrapping it in a more elegant API. OSMnx also uses Geopandas, which in turn extends the Pandas library with geocoding features.

!pip install osmnx
!pip install matplotlib

import osmnx as ox
import matplotlib
import matplotlib.pyplot as plt

print(ox.__version__)
print(matplotlib.__version__)
Requirement already satisfied: osmnx in /opt/conda/lib/python3.12/site-packages (2.0.5)
Requirement already satisfied: geopandas>=1.0.1 in /opt/conda/lib/python3.12/site-packages (from osmnx) (1.1.1)
Requirement already satisfied: networkx>=2.5 in /opt/conda/lib/python3.12/site-packages (from osmnx) (3.4.2)
Requirement already satisfied: numpy>=1.22 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.1.3)
Requirement already satisfied: pandas>=1.4 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.2.3)
Requirement already satisfied: requests>=2.27 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.32.3)
Requirement already satisfied: shapely>=2.0 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.1.1)
Requirement already satisfied: pyogrio>=0.7.2 in /opt/conda/lib/python3.12/site-packages (from geopandas>=1.0.1->osmnx) (0.11.0)
Requirement already satisfied: packaging in /opt/conda/lib/python3.12/site-packages (from geopandas>=1.0.1->osmnx) (24.2)
Requirement already satisfied: pyproj>=3.5.0 in /opt/conda/lib/python3.12/site-packages (from geopandas>=1.0.1->osmnx) (3.7.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.12/site-packages (from pandas>=1.4->osmnx) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.12/site-packages (from pandas>=1.4->osmnx) (2024.1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.12/site-packages (from pandas>=1.4->osmnx) (2025.1)
Requirement already satisfied: charset_normalizer<4,>=2 in /opt/conda/lib/python3.12/site-packages (from requests>=2.27->osmnx) (3.4.1)
Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.12/site-packages (from requests>=2.27->osmnx) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.12/site-packages (from requests>=2.27->osmnx) (2.3.0)
Requirement already satisfied: certifi>=2017.4.17 in /opt/conda/lib/python3.12/site-packages (from requests>=2.27->osmnx) (2025.1.31)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas>=1.4->osmnx) (1.17.0)
Requirement already satisfied: matplotlib in /opt/conda/lib/python3.12/site-packages (3.10.1)
Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (1.3.1)
Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (4.56.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (1.4.8)
Requirement already satisfied: numpy>=1.23 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (2.1.3)
Requirement already satisfied: packaging>=20.0 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (24.2)
Requirement already satisfied: pillow>=8 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (11.1.0)
Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (3.2.1)
Requirement already satisfied: python-dateutil>=2.7 in /opt/conda/lib/python3.12/site-packages (from matplotlib) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.12/site-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)
2.0.5
3.10.1

OpenstreetMap Data Structures and Constructs

OSM abstracts geographic features as geometric primitives: points (nodes), lines (ways) and polygons (relations). Openstreetmap uses these primitives as the basis to encode urban constructs. For example, points locate urban entities such as cafes, museums or hospitals. Together they are conceptually known as points of interest (POI). As building blocks, an ordered set of points constitute lines that represent road connections. In turn these lines become the building blocks of polygons which stand for physical entities (buildings or parks) or virtual regions (towns, districts, etc.)

OSM assigns unique IDs to every primitive it tracks. Some OSMnx functions can lookup these primitives by their canonical name or by a string representation of their OSM ID. For entities that are nondescript – such as a corner of a park – we only have the OSM ID as a reference handle.

For example, as OSM sees it, Ueno may refer to a railway station in Tokyo, Japan; at the same time it also refers to the administrative boundary in the metropolis. The first entity is a node while the other is a relation. The OSM ID distinguishes between the two clearly (“N” as a node; “R” as a relation):

  • Ueno Station: “N8300320717”
  • Ueno Admin: “R18158684”

Acquiring Points of Interest (POI) Data

The geodataframe is the basic data structure of OSMnx. Organized as a tabular set of rows and columns, the GDF encodes urban data such as geometry, address, amenity categories, contact information (telephone, email or website) – information we deal with when we talk about places we live or work in, a destinations we travel to or a region worth noting. Essentially, working with OSMnx involves fetching geodataframes from Openstreetmap and manipulating the information in order to transform it in ways that are relevant to us, perhaps as a table, a map or as a data plot.

Let’s get the information for Minato, a city in Metopolitan Tokyo (not to be confused with Minato Ward in Nagoya). The city is outlined in orange in the screenshot above taken from Openstreetmap. The following code uses features_from_place() to query OSM. We supply the place we’re interested in – formally known as the OSM administrative boundary – along with a dictionary of tags to filter the information we want.

OSM tags are organized in a sets of keys and tags. In Python they are reprsented as a dictionary type. A handy reference for the top-level OSM keys are found here.

minato_poi = ox.features_from_place('Minato, Tokyo', tags={'amenity': ['cafe', 'pub'], 'tourism':['museum', 'hotel', 'attraction'], 'building':['office', 'retail']})
print(f"{len(minato_poi)} elements in {type(minato_poi)}\n\n")
print(minato_poi.columns)
minato_poi.head(3)
1304 elements in <class 'geopandas.geodataframe.GeoDataFrame'>


Index(['geometry', 'amenity', 'branch', 'name', 'name:en', 'phone', 'source',
       'wheelchair', 'was:cuisine', 'was:name',
       ...
       'type', 'alt_name:es', 'communication:radio', 'contact:tiktok',
       'man_made', 'material', 'name:et', 'tower:construction', 'tower:type',
       'building:part'],
      dtype='object', length=207)
geometry amenity branch name name:en phone source wheelchair was:cuisine was:name ... type alt_name:es communication:radio contact:tiktok man_made material name:et tower:construction tower:type building:part
element id
node 393075631 POINT (139.74866 35.64395) cafe 田町東口店 エクセルシオール EXELCIOR 03-5730-2620 image,2012-10-19;Bing yes NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
474605280 POINT (139.75225 35.66847) pub NaN 山本魚吉商店 NaN NaN NaN NaN お好み焼 泉州 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
474605284 POINT (139.75436 35.66781) pub NaN 月島 Tsukishima NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

3 rows × 207 columns

Can we query OSM for information and visualize it as a plot? Yes, we can. In the example below we turn to the geocode_to_gdf() to retrieve polygonal information suitable for plotting. Commented out, we also show the same function call using the OSM ID for Minato City: 1761717.

admin_minato = ox.geocode_to_gdf('Minato, Tokyo')
print(type(admin_minato))
admin_minato.plot(color='darkgrey', edgecolor='k', figsize=(6,6))

# osm_id = "R1761717"
# gdf = ox.geocode_to_gdf(query=osm_id, by_osmid=True)
<class 'geopandas.geodataframe.GeoDataFrame'>

Using Matplotlib, we can combine information about Minato points of interest (POI) together with its polygonal representation into one infographic. In the example below, we make more than one plot() call to accomplish this. Note that we plot POIs by selecting a column-key of the GeoDataFrame, in this case amenity.

# Create a plot to visualize the admin boundary and POIs
f, ax = plt.subplots(1, 1, figsize=(6,6))

# Plot the administrative boundary
admin_minato.plot(ax=ax, color='darkgrey', edgecolor='k')

# Plot the amenity POI
minato_poi.plot(column='amenity', ax=ax, alpha=0.5, edgecolor='red', legend=True)

# Customize the plot
ax.axis('off')
plt.show()

Acquiring Polygonal Data

While plotting points of intererest is useful, the information doesn’t convey the geographic scope of particular urban features such as buildings, for example. These are urban entities better visualized as polygons.

Let’s begin by re-querying OSM about the Minato administration boundary. Then let’s focus on a subset of GeoDataFrame, the polygons for this administrative region. Finally, with the polygonal data in hand, let’s filter the information according to the same tags we’ve used before.

minato_district = ox.geocode_to_gdf('Minato, Tokyo')
print(type(minato_district))

minato_polygon = minato_district.geometry.values[0]
print(type(minato_polygon))

minato_parks = ox.features_from_polygon(minato_polygon, tags={'leisure': 'park'})
print(type(minato_parks))
<class 'geopandas.geodataframe.GeoDataFrame'>
<class 'shapely.geometry.polygon.Polygon'>
<class 'geopandas.geodataframe.GeoDataFrame'>

What is the shapely data type? According to the Shapely documentation:

Shapely is a BSD-licensed Python package for manipulation and analysis of planar geometric objects.

print(f"{len(minato_parks)} elements in {type(minato_parks)}\n\n")
minato_parks.columns
148 elements in <class 'geopandas.geodataframe.GeoDataFrame'>

Index(['geometry', 'created_by', 'leisure', 'name', 'name:en', 'fee',
       'name:de', 'name:es', 'name:ja', 'name:zh', 'opening_hours', 'tourism',
       'wikidata', 'wikimedia_commons', 'wikipedia', 'name:ja_rm', 'source',
       'layer', 'note', 'note:ja', 'source_ref', 'toilets:wheelchair',
       'wheelchair', 'name:ko', 'name:ja_kana', 'description', 'operator',
       'addr:block_number', 'addr:city', 'addr:housenumber',
       'addr:neighbourhood', 'addr:province', 'addr:quarter', 'ref',
       'addr:postcode', 'surface', 'name:ru', 'wikipedia:en', 'check_date',
       'smoking', 'operator:type', 'alt_name:en', 'area', 'name:ja-Hira',
       'name:ja-Latn', 'type', 'roof:material', 'access', 'name:ar',
       'name:ceb', 'name:fa', 'name:fr', 'name:it'],
      dtype='object')

OK, as we’ve done earlier for the POIs, let’s overlay plots for the two polygonal data sets we’ve gathered.

# Create a plot to visualize the admin boundary and park polygons
f, ax = plt.subplots(1, 1, figsize=(6, 6))

# Plot the Minato administrative boundary
minato_district.plot(ax=ax, color='none', edgecolor='k')

# Plot the parks in Minato City
minato_parks.plot(ax=ax, color='green', alpha=0.5, edgecolor='darkgreen')

# Customize the plot
ax.axis('off')
plt.show()

Now that we’ve looked at the park amenities at Minato City, let’s inspect all the building features regardless of subtype and pass it onto Matplotlib.

minato_buildings = ox.features_from_polygon(minato_polygon, tags={'building': True})
print(f"{len(minato_buildings)} elements in {type(minato_buildings)}\n\n")
minato_buildings.columns
26175 elements in <class 'geopandas.geodataframe.GeoDataFrame'>

Index(['geometry', 'building', 'name', 'note', 'note:ja', 'source',
       'source_ref', 'name:en', 'level', 'name:ja',
       ...
       'type', 'name:zh-Hans-CN', 'name:zh-Hant-TW', 'alt_name:es',
       'alt_name:zh', 'communication:radio', 'contact:tiktok', 'name:et',
       'alt_name_1', 'rugby'],
      dtype='object', length=296)
f, ax = plt.subplots(1, 1, figsize=(6, 6))

# Plot the Minato administrative boundary
minato_district.plot(ax=ax, color='none', edgecolor='k')

# Plot the parks in Minato City
minato_buildings.plot(ax=ax, cmap = 'Greys', edgecolor = 'black', alpha = 0.7, linewidth = 0.5)

# Customize the plot
ax.axis('off')
plt.show()

With a little variation to the Matplotlib function calls, we can visualize the parks and buildings maps together.

f, ax = plt.subplots(1, 2, figsize=(7, 4))

# Plot the Minato administrative boundary
minato_district.plot(ax=ax[0], color='none', edgecolor='k')

# Plot the parks in Minato City
minato_parks.plot(ax=ax[0], color='green', alpha=0.5, edgecolor='darkgreen')

# Plot the Minato administrative boundary
minato_district.plot(ax=ax[1], color='none', edgecolor='k')

# Plot the parks in Minato City
minato_buildings.plot(ax=ax[1], cmap = 'Greys', edgecolor = 'black', alpha = 0.7, linewidth = 0.5)

plt.show()

Acquiring Graph Data

Urban analytics wouldn’t be complete without road networks. In OSM, that information is embedded in GDFs as graphs where a line represents a path and a node represents an intersection. OSM graphs are multi-directed graphs: a node is connected with roadways that are inbound or outbound (directed); several paths converge on a node junction (multi connected).

Let’s see how we can retrieve the graph data for Minato City.

The sequence of OSMnx operations are: * Retrieve GeoDataFrame data from OSM * Extract the polygonal data from the GDF * Given the set polygons, retrieve the graph network from OSM

minato_district = ox.geocode_to_gdf('Minato, Tokyo')
print(type(minato_district))

minato_polygon = minato_district.geometry.values[0]
print(type(minato_polygon))
<class 'geopandas.geodataframe.GeoDataFrame'>
<class 'shapely.geometry.polygon.Polygon'>
# Download the road network for all transport modes
minato_roads = ox.graph_from_polygon(minato_polygon, network_type='drive')
print("Type of the road network graph (all modes):", type(minato_roads))
print("Number of nodes (all modes):", minato_roads.number_of_nodes())
print("Number of edges (all modes):", minato_roads.number_of_edges())
Type of the road network graph (all modes): <class 'networkx.classes.multidigraph.MultiDiGraph'>
Number of nodes (all modes): 2971
Number of edges (all modes): 6624

At this point our goal is to display the road network as a map bounded by the Minato City administrative region. To get there, we first need to transform the graph data into another GeoDataFrame.

# Convert the network graph to GeoDataFrames for nodes and edges
nodes, edges = ox.graph_to_gdfs(minato_roads)

# Display the first few rows of the nodes GeoDataFrame
display(nodes.head(3))

# Display the first few rows of the edges GeoDataFrame
display(edges.head(3))

# Print the features stored in each GeoDataFrame
print("Keys in the nodes table:", list(nodes.keys()))
print("Keys in the edges table:", list(edges.keys()))
print()

# Print the total number of nodes and edges
print("Number of nodes in the node table:", len(nodes))
print("Number of edges in the node table:", len(edges))
y x highway ref street_count junction geometry
osmid
31236584 35.634935 139.768683 motorway_junction 1101 3 NaN POINT (139.76868 35.63494)
31236646 35.634417 139.777989 NaN NaN 3 NaN POINT (139.77799 35.63442)
31236657 35.633476 139.778439 NaN NaN 3 NaN POINT (139.77844 35.63348)
osmid bridge highway lanes oneway reversed length geometry name ref maxspeed access tunnel width junction
u v key
31236584 31236646 0 [4848889, 4848756, 820214407] yes motorway_link 1 True False 971.162374 LINESTRING (139.76868 35.63494, 139.76895 35.6... NaN NaN NaN NaN NaN NaN NaN
31236646 573342136 0 333682057 yes tertiary NaN True False 40.061292 LINESTRING (139.77799 35.63442, 139.77824 35.6... NaN NaN NaN NaN NaN NaN NaN
31236657 298984113 0 863283179 yes tertiary NaN True False 101.078790 LINESTRING (139.77844 35.63348, 139.77788 35.6... NaN NaN NaN NaN NaN NaN NaN
Keys in the nodes table: ['y', 'x', 'highway', 'ref', 'street_count', 'junction', 'geometry']
Keys in the edges table: ['osmid', 'bridge', 'highway', 'lanes', 'oneway', 'reversed', 'length', 'geometry', 'name', 'ref', 'maxspeed', 'access', 'tunnel', 'width', 'junction']

Number of nodes in the node table: 2971
Number of edges in the node table: 6624

No we can take this GDF and visualize it.

f, ax = plt.subplots(1,1,figsize=(6,6))

# Plot the Minato administrative boundary
minato_district.plot(ax=ax, color='none', edgecolor='k')

nodes.plot(ax=ax, color = 'blue', markersize = 5, alpha = 0.9)
edges.plot(ax=ax, color = 'darkgrey', linewidth = 0.4, alpha = 0.9)

ax.axis('off')
(np.float64(139.70503459),
 np.float64(139.78663941),
 np.float64(35.620055535000006),
 np.float64(35.685632365))

Adding a Basemap

In the introductory section we showed a screen shot of Minato City taken directly from the Openstreetmap site. We can also achieve something like that with OSMnx combined with the Contextily package, overlaying OSM geometries over a map image in the background.

The Contextily site describes itself as a package to retrieve tile maps from the internet. In the background, the Contextily API queries a number of tile map providers, such as ESRI or CartoDB. For our purposes, we’ll re-render on of our Minato POI map (cafe, library, etc.) with a basemap provided by Contextily.

!pip install contextily
import contextily as ctx

print(ctx.__version__)
Requirement already satisfied: contextily in /opt/conda/lib/python3.12/site-packages (1.6.2)
Requirement already satisfied: geopy in /opt/conda/lib/python3.12/site-packages (from contextily) (2.4.1)
Requirement already satisfied: matplotlib in /opt/conda/lib/python3.12/site-packages (from contextily) (3.10.1)
Requirement already satisfied: mercantile in /opt/conda/lib/python3.12/site-packages (from contextily) (1.2.1)
Requirement already satisfied: pillow in /opt/conda/lib/python3.12/site-packages (from contextily) (11.1.0)
Requirement already satisfied: rasterio in /opt/conda/lib/python3.12/site-packages (from contextily) (1.4.3)
Requirement already satisfied: requests in /opt/conda/lib/python3.12/site-packages (from contextily) (2.32.3)
Requirement already satisfied: joblib in /opt/conda/lib/python3.12/site-packages (from contextily) (1.4.2)
Requirement already satisfied: xyzservices in /opt/conda/lib/python3.12/site-packages (from contextily) (2025.1.0)
Requirement already satisfied: geographiclib<3,>=1.52 in /opt/conda/lib/python3.12/site-packages (from geopy->contextily) (2.0)
Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (1.3.1)
Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (4.56.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (1.4.8)
Requirement already satisfied: numpy>=1.23 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (2.1.3)
Requirement already satisfied: packaging>=20.0 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (24.2)
Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (3.2.1)
Requirement already satisfied: python-dateutil>=2.7 in /opt/conda/lib/python3.12/site-packages (from matplotlib->contextily) (2.9.0.post0)
Requirement already satisfied: click>=3.0 in /opt/conda/lib/python3.12/site-packages (from mercantile->contextily) (8.1.8)
Requirement already satisfied: affine in /opt/conda/lib/python3.12/site-packages (from rasterio->contextily) (2.4.0)
Requirement already satisfied: attrs in /opt/conda/lib/python3.12/site-packages (from rasterio->contextily) (25.3.0)
Requirement already satisfied: certifi in /opt/conda/lib/python3.12/site-packages (from rasterio->contextily) (2025.1.31)
Requirement already satisfied: cligj>=0.5 in /opt/conda/lib/python3.12/site-packages (from rasterio->contextily) (0.7.2)
Requirement already satisfied: click-plugins in /opt/conda/lib/python3.12/site-packages (from rasterio->contextily) (1.1.1.2)
Requirement already satisfied: charset_normalizer<4,>=2 in /opt/conda/lib/python3.12/site-packages (from requests->contextily) (3.4.1)
Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.12/site-packages (from requests->contextily) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.12/site-packages (from requests->contextily) (2.3.0)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.12/site-packages (from python-dateutil>=2.7->matplotlib->contextily) (1.17.0)
1.6.2
# Create a plot to visualize the admin boundary and points of interest
f, ax = plt.subplots(1, 1, figsize=(6,6))

# Plot the administrative boundary
admin_minato.plot(ax=ax, color='None', edgecolor='k')

# Plot the amenity POI
minato_poi.plot(column='amenity', ax=ax, alpha=0.5, edgecolor='red', legend=True)

# Query ESRI to add basemap using Contextily
ctx.add_basemap(ax, crs = admin_minato.crs, url=ctx.providers.Esri.WorldTopoMap)

# Customize the plot
ax.axis('off')
plt.show()