Street Network Orientation Explained
On this page
Problem statement
Orientation entropy reduces a street map to one number: the Shannon entropy of its street bearings, binned by compass direction. Its floor is log 4 ≈ 1.386, a perfect grid with every segment in one of four bins; with 36 bins of 10° its ceiling is log 36 ≈ 3.584, where every direction is equally common. Rescaled between the two it becomes the order parameter φ — 1 for a grid, 0 for no preferred direction — a summary of how planned a city's form is that can be set beside its age, its growth or how people travel in it.
The number is simple and its inputs are full of choices: which streets, how far out, whether segments are weighted by length, how many bins, whether a two-way street counts once or twice. Each choice changes the result, and two published entropies are comparable only if they made the same ones.
Quick answer
Compute undirected bearings for every street segment, bin them, and take the Shannon entropy of the distribution:
import numpy as np, 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"
PLACES = {"Chicago Loop": (41.8837, -87.6325), "Barcelona Eixample": (41.3928, 2.1633),
"Den Haag centre": (52.0799, 4.3113), "Rome centro": (41.8986, 12.4768)}
def orientation(lat, lon, dist=1500):
G = ox.graph_from_point((lat, lon), dist=dist, network_type="drive")
Gu = ox.bearing.add_edge_bearings(ox.convert.to_undirected(G))
H = ox.bearing.orientation_entropy(Gu, num_bins=36, weight="length")
H_max, H_grid = np.log(36), np.log(4)
order = 1 - ((H - H_grid) / (H_max - H_grid)) ** 2 # 1 = perfect grid, 0 = uniform
return Gu, H, order
for name, (lat, lon) in PLACES.items():
Gu, H, order = orientation(lat, lon)
print(f"{name:<20} {len(Gu.edges):>5} segments, entropy {H:.3f} (max {np.log(36):.3f}), "
f"order φ = {order:.2f}")
Chicago Loop 1469 segments, entropy 2.170 (max 3.584), order φ = 0.87
Barcelona Eixample 1854 segments, entropy 2.761 (max 3.584), order φ = 0.61
Den Haag centre 1892 segments, entropy 2.914 (max 3.584), order φ = 0.52
Rome centro 1703 segments, entropy 3.482 (max 3.584), order φ = 0.09
Entropy runs from log 4 ≈ 1.386 for a perfect four-way grid to log 36 ≈ 3.584 when every 10° bin is equally full. The order parameter φ rescales it so that a grid is 1 and a uniform spread is 0.
Network and extent, undirected, geographic bearings, length weights
1. Choose the network and the extent
Drivable streets, all streets, or paths change the answer; so does the radius. A city centre and its whole municipality are different questions. State both.
2. Make the graph undirected
A two-way street is two directed edges pointing in opposite directions. Leaving both in doubles the weight of two-way streets relative to one-way ones and mirrors every bearing. Convert to undirected first.
3. Compute bearings in geographic coordinates
A bearing is the compass direction from one end of a segment to the other. osmnx computes it on the unprojected graph; in a projected CRS, "north" is grid north, which can differ by several degrees from true north away from the projection's central meridian.
4. Weight by length
Counting segments gives a short stub the same weight as a long avenue. Length weighting measures how much of the street network runs in each direction, which is usually the intent.
5. Bin symmetrically and fold
Streets have no direction: a bearing of 10° is the same street as 190°. Bin across the full circle with each street counted in both directions, or fold to 0–180°. Centre the bins on the cardinal directions so a north–south grid is not split between two bins.
How each choice moves Rome's entropy
The data used on this page
OpenStreetMap's drive networks within 1.5 km of four centres — Chicago's Loop, Barcelona's Eixample, central Den Haag and central Rome — and around Den Haag at radii from 500 m to 3 km, 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, so the segment counts stay put.
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-network-orientation-explained-2026-10-10.zip", timeout=300)
reply.raise_for_status()
with zipfile.ZipFile(io.BytesIO(reply.content)) as archive:
archive.extractall("cache")
Example 1 — the effect of each choice
G = ox.graph_from_point(PLACES["Rome centro"], dist=1500, network_type="drive")
directed = ox.bearing.add_edge_bearings(G)
undirected = ox.bearing.add_edge_bearings(ox.convert.to_undirected(G))
for label, g, w in (("directed, count", directed, None),
("undirected, count", undirected, None),
("undirected, length", undirected, "length")):
print(f"{label:<20} entropy {ox.bearing.orientation_entropy(g, num_bins=36, weight=w):.3f}")
directed, count entropy 3.465
undirected, count entropy 3.475
undirected, length entropy 3.482
In Rome the three choices move entropy by less than 0.02 and φ by about 0.01: when bearings are already spread almost evenly, counting them differently changes little. Mirroring is also harmless once bearings are binned over the full circle, because every undirected street is counted both ways anyway. The choices still belong in the method — a comparison is only fair if every city was computed the same way.
Example 2 — scale dependence
for dist in (500, 1000, 2000, 3000):
_, H, order = orientation(*PLACES["Den Haag centre"], dist=dist)
print(f"radius {dist:>5} m: entropy {H:.3f}, φ {order:.2f}")
radius 500 m: entropy 2.987, φ 0.47
radius 1000 m: entropy 2.914, φ 0.52
radius 2000 m: entropy 2.981, φ 0.47
radius 3000 m: entropy 3.152, φ 0.35
The value wobbles between 500 m and 2 km and then climbs: at 3 km, neighbourhoods laid out on other grids enter the sample and the combined distribution spreads out. A city's orientation entropy is a property of the chosen extent, not of the city.
Example 3 — the dominant directions
Gu, H, order = orientation(*PLACES["Barcelona Eixample"])
b = np.array([d["bearing"] for _, _, d in Gu.edges(data=True)]) % 180
L = np.array([d["length"] for _, _, d in Gu.edges(data=True)])
hist, edges = np.histogram(b, bins=18, range=(0, 180), weights=L)
top = np.argsort(hist)[::-1][:3]
for i in top:
print(f"{edges[i]:>5.0f}–{edges[i + 1]:<4.0f}°: {hist[i] / L.sum():.0%} of street length")
130–140 °: 34% of street length
40–50 °: 32% of street length
50–60 °: 8% of street length
Two 10° bins, 90° apart, hold two-thirds of the street length: the Eixample grid, rotated about 45° from north. Yet φ puts it at 0.61, well below Chicago's 0.87. osmnx centres its bins on 0°, 10°, 20° and so on, so streets running near 45° sit close to a bin edge and each spike spills into its neighbour. Entropy measures spread across bins, not order, and a rotated grid pays for its rotation; the bearings themselves say which way it points.
Entropy as a summary, and the order parameter
Why entropy is the right summary
Shannon entropy measures how unpredictable a draw from a distribution is. For bearings, low entropy means knowing one street's direction tells you a lot about the next; high entropy means it tells you little. That is exactly the intuitive difference between a grid and a tangle, expressed without reference to any particular set of angles.
Why φ is easier to read
Real cities have entropies between about 2.8 and 3.58, a narrow band near the maximum. Rescaling between a four-way grid and the uniform distribution, and squaring, spreads that band out so that differences between cities are visible.
Why the network definition matters
Drivable networks omit footpaths and alleys, which in old centres are often the most irregular part; including them raises entropy. Motorway junctions and ramps add curved segments at arbitrary angles. The same city measured on two networks can move noticeably on the φ scale.
Curved streets, bins and grid north
- Curved streets are represented by their end-to-end bearing unless the geometry is split into straight pieces.
- Simplified graphs (osmnx's default) merge chains of segments into one edge; unsimplified graphs give more, shorter segments.
- Bin count: 36 bins of 10° is the common choice; fewer bins lower the maximum entropy and change φ.
- Polar plots are the standard visualisation;
ox.plot.plot_orientationdraws them. - Grid north vs true north only matters for a projected graph; osmnx uses latitude and longitude.
Internal links
- How to measure street orientation entropy — the full workflow and plots
- Street orientation looks wrong — the fix guide for doubled and projected bearings
- Urban morphology explained — orientation among other form measures
- Street networks as graphs explained — nodes, edges and simplification
- How to build a street network graph — getting the graph
- How to calculate street network metrics — connectivity measures alongside orientation
FAQ
What is street orientation entropy?
The Shannon entropy of the distribution of street bearings across compass bins. Low for grids, high for irregular networks.
What is the orientation order φ?
A rescaling of entropy: 1 for a perfect four-way grid, 0 for bearings spread uniformly.
Should I weight streets by length?
Usually yes: it measures how much network runs in each direction rather than how many segments do.
Does the radius matter?
Yes. Larger areas mix neighbourhoods with different grids and raise entropy.
Can two cities with the same entropy look different?
Yes. Entropy measures spread, not direction; a rotated grid scores the same as a north–south one.