How to calculate 15-minute drive time polygons in Python
In modern retail site selection automation, accurately defining a 15-minute drive time catchment area is foundational for evaluating market penetration, competitor proximity, and demographic accessibility. Network-based isochrones provide the spatial precision required by retail planners, real estate analysts, and location intelligence teams; Euclidean buffers do not. This reference details the exact procedural implementation, configuration parameters, and pipeline recovery strategies required to execute this workflow reliably in production environments.
Prerequisites & Environment Configuration
The core of this workflow relies on the OpenRouteService (ORS) /v2/isochrones/driving-car endpoint, which computes reachable areas based on OpenStreetMap routing profiles. Before initiating requests, ensure your Python environment includes requests, geopandas, shapely, and pandas. The API key must be passed in the Authorization header as a plain string (not Bearer <key>). When Configuring OpenRouteService for Drive-Time Maps, the most critical payload parameters are locations, range_type, range, and the profile embedded in the URL path. For a strict 15-minute drive time polygon, set range_type to "time" and range to [900] (900 seconds).
Coordinate order: ORS strictly expects [longitude, latitude] arrays. Swapping these is the most common source of null or ocean-bound polygons. Always validate input coordinates against WGS84 bounds before submission.
Production-Ready Implementation
The following function constructs the request payload, handles HTTP and network exceptions, and converts the resulting GeoJSON into a validated GeoDataFrame. It implements exponential backoff, connection pooling, and explicit geometry sanitization.
import requests
import geopandas as gpd
import pandas as pd
from shapely.geometry import shape
from shapely.validation import make_valid
import time
import logging
from typing import Optional
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
def fetch_15min_isochrone(
api_key: str,
longitude: float,
latitude: float,
retries: int = 3,
timeout: int = 30,
session: Optional[requests.Session] = None
) -> gpd.GeoDataFrame:
"""
Calculates a 15-minute drive time polygon using the OpenRouteService API.
ORS expects [longitude, latitude] order in the 'locations' array.
The API key is passed in the 'Authorization' header (not as a Bearer token).
Returns:
GeoDataFrame with the isochrone polygon in EPSG:4326.
Raises:
RuntimeError: if all retry attempts are exhausted.
"""
url = "https://api.openrouteservice.org/v2/isochrones/driving-car"
headers = {
"Authorization": api_key,
"Content-Type": "application/json"
}
payload = {
"locations": [[longitude, latitude]], # ORS: [lon, lat]
"range_type": "time",
"range": [900], # 900 seconds = 15 minutes
"units": "m",
"attributes": ["area", "reachfactor"],
"smoothing": 0.5
}
client = session or requests.Session()
for attempt in range(retries):
try:
response = client.post(url, json=payload, headers=headers, timeout=timeout)
response.raise_for_status()
geojson = response.json()
if "features" not in geojson or not geojson["features"]:
raise ValueError("API returned an empty feature collection.")
feature = geojson["features"][0]
raw_geom = shape(feature["geometry"])
valid_geom = make_valid(raw_geom)
gdf = gpd.GeoDataFrame(
pd.DataFrame([feature.get("properties", {})]),
geometry=[valid_geom],
crs="EPSG:4326"
)
logging.info(
"Generated 15-minute isochrone for (%.6f, %.6f)", longitude, latitude
)
return gdf
except requests.exceptions.HTTPError as e:
status = response.status_code
if status == 429:
wait_time = (2 ** attempt) * 5
logging.warning("Rate limit exceeded. Retrying in %d seconds...", wait_time)
time.sleep(wait_time)
elif status >= 500:
wait_time = (2 ** attempt) * 2
logging.warning("Server error (%d). Retrying in %d seconds...", status, wait_time)
time.sleep(wait_time)
else:
logging.error("HTTP %d: %s", status, response.text)
raise
except requests.exceptions.RequestException as e:
wait_time = (2 ** attempt) * 3
logging.warning("Network error: %s. Retrying in %d seconds...", str(e), wait_time)
time.sleep(wait_time)
raise RuntimeError(f"Failed to fetch isochrone after {retries} attempts.")
Geometry Validation & Pipeline Resilience
Routing engines frequently return self-intersecting polygons or topological artifacts when processing complex urban grids or coastal routes. Passing raw GeoJSON directly into spatial joins triggers TopologyException errors during downstream analysis. The shapely.validation.make_valid() routine resolves these issues by decomposing invalid rings into valid multi-polygons, ensuring compatibility with GeoPandas spatial operations and retail catchment aggregation workflows.
For automated site selection, project the resulting GeoDataFrame to a local metric CRS (e.g., the appropriate UTM zone) before calculating acreage or running proximity buffers. This eliminates distortion inherent to WGS84 and guarantees accurate market area calculations.
Production Deployment & Scaling Considerations
When processing hundreds of candidate retail locations, synchronous single-threaded requests become a bottleneck. Implement asynchronous request batching or multiprocessing to maximize throughput while respecting API rate limits. Cache successful responses using Redis or a local SQLite spatial database to prevent redundant network calls for identical coordinates.
For enterprise deployments, consider self-hosting routing engines like OSRM or Valhalla to eliminate external dependencies and guarantee consistent latency for high-frequency location intelligence queries. Transitioning from ORS to a self-hosted engine is covered in Optimizing Batch Isochrone Generation with OSRM.
Conclusion
Accurately computing drive-time catchments requires strict adherence to network routing parameters, robust error handling, and geometric validation. By leveraging the OpenRouteService API with exponential backoff and Shapely’s topology repair functions, retail analytics teams can automate reliable, reproducible isochrone generation at scale. This foundation enables precise market penetration modeling, competitive gap analysis, and data-driven real estate acquisition strategies.