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.

From a centre point through graph download, undirecting, bearings and entropy to a polar plot. the same five steps for every citygraph_from_point1.5 km, driveto_undirectedeach street onceadd_edge_bearingslat/lon, notprojectedorientation_entropy36 bins, by lengthplot_orientationcheck the pictureSteps two and four are where published values most often differ spatialworkflow.io
Five steps, the same for every city β€” that is what makes the numbers comparable.

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.

Segments, orientation entropy and order φ for six city centres, drivable network within 1.5 km. six centres, one methodcentresegmentsentropyφManhattan Midtown1,2131.7000.98Chicago Loop1,4692.1700.87Barcelona Eixample1,8542.7610.61Den Haag centre1,8922.9140.52Paris centre1,5643.4420.13Rome centro1,7033.4820.09Entropy runs from 1.386 (a perfect grid) to 3.584 (uniform) with 36 bins spatialworkflow.io
The same 1.5 km radius and drivable network in each centre.

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.

Orientation order Ο† on the drivable network and on all streets and paths for three city centres. Ο†: drive network against all streetsChicago Loop, drive0.87Chicago Loop, all0.82Den Haag centre, drive0.52Den Haag centre, all0.45Rome centro, drive0.09Rome centro, all0.0700.51Footpaths lower Ο† everywhere, by different amounts spatialworkflow.io
Network type is part of the measurement: drive and all-streets values are not comparable.

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_point uses a bounding box by default; dist_type="network" measures along streets instead.
  • Simplification: osmnx simplifies by default; set simplify=False to 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.

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.