GIS & Geospatial Data (QGIS, GeoPandas, PostGIS) Module 5 – Python: Shapely and GeoPandas
Street networks, routing and isochrones
Model streets as a graph with lengths, speeds and one-way rules, find the shortest and the fastest route, and build walking isochrones against buffers.
What you will learn
- Model a street network as a graph with lengths, speeds and one-way rules
- Compute shortest and fastest paths with Dijkstra's algorithm, and explain why they differ
- Build walking service areas, and compare them with straight-line buffers
- Use OpenStreetMap road data with its tags and within its licence
Before you start
What you’ll learn · Before you start
What you will learn
- Model a street network as a graph with lengths, speeds and one-way rules
- Compute shortest and fastest paths with Dijkstra's algorithm, and explain why they differ
- Build walking service areas, and compare them with straight-line buffers
- Use OpenStreetMap road data with its tags and within its licence
Before you start
On this page
A street corner 781 m from clinic C2 looks inside a 10-minute walk, yet along the streets it is 1,100 m away: 13.8 minutes. Distance on a map is not distance on foot, and the gap decides whom a clinic serves.
Here you turn street networks into graphs with lengths and times, find the shortest and the fastest route with Dijkstra’s algorithm, and draw isochrones, the areas a walk reaches in a set time. Then you route the whole town and measure how far buffers overstate what clinics reach.
A street network is a graph of junctions and weighted streets
Routing needs a graph: the junctions are its nodes and the streets between them its edges. Each edge carries weights,
its length and the time it takes, which is the length divided by the speed. A two-way street becomes an edge in each
direction and a one-way street a single edge, so a router models street networks as directed graphs, such as NetworkX’s
DiGraph (NetworkX) and the MultiDiGraph of OSMnx
(OSMnx). Step through two streets becoming edges:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Two junctions. A street runs 900 m between junctions A and B. The junctions become the nodes of the graph; the street will become its edges.
- A two-way street. Traffic may use the street in both directions, so it becomes two edges, A to B and B to A. A graph for walking treats every street like this.
- Weights on the edges. Each edge carries weights: its length, 900 m, and its time at 20 km/h, 162 s. A router adds up whichever weight it is told to minimise.
- A one-way street. A one-way street from C to D becomes a single edge, C to D. There is no edge from D back to C.
- The way back. A route from D to C cannot use that street, so it must find another way round. On one-way streets the way there and the way back can differ in length and time.
The first graph has six junctions and seven streets; one of them, from C to D, is a 60 km/h road:
| Street | Length | Speed | Time |
|---|---|---|---|
| A to B | 900 m | 20 km/h | 162 s |
| A to C | 600 m | 20 km/h | 108 s |
| B to E | 500 m | 20 km/h | 90 s |
| B to F | 1,100 m | 20 km/h | 198 s |
| C to D | 1,800 m | 60 km/h | 108 s |
| D to F | 700 m | 20 km/h | 126 s |
| E to F | 800 m | 20 km/h | 144 s |
The road from C to D is three times as long as the lane from A to C, and takes the same 108 s.
Checkpoint
Check: A street network as a graph
Shortest and fastest routes differ, and Dijkstra’s algorithm finds either
Dijkstra's algorithm: A way to find the shortest routes from a start in a graph with weights of 0 or more: settle the nearest junction not yet settled, then check whether going through it shortens the way to each neighbour. settles the junctions in order of their distance from the start: it takes the nearest junction not yet settled, then checks whether going through it shortens the way to each neighbour (Dijkstra). With no negative weights, a settled junction never gets a shorter route later. Run it by length from A, and watch the table of distances fill:
- being compared or looked at
- being moved or written
- in its final place
- the key, pivot or target
- visited or ruled out
Pseudo-code
-
dist(start) ← 0; dist(v) ← ∞ for every other v; add start to the queue -
while the queue is not empty: -
u ← the queued node with the smallest dist; settle u -
for each edge u → v with weight w: -
if dist(u) + w < dist(v): dist(v) ← dist(u) + w; prev(v) ← u; queue v
| Node | dist | prev |
|---|---|---|
| A | 0 | — |
| B | ∞ | — |
| C | ∞ | — |
| D | ∞ | — |
| E | ∞ | — |
| F | ∞ | — |
Shortest distances from A. Every node starts at ∞ except A at 0. Each round settles the queued node with the smallest distance: with no negative weights, no later path can beat it.
When it finishes: A: 0, B: 900, C: 600, D: 2400, E: 1400, F: 2000.
Try your own input
Then by time, in seconds, on the same streets:
- being compared or looked at
- being moved or written
- in its final place
- the key, pivot or target
- visited or ruled out
Pseudo-code
-
dist(start) ← 0; dist(v) ← ∞ for every other v; add start to the queue -
while the queue is not empty: -
u ← the queued node with the smallest dist; settle u -
for each edge u → v with weight w: -
if dist(u) + w < dist(v): dist(v) ← dist(u) + w; prev(v) ← u; queue v
| Node | dist | prev |
|---|---|---|
| A | 0 | — |
| B | ∞ | — |
| C | ∞ | — |
| D | ∞ | — |
| E | ∞ | — |
| F | ∞ | — |
Shortest distances from A. Every node starts at ∞ except A at 0. Each round settles the queued node with the smallest distance: with no negative weights, no later path can beat it.
When it finishes: A: 0, B: 162, C: 108, D: 216, E: 252, F: 342.
Try your own input
The program does both in plain Python; press Run, then lower the road’s speed to 40 km/h and run it again:
"""Dijkstra's algorithm on six junctions and seven streets, run twice: once by length in metres, once by driving time
in seconds. The street from C to D is a 60 km/h road; every other street is a 20 km/h lane."""
import heapq
STREETS = [ # (junction, junction, length in m, speed in km/h)
("A", "B", 900, 20), ("A", "C", 600, 20), ("B", "E", 500, 20), ("B", "F", 1_100, 20),
("C", "D", 1_800, 60), ("D", "F", 700, 20), ("E", "F", 800, 20),
]
def graph(cost):
"""Each junction's neighbours with the cost of the street to them, both ways."""
g = {}
for a, b, metres, kmh in STREETS:
w = metres if cost == "length" else round(metres / (kmh / 3.6)) # seconds at that speed
g.setdefault(a, []).append((b, w))
g.setdefault(b, []).append((a, w))
return g
def dijkstra(g, start):
"""Settle the nearest junction not yet settled, then try every street out of it."""
best, came_from, settled, queue = {start: 0}, {}, [], [(0, start)]
while queue:
d, node = heapq.heappop(queue)
if node in settled:
continue # an older, longer entry for a junction already settled
settled.append(node)
for nxt, w in g[node]:
if d + w < best.get(nxt, float("inf")):
best[nxt], came_from[nxt] = d + w, node
heapq.heappush(queue, (d + w, nxt))
return best, came_from, settled
def path(came_from, end):
out = [end]
while out[-1] in came_from:
out.append(came_from[out[-1]])
return "-".join(reversed(out))
metres = {(a, b): m for a, b, m, _ in STREETS} | {(b, a): m for a, b, m, _ in STREETS}
seconds = {(a, b): round(m / (v / 3.6)) for a, b, m, v in STREETS} | {(b, a): round(m / (v / 3.6)) for a, b, m, v in STREETS}
for cost, unit in (("length", "m"), ("time", "s")):
best, came_from, settled = dijkstra(graph(cost), "A")
print(f"by {cost}: settled in the order " + ", ".join(f"{n} {best[n]:,} {unit}" for n in settled))
route = path(came_from, "F").split("-")
legs = list(zip(route, route[1:]))
print(f" A to F: {'-'.join(route)}, {sum(metres[s] for s in legs):,} m in {sum(seconds[s] for s in legs)} s") Output
by length: settled in the order A 0 m, C 600 m, B 900 m, E 1,400 m, F 2,000 m, D 2,400 m A to F: A-B-F, 2,000 m in 360 s by time: settled in the order A 0 s, C 108 s, B 162 s, D 216 s, E 252 s, F 342 s A to F: A-C-D-F, 3,100 m in 342 s
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dijkstra.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
By length the route to F is A-B-F, 2,000 m in 360 s. By time it is A-C-D-F, 3,100 m in 342 s: 1,100 m longer and 18 s faster, because the road makes up in speed what it adds in metres.
Asking for the fastest route without saying so
OSMnx’s shortest_path minimises weight="length" unless told otherwise, so it returns the shortest route. For the
fastest, add speeds and travel times to the graph, then pass weight="travel_time".
Checkpoint
Check: Shortest and fastest routes with Dijkstra's algorithm
A service area follows the streets, and a buffer ignores them
An isochrone (service area): The area a walk or a drive reaches from a place within a set time, measured along the street network rather than in a straight line., or service area, is everything a walk or a drive reaches from a place in a set
time. Dijkstra’s algorithm with a cutoff gives it: NetworkX’s single_source_dijkstra_path_length stops at a “length
(sum of edge weights) at which the search is stopped”
(NetworkX).
The program searches from clinic C2 over the town’s walking network, a street every 100 m and none in a park, and keeps
the junctions within 800 m, 10 minutes at 80 m a minute:
"""The track's synthetic town in metres of UTM zone 43N (EPSG:32643): 12 square wards of 2 km, 5,000 households
from a seeded generator and 8 clinics. Every lesson of the track asks its questions about this town."""
import random
E0, N0 = 500_000, 2_000_000 # the town's south-west corner: easting and northing in metres
WARD = 2_000 # each ward is a 2 km square
COLS, ROWS = 4, 3 # 4 wards from west to east, 3 from south to north
# Metres east and north of the south-west corner. C2 stands exactly on the edge of W02 and W03, on purpose.
CLINICS = {
"C1": (1_500, 5_200), "C2": (4_000, 5_000), "C3": (6_500, 4_200), "C4": (2_700, 3_100),
"C5": (5_200, 2_800), "C6": (900, 1_100), "C7": (5_600, 1_200), "C8": (7_400, 900),
}
def ward_bounds():
"""W01 to W12, north-west first, each as (min x, min y, max x, max y)."""
out = {}
for row in range(ROWS):
for col in range(COLS):
x0, y0 = E0 + col * WARD, N0 + (ROWS - 1 - row) * WARD
out[f"W{row * COLS + col + 1:02d}"] = (x0, y0, x0 + WARD, y0 + WARD)
return out
def households(seed=42, n=5_000):
"""(id, x, y) of n households, most of them around three neighbourhood centres."""
rng = random.Random(seed)
centres = [(2_600, 4_300, 700), (5_600, 1_500, 900), (6_900, 4_700, 500)]
homes = []
while len(homes) < n:
if rng.random() < 0.7:
cx, cy, spread = rng.choice(centres)
# The sum of three random numbers clusters around its middle, like a bell curve.
x = cx + spread * 2 * (rng.random() + rng.random() + rng.random() - 1.5)
y = cy + spread * 2 * (rng.random() + rng.random() + rng.random() - 1.5)
else:
x, y = rng.uniform(0, COLS * WARD), rng.uniform(0, ROWS * WARD)
if 0 < x < COLS * WARD and 0 < y < ROWS * WARD:
homes.append((f"H{len(homes) + 1:04d}", round(E0 + x, 1), round(N0 + y, 1)))
return homes
def clinics():
"""(id, x, y) of the 8 clinics."""
return [(k, E0 + x, N0 + y) for k, (x, y) in CLINICS.items()] """The track's town as street networks, in metres of UTM zone 43N. For walking: a junction every 100 m, a river at
1,950 m north of the town's south edge that only three bridges cross, and a park with no streets inside; every street
can be walked both ways. For driving: the same streets as one-way or two-way edges with a speed each."""
import networkx as nx
import shapely
from town import E0, N0
W, H, STEP = 8_000, 6_000, 100
RIVER = N0 + 1_950 # between the rows of junctions at 1,900 and 2,000 m north
BRIDGES = (E0 + 1_000, E0 + 3_500, E0 + 6_000)
PARK = (E0 + 4_400, N0 + 4_200, E0 + 5_400, N0 + 4_800) # west, south, east, north
def streets():
"""Every street between two neighbouring junctions, as ((x1, y1), (x2, y2))."""
out = []
for x in range(E0, E0 + W + 1, STEP):
for y in range(N0, N0 + H + 1, STEP):
for end in ((x + STEP, y), (x, y + STEP)):
if end[0] > E0 + W or end[1] > N0 + H:
continue
if y < RIVER < end[1] and x not in BRIDGES:
continue # no bridge here
mx, my = (x + end[0]) / 2, (y + end[1]) / 2
if PARK[0] < mx < PARK[2] and PARK[1] < my < PARK[3]:
continue # inside the park
out.append(((x, y), end))
return out
def walk_network():
g = nx.Graph()
g.add_edges_from(streets(), length=STEP)
return g
def nearest_junction(g):
"""A function that gives the junction nearest to a point: the corner of its block, or the park's edge for a
point inside the park, found with a spatial index of the junctions."""
nodes = list(g)
tree = shapely.STRtree(shapely.points(nodes))
def nearest(x, y):
node = (round(x / STEP) * STEP, round(y / STEP) * STEP)
return node if node in g else nodes[tree.nearest(shapely.Point(x, y))]
return nearest
def speed(a, b):
"""km/h: the ring road around the town's edge 60, Station Road (x = 503,500) 40, every other street 20."""
if a[0] == b[0] in (E0, E0 + W) or a[1] == b[1] in (N0, N0 + H):
return 60
if a[0] == b[0] == E0 + 3_500:
return 40
return 20
def drive_network():
"""Directed: the streets of the market blocks (x 503,000 to 504,000, y 2,002,500 to 2,003,500) are one-way,
eastwards and northwards; every other street runs both ways."""
g = nx.DiGraph()
for a, b in streets():
minutes = STEP / (speed(a, b) * 1000 / 60)
g.add_edge(a, b, length=STEP, minutes=minutes)
market = E0 + 3_000 <= min(a[0], b[0]) and max(a[0], b[0]) <= E0 + 4_000 and N0 + 2_500 <= min(a[1], b[1]) and max(a[1], b[1]) <= N0 + 3_500
if not market:
g.add_edge(b, a, length=STEP, minutes=minutes)
return g """Where can you walk in 10 minutes from clinic C2? Dijkstra's algorithm over the town's streets finds every junction
within 800 m on foot, at 80 m a minute; the streets between them, widened by half a block on each side, make the
walk's service area. Beside it, the 800 m circle that a buffer around C2 would draw."""
import math
import networkx as nx
import shapely
from streets import STEP, nearest_junction, walk_network
from town import CLINICS, E0, N0, households
SPEED = 80 # metres a minute on foot, 4.8 km/h
REACH = 10 * SPEED
g = walk_network()
junction = nearest_junction(g)
c2 = (E0 + CLINICS["C2"][0], N0 + CLINICS["C2"][1]) # C2 stands on a junction
on_foot = nx.single_source_dijkstra_path_length(g, c2, weight="length") # metres from C2 to every junction
reached = {n for n, d in on_foot.items() if d <= REACH}
streets = [shapely.LineString([a, b]) for a, b in g.edges() if a in reached and b in reached]
walk = shapely.union_all(shapely.buffer(streets, STEP / 2))
circle = shapely.Point(c2).buffer(REACH, quad_segs=16)
print(f"junctions within a 10-minute walk of C2: {len(reached)}")
shared = walk.intersection(circle).area # widened by half a block, the walk's four tips reach a little past the circle
print(f"the walk's service area: {walk.area / 1e6:.3f} km², {shared / 1e6:.3f} km² of it inside the 800 m circle")
print(f"the circle: {circle.area / 1e6:.3f} km², of which the walk covers {shared / circle.area:.1%}")
inside = [n for n in g if math.dist(n, c2) <= REACH]
far = max(inside, key=lambda n: (on_foot[n], n))
dx, dy = far[0] - c2[0], far[1] - c2[1]
print(f"the circle holds {len(inside)} junctions; the farthest on foot, {abs(dx)} m {'east' if dx > 0 else 'west'} and {abs(dy)} m "
f"{'north' if dy > 0 else 'south'} of C2, is {math.dist(far, c2):.0f} m away in a straight line and {on_foot[far]:,} m on foot: {on_foot[far] / SPEED:.1f} minutes")
def walk_metres(x, y):
"""Straight to the nearest junction, then along the streets."""
node = junction(x, y)
return math.dist((x, y), node) + on_foot[node]
homes = households()
by_circle = sum(1 for _, x, y in homes if math.dist((x, y), c2) <= REACH)
by_walk = sum(1 for _, x, y in homes if walk_metres(x, y) <= REACH)
print(f"households: {by_circle} within 800 m in a straight line, {by_walk} within a 10-minute walk") Output
junctions within a 10-minute walk of C2: 144 the walk's service area: 1.421 km², 1.405 km² of it inside the 800 m circle the circle: 2.007 km², of which the walk covers 70.0% the circle holds 189 junctions; the farthest on foot, 600 m east and 500 m north of C2, is 781 m away in a straight line and 1,100 m on foot: 13.8 minutes households: 160 within 800 m in a straight line, 87 within a 10-minute walk
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 walk_area.py
A 10-minute walk from C2 against an 800 m circle
Text description of the diagram
The streets form a square grid, one street every 100 m, except in a park south-east of C2, which has none. Clinic C2 stands on a junction in the middle.
The area reached on foot in 10 minutes, 800 m along the streets, is shaded blue. On a grid it is a diamond with stepped edges: going north, south, east or west, the walk reaches 800 m, but towards a corner it must go along two sides of each block, so it reaches only about 566 m from C2 in each diagonal direction. Its south-east corner is cut away where the park has no streets. It covers 1.42 km².
The dashed circle is everything within 800 m of C2 in a straight line, 2.01 km². The walk covers 70.0 % of it; only the four tips of the diamond, widened by half a block, reach a little past the circle. A junction marked inside the circle, 600 m east and 500 m north of C2, is 781 m away in a straight line but 1,100 m on foot.
The walk reaches 144 junctions and covers 70.0 % of the 800 m circle, because a walk along a grid must go round the blocks. 160 households live within 800 m of C2 in a straight line, but only 87 within a 10-minute walk.
Key idea
Isochrones follow the streets and buffers do not, so a buffer counts people whom a walk of the same time never reaches.
Road data for real isochrones usually comes from OpenStreetMap, under the Open Database License: credit “OpenStreetMap” and its licence, usually as a link to its copyright page, and distribute what you build on the data only under the same licence (OpenStreetMap, copyright; attribution guidelines). Three things then decide how far routing on such data can be trusted:
- OSM tags set the edges:
oneway=yesdrops the reverse edge, andmaxspeedsets the speed. - Measured coverage compares every clinic’s walk with its buffer, ward by ward.
- Turn restrictions, slopes and traffic are what a simple graph leaves out.
Checkpoint
Check: Service areas against buffers
Routing the town: one-way streets, speeds and OSM tags, in code Pro: opens with a Pro, Premium or Ultimate pass
Every street of the town becomes a weighted, directed edge here, and the same is done from OpenStreetMap’s own tags. The runs show where the fastest route leaves the shortest, and what one-way streets do to the way back.
In this part:
- Shortest and fastest drives across the town
- OpenStreetMap’s tags become edges, speeds and travel times
3 runnable examples (OSM, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Every clinic's walk against its buffer, measured Pro: opens with a Pro, Premium or Ultimate pass
A coverage report built on buffers answers a question nobody walks. This part walks from all eight clinics at once and compares every household, and every ward, with the straight-line answer.
In this part:
- Households within a walk, from one run of Dijkstra’s algorithm
- What the numbers depend on
- Each ward’s share within 1 km, by road and in a straight line
1 diagram · 1 runnable example (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
What real routing adds: turns, slopes, traffic and checks Pro: opens with a Pro, Premium or Ultimate pass
A graph of lengths and speeds answers the first questions; real streets add rules it does not hold. This part names them with what each needs from the data, and the checks and tools that find a network’s faults before a route does.
In this part:
- Turn restrictions, slopes and time of day
- Checking a network before routing on it
about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: the walk to the nearest clinic
Write the multi-source walk yourself, then the share of households within a limit:
Exercise · Medium · Python
Minutes on foot to the nearest clinic, and the share within a limit
Write two functions in walk.py.
walk_minutes(streets, clinics, speed=80) takes streets, a list of (a, b, metres) that can each be walked both ways, and clinics, a list of junctions. It returns {junction: minutes} for every junction that can be reached: the minutes to the nearest clinic at speed metres a minute, rounded to 1 decimal. A junction that no clinic can reach is left out. Start Dijkstra's algorithm from every clinic at once, each at 0 minutes.
share_within(minutes, homes, limit) takes that result, homes, a list with the nearest junction of each household, and a limit in minutes. It returns the share of households within the limit, rounded to 3 decimals; a household at a junction that cannot be reached counts as outside.
The sample tests use a street of four junctions, two clinics, a part of town that no bridge reaches, and a slower walk.
Starter code · walk.py
import heapq
def walk_minutes(streets, clinics, speed=80):
"""{junction: minutes on foot to the nearest clinic}, rounded to 1 decimal, for every junction a clinic reaches."""
# Replace this line: run Dijkstra's algorithm from all the clinics at once, then turn metres into minutes.
return {}
def share_within(minutes, homes, limit):
"""The share of homes within `limit` minutes of a clinic, rounded to 3 decimals."""
# Replace this line: count the homes whose junction has minutes at or below the limit.
return 0.0 The sample tests · test_walk.py
from walk import share_within, walk_minutes
ROW = [("A", "B", 80), ("B", "C", 80), ("C", "D", 80)] # four junctions, 80 m apart
def test_one_clinic_at_the_end():
"""counts the minutes along the row from one clinic at 80 m a minute"""
assert walk_minutes(ROW, ["A"]) == {"A": 0.0, "B": 1.0, "C": 2.0, "D": 3.0}
def test_the_nearest_of_two_clinics():
"""gives each junction the minutes to the nearer of two clinics"""
assert walk_minutes(ROW, ["A", "D"]) == {"A": 0.0, "B": 1.0, "C": 1.0, "D": 0.0}
def test_a_part_of_town_without_a_bridge():
"""leaves out junctions no clinic can reach, and counts their households as outside"""
streets = ROW + [("E", "F", 120)]
minutes = walk_minutes(streets, ["A"])
assert "E" not in minutes and "F" not in minutes
assert share_within(minutes, ["B", "C", "D", "F"], 2.0) == 0.5
def test_the_share_counts_the_limit_itself():
"""counts a household exactly at the limit as within it"""
assert share_within(walk_minutes(ROW, ["A"]), ["A", "B", "C", "D"], 2.0) == 0.75
def test_a_slower_walk():
"""uses the speed it is given: at 40 m a minute every walk takes twice as long"""
assert walk_minutes(ROW, ["A"], speed=40) == {"A": 0.0, "B": 2.0, "C": 4.0, "D": 6.0}
assert share_within(walk_minutes(ROW, ["A"], speed=40), ["A", "B", "C", "D"], 3.0) == 0.5 A hint
Put every clinic in the queue with distance 0, as heapq pairs (0, clinic), and run the usual loop: pop the nearest junction, skip it if it is already settled, and push each neighbour whose distance gets shorter. Divide the metres by speed only at the end, so the rounding happens once.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Practice that adapts to you
Practise this lesson
9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is an isochrone, and why is it not a circle?
An isochrone is the area a walk or a drive reaches from a place within a set time. It follows the streets, so it bends round blocks and stops at a river without a bridge, while a circle takes every point within a distance in a straight line. From C2, a 10-minute walk covers 70.0 % of the 800 m circle and reaches 87 households, not 160.
The module’s question bank holds three routing questions for interviews, with a first step for each:
-
Fresher to mid
A district report says 2,973 of the town's 5,000 households live within 1 km of a clinic, from 1 km buffers round the 8 clinics. Residents south of the river say they walk much further. How do you check the claim, and what figure would you report instead?
-
Mid to senior
A delivery app's routes look short on the map, but drivers say they are slow. A colleague finds that the router was called with its default settings. What do you check, and how do you make the routes fast?
-
Senior to staff
A state plans ambulance bases so that 90 % of villages are within 30 minutes by road. The first analysis routes on OpenStreetMap roads at posted speeds, at all hours. What would you add before trusting the result, and how would you present its uncertainty?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How is a street network modelled for routing?
As a directed graph: junctions are nodes and streets are edges weighted by their length and their time, the length divided by the speed. A two-way street gives an edge in each direction, a one-way street a single edge.
-
How long do a 1,800 m road at 60 km/h and a 600 m lane at 20 km/h each take?
108 s each: time is length over speed, so a road three times as long takes no longer when it is three times as fast.
-
How does Dijkstra's algorithm find a shortest route?
It settles the junctions one by one, the closest to the start first, and after each one checks whether going through it shortens the way to its neighbours. With no negative weights, a settled junction never gets a shorter route later.
-
On the lesson's six-junction graph, how do the shortest and the fastest route from A to F differ?
By length it is A-B-F, 2,000 m in 360 s; by time it is A-C-D-F, 3,100 m in 342 s: 1,100 m longer and 18 s faster, because the 60 km/h road makes up in speed what it adds in metres. OSMnx's shortest_path minimises length unless given weight="travel_time".
-
What is an isochrone, and how is one computed?
The area a walk or a drive reaches from a place within a set time. Dijkstra's algorithm with a cutoff gives it, as NetworkX's single_source_dijkstra_path_length does: keep every junction reached within the limit.
-
How far did an 800 m circle overstate clinic C2's 10-minute walk?
The walk reached 144 junctions and covered 70.0 % of the circle, because a walk on a street grid goes round the blocks; it reached 87 households against the 160 within 800 m in a straight line.
-
What does using OpenStreetMap road data require?
A credit to OpenStreetMap and its licence, the Open Database License, usually as a link to its copyright page, and sharing what you build on the data only under the same licence.
Key takeaways
- Street networks are directed graphs of junctions and weighted streets, with one edge for a one-way street.
- Dijkstra’s algorithm finds the shortest route by length and the fastest by time: 2,000 m in 360 s against 3,100 m in 342 s on the test graph.
- Isochrones follow the streets: C2’s 10-minute walk covers 70.0 % of the 800 m circle.
Terms in this lesson
- Dijkstra's algorithm
- A way to find the shortest routes from a start in a graph with weights of 0 or more: settle the nearest junction not yet settled, then check whether going through it shortens the way to each neighbour.
- isochrone (service area)
- The area a walk or a drive reaches from a place within a set time, measured along the street network rather than in a straight line.
References
- NetworkX: single_source_dijkstra_path_length (NetworkX developers)
- NetworkX: DiGraph, directed graphs (NetworkX developers)
- OSMnx user reference (OSMnx contributors)
- A note on two problems in connexion with graphs (E. W. Dijkstra, Numerische Mathematik)
- Copyright and licence (OpenStreetMap Foundation)
- Attribution guidelines (OpenStreetMap Foundation)
- Relation: restriction (OpenStreetMap wiki)
- QGIS processing algorithms: network analysis (QGIS Project)
- pgRouting: pgr_drivingDistance (pgRouting contributors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress