Files
apartment-hunt/apartments-routing.ipynb
Alex Fox 8449f13314 init
2026-01-07 23:20:25 -05:00

375 lines
32 KiB
Plaintext

{
"cells": [
{
"cell_type": "code",
"execution_count": 14,
"id": "ca04f457-164b-447a-8a7e-7864892c221b",
"metadata": {
"collapsed": true,
"jupyter": {
"outputs_hidden": true
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Defaulting to user installation because normal site-packages is not writeable\n",
"Requirement already satisfied: geopy in /home/arfox/.local/lib/python3.12/site-packages (2.4.1)\n",
"Requirement already satisfied: geographiclib<3,>=1.52 in /home/arfox/.local/lib/python3.12/site-packages (from geopy) (2.1)\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Defaulting to user installation because normal site-packages is not writeable\n",
"Requirement already satisfied: coloraide in /home/arfox/.local/lib/python3.12/site-packages (6.2)\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Defaulting to user installation because normal site-packages is not writeable\n",
"Requirement already satisfied: matplotlib in /home/arfox/.local/lib/python3.12/site-packages (3.8.4)\n",
"Requirement already satisfied: contourpy>=1.0.1 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (1.3.1)\n",
"Requirement already satisfied: cycler>=0.10 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (0.12.1)\n",
"Requirement already satisfied: fonttools>=4.22.0 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (4.56.0)\n",
"Requirement already satisfied: kiwisolver>=1.3.1 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (1.4.8)\n",
"Requirement already satisfied: numpy>=1.21 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (1.26.4)\n",
"Requirement already satisfied: packaging>=20.0 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (23.2)\n",
"Requirement already satisfied: pillow>=8 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (11.1.0)\n",
"Requirement already satisfied: pyparsing>=2.3.1 in /home/arfox/.local/lib/python3.12/site-packages (from matplotlib) (3.2.1)\n",
"Requirement already satisfied: python-dateutil>=2.7 in /opt/miniconda3/envs/jupyter/lib/python3.12/site-packages (from matplotlib) (2.9.0.post0)\n",
"Requirement already satisfied: six>=1.5 in /opt/miniconda3/envs/jupyter/lib/python3.12/site-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Defaulting to user installation because normal site-packages is not writeable\n",
"Requirement already satisfied: requests in /home/arfox/.local/lib/python3.12/site-packages (2.32.3)\n",
"Requirement already satisfied: charset-normalizer<4,>=2 in /home/arfox/.local/lib/python3.12/site-packages (from requests) (3.4.1)\n",
"Requirement already satisfied: idna<4,>=2.5 in /home/arfox/.local/lib/python3.12/site-packages (from requests) (3.10)\n",
"Requirement already satisfied: urllib3<3,>=1.21.1 in /home/arfox/.local/lib/python3.12/site-packages (from requests) (2.4.0)\n",
"Requirement already satisfied: certifi>=2017.4.17 in /home/arfox/.local/lib/python3.12/site-packages (from requests) (2025.1.31)\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Defaulting to user installation because normal site-packages is not writeable\n",
"Requirement already satisfied: scipy in /home/arfox/.local/lib/python3.12/site-packages (1.15.2)\n",
"Requirement already satisfied: numpy<2.5,>=1.23.5 in /home/arfox/.local/lib/python3.12/site-packages (from scipy) (1.26.4)\n",
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install geopy\n",
"%pip install coloraide\n",
"%pip install matplotlib\n",
"%pip install requests\n",
"%pip install scipy"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "839b8a06-f643-4896-b9db-5640d83d5511",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"from IPython.display import JSON, display, HTML\n",
"from datetime import datetime\n",
"from dataclasses import dataclass\n",
"from geopy.geocoders import Nominatim\n",
"from coloraide import Color\n",
"from scipy import stats\n",
"from collections import OrderedDict\n",
"import requests\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np"
]
},
{
"cell_type": "markdown",
"id": "b47d4ce9-0bfe-4db1-b5f0-a033034f70c2",
"metadata": {},
"source": [
"I self host [Nominatim](https://nominatim.org/) with New York State loaded, it took forever to load. If you want to self host, I'd recommend you get the NYC and surrounding area from Interline: https://app.interline.io/osm_extracts/interactive_view\n",
"\n",
"If I remember correctly, the default Nominatim configuration connects to a free public server that requests you stay under one geocoding request per second."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "7b42f081-fac6-48f9-9c87-6b4782ebe15b",
"metadata": {},
"outputs": [],
"source": [
"# geolocator = Nominatim(\n",
"# domain=\"192.168.68.131:8088\",\n",
"# scheme=\"http\",\n",
"# user_agent=\"apt-hunt-2026\",\n",
"# timeout=5 # My instance of Nominatim is slow and the default timeout is only one second\n",
"# )\n",
"\n",
"# user_agent can basically be anything, but if you remove it it'll yell at you to pick something to identify yourself\n",
"geolocator = Nominatim(\n",
" user_agent=\"apt-hunt-2026\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "3880b549-c73c-4aa1-8524-02ef67fa2a7a",
"metadata": {},
"source": [
"I'm guessing you might know this part better than me, the routing library uses GraphQL and I only have a cursory understanding of it and haven't learned how to use any of the libraries like [Strawberry](https://strawberry.rocks/). I'm also not positive I have all the right modes of transit listed in this query yet."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "406fe6a5-c933-4324-a2e7-a176a0dfdb11",
"metadata": {},
"outputs": [],
"source": [
"def build_query(origin_lat: float, origin_lon: float,\n",
" dest_lat: float, dest_lon: float,\n",
" earliest_departure: str = \"2026-01-05T9:30-05:00\") -> str:\n",
" query_template = f\"\"\"\n",
"query {{\n",
" planConnection(\n",
" origin: {{\n",
" location: {{ coordinate: {{ latitude: {origin_lat}, longitude: {origin_lon} }} }}\n",
" }}\n",
" destination: {{\n",
" location: {{ coordinate: {{ latitude: {dest_lat}, longitude: {dest_lon} }} }}\n",
" }}\n",
" dateTime: {{ earliestDeparture: \"{earliest_departure}\" }}\n",
" modes: {{\n",
" direct: [WALK]\n",
" transit: {{ transit: [{{ mode: BUS }}, {{ mode: RAIL }}, {{ mode: SUBWAY }}] }}\n",
" }}\n",
" ) {{\n",
" edges {{\n",
" node {{\n",
" start\n",
" end\n",
" legs {{\n",
" mode\n",
" from {{\n",
" name\n",
" lat\n",
" lon\n",
" departure {{\n",
" scheduledTime\n",
" estimated {{\n",
" time\n",
" delay\n",
" }}\n",
" }}\n",
" }}\n",
" to {{\n",
" name\n",
" lat\n",
" lon\n",
" arrival {{\n",
" scheduledTime\n",
" estimated {{\n",
" time\n",
" delay\n",
" }}\n",
" }}\n",
" }}\n",
" route {{\n",
" gtfsId\n",
" longName\n",
" shortName\n",
" }}\n",
" legGeometry {{\n",
" points\n",
" }}\n",
" }}\n",
" }}\n",
" }}\n",
" }}\n",
"}}\n",
"\"\"\"\n",
" \n",
" return query_template.strip()"
]
},
{
"cell_type": "markdown",
"id": "35c24bc5-fd6d-49ce-a835-2c3b0285c617",
"metadata": {},
"source": [
"I self host [OpenTripPlanner](https://docs.opentripplanner.org/en/latest/), I'm not sure if there are any easy public instances like Nominatim has. I can set up some auth for mine if you want to connect to it, right now it's only available on my home network. And it looks like PATH might be a bit more of a pain than the other transit networks, hopefully I'll have that one figured out soon."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "6e8d4cb0-735f-41b7-a765-26ce9dc56acd",
"metadata": {},
"outputs": [],
"source": [
"def execute_plan_query(from_addr, to_addr):\n",
" url = \"http://192.168.68.138:8080/otp/gtfs/v1\"\n",
" headers = {\"Content-Type\": \"application/json\"}\n",
" \n",
" query = build_query(\n",
" origin_lat=from_addr.latitude,\n",
" origin_lon=from_addr.longitude,\n",
" dest_lat=to_addr.latitude,\n",
" dest_lon=to_addr.longitude,\n",
" earliest_departure=\"2026-01-012T15:30-05:00\"\n",
" )\n",
"\n",
" response = requests.post(\n",
" url,\n",
" json={\"query\": query},\n",
" headers=headers\n",
" )\n",
" \n",
" return response.json()"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "42e41f8e-8692-4b95-a4cd-242074a1ec30",
"metadata": {},
"outputs": [],
"source": [
"query = {\n",
" \"street\": \"330 34th Street\",\n",
" \"city\": \"NEW YORK\",\n",
" \"county\": \"New York\",\n",
" \"state\": \"NY\",\n",
" \"postalcode\": \"10001\",\n",
"}\n",
"jfk36 = geolocator.geocode(query, timeout=5)"
]
},
{
"cell_type": "code",
"execution_count": 93,
"id": "a7d72437-cb11-47db-b5d8-c6971836a8cd",
"metadata": {},
"outputs": [],
"source": [
"destinations = OrderedDict([\n",
" (\"JFK36\", jfk36),\n",
"])"
]
},
{
"cell_type": "code",
"execution_count": 94,
"id": "00fed8bd-25cc-44d8-ab39-56a8e3425a5b",
"metadata": {},
"outputs": [],
"source": [
"query = {\n",
" \"street\": \"373 Washington Ave\",\n",
" \"city\": \"NEW YORK\",\n",
" \"county\": \"Brooklyn\",\n",
" \"state\": \"NY\",\n",
" \"postalcode\": \"11238\",\n",
"}\n",
"home = geolocator.geocode(query)"
]
},
{
"cell_type": "code",
"execution_count": 95,
"id": "b1fb109b-d7be-4566-994c-74805ff66e4a",
"metadata": {},
"outputs": [],
"source": [
"all_trips = {}\n",
"\n",
"all_starts = {\n",
" \"Home\": home,\n",
"}\n",
"\n",
"for label, start_loc in all_starts.items():\n",
" all_trips[label] = []\n",
" \n",
" for _, dest in destinations.items():\n",
" trips = execute_plan_query(start_loc, dest)['data']['planConnection']['edges']\n",
" \n",
" trip_durations = [\n",
" (datetime.fromisoformat(trip['node']['end']) - datetime.fromisoformat(trip['node']['start']))\n",
" for trip in trips\n",
" ]\n",
" trips_in_seconds = [trip.seconds for trip in trip_durations]\n",
" trips_in_minutes = [seconds / 60 for seconds in trips_in_seconds]\n",
" all_trips[label].append(trips_in_minutes)"
]
},
{
"cell_type": "code",
"execution_count": 96,
"id": "858ed8a1-5727-4b2c-a482-57733a6eafdc",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAEiCAYAAAAWHJuuAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAN6tJREFUeJzt3Xm4VmW9P/73HmADMqqMyqCMMipahlqJkojKVyyHlHJKj6dwANRfUjmQJdZRK9MvDqV0KrX0q+nxiGYqdCxMQPGIOWEOHEVRExCHDez9/P7w8jnu4FE37M120+t1Xeu61nOve631eR65W+e8ublXWaFQKAQAAAAAAFhPeVMXAAAAAAAAn1RCdAAAAAAAKEGIDgAAAAAAJQjRAQAAAACgBCE6AAAAAACUIEQHAAAAAIAShOgAAAAAAFCCEB0AAAAAAEoQogMAAAAAQAlCdAAAAAAAKEGIDgAADWTWrFkpKyvLggULNnh87733ztChQzdzVQAAwKYQogMAAAAAQAlCdAAAAAAAKEGIDgAATWTdunU5//zz07dv31RVVaVPnz751re+lerq6jr9+vTpk4MOOihz5szJbrvtltatW2fYsGGZM2dOkuTmm2/OsGHD0qpVq+y66655+OGH17vXE088kUMPPTRbb711WrVqld122y233Xbb5viaAADQrAnRAQCgga1cuTKvvfbaetvatWvr9DvhhBNyzjnnZOTIkfnRj36Uz3/+85kxY0a+/OUvr3fNJUuW5Kijjsr48eMzY8aMvPHGGxk/fnx+/etfZ8qUKfnKV76S6dOn55lnnsnhhx+e2tra4rmPPfZYPvOZz+Txxx/PWWedlYsvvjhbbbVVJkyYkFtuuaXRfw8AAGjOygqFQqGpiwAAgC3BrFmzctxxx31onyFDhmTx4sV55JFHsvPOO+eEE07I1VdfXTx+5pln5qKLLsq9996b0aNHJ3lvJvrzzz+fP//5zxk1alSS5Pe//33Gjh2b1q1b54knnkivXr2SJFdddVVOOumk3Hfffdl7772TJGPGjMny5cszf/78VFVVJUkKhUL22muvvPrqq3nqqaca+qcAAIAthpnoAADQwC6//PLcfffd623Dhw8v9rnjjjuSJFOnTq1z7umnn54k+c///M867YMHDy4G6Emy++67J0n22WefYoD+wfa//e1vSZK///3vuffee3P44YfnzTffLM6Kf/311zN27Ng8/fTTefHFFxvqqwMAwBansqkLAACALc2nP/3p7Lbbbuu1d+rUKa+99lqS5Pnnn095eXn69etXp0+3bt3SsWPHPP/883XaPxiUJ0mHDh2SJD179txg+xtvvJHkvWVgCoVCzj777Jx99tkbrHf58uXZbrvtPu7XAwCAfypCdAAAaEJlZWUfq19FRUW92t9ftfH9tdHPOOOMjB07doN9/zHIBwAA/pcQHQAAmkDv3r1TW1ubp59+OjvttFOx/ZVXXsmKFSvSu3fvBrnPjjvumCRp0aJFxowZ0yDXBACAfybWRAcAgCZwwAEHJEl+/OMf12m/5JJLkiQHHnhgg9ynS5cu2XvvvXPllVdm2bJl6x1/9dVXG+Q+AACwpTITHQAAmsCIESNyzDHH5KqrrsqKFSvy+c9/Pg8++GB+8YtfZMKECRk9enSD3evyyy/PXnvtlWHDhuXEE0/MjjvumFdeeSXz5s3L//zP/+SRRx5psHsBAMCWRogOAABN5Gc/+1l23HHHzJo1K7fccku6deuWadOm5dxzz23Q+wwePDgLFizI9OnTM2vWrLz++uvp0qVLdtlll5xzzjkNei8AANjSlBXef+MQAAAAAABQhzXRAQAAAACgBCE6AAAAAACUIEQHAAAAAIAShOgAAAAAAFCCEB0AAAAAAEoQogMAAAAAQAmVm/uGtbW1eemll9KuXbuUlZVt7tsDAAAAALCFKxQKefPNN9OjR4+Ul2/aXPLNHqK/9NJL6dmz5+a+LQAAAAAA/2SWLl2a7bfffpOusdlD9Hbt2iV5r/j27dtv7tsDsAWqrlmTMx+9PEnyb8MmpaqiZRNXBAAAADSlVatWpWfPnsU8elNs9hD9/SVc2rdvL0QHoEFU16xJy7atkrz3fBGiAwAAAEkaZElxLxYFAAAAAIAShOgAAAAAAFCCEB0AAAAAAEqo95roL774Yr75zW9m9uzZefvtt9OvX79ce+212W233RqjPgD4SC3KK3N6/yOL+wAAAGy6mpqarF27tqnLgA1q0aJFKioqNsu96pU0vPHGG9lzzz0zevTozJ49O507d87TTz+dTp06NVZ9APCRysvKM7Bdr6YuAwAAYItQKBTy8ssvZ8WKFU1dCnyojh07plu3bg3y8tAPU68Q/Qc/+EF69uyZa6+9tti2ww47NHhRAAAAAEDTeD9A79KlS9q0adPoASXUV6FQyNtvv53ly5cnSbp3796o96tXiH7bbbdl7NixOeywwzJ37txst912+cY3vpETTzyx5DnV1dWprq4ufl61atXGVwsAG7CuUJP/eu2RJMlntx2RyrLN88+5AAAAtjQ1NTXFAH2bbbZp6nKgpNatWydJli9fni5dujTq0i71erHo3/72t8ycOTP9+/fPXXfdla9//es59dRT84tf/KLkOTNmzEiHDh2KW8+ePTe5aAC2fJctuam4fZSa2ppcv/TuXL/07tTU1myG6gAAALZM76+B3qZNmyauBD7a+39OG3vt/nqF6LW1tRk5cmQuuOCC7LLLLvmXf/mXnHjiibniiitKnjNt2rSsXLmyuC1dunSTiwYAAAAAGo8lXGgONtef03qF6N27d8/gwYPrtO2000554YUXSp5TVVWV9u3b19kAAAAAAKA5qFeIvueee+bJJ5+s0/bUU0+ld+/eDVoUAAAAAAB8EtQrRJ8yZUoeeOCBXHDBBVmyZEmuu+66XHXVVZk0aVJj1QcAAAAA8JGOPfbYTJgwobhfVla23rZkyZL1+r7vpptuSqtWrXLxxRcnSWbOnJnhw4cXV9cYNWpUZs+evd59582bl3322SdbbbVV2rdvn8997nN55513GvW7flIsWbIk7dq1S8eOHdc7duONN2bQoEFp1apVhg0bljvuuGPzF9hA6hWif+pTn8ott9yS66+/PkOHDs3555+fH//4x5k4cWJj1QcAAAAAUG/7779/li1bVmfbYYcdNtj3Zz/7WSZOnJiZM2fm9NNPT5Jsv/32ufDCC7Nw4cIsWLAg++yzTw4++OA89thjxfPmzZuX/fffP/vtt18efPDBzJ8/PyeffHLKy+sVuzZLa9euzZFHHpnPfvaz6x3785//nCOPPDJf+9rX8vDDD2fChAmZMGFCFi9e3ASVbrp6/9c86KCD8uijj+bdd9/N448/nhNPPLEx6gIAAAAA2GhVVVXp1q1bna2iomK9fj/84Q9zyimn5IYbbshxxx1XbB8/fnwOOOCA9O/fPwMGDMj3v//9tG3bNg888ECxz5QpU3LqqafmrLPOypAhQzJw4MAcfvjhqaqqKlnX3nvvnVNOOSWTJ09Op06d0rVr11x99dV56623ctxxx6Vdu3bp16/ferPeFy9enHHjxqVt27bp2rVrvvrVr+a1114rHr/zzjuz1157pWPHjtlmm21y0EEH5Zlnnikef+6551JWVpabb745o0ePTps2bTJixIjMmzdvo37f73znOxk0aFAOP/zw9Y795Cc/yf77758zzzwzO+20U84///yMHDkyl112WbFPnz598r3vfS9HH3102rZtm969e+e2227Lq6++moMPPjht27bN8OHDs2DBgo2qryFt+X8lAsAWr7K8Mif3PTQn9z00leWVTV0OAADAFqe6Zk3JbW3tuo/dd03t2o/Vd3P55je/mfPPPz+33357DjnkkJL9ampqcsMNN+Stt97KqFGjkiTLly/PX/7yl3Tp0iV77LFHunbtms9//vO5//77P/K+v/jFL7LtttvmwQcfzCmnnJKvf/3rOeyww7LHHnvkoYceyn777ZevfvWrefvtt5MkK1asyD777JNddtklCxYsyJ133plXXnmlToD91ltvZerUqVmwYEHuueeelJeX55BDDkltbW2de3/729/OGWeckUWLFmXAgAE58sgjs27d//43LCsry6xZsz60/nvvvTc33nhjLr/88g0enzdvXsaMGVOnbezYsesF9j/60Y+y55575uGHH86BBx6Yr371qzn66KPzla98JQ899FD69u2bo48+OoVC4SN/08YkaQCg2asoK8/wDn2bugwAAIAt1imP/KjksaHtd8yp/Q4rfj790cvWC8vfN6Btz5wx4Kji52mPXZHV69ZfP/yqkd/chGrfc/vtt6dt27bFz+PGjcuNN95Y/Dx79uzceuutueeee7LPPvts8BqPPvpoRo0alXfffTdt27bNLbfcksGDBydJ/va3vyVJzjvvvFx00UXZeeed8+///u/Zd999s3jx4vTv379kbSNGjMh3vvOdJMm0adNy4YUXZtttty2u+nHOOedk5syZ+e///u985jOfyWWXXZZddtklF1xwQfEa11xzTXr27JmnnnoqAwYMyJe+9KU697jmmmvSuXPn/PWvf83QoUOL7WeccUYOPPDAJMn06dMzZMiQLFmyJIMGDUqSDBw4MB06dChZ++uvv55jjz02v/rVr9K+ffsN9nn55ZfTtWvXOm1du3bNyy+/XKftgAMOyEknnVTnO3/qU5/KYYe99+fpm9/8ZkaNGpVXXnkl3bp1K1lTYxOiAwAAAABbnNGjR2fmzJnFz1tttVWd48OHD89rr72Wc889N5/+9KfrBO7vGzhwYBYtWpSVK1fmpptuyjHHHJO5c+dm8ODBxRneJ510UnEZmF122SX33HNPrrnmmsyYMaNkbcOHDy/uV1RUZJtttsmwYcOKbe8H0MuXL0+SPPLII7nvvvs2WOMzzzyTAQMG5Omnn84555yTv/zlL3nttdeK9b3wwgt1QvQP3rt79+7F+7wfoj/xxBMl606SE088MUcddVQ+97nPfWi/j+ODtbz/nUv9DkJ0ANgE6wo1efDvf02SfHrrwaksW3+NOwAAADbeT0dMKXmsvKzuitEXDzu5ZN+ysrI6n2cM+ddNK+xDbLXVVunXr1/J49ttt11uuummjB49Ovvvv39mz56ddu3a1enTsmXL4jV23XXXzJ8/Pz/5yU9y5ZVXFgPo92emv2+nnXbKCy+88KG1tWjRos7nsrKyOm3v/07vB+GrV6/O+PHj84Mf/GC9a71fx/jx49O7d+9cffXV6dGjR2prazN06NCsWVN3eZwPu8/Hce+99+a2227LRRddlCQpFAqpra1NZWVlrrrqqhx//PHp1q1bXnnllTrnbWg2+YZq2dT6GoMQHYBmr6a2JrOevyNJsmvHgancwItiAAAA2HhVFS2bvG9j6N27d+bOnVsM0u+88871gvQPqq2tTXV1dZL3XozZo0ePPPnkk3X6PPXUUxk3blyD1jly5Mj8v//3/9KnT59UVq4f6b7++ut58sknc/XVV+ezn/1sknystdk3xrx581JTU1P8fOutt+YHP/hB/vznP2e77bZLkowaNSr33HNPJk+eXOx39913F9eTb268WBQAAAAA+KfVs2fPzJkzJ8uXL8/YsWOzatWqJO+tVf7HP/4xzz33XB599NFMmzYtc+bMycSJE5O8N0v6zDPPzKWXXpqbbropS5Ysydlnn50nnngiX/va1xq0xkmTJuXvf/97jjzyyMyfPz/PPPNM7rrrrhx33HGpqalJp06dss022+Sqq67KkiVLcu+992bq1Kkbda9BgwbllltuKXl8p512ytChQ4vbdtttl/Ly8gwdOjSdOnVKkpx22mm58847c/HFF+eJJ57IeeedlwULFuTkk0v/K4VPMiE6AAAAANDsvb+kyMbYfvvtM2fOnLz22mvFIH358uU5+uijM3DgwOy7776ZP39+7rrrrnzhC18onjd58uRMmzYtU6ZM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/9+FQmGTr1WvEH1jzJgxI9OnT1+vvWfPno19awCa2P/XBNf5Rc5roLtuvA+rt6F+EwAAAOCjvf766+nQocMmXaNeIfq2226bioqKvPLKK3XaX3nllXTr1m2D50ybNi1Tp04tfl6xYkV69+6dF154YZOLh38Wq1atSs+ePbN06dK0b9++qcuBZsG4gfozbqD+jBuoP+MG6s+4gfpbuXJlevXqla233nqTr1WvEL1ly5bZddddc88992TChAlJktra2txzzz05+eSTN3hOVVVVqqqq1mvv0KGDQQ/11L59e+MG6sm4gfozbqD+jBuoP+MG6s+4gforL6/Xa0E3qN7LuUydOjXHHHNMdtttt3z605/Oj3/847z11ls57rjjNrkYAAAAAAD4JKl3iH7EEUfk1VdfzTnnnJOXX345O++8c+688871XjYKAAAAAADN3Ua9WPTkk08uuXzLR6mqqsq55567wSVegA0zbqD+jBuoP+MG6s+4gfozbqD+jBuov4YcN2WFQqHQADUBAAAAAMAWZ9NXVQcAAAAAgC2UEB0AAAAAAEoQogMAAAAAQAmNEqLPmDEjn/rUp9KuXbt06dIlEyZMyJNPPlmnz7vvvptJkyZlm222Sdu2bfOlL30pr7zySmOUA83CzJkzM3z48LRv3z7t27fPqFGjMnv27OJxYwY+2oUXXpiysrJMnjy52GbsQF3nnXdeysrK6myDBg0qHjdmYMNefPHFfOUrX8k222yT1q1bZ9iwYVmwYEHxeKFQyDnnnJPu3bundevWGTNmTJ5++ukmrBiaVp8+fdZ73pSVlWXSpElJPG9gQ2pqanL22Wdnhx12SOvWrdO3b9+cf/75+eDrDD1vYH1vvvlmJk+enN69e6d169bZY489Mn/+/OLxhhg3jRKiz507N5MmTcoDDzyQu+++O2vXrs1+++2Xt956q9hnypQp+Y//+I/ceOONmTt3bl566aV88YtfbIxyoFnYfvvtc+GFF2bhwoVZsGBB9tlnnxx88MF57LHHkhgz8FHmz5+fK6+8MsOHD6/TbuzA+oYMGZJly5YVt/vvv794zJiB9b3xxhvZc88906JFi8yePTt//etfc/HFF6dTp07FPj/84Q9z6aWX5oorrshf/vKXbLXVVhk7dmzefffdJqwcms78+fPrPGvuvvvuJMlhhx2WxPMGNuQHP/hBZs6cmcsuuyyPP/54fvCDH+SHP/xhfvrTnxb7eN7A+k444YTcfffd+eUvf5lHH300++23X8aMGZMXX3wxSQONm8JmsHz58kKSwty5cwuFQqGwYsWKQosWLQo33nhjsc/jjz9eSFKYN2/e5igJmoVOnToVfvaznxkz8BHefPPNQv/+/Qt333134fOf/3zhtNNOKxQKnjewIeeee25hxIgRGzxmzMCGffOb3yzstddeJY/X1tYWunXrVvi3f/u3YtuKFSsKVVVVheuvv35zlAifeKeddlqhb9++hdraWs8bKOHAAw8sHH/88XXavvjFLxYmTpxYKBQ8b2BD3n777UJFRUXh9ttvr9M+cuTIwre//e0GGzebZU30lStXJkm23nrrJMnChQuzdu3ajBkzpthn0KBB6dWrV+bNm7c5SoJPtJqamtxwww156623MmrUKGMGPsKkSZNy4IEH1hkjiecNlPL000+nR48e2XHHHTNx4sS88MILSYwZKOW2227LbrvtlsMOOyxdunTJLrvskquvvrp4/Nlnn83LL79cZ+x06NAhu+++u7EDSdasWZNf/epXOf7441NWVuZ5AyXsscceueeee/LUU08lSR555JHcf//9GTduXBLPG9iQdevWpaamJq1atarT3rp169x///0NNm4qG6ziEmprazN58uTsueeeGTp0aJLk5ZdfTsuWLdOxY8c6fbt27ZqXX365sUuCT6xHH300o0aNyrvvvpu2bdvmlltuyeDBg7No0SJjBkq44YYb8tBDD9VZ7+x9njewvt133z2zZs3KwIEDs2zZskyfPj2f/exns3jxYmMGSvjb3/6WmTNnZurUqfnWt76V+fPn59RTT03Lli1zzDHHFMdH165d65xn7MB7fve732XFihU59thjk/i/0aCUs846K6tWrcqgQYNSUVGRmpqafP/738/EiROTxPMGNqBdu3YZNWpUzj///Oy0007p2rVrrr/++sybNy/9+vVrsHHT6CH6pEmTsnjx4jprbQIbNnDgwCxatCgrV67MTTfdlGOOOSZz585t6rLgE2vp0qU57bTTcvfdd6/3t87Ahr0/kylJhg8fnt133z29e/fOb3/727Ru3boJK4NPrtra2uy222654IILkiS77LJLFi9enCuuuCLHHHNME1cHn3w///nPM27cuPTo0aOpS4FPtN/+9rf59a9/neuuuy5DhgzJokWLMnny5PTo0cPzBj7EL3/5yxx//PHZbrvtUlFRkZEjR+bII4/MwoULG+wejbqcy8knn5zbb7899913X7bffvtie7du3bJmzZqsWLGiTv9XXnkl3bp1a8yS4BOtZcuW6devX3bdddfMmDEjI0aMyE9+8hNjBkpYuHBhli9fnpEjR6aysjKVlZWZO3duLr300lRWVqZr167GDnyEjh07ZsCAAVmyZInnDZTQvXv3DB48uE7bTjvtVFwK6f3x8corr9TpY+xA8vzzz+cPf/hDTjjhhGKb5w1s2JlnnpmzzjorX/7ylzNs2LB89atfzZQpUzJjxowknjdQSt++fTN37tysXr06S5cuzYMPPpi1a9dmxx13bLBx0ygheqFQyMknn5xbbrkl9957b3bYYYc6x3fddde0aNEi99xzT7HtySefzAsvvJBRo0Y1RknQLNXW1qa6utqYgRL23XffPProo1m0aFFx22233TJx4sTivrEDH2716tV55pln0r17d88bKGHPPffMk08+WaftqaeeSu/evZMkO+ywQ7p161Zn7KxatSp/+ctfjB3+6V177bXp0qVLDjzwwGKb5w1s2Ntvv53y8rpRXUVFRWpra5N43sBH2WqrrdK9e/e88cYbueuuu3LwwQc32LhplOVcJk2alOuuuy633npr2rVrV1xfpkOHDmndunU6dOiQr33ta5k6dWq23nrrtG/fPqecckpGjRqVz3zmM41REnziTZs2LePGjUuvXr3y5ptv5rrrrsucOXNy1113GTNQQrt27Yrv23jfVlttlW222abYbuxAXWeccUbGjx+f3r1756WXXsq5556bioqKHHnkkZ43UMKUKVOyxx575IILLsjhhx+eBx98MFdddVWuuuqqJElZWVkmT56c733ve+nfv3922GGHnH322enRo0cmTJjQtMVDE6qtrc21116bY445JpWV/xs/eN7Aho0fPz7f//7306tXrwwZMiQPP/xwLrnkkhx//PFJPG+glLvuuiuFQiEDBw7MkiVLcuaZZ2bQoEE57rjjGm7cFBpBkg1u1157bbHPO++8U/jGN75R6NSpU6FNmzaFQw45pLBs2bLGKAeaheOPP77Qu3fvQsuWLQudO3cu7LvvvoXf//73xePGDHw8n//85wunnXZa8bOxA3UdccQRhe7duxdatmxZ2G677QpHHHFEYcmSJcXjxgxs2H/8x38Uhg4dWqiqqioMGjSocNVVV9U5XltbWzj77LMLXbt2LVRVVRX23XffwpNPPtlE1cInw1133VVIssGx4HkD61u1alXhtNNOK/Tq1avQqlWrwo477lj49re/Xaiuri728byB9f3mN78p7LjjjoWWLVsWunXrVpg0aVJhxYoVxeMNMW7KCoVCoeHzfwAAAAAAaP4a9cWiAAAAAADQnAnRAQAAAACgBCE6AAAAAACUIEQHAAAAAIAShOgAAAAAAFCCEB0AAAAAAEoQogMAAAAAQAlCdAAAAAAAKEGIDgAAAAAAJQjRAQBgE8yaNSsdO3bcLPd68skn061bt7z55pubdJ0+ffrkxz/+ccMU1YjuvPPO7LzzzqmtrW3qUgAA+CcmRAcA4BPv2GOPTVlZWcrKytKiRYt07do1X/jCF3LNNdds1oB1Q+HzEUcckaeeemqz3H/atGk55ZRT0q5du026zvz58/Mv//IvDVTVe84777zsvPPODXrN/fffPy1atMivf/3rBr0uAADUhxAdAIBmYf/998+yZcvy3HPPZfbs2Rk9enROO+20HHTQQVm3bt1GX7dQKGzS+a1bt06XLl02+vyP64UXXsjtt9+eY489dpOv1blz57Rp02bTi9oMjj322Fx66aVNXQYAAP/EhOgAADQLVVVV6datW7bbbruMHDky3/rWt3Lrrbdm9uzZmTVrVpLkueeeS1lZWRYtWlQ8b8WKFSkrK8ucOXOSJHPmzElZWVlmz56dXXfdNVVVVbn//vvzzDPP5OCDD07Xrl3Ttm3bfOpTn8of/vCH4nX23nvvPP/885kyZUpxVnyy4eVcZs6cmb59+6Zly5YZOHBgfvnLX9Y5XlZWlp/97Gc55JBD0qZNm/Tv3z+33Xbbh37/3/72txkxYkS22267Ytv797799tszcODAtGnTJoceemjefvvt/OIXv0ifPn3SqVOnnHrqqampqSme948z6j+qng19x9/97nd1foPp06fnkUceKf427/83WbFiRU444YR07tw57du3zz777JNHHnmkeJ1HHnkko0ePTrt27dK+ffvsuuuuWbBgQfH4+PHjs2DBgjzzzDMf+vsAAEBjEaIDANBs7bPPPhkxYkRuvvnmep971lln5cILL8zjjz+e4cOHZ/Xq1TnggANyzz335OGHH87++++f8ePH54UXXkiS3Hzzzdl+++3z3e9+N8uWLcuyZcs2eN1bbrklp512Wk4//fQsXrw4J510Uo477rjcd999dfpNnz49hx9+eP77v/87BxxwQCZOnJi///3vJev9r//6r+y2227rtb/99tu59NJLc8MNN+TOO+/MnDlzcsghh+SOO+7IHXfckV/+8pe58sorc9NNN33o71Hfej7oiCOOyOmnn54hQ4YUf5sjjjgiSXLYYYdl+fLlmT17dhYuXJiRI0dm3333LV574sSJ2X777TN//vwsXLgwZ511Vlq0aFG8dq9evdK1a9f813/918eqBQAAGpoQHQCAZm3QoEF57rnn6n3ed7/73XzhC19I3759s/XWW2fEiBE56aSTMnTo0PTv3z/nn39++vbtW5yRvfXWW6eioiLt2rVLt27d0q1btw1e96KLLsqxxx6bb3zjGxkwYECmTp2aL37xi7nooovq9Dv22GNz5JFHpl+/frnggguyevXqPPjggyXrff7559OjR4/12teuXZuZM2dml112yec+97kceuihuf/++/Pzn/88gwcPzkEHHZTRo0evF+L/o/rW80GtW7dO27ZtU1lZWfxtWrdunfvvvz8PPvhgbrzxxuy2227p379/LrroonTs2LEY6r/wwgsZM2ZMBg0alP79++ewww7LiBEj6ly/R48eef755z9WLQAA0NCE6AAANGuFQqG4rEh9/OOs7tWrV+eMM87ITjvtlI4dO6Zt27Z5/PHHizPRP67HH388e+65Z522PffcM48//nidtuHDhxf3t9pqq7Rv3z7Lly8ved133nknrVq1Wq+9TZs26du3b/Fz165d06dPn7Rt27ZO24dde2Pq+TgeeeSRrF69Ottss03atm1b3J599tni8ixTp07NCSeckDFjxuTCCy/c4LItrVu3zttvv71JtQAAwMaqbOoCAABgUzz++OPZYYcdkiTl5e/NESkUCsXja9eu3eB5W221VZ3PZ5xxRu6+++5cdNFF6devX1q3bp1DDz00a9asaZS6P7hkSfLeuuS1tbUl+2+77bZ54403PtZ16nvtj6qnvLy8zm+alP5dP2j16tXp3r17cT36D3p/jfXzzjsvRx11VP7zP/8zs2fPzrnnnpsbbrghhxxySLHv3//+93Tu3Pkj7wcAAI3BTHQAAJqte++9N48++mi+9KUvJUkxaP3geuUffMnoh/nTn/6UY489NoccckiGDRuWbt26rbdMTMuWLeu8oHNDdtppp/zpT39a79qDBw/+WHWUsssuu+Svf/3rJl1jY3Xu3Dlvvvlm3nrrrWLbP/6uG/ptRo4cmZdffjmVlZXp169fnW3bbbct9hswYECmTJmS3//+9/niF7+Ya6+9tnjs3XffzTPPPJNddtmlcb4cAAB8BCE6AADNQnV1dV5++eW8+OKLeeihh3LBBRfk4IMPzkEHHZSjjz46yXvLfnzmM58pvjB07ty5+c53vvOxrt+/f//cfPPNWbRoUR555JEcddRR683e7tOnT/74xz/mxRdfzGuvvbbB65x55pmZNWtWZs6cmaeffjqXXHJJbr755pxxxhmb9P3Hjh2befPmfWSI3xh23333tGnTJt/61rfyzDPP5LrrrsusWbPq9OnTp0+effbZLFq0KK+99lqqq6szZsyYjBo1KhMmTMjvf//7PPfcc/nzn/+cb3/721mwYEHeeeednHzyyZkzZ06ef/75/OlPf8r8+fOz0047Fa/7wAMPpKqqKqNGjdrM3xoAAN4jRAcAoFm4884707179/Tp0yf7779/7rvvvlx66aW59dZbU1FRUex3zTXXZN26ddl1110zefLkfO973/tY17/kkkvSqVOn7LHHHhk/fnzGjh2bkSNH1unz3e9+N88991z69u1bcnmRCRMm5Cc/+UkuuuiiDBkyJFdeeWWuvfba7L333hv93ZNk3LhxqayszB/+8IdNus7G2HrrrfOrX/0qd9xxR4YNG5brr78+5513Xp0+X/rSl7L//vtn9OjR6dy5c66//vqUlZXljjvuyOc+97kcd9xxGTBgQL785S/n+eefT9euXVNRUZHXX389Rx99dAYMGJDDDz8848aNy/Tp04vXvf766zNx4sS0adNmM39rAAB4T1nhHxc3BAAAPpEuv/zy3HbbbbnrrruaupTN4rXXXsvAgQOzYMGC4rr3AACwuXmxKAAANBMnnXRSVqxYkTfffDPt2rVr6nIa3XPPPZf/+3//rwAdAIAmZSY6AAAAAACUYE10AAAAAAAoQYgOAAAAAAAlCNEBAAAAAKAEIToAAAAAAJQgRAcAAAAAgBKE6AAAAAAAUIIQHQAAAAAAShCiAwAAAABACUJ0AAAAAAAo4f8Hov0hn553noYAAAAASUVORK5CYII=",
"text/plain": [
"<Figure size 1500x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"colors = [\n",
" color.convert(\"srgb\")\n",
" for color in Color.steps(['lch(75% 50 0)', 'lch(75% 50 300)'], steps=len(destinations), space='lch', hue='longer')\n",
"]\n",
"\n",
"colorful_destinations = list(zip(destinations, colors))\n",
"\n",
"for label, start_loc in all_starts.items():\n",
" fig, axes = plt.subplots(1, 1, figsize=(15, 3))\n",
" \n",
" for durations, dest in zip(all_trips[label], colorful_destinations):\n",
" axes.hist(durations, bins=20, alpha=0.7, color=dest[1].to_string(hex=True))\n",
" axes.axvline(np.mean(durations), color=dest[1].to_string(hex=True), linestyle='--', \n",
" label=f'{dest[0]} mean: {np.mean(durations):.0f}m')\n",
" \n",
" axes.set_title(label)\n",
" axes.set_xlabel('Duration (minutes)')\n",
" axes.legend()\n",
"\n",
" axes.set_xlim(20, 90)\n",
" \n",
" plt.tight_layout()\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7bf48fc2-1cae-448a-ac20-fc7afaa82c9b",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Fresh Python3.12",
"language": "python",
"name": "fresh-python312"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}