How to Measure Street Orientation Entropy in Python
On this page
Problem statement
Manhattan Midtown scores an orientation order of 0.980 and Rome's centre 0.090, and the two can be compared only because they were measured the same way: the same radius around a fixed point, the same network type, 36 bins, segments weighted by length, and every street made undirected first. With osmnx that is a short loop per place β download, make undirected, add bearings, compute entropy β plus a polar plot of each city to check the number against the picture.
This guide runs that loop for six city centres, compares drivable and complete networks, and writes the polar plots to a single figure.
Quick answer
import numpy as np, pandas as pd, osmnx as ox
ox.settings.overpass_url = "https://maps.mail.ru/osm/tools/overpass/api" # overpass-api.de mirror
ox.settings.overpass_rate_limit = False
ox.settings.http_user_agent = "spatialworkflow-examples/1.0"
CITIES = {"Chicago Loop": (41.8837, -87.6325), "Manhattan Midtown": (40.7549, -73.9840),
"Barcelona Eixample": (41.3928, 2.1633), "Paris centre": (48.8566, 2.3522),
"Den Haag centre": (52.0799, 4.3113), "Rome centro": (41.8986, 12.4768)}
H_MAX, H_GRID = np.log(36), np.log(4)
graphs, rows = {}, []
for name, point in CITIES.items():
G = ox.graph_from_point(point, dist=1500, network_type="drive")
Gu = ox.bearing.add_edge_bearings(ox.convert.to_undirected(G))
graphs[name] = Gu
H = ox.bearing.orientation_entropy(Gu, num_bins=36, weight="length")
rows.append({"city": name, "segments": len(Gu.edges), "entropy": H,
"phi": 1 - ((H - H_GRID) / (H_MAX - H_GRID)) ** 2})
table = pd.DataFrame(rows).sort_values("phi", ascending=False)
print(table.round(3).to_string(index=False))
city segments entropy phi
Manhattan Midtown 1213 1.700 0.980
Chicago Loop 1469 2.170 0.873
Barcelona Eixample 1854 2.761 0.609
Den Haag centre 1892 2.914 0.517
Paris centre 1564 3.442 0.125
Rome centro 1703 3.482 0.090
Six centres, one method, a clean ordering from the planned American grids through the rotated Eixample to Paris and Rome.
Fixed extents, a network type, entropy, plots
1. Fix the extent for every place
A radius around a centre point is the simplest comparable extent. Administrative boundaries differ wildly in how much suburb they include; a 1.5 km radius compares like with like.
2. Choose the network type
drive for the vehicular structure, all to include footpaths and alleys. Keep it the same for every city.
3. Undirect, add bearings, compute entropy
ox.convert.to_undirected, then ox.bearing.add_edge_bearings, then ox.bearing.orientation_entropy with weight="length" and 36 bins.
4. Plot every city
ox.plot.plot_orientation draws the polar histogram. A number that does not match its picture is a bug β usually a directed graph or a projected one.
5. Save the graphs
Downloading street networks is the slow part. Save each graph with ox.save_graphml so the analysis can be rerun without hitting Overpass.
Polar plots for six cities
The data used on this page
OpenStreetMap's drive networks within 1.5 km of six centres β Chicago's Loop, Midtown Manhattan, Barcelona's Eixample, central Paris, central Den Haag and central Rome β and, for Example 2, the full street-and-path networks of Chicago, Den Haag and Rome, fetched by the quick answer through OSMnx and the maps.mail.ru Overpass mirror. The setup unpacks those answers as saved on 10 October 2026.
import io
import os
import requests
import zipfile
# OpenStreetMap as Overpass returned it on 10 October 2026, saved as osmnx's own cache so the counts
# below do not drift as OSM is edited; delete the cache folder to query live instead
if not os.path.exists("cache"):
reply = requests.get("https://data.spatialworkflow.io/osm-cache-street-orientation-entropy-python-2026-10-10.zip", timeout=300)
reply.raise_for_status()
with zipfile.ZipFile(io.BytesIO(reply.content)) as archive:
archive.extractall("cache")
Example 1 β polar plots in one figure
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 3, figsize=(10, 7), subplot_kw={"projection": "polar"})
for ax, (name, Gu) in zip(axes.ravel(), graphs.items()):
ox.plot.plot_orientation(Gu, ax=ax, title=name, num_bins=36, weight="length",
area=True, color="#0ea5e9")
fig.tight_layout()
fig.savefig("street_orientation.png", dpi=150)
print("saved street_orientation.png")
saved street_orientation.png
The polar histograms make the table readable. Chicago is four spikes on the compass points. Manhattan is four spikes turned about 29Β° β and still scores the highest Ο, because its angle lands inside the 10Β° bins rather than on their edges. The Eixample's spikes, turned 45Β°, each spread across two bins, which is why it trails both. Paris and Rome are ragged circles with a few favoured directions.
Example 2 β drive network against all streets
out = []
for name in ("Den Haag centre", "Rome centro", "Chicago Loop"):
for net in ("drive", "all"):
G = ox.graph_from_point(CITIES[name], dist=1500, network_type=net)
Gu = ox.bearing.add_edge_bearings(ox.convert.to_undirected(G))
H = ox.bearing.orientation_entropy(Gu, num_bins=36, weight="length")
out.append({"city": name, "network": net, "segments": len(Gu.edges),
"phi": 1 - ((H - H_GRID) / (H_MAX - H_GRID)) ** 2})
print(pd.DataFrame(out).pivot(index="city", columns="network", values="phi").round(3).to_string())
network all drive
city
Chicago Loop 0.822 0.873
Den Haag centre 0.447 0.517
Rome centro 0.069 0.090
Adding footpaths and service roads lowers Ο in all three centres, and not by the same amount: by 0.05 in Chicago, 0.07 in Den Haag and 0.02 in Rome, where there was little order left to lose. Comparing a published drive-network value with an all-streets value is comparing two different measurements.
Example 3 β save and reload
for name, Gu in graphs.items():
ox.save_graphml(Gu, f"{name.lower().replace(' ', '_')}.graphml")
reloaded = ox.load_graphml("rome_centro.graphml")
H = ox.bearing.orientation_entropy(reloaded, num_bins=36, weight="length")
print(f"Rome from disk: {len(reloaded.edges)} segments, entropy {H:.3f}")
Rome from disk: 1703 segments, entropy 3.482
GraphML keeps the bearings and lengths as edge attributes, so the reloaded graph gives the same entropy without another download.
Fixed radii and length weights
Why a fixed radius
Boeing's comparison of 100 cities used the whole city, which mixes a planned core with unplanned suburbs in proportions that depend on where each municipality happens to end. A fixed radius around a centre compares cores; a fixed radius around many sample points describes a whole city more fairly than either.
Why length weighting
Grids consist of long straight blocks; irregular centres of many short segments. Counting segments gives the irregular centre more weight per metre of street than the grid, exaggerating the difference. Length weighting makes each metre count once.
Why the plots matter as much as the numbers
Entropy compresses a distribution into one number, and very different distributions share values. The polar plot shows whether low entropy is one grid or two overlapping ones, and whether high entropy is a genuinely irregular fabric or a ring road with radial avenues.
Overpass limits, simplification and rivers
- Overpass limits: six 1.5 km networks are quick; hundreds of cities need caching and polite pacing.
dist_type:graph_from_pointuses a bounding box by default;dist_type="network"measures along streets instead.- Simplification: osmnx simplifies by default; set
simplify=Falseto keep every vertex-to-vertex segment. - Coastal and river cities have fewer streets on one side; that does not change entropy but does change the plot's balance.
- Multiple centres: for polycentric cities, average over several sample points.
Internal links
- Street network orientation explained β what entropy and Ο mean
- Street orientation looks wrong β the fix guide
- How to calculate street network metrics β connectivity alongside orientation
- How to build a street network graph β graph download options
- Urban morphology explained β orientation in context
- How to query the Overpass API from Python β what osmnx does underneath
FAQ
How do I calculate street orientation entropy with osmnx?
Download the graph, convert it to undirected, add edge bearings and call ox.bearing.orientation_entropy with weight="length".
How do I plot street orientation?
ox.plot.plot_orientation(G) draws a polar histogram of bearings.
Which network type should I use?
Whichever fits the question β drive for vehicle structure, all for everything β the same for every city compared.
What radius should I use?
A fixed radius, such as 1.5 km, around each centre makes cores comparable.
Why does my entropy differ from a published value?
Different extent, network type, weighting, bin count or a directed graph.
How do I avoid downloading networks repeatedly?
Save each graph with ox.save_graphml and reload it with ox.load_graphml.