#!/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())