Isochrone Analysis from Points of Interest

This notebook demonstrates how to generate accessibility isochrones from single or multiple points using different methods:

  • Simple radius and ways isochrones

  • Stepped isochrones with customizable intervals

# Install required packages (uncomment if needed)
# !pip install objectnat iduedu
# Import necessary libraries
from iduedu import get_intermodal_graph, get_4326_boundary
import geopandas as gpd
from shapely import Point
from objectnat import get_accessibility_isochrones, get_accessibility_isochrone_stepped

1. Load Intermodal Graph

Load a multimodal transportation graph (roads, public transport, etc.) for a specific region using its OSM ID.

# Load boundary and graph for a specific region using OSM ID 1114252.
poly = get_4326_boundary(osm_id=1114252)
G_intermodal = get_intermodal_graph(territory=poly, clip_by_territory=True)

2. Create Points of Interest

Define one or more source points from which isochrones will be generated.

# Define a single point of interest
point = gpd.GeoDataFrame(geometry=[Point(30.27060176, 59.93546846)], crs=4326)

3. Generate Radius Isochrones

Create circular isochrones using a travel time threshold (e.g. 10 minutes).

isochrones_radius, stops_r, routes_r = get_accessibility_isochrones(
    isochrone_type='radius',
    points=point,
    weight_type="time_min",
    weight_value=10,
    nx_graph=G_intermodal
)

# Visualize
m = isochrones_radius.explore(tiles='CartoDB Positron')
stops_r.explore(m=m)
routes_r.explore(m=m, column='type')

4. Generate Ways Isochrones

Create road network-based polygons representing reachable areas within a time or distance threshold.

isochrones_ways, stops_w, routes_w = get_accessibility_isochrones(
    isochrone_type='ways',
    points=point,
    weight_type="time_min",
    weight_value=10,
    nx_graph=G_intermodal
)

# Visualize
m = isochrones_ways.explore(tiles='CartoDB Positron')
stops_w.explore(m=m)
routes_w.explore(m=m, column='type')

5. Compare Isochrone Types

Overlay both types of isochrones to compare coverage.

m = isochrones_radius.explore(tiles='CartoDB Positron', color='blue', name='Radius')
isochrones_ways.explore(m=m, color='red', name='Ways')

6. Generate Stepped Isochrones (Radius)

Create concentric buffer zones with stepped intervals (e.g. every 3 minutes).

stepped_radius, stops_s1, routes_s1 = get_accessibility_isochrone_stepped(
    isochrone_type='radius',
    point=point,
    weight_type="time_min",
    weight_value=15,
    nx_graph=G_intermodal,
    step=3
)

stepped_radius.explore(tiles='CartoDB Positron', column='dist')

7. Generate Stepped Isochrones (Ways)

Create layered polygons in the road network with custom intervals (e.g. every 3 minutes).

stepped_ways, stops_s2, routes_s2 = get_accessibility_isochrone_stepped(
    isochrone_type='ways',
    point=point,
    weight_type="time_min",
    weight_value=15,
    nx_graph=G_intermodal,
    step=3
)
stepped_ways.explore(tiles='CartoDB Positron', column='dist')

8. Generate Stepped Isochrones (Separate)

Create distinct buffer rings for each interval.

stepped_separate, stops_s3, routes_s3 = get_accessibility_isochrone_stepped(
    isochrone_type='separate',
    point=point,
    weight_type="time_min",
    weight_value=10,
    nx_graph=G_intermodal,
    step=2
)

stepped_separate.explore(tiles='CartoDB Positron', column='dist')

Key Parameter Summary:

  • isochrone_type: 'radius', 'ways', or 'separate'

  • weight_type: 'time_min' (minutes) or 'length_meter' (meters)

  • weight_value: total cutoff (e.g. 10 minutes)

  • step: interval size for stepped isochrones (optional)

  • Additional: buffer_factor, road_buffer_size

Animation for stepped isochrones

from objectnat.methods.utils.graph_utils import graph_to_gdf
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from shapely import Point
import geopandas as gpd
from objectnat import get_accessibility_isochrone_stepped

edges = graph_to_gdf(G_intermodal, nodes=False)
point = gpd.GeoDataFrame(geometry=[Point(30.27060176, 59.93546846)], crs=4326).to_crs(edges.crs)
bbox = gpd.GeoDataFrame(geometry=[poly], crs=4326).to_crs(edges.crs)

type_colors = {
    'walk': '#a3a3a3',
    'bus': '#1f77b4',
    'trolleybus': '#2ca02c',
    'tram': '#ff7f0e',
    'subway': '#9467bd',
    'boarding': '#8c564b'
}

edges['color'] = edges['type'].map(type_colors)

steps = [0.1, 0.5, 1, 2, 3, 4, 5]

fig, ax = plt.subplots(figsize=(10, 8), dpi=150)
plt.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)

edges_plot = edges.plot(ax=ax, color=edges['color'], alpha=0.5, linewidth=0.1, legend=True)
bbox.boundary.plot(ax=ax, color='black', linestyle='--', linewidth=1)
point.plot(ax=ax, color='red', markersize=50)
ax.set_axis_off()


def update(step):
    for coll in ax.collections:
        if coll.get_label() == 'isochrone':
            coll.remove()

    result = get_accessibility_isochrone_stepped(
        isochrone_type='separate',
        point=point,
        weight_type="time_min",
        weight_value=15,
        nx_graph=G_intermodal,
        step=step
    )
    result.plot(ax=ax, alpha=1, label='isochrone', column='dist', legend=False)
    ax.set_title(f'Isochrone with step = {step} minutes')


ani = FuncAnimation(
    fig,
    update,
    frames=steps,
    repeat=True,
    interval=2000
)

ani.save('isochrone_animation.gif', writer='pillow', fps=1)