Files
Xpress-rider/tool/compare_route_distances.py
2026-08-12 10:50:45 +05:30

133 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Compare OSRM road distances against previously-recorded (Google) distances.
Routing distance feeds `riderkms` -> `ridercharges`, so a systematic difference
between the old and new routing backend changes what riders get paid. Run this
against a sample of completed deliveries BEFORE rolling out.
Input CSV (header required), one row per completed delivery:
origin_lat,origin_lng,dest_lat,dest_lng,recorded_km
11.0168,76.9558,10.9984,76.9776,4.6
11.0510,76.9910,11.0168,76.9558,6.1
Usage:
python3 tool/compare_route_distances.py deliveries.csv
python3 tool/compare_route_distances.py deliveries.csv \\
--osrm https://osrm.your-domain.in
"""
import argparse
import csv
import json
import statistics
import subprocess
import sys
import urllib.parse
import urllib.request
DEFAULT_OSRM = "https://routing.openstreetmap.de/routed-car"
USER_AGENT = "nearle-distance-check"
def _fetch(url, timeout):
"""GET a URL, falling back to curl when Python's SSL stack is too old.
Some systems ship a Python linked against an OpenSSL that fails the
handshake against modern servers; curl on the same box usually works.
"""
try:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.read().decode()
except Exception: # noqa: BLE001
result = subprocess.run(
["curl", "-sS", "--max-time", str(timeout), "-A", USER_AGENT, url],
capture_output=True, text=True,
)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or "curl failed") from None
return result.stdout
def osrm_km(base, o_lat, o_lng, d_lat, d_lng, timeout=25):
"""Road distance in km, or None. Note OSRM takes lon,lat."""
url = (
f"{base}/route/v1/driving/"
f"{o_lng},{o_lat};{d_lng},{d_lat}"
f"?{urllib.parse.urlencode({'overview': 'false'})}"
)
data = json.loads(_fetch(url, timeout))
if data.get("code") != "Ok" or not data.get("routes"):
return None
return data["routes"][0]["distance"] / 1000.0
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("csv_path", help="CSV of completed deliveries")
ap.add_argument("--osrm", default=DEFAULT_OSRM,
help=f"OSRM base URL (default: {DEFAULT_OSRM})")
args = ap.parse_args()
deltas, failures = [], 0
print(f"{'recorded':>10} {'osrm':>10} {'delta':>10} {'pct':>8}")
print("-" * 42)
with open(args.csv_path, newline="") as fh:
for row in csv.DictReader(fh):
try:
recorded = float(row["recorded_km"])
km = osrm_km(
args.osrm,
float(row["origin_lat"]), float(row["origin_lng"]),
float(row["dest_lat"]), float(row["dest_lng"]),
)
except Exception as exc: # noqa: BLE001
print(f" ERROR: {exc}", file=sys.stderr)
failures += 1
continue
if km is None:
print(" no route found", file=sys.stderr)
failures += 1
continue
delta = km - recorded
pct = (delta / recorded * 100) if recorded else 0.0
deltas.append(pct)
print(f"{recorded:9.2f}k {km:9.2f}k {delta:+9.2f}k {pct:+7.1f}%")
if not deltas:
print("\nNo comparable rows.", file=sys.stderr)
return 1
mean = statistics.fmean(deltas)
median = statistics.median(deltas)
print("-" * 42)
print(f"rows compared : {len(deltas)} (failed: {failures})")
print(f"mean delta : {mean:+.1f}%")
print(f"median delta : {median:+.1f}%")
if len(deltas) > 1:
print(f"std dev : {statistics.stdev(deltas):.1f}%")
print()
if abs(median) < 3:
print("OK — no meaningful bias. Rider pay is unaffected.")
elif abs(median) < 8:
print("MILD BIAS — acceptable, but tell finance the average trip "
f"distance shifts {median:+.1f}%.")
else:
print(f"SIGNIFICANT BIAS ({median:+.1f}%) — investigate before rollout. "
"Usually means missing OSM roads in your service area; consider "
"contributing the missing roads to OpenStreetMap, which fixes it "
"permanently and for everyone.")
return 0
if __name__ == "__main__":
sys.exit(main())