Tuning Routing Profiles for Retail Vehicle Types
This page solves one exact task: producing routing profiles that describe the vehicles a retail business actually cares about — the shopper’s car, the delivery van, the articulated lorry serving a distribution centre — instead of using one generic car profile for all three.
The distinction matters because these vehicles do not share a road network. A van is barred from some residential streets and permitted in loading bays; a lorry cannot pass under a low bridge or turn into a tight service yard; a shopper’s car goes almost everywhere and parks. Modelling all three with one profile gives one of them a correct answer and the other two something plausible and wrong.
Prerequisites
- A routing engine whose profile is a file you control, per preparing OSM network extracts for routing.
- An extract retaining the tags each profile reads — access, dimension, weight and surface tags as well as highway classes.
- Observed trips for the vehicle types you can measure. Delivery telematics for vans and lorries; loyalty-app timings or a commercial travel-time dataset for shopper cars.
- Python packages:
pandasandnumpyfor the fitting,httpxfor probing. Install withpip install pandas numpy httpx.
Configuration and execution parameters
| Parameter | Shopper car | Delivery van | Heavy goods |
|---|---|---|---|
| Highway classes | all public roads | all public, plus service | primary and above, plus permitted access |
| Access tags read | access, motor_vehicle |
plus goods, delivery |
plus hgv |
| Dimension limits | ignored | height, width | height, width, weight, length |
| Speed basis | 0.8 × limit, class-adjusted | 0.75 × car speed | 0.65 × car speed, capped |
| Turn penalty | 8 s | 12 s | 25 s |
| U-turn penalty | 20 s | 40 s | effectively barred |
| Service roads | allowed | allowed | permitted only |
| Living streets | allowed | restricted | barred |
The speed columns are ratios rather than absolutes on purpose. A van does not travel at a fixed speed; it travels somewhat slower than a car on the same road for reasons of acceleration, load and driver behaviour, and expressing that as a ratio means an improvement to the car speed table propagates to the others rather than leaving them stale.
Annotated implementation
Fitting a speed table means comparing modelled durations against observed ones and adjusting per road class until the residuals are small and unbiased. The routine below does that fit and — importantly — reports where the observations were too thin to support it.
from __future__ import annotations
import numpy as np
import pandas as pd
MIN_OBS_PER_CLASS = 200
def fit_class_factors(observed: pd.DataFrame, modelled: pd.DataFrame) -> pd.DataFrame:
"""Per road class, the factor that best aligns modelled with observed time.
observed: trip_id, class_share columns (fraction of the trip on each class),
duration_s
modelled: trip_id, duration_s from the current profile
"""
joined = observed.merge(modelled, on="trip_id", suffixes=("_obs", "_mod"))
classes = [c for c in observed.columns if c.startswith("share_")]
# Weighted least squares over class shares: how much of the residual does
# each class explain? A class present in few trips gets a wide interval and
# should NOT be adjusted on that evidence.
x = joined[classes].to_numpy()
ratio = (joined["duration_s_obs"] / joined["duration_s_mod"]).to_numpy()
factors, *_ = np.linalg.lstsq(x, ratio, rcond=None)
coverage = (x > 0).sum(axis=0)
return pd.DataFrame({
"road_class": [c.removeprefix("share_") for c in classes],
"factor": np.round(factors, 3),
"trips_observed": coverage,
# Below the threshold the factor is noise dressed as a measurement.
"apply": coverage >= MIN_OBS_PER_CLASS,
})
def residual_bias(observed: pd.Series, modelled: pd.Series) -> dict:
"""A good fit is unbiased as well as small — check both."""
resid = observed - modelled
return {
"median_s": float(resid.median()),
"mae_s": float(resid.abs().mean()),
"pct_within_10": float((resid.abs() / observed < 0.10).mean()),
}
The apply column is what keeps the fit honest. A road class appearing in twelve observed trips will produce a factor, and that factor will be shaped by whatever those twelve trips happened to encounter. Marking it as unsupported and leaving the default in place is both more accurate and more explainable than a number derived from a dozen journeys.
Failure modes and debugging
Fitting on the wrong population. Delivery telematics measure trips made by professional drivers on a schedule, often outside peak hours. Fitting a shopper-car profile on that data produces a profile that is systematically fast for the population it will be applied to. Fit each profile on trips made by the vehicle it describes, and where that data does not exist, say so rather than substituting.
Over-fitting to one market. A speed table fitted entirely on trips in one dense metro will be wrong in every rural market, and the direction is not obvious — rural roads are faster in some classes and considerably slower in others. Stratifying the fit by density band, even coarsely, avoids exporting one market’s traffic to the whole estate.
Dimension limits that are not in the data. Height and weight restrictions are recorded in the map inconsistently, so a lorry profile that trusts them entirely will route under bridges that are not tagged. This is a known and unavoidable gap; the mitigation is to treat lorry routing as advisory for planning purposes and to keep a manual exception list of known constraints per market.
One profile edited for a specific complaint. Somebody reports that a route looks slow, a class factor is nudged, and every catchment in the estate shifts. Profile changes deserve the same treatment as any other pipeline change: a version, a regression run against the fixed route set, and a note about what evidence motivated them.
Verification
- Route a known-restricted street per profile. A living street should be barred to lorries and open to cars; a delivery-only service road should be open to the van profile and closed to the car. One probe per rule proves the tags are being read.
- Compare contour areas across profiles for the same origins, as in the table above. The ratios should be stable across markets; a market where the van and car contours are identical usually means the access tags are not being applied.
- Check the fit’s residual bias, not only its error. A profile with a small mean error and a consistent sign is still systematically wrong.
- Re-run the fixed route set after any profile change, and treat the comparison as part of the change rather than as a follow-up.
Frequently Asked Questions
Is a separate lorry profile worth it for a retail site-selection model?
For the shopper catchment, no — that is a car question. For the site itself, frequently yes: whether a candidate location can be serviced by the standard delivery vehicle is a hard constraint that kills sites late in diligence, and discovering it from a routing profile during screening is far cheaper than discovering it from a driver. Running the lorry profile against the shortlist rather than the whole candidate pool keeps the cost proportionate.
How often do profiles need refitting?
Annually, or when the observation set grows materially. Road speeds change slowly outside of specific interventions, and a profile refitted every month on slightly different samples produces churn that is indistinguishable from noise. What does deserve a prompt refit is a market whose traffic conditions changed structurally — a new bypass, a congestion charge — since those move a whole class rather than a few edges.
Should the shopper profile model parking?
Not inside the routing graph, and yes as a site attribute. Parking search time is real, it can be several minutes in a dense centre, and it is a property of the destination rather than of the route — so encoding it as an edge cost puts it in the wrong place and makes it invisible to anyone reading the profile. Adding a per-site access penalty derived from format and location keeps the routing honest and puts the parking assumption where a reviewer will find it.
What if no observed trips exist at all?
Use published speed data if any is available for the market, fall back on a fraction of posted limits, and record the choice in the build metadata as an assumption rather than a fit. Then plan the measurement: even a few hundred timed trips from a delivery operation give enough signal to check whether the assumption is badly wrong, which is the only question that matters at that stage.
Where should the profiles themselves live?
In version control, beside the pipeline code, and hashed into the build identifier. Profiles are code — they contain speed tables, access logic and penalties that determine every duration the business plans on — and treating them as configuration that lives on a build host is how an edit made during an incident becomes permanent and unattributable. Reviewing a profile change as a pull request, with the route-set regression attached, is the same discipline applied to any other change that moves numbers.
Related
- Preparing OSM Network Extracts for Routing — where the profile fits in the build.
- Modeling Turn Restrictions and One-Way Streets in Catchments — the rules the profile decides to honour.
- Implementing Multi-Modal Routing for Urban Retail — profiles for modes rather than vehicle classes.