Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, networkx 3.7, osmnx 2.1.1 and shapely 2.1.2
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A street network is a graph of junctions and weighted streets · Shortest and fastest routes differ, and Dijkstra's algorithm finds either · A service area follows the streets, and a buffer ignores them. With a pass: 3 in-depth parts and 3 interview answers, about 12 minutes.

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

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:

Interactive · step by step From streets to the edges of a graph Step through a two-way street and a one-way street becoming edges, and see what a router can then do with them.
From streets to the edges of a graph, step 5 of 5. Showing Junction A (900 m, 162 s to B), Junction B (900 m, 162 s to A), Junction C, Junction D. In focus: Junction D.A to BB to AC to D onlyJunction A900 m, 162 s to BJunction B900 m, 162 s to AJunction CJunction Dno edge back to C

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Question 1 of 2 How many edges does a directed street graph give a two-way street, and how many a one-way street?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Two for the two-way street, one for the one-way street

    A directed graph holds the rule itself: an edge for every direction traffic may go, and none where it may not.

  2. Question 2 of 2 A lane of 500 m has a speed of 20 km/h. How many seconds does its edge take?

    Type a number.

    A hint

    20 km/h is 20,000 m in 3,600 s.

    Show the answer: Question 2 of 2

    Answer: 90 s

    500 ÷ (20,000 ÷ 3,600) = 90 s, the time of the street from B to E in the table.

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:

Interactive · step through it Dijkstra's algorithm by length, in metres Settle the closest unsettled node, then relax its edges; the table keeps every distance.
Graph of 6 nodes and 7 edges. queue (by dist): A 0.90060050011001800700800A0B∞C∞D∞E∞F∞queue (by dist):A 0
  • being compared or looked at
  • being moved or written
  • in its final place
  • the key, pivot or target
  • visited or ruled out

Pseudo-code

  1. dist(start) ← 0; dist(v) ← ∞ for every other v; add start to the queue
  2. while the queue is not empty:
  3. u ← the queued node with the smallest dist; settle u
  4. for each edge u → v with weight w:
  5. if dist(u) + w < dist(v): dist(v) ← dist(u) + w; prev(v) ← u; queue v
Distances found so far
Nodedistprev
A0—
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.

Settled 0
Relaxations 0

Then by time, in seconds, on the same streets:

Interactive · step through it Dijkstra's algorithm by time, in seconds Settle the closest unsettled node, then relax its edges; the table keeps every distance.
Graph of 6 nodes and 7 edges. queue (by dist): A 0.16210890198108126144A0B∞C∞D∞E∞F∞queue (by dist):A 0
  • being compared or looked at
  • being moved or written
  • in its final place
  • the key, pivot or target
  • visited or ruled out

Pseudo-code

  1. dist(start) ← 0; dist(v) ← ∞ for every other v; add start to the queue
  2. while the queue is not empty:
  3. u ← the queued node with the smallest dist; settle u
  4. for each edge u → v with weight w:
  5. if dist(u) + w < dist(v): dist(v) ← dist(u) + w; prev(v) ← u; queue v
Distances found so far
Nodedistprev
A0—
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.

Settled 0
Relaxations 0

The program does both in plain Python; press Run, then lower the road’s speed to 40 km/h and run it again:

Shortest and fastest from A to F Python · graph/dijkstra.py
"""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

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

  1. Question 1 of 2 On the test graph, why is the fastest route from A to F longer than the shortest?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It uses the 60 km/h road from C to D, which saves time although it adds 1,100 m

    Distance and time are different weights, so their minima are different routes whenever a faster road is longer.

  2. Question 2 of 2 Which junction does Dijkstra's algorithm settle next?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The unsettled junction with the smallest distance found so far

    Take the nearest unsettled junction, then try every street out of it; that is the whole loop.

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:

town.py: the track's town, in metres of UTM zone 43N Python · town/town.py
"""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()]
streets.py: the town's streets for walking and for driving Python · town/streets.py
"""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
C2's 10-minute walk against an 800 m circle Python · town/walk_area.py
"""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

Around clinic C2, a diamond-shaped 10-minute walk area mostly inside the dashed 800 m circle, cut where a park has no streets.C2: a 10-minute walk and an 800 m circlestreets every 100 m; no streets in the parkC21,100 m on footparkreached on foot in 10 minutes: 1.42 km²within 800 m in a straight line: 2.01 km²the walk covers 70.0 % of the circle

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=yes drops the reverse edge, and maxspeed sets 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

  1. Question 1 of 2 Of the 800 m circle's 2.007 km² around C2, 1.405 km² lie inside the 10-minute walk area. What percentage of the circle does the walk cover?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 70 % (anything from 69.9 to 70.1 counts)

    1.405 ÷ 2.007 = 70.0 %. On a street grid, a walk must go round the blocks, so it reaches a diamond, not the circle; only the diamond's four tips, widened by half a block, reach a little past it.

  2. Question 2 of 2 A report counts the households within 800 m of C2 with a buffer and calls them its 10-minute walkers. What does the lesson's town say about that count?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It is 160 households, while only 87 can walk there in 10 minutes

    Buffers overstate walking coverage wherever streets are not straight lines from the clinic, which is everywhere.

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.

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.

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.

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.

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.

  1. Question 1 of 9 In a street graph, what are the nodes and what are the edges?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: Junctions are nodes, and the streets between them are edges

    Junctions as nodes and streets as edges with weights: that is the graph Dijkstra's algorithm searches.

  2. Question 2 of 9 A street graph has 12 two-way streets and 5 one-way streets. How many directed edges does it get?

    Type a number.

    Show the answer: Question 2 of 9

    Answer: 29 edges

    12 × 2 + 5 × 1 = 29: one edge for each direction in which traffic may use a street.

  3. Question 3 of 9 A planner wants walking routes for older people, who walk at about 50 m a minute. What must change in the graph?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Only the times, recomputed from each street's length at the slower speed

    Keep lengths as the graph's fact and derive times for the traveller; one graph serves many speeds.

  4. Question 4 of 9 Why must Dijkstra's algorithm have weights of 0 or more?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Because it settles a junction for good once it is the nearest left, which a negative street could undo

    Lengths and times are never negative, which is why Dijkstra's algorithm suits street networks.

  5. Question 5 of 9 On the test graph, how many seconds does the shortest route by length, A-B-F, take?

    Type a number.

    Show the answer: Question 5 of 9

    Answer: 360 s

    162 + 198 = 360 s, 18 s more than the fastest route, A-C-D-F, at 342 s.

  6. Question 6 of 9 The road from C to D is slowed to 40 km/h. Which route from A to F is now fastest?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: A-B-F, because the road now takes 162 s and A-C-D-F 396 s

    The fastest route depends on every speed, so a change to one road can move it; rerun the program to see it.

  7. Question 7 of 9 What is an isochrone?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: The area a walk or a drive reaches from a place within a set time, along the network

    Dijkstra's algorithm with a cutoff gives the junctions within the time; the reached streets form the area.

  8. Question 8 of 9 A team publishes, as open data, a table of street isochrones made from an OpenStreetMap extract. What does the licence ask?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: Credit to OpenStreetMap with a link to its copyright page, and the table shared under the ODbL

    Credit and share-alike: name OpenStreetMap and its copyright page, and share derived databases on ODbL terms.

  9. Question 9 of 9 On a perfect grid with no obstacles, why does a walk of 800 m reach only about 566 m towards each diagonal?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Going diagonally means walking along two sides of each block, so 800 m on foot reaches about 566 m in a straight line

    400 m east and 400 m north is 800 m on foot but √(400² + 400²) ≈ 566 m in a straight line, so the walk is a diamond inside the circle.

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:

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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".

  5. 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.

  6. 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.

  7. 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.