Analysis of road curvature features derived from OS Open Roads geometry and their coverage, distributions, and modelling caveats.
1 Purpose
This page turns the curvature research note into a concrete, reproducible inspection of the Open Roads geometry. The aim is not to produce engineering-grade curve radius. It is to check whether the centreline geometry can support a conservative link-level ranking signal for the collision model.
The background argument is:
Horizontal curves are a recognised crash-risk factor, especially for roadway-departure crashes.
OS Open Roads gives broad coverage and stable enough link geometry for feature engineering, but it is a generalised 1:25,000-scale product.
Curvature should therefore be treated as a screening/ranking proxy rather than a survey-grade design measure.
The implementation uses a vertex-density gate by road class so sparse geometry is left as missing instead of being silently interpreted as straight.
The full background note is in docs/notes/deep-research-roadcurvature.md in Github.
2 Method
For each road link, the curvature module does the following:
Load OS Open Roads geometry and reproject it to a metre-based CRS if needed.
Normalise the geometry to a single LineString; disjoint MultiLineString geometries keep the longest part.
Count original geometry vertices and calculate vertices_per_km.
Decide which road classes pass the operational geometry-quality gate: median vertices_per_km >= 40 and 25th percentile vertices_per_km >= 20.
Resample eligible links at 15 m spacing.
Calculate absolute turning angle at each interior resampled point.
Store three features: mean_curvature_deg_per_km, max_curvature_deg_per_km, and sinuosity.
mean_curvature_deg_per_km is total absolute turning angle per kilometre of link. It is therefore interpretable as “how much the link turns overall”. max_curvature_deg_per_km is the sharpest local turning-angle density found on the resampled line, capped at 10,000 deg/km to suppress single-vertex artefacts. sinuosity is link_length / straight_line_distance, clipped at 5.0 for near-closed loop-like geometries.
Show code
ifnot OPENROADS_PATH.exists():raiseFileNotFoundError(f"Open Roads parquet not found: {OPENROADS_PATH}")openroads = gpd.read_parquet(OPENROADS_PATH)print(f"Loaded {len(openroads):,} OS Open Roads links from {OPENROADS_PATH.relative_to(ROOT)}")print(f"Source CRS: {openroads.crs}")
Show code
gdf = openroads.copy()gdf["geometry"] = gdf.geometry.apply(normalise_linestring)gdf = gdf.loc[gdf.geometry.notna()].copy()if gdf.crs isNone:raiseValueError("Input CRS is missing; curvature needs a metric CRS.")units = (getattr(gdf.crs.axis_info[0], "unit_name", "").lower()if gdf.crs.axis_infoelse"")if"metre"notin units and"meter"notin units: gdf = gdf.to_crs(27700)gdf["calc_length_m"] = gdf.geometry.lengthgdf = gdf.loc[gdf["calc_length_m"] >0].copy()gdf["vertex_count"] = gdf.geometry.apply(vertex_count)gdf["vertices_per_km"] = gdf["vertex_count"] / (gdf["calc_length_m"] /1000)print(f"Metric CRS used for curvature: {gdf.crs}")print(f"Usable non-empty LineString links: {len(gdf):,}")
3 Geometry Quality Gate
The gate is deliberately operational. It is not an OS-published standard; it is a guardrail for using 15 m resampling on already-generalised Open Roads centreline geometry. Classes that fail the gate should keep curvature as NaN so sparse linework does not become a false zero-curvature signal.
The table and plots below use a reproducible random sample from the actual OS Open Roads parquet. The sample is restricted to links with at least three vertices and length between 30 m and 2 km, so every plotted section has enough geometry to inspect. Curvature is calculated for every sampled section for diagnostic visibility; the passes_class_gate column shows whether production code would persist those values or leave them missing.
Black lines are the original OS Open Roads link geometry. Blue points are the 15 m resampled points used for turning-angle calculation. Red points are the start and end nodes.
5 Start/End Nodes and Geometry Coordinates
This table exposes the node-level geometry used by the plots. Coordinates are in the metric CRS used for curvature calculation, currently British National Grid if the source parquet is WGS84.
The next table lists the first few original geometry vertices per sampled road section. It is intentionally compact: enough to show the actual coordinate sequence, without printing every point for long links.
6 How the Curvature Code Works
The production module lives at src/road_risk/features/road_curvature.py
The key pieces are:
normalise_linestring() handles empty geometry, LineString, and MultiLineString cases before feature calculation.
ensure_metric_crs() converts longitude/latitude geometry to EPSG:27700 so spacing and length calculations are in metres.
resample_linestring() interpolates points every 15 m and keeps the link end point, so short residual segments are still represented.
turning_angle_features() computes angle changes between consecutive resampled segments, converts those into degrees per kilometre, and returns: mean_curvature_deg_per_km, max_curvature_deg_per_km, and sinuosity.
main() runs the vertex-density gate by road_classification, computes features only for passing classes, writes the columns to data/features/network_features.parquet, and writes QA CSV summaries.
The deliberate modelling choice is missingness over false certainty. If a road class fails the vertex-density gate, its curvature features stay NaN rather than being filled with zero. That preserves the distinction between “this link is straight in usable geometry” and “the source geometry is too sparse to support curvature”.
7 Interpretation
For modelling, the expected useful signal is relative rather than absolute: links with more turning per kilometre should rank above straighter links. The numbers should not be read as design-speed radius or engineering inventory values. That limitation is acceptable for a link-level risk model as long as the feature is documented, quality-gated, and evaluated against held-out collision outcomes before being promoted into the main model feature lists.