new changes maps
This commit is contained in:
132
tool/compare_route_distances.py
Executable file
132
tool/compare_route_distances.py
Executable file
@@ -0,0 +1,132 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user