Alexander Hess
1ee63daeb9
Add a short title/description into the filename of a notebook holding exercises. In the past, students often did not find the exercise files right away in class.
1502 lines
51 KiB
Text
1502 lines
51 KiB
Text
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_mb.png\">](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/11_classes/01_exercises.ipynb)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Chapter 11: Classes & Instances (Coding Exercises)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"The exercises below assume that you have read the [first part <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_nb.png\">](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/11_classes/00_content.ipynb) of Chapter 11.\n",
|
|
"\n",
|
|
"The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Berlin Tourist Guide: A Traveling Salesman Problem"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"This notebook is a hands-on and tutorial-like application to show how to load data from web services like [Google Maps](https://developers.google.com/maps) and use them to solve a logistics problem, namely a **[Traveling Salesman Problem <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Traveling_salesman_problem)**.\n",
|
|
"\n",
|
|
"Imagine that a tourist lands at Berlin's [Tegel Airport <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Berlin_Tegel_Airport) in the morning and has his \"connecting\" flight from [Schönefeld Airport <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Berlin_Sch%C3%B6nefeld_Airport) in the evening. By the time, the flights were scheduled, the airline thought that there would be only one airport in Berlin.\n",
|
|
"\n",
|
|
"Having never been in Berlin before, the tourist wants to come up with a plan of sights that he can visit with a rental car on his way from Tegel to Schönefeld.\n",
|
|
"\n",
|
|
"With a bit of research, he creates a `list` of `sights` like below."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"arrival = \"Berlin Tegel Airport (TXL), Berlin\"\n",
|
|
"\n",
|
|
"sights = [\n",
|
|
" \"Alexanderplatz, Berlin\",\n",
|
|
" \"Brandenburger Tor, Pariser Platz, Berlin\",\n",
|
|
" \"Checkpoint Charlie, Friedrichstraße, Berlin\",\n",
|
|
" \"Kottbusser Tor, Berlin\",\n",
|
|
" \"Mauerpark, Berlin\",\n",
|
|
" \"Siegessäule, Berlin\",\n",
|
|
" \"Reichstag, Platz der Republik, Berlin\",\n",
|
|
" \"Soho House Berlin, Torstraße, Berlin\",\n",
|
|
" \"Tempelhofer Feld, Berlin\",\n",
|
|
"]\n",
|
|
"\n",
|
|
"departure = \"Berlin Schönefeld Airport (SXF), Berlin\""
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"With just the street addresses, however, he cannot calculate a route. He needs `latitude`-`longitude` coordinates instead. While he could just open a site like [Google Maps](https://www.google.com/maps) in a web browser, he wonders if he can download the data with a bit of Python code using a [web API <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Web_API) offered by [Google](https://www.google.com).\n",
|
|
"\n",
|
|
"So, in this notebook, we solve the entire problem with code."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Geocoding"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"In order to obtain coordinates for the given street addresses above, a process called **geocoding**, we use the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start).\n",
|
|
"\n",
|
|
"**Q1**: Familiarize yourself with this [documentation](https://developers.google.com/maps/documentation/geocoding/start), register a developer account, create a project, and [create an API key](https://console.cloud.google.com/apis/credentials) that is necessary for everything to work! Then, [enable the Geocoding API](https://console.developers.google.com/apis/library/geocoding-backend.googleapis.com) and link a [billing account](https://console.developers.google.com/billing)!\n",
|
|
"\n",
|
|
"Info: The first 200 Dollars per month are not charged (cf., [pricing page](https://cloud.google.com/maps-platform/pricing/)), so no costs will incur for this tutorial. You must sign up because Google simply wants to know the people using its services.\n",
|
|
"\n",
|
|
"**Q2**: Assign the API key as a `str` object to the `key` variable!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"key = \" < your API key goes here > \""
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"To use external web services, our application needs to make HTTP requests just like web browsers do when surfing the web.\n",
|
|
"\n",
|
|
"We do not have to implement this on our own. Instead, we use the official Python Client for the Google Maps Services provided by Google in one of its corporate [GitHub repositories <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/googlemaps).\n",
|
|
"\n",
|
|
"**Q3**: Familiarize yourself with the [googlemaps <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/googlemaps/google-maps-services-python) package! Then, install it with the `pip` command line tool!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"!pip install googlemaps"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q4**: Finish the following code cells and instantiate a `Client` object named `api`! Use the `key` from above. `api` provides us with a lot of methods to talk to the API."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import googlemaps"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"api = googlemaps.Client(...)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"api"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"type(api)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q5**: Execute the next code cell to list the methods and attributes on the `api` object!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"[x for x in dir(api) if not x.startswith(\"_\")]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"To obtain all kinds of information associated with a street address, we call the `geocode()` method with the address as the sole argument.\n",
|
|
"\n",
|
|
"For example, let's search for Brandenburg Gate. Its street address is `\"Brandenburger Tor, Pariser Platz, Berlin\"`.\n",
|
|
"\n",
|
|
"**Q6**: Execute the next code cell!\n",
|
|
"\n",
|
|
"Hint: If you get an error message, follow the instructions in it to debug it.\n",
|
|
"\n",
|
|
"If everything works, we receive a `list` with a single `dict` in it. That means the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) only knows about one place at the address. Unfortunately, the `dict` is pretty dense and hard to read."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q7**: Capture the first and only search result in the `brandenburg_gate` variable and \"pretty print\" it with the help of the [pprint() <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/pprint.html#pprint.pprint) function in the [pprint <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/pprint.html) module in the [standard library <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/index.html)!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"response = api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate = ..."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"The `dict` has several keys that are of use for us: `\"formatted_address\"` is a cleanly formatted version of the address. `\"geometry\"` is a nested `dict` with several `lat`-`lng` coordinates representing the place where `\"location\"` is the one we need for our calculations. Lastly, `\"place_id\"` is a unique identifier that allows us to obtain further information about the address from other Google APIs."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from pprint import pprint"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"pprint(brandenburg_gate)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Place` Class"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"To keep our code readable and maintainable, we create a `Place` class to manage the API results in a clean way.\n",
|
|
"\n",
|
|
"The `.__init__()` method takes a `street_address` (e.g., an element of `sights`) and a `client` argument (e.g., an object like `api`) and stores them on `self`. The place's `.name` is parsed out of the `street_address` as well: It is the part before the first comma. Also, the instance attributes `.latitude`, `.longitude`, and `.place_id` are initialized to `None`.\n",
|
|
"\n",
|
|
"**Q8**: Finish the `.__init__()` method according to the description!\n",
|
|
"\n",
|
|
"The `.sync_from_google()` method uses the internally kept `client` and synchronizes the place's state with the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start). In particular, it updates the `.address` with the `formatted_address` and stores the values for `.latitude`, `.longitude`, and `.place_id`. It enables method chaining.\n",
|
|
"\n",
|
|
"**Q9**: Implement the `.sync_from_google()` method according to the description!\n",
|
|
"\n",
|
|
"**Q10**: Add a read-only `.location` property on the `Place` class that returns the `.latitude` and `.longitude` as a `tuple`!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Place:\n",
|
|
" \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n",
|
|
"\n",
|
|
" # answer to Q8\n",
|
|
" def __init__(self, street_address, *, client):\n",
|
|
" \"\"\"Create a new place.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" street_address (str): street address of the place\n",
|
|
" client (googlemaps.Client): access to the Google Maps Geocoding API\n",
|
|
" \"\"\"\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
"\n",
|
|
" def __repr__(self):\n",
|
|
" cls, name = self.__class__.__name__, self.name\n",
|
|
" synced = \" [SYNCED]\" if self.place_id else \"\"\n",
|
|
" return f\"<{cls}: {name}{synced}>\"\n",
|
|
"\n",
|
|
" # answer to Q9\n",
|
|
" def sync_from_google(self):\n",
|
|
" \"\"\"Download the place's coordinates and other info.\"\"\"\n",
|
|
" response = ...\n",
|
|
" first_hit = ...\n",
|
|
" ... = first_hit[...]\n",
|
|
" ... = first_hit[...][...][...]\n",
|
|
" ... = first_hit[...][...][...]\n",
|
|
" ... = first_hit[...]\n",
|
|
" return ...\n",
|
|
"\n",
|
|
" # answer to Q10\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ..."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q11**: Verify that the instantiating a `Place` object works!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q12**: What do the angle brackets `<` and `>` mean in the text representation?"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
" < your answer >"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Now, we can obtain the geo-data from the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) in a clean way. As we enabled method chaining for `.sync_from_google()`, we get back the instance after calling the method.\n",
|
|
"\n",
|
|
"**Q13**: Verify that the `.sync_from_google()` method works!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate.sync_from_google()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate.address"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate.place_id"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate.location"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Place` Class (continued): Batch Synchronization"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q14**: Add an alternative constructor method named `.from_addresses()` that takes an `addresses`, a `client`, and a `sync` argument! `addresses` is a finite iterable of `str` objects (e.g., like `sights`). The method returns a `list` of `Place`s, one for each `str` in `addresses`. All `Place`s are initialized with the same `client`. `sync` is a flag and defaults to `False`. If it is set to `True`, the alternative constructor invokes the `.sync_from_google()` method on the `Place`s before returning them."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"code_folding": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Place:\n",
|
|
" \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n",
|
|
"\n",
|
|
" # answers from above\n",
|
|
"\n",
|
|
" # answer to Q14\n",
|
|
" ...\n",
|
|
" def from_addresses(cls, addresses, *, client, sync=False):\n",
|
|
" \"\"\"Create new places in a batch.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" addresses (iterable of str's): the street addresses of the places\n",
|
|
" client (googlemaps.Client): access to the Google Maps Geocoding API\n",
|
|
" Returns:\n",
|
|
" list of Places\n",
|
|
" \"\"\"\n",
|
|
" places = ...\n",
|
|
" for address in addresses:\n",
|
|
" place = ...\n",
|
|
" if sync:\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" return places"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q15**: Verify that the alternative constructor works with and without the `sync` flag!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"Place.from_addresses(sights, client=api)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"Place.from_addresses(sights, client=api, sync=True)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Visualization"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"For geo-data it always makes sense to plot them on a map. We use the third-party library [folium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/python-visualization/folium) to achieve that.\n",
|
|
"\n",
|
|
"**Q16**: Familiarize yourself with [folium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/python-visualization/folium) and install it with the `pip` command line tool!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"!pip install folium"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q17**: Execute the code cells below to create an empty map of Berlin!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import folium"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin = folium.Map(location=(52.513186, 13.3944349), zoom_start=14)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"type(berlin)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"`folium.Map` instances are shown as interactive maps in Jupyter notebooks whenever they are the last expression in a code cell."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"In order to put something on the map, [folium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/python-visualization/folium) works with so-called `Marker` objects.\n",
|
|
"\n",
|
|
"**Q18**: Review its docstring and then create a marker `m` with the location data of Brandenburg Gate! Use the `brandenburg_gate` object from above!\n",
|
|
"\n",
|
|
"Hint: You may want to use HTML tags for the `popup` argument to format the text output on the map in a nicer way. So, instead of just passing `\"Brandenburger Tor\"` as the `popup` argument, you could use, for example, `\"<b>Brandenburger Tor</b><br/>(Pariser Platz, 10117 Berlin, Germany)\"`. Then, the name appears in bold and the street address is put on the next line. You could use an f-string to parametrize the argument."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"folium.Marker?"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"m = folium.Marker(\n",
|
|
" location=...,\n",
|
|
" popup=...,\n",
|
|
" tooltip=...,\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"type(m)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q19**: Execute the next code cells that add `m` to the `berlin` map!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"m.add_to(berlin)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Place` Class (continued): Marker Representation"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q20**: Finish the `.as_marker()` method that returns a `Marker` instance when invoked on a `Place` instance! The method takes an optional `color` argument that uses [folium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/python-visualization/folium)'s `Icon` type to control the color of the marker."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Place:\n",
|
|
" \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n",
|
|
"\n",
|
|
" # answers from above\n",
|
|
"\n",
|
|
" # answer to Q20\n",
|
|
" def as_marker(self, *, color=\"blue\"):\n",
|
|
" \"\"\"Create a Marker representation of the place.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" color (str): color of the marker, defaults to \"blue\"\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" marker (folium.Marker)\n",
|
|
"\n",
|
|
" Raises:\n",
|
|
" RuntimeError: if the place is not synchronized with\n",
|
|
" the Google Maps Geocoding API\n",
|
|
" \"\"\"\n",
|
|
" if not self.place_id:\n",
|
|
" raise RuntimeError(\"must synchronize with Google first\")\n",
|
|
" return folium.Marker(\n",
|
|
" location=...,\n",
|
|
" popup=...,\n",
|
|
" tooltip=...,\n",
|
|
" icon=folium.Icon(color=color),\n",
|
|
" )"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q21**: Execute the next code cells that create a new `Place` and obtain a `Marker` for it!\n",
|
|
"\n",
|
|
"Notes: Without synchronization, we get a `RuntimeError`. `.as_marker()` can be chained right after `.sync_from_google()`"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate.as_marker()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"brandenburg_gate.sync_from_google().as_marker()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q22**: Use the alternative `.from_addresses()` constructor to create a `list` named `places` with already synced `Place`s!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"places = Place.from_addresses(sights, client=api, sync=True)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"places"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Map` Class"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"slideshow": {
|
|
"slide_type": "subslide"
|
|
}
|
|
},
|
|
"source": [
|
|
"To make [folium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/python-visualization/folium)'s `Map` class work even better with our `Place` instances, we write our own `Map` class wrapping [folium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/python-visualization/folium)'s. Then, we add further functionality to the class throughout this tutorial.\n",
|
|
"\n",
|
|
"The `.__init__()` method takes mandatory `name`, `center`, `start`, `end`, and `places` arguments. `name` is there for convenience, `center` is the map's initial center, `start` and `end` are `Place` instances, and `places` is a finite iterable of `Place` instances. Also, `.__init__()` accepts an optional `initial_zoom` argument defaulting to `12`.\n",
|
|
"\n",
|
|
"Upon initialization, a `folium.Map` instance is created and stored as an implementation detail `_map`. Also, `.__init__()` puts markers for each place on the `_map` object: `\"green\"` and `\"red\"` markers for the `start` and `end` locations and `\"blue\"` ones for the `places` to be visited. To do that, `.__init__()` invokes another `.add_marker()` method on the `Map` class, once for every `Place` object. `.add_marker()` itself invokes the `.add_to()` method on the `folium.Marker` representation of a `Place` instance and enables method chaining.\n",
|
|
"\n",
|
|
"To keep the state in a `Map` instance consistent, all passed in arguments except `name` are treated as implementation details. Otherwise, a user of the `Map` class could, for example, change the `start` attribute, which would not be reflected in the internally kept `folium.Map` object.\n",
|
|
"\n",
|
|
"**Q23**: Implement the `.__init__()` and `.add_marker()` methods on the `Map` class as described!\n",
|
|
"\n",
|
|
"**Q24**: Add a `.show()` method on the `Map` class that simply returns the internal `folium.Map` object!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Map:\n",
|
|
" \"\"\"A map with plotting and routing capabilities.\"\"\"\n",
|
|
"\n",
|
|
" # answer to Q23\n",
|
|
" def __init__(self, name, center, start, end, places, initial_zoom=12):\n",
|
|
" \"\"\"Create a new map.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" name (str): name of the map\n",
|
|
" center (float, float): coordinates of the map's center\n",
|
|
" start (Place): start of the tour\n",
|
|
" end (Place): end of the tour\n",
|
|
" places (iterable of Places): the places to be visitied\n",
|
|
" initial_zoom (integer): zoom level according to folium's\n",
|
|
" specifications; defaults to 12\n",
|
|
" \"\"\"\n",
|
|
" self.name = name\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" ... = folium.Map(...)\n",
|
|
"\n",
|
|
" # Add markers to the map.\n",
|
|
" ...\n",
|
|
" ...\n",
|
|
" for place in places:\n",
|
|
" ...\n",
|
|
"\n",
|
|
" def __repr__(self):\n",
|
|
" return f\"<Map: {self.name}>\"\n",
|
|
"\n",
|
|
" # answer to Q24\n",
|
|
" def show(self):\n",
|
|
" \"\"\"Return a folium.Map representation of the map.\"\"\"\n",
|
|
" return ...\n",
|
|
"\n",
|
|
" # answer to Q23\n",
|
|
" def add_marker(self, marker):\n",
|
|
" \"\"\"Add a marker to the map.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" marker (folium.Marker): marker to be put on the map\n",
|
|
" \"\"\"\n",
|
|
" ...\n",
|
|
" return ..."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Let's put all the sights, the two airports, and three more places, the [Bundeskanzleramt <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/German_Chancellery), the [Olympic Stadium <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Olympiastadion_%28Berlin%29), and the [East Side Gallery <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/East_Side_Gallery), on the map.\n",
|
|
"\n",
|
|
"**Q25**: Execute the next code cells to create a map of Berlin with all the places on it!\n",
|
|
"\n",
|
|
"Note: Because we implemented method chaining everywhere, the code below is only *one* expression written over several lines. It almost looks like a self-explanatory and compact \"language\" on its own."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin = (\n",
|
|
" Map(\n",
|
|
" \"Sights in Berlin\",\n",
|
|
" center=(52.5015154, 13.4066838),\n",
|
|
" start=Place(arrival, client=api).sync_from_google(),\n",
|
|
" end=Place(departure, client=api).sync_from_google(),\n",
|
|
" places=places,\n",
|
|
" initial_zoom=10,\n",
|
|
" )\n",
|
|
" .add_marker(\n",
|
|
" Place(\"Bundeskanzleramt, Willy-Brandt-Straße, Berlin\", client=api)\n",
|
|
" .sync_from_google()\n",
|
|
" .as_marker(color=\"orange\")\n",
|
|
" )\n",
|
|
" .add_marker(\n",
|
|
" Place(\"Olympiastadion Berlin\", client=api)\n",
|
|
" .sync_from_google()\n",
|
|
" .as_marker(color=\"orange\")\n",
|
|
" )\n",
|
|
" .add_marker(\n",
|
|
" Place(\"East Side Gallery, Berlin\", client=api)\n",
|
|
" .sync_from_google()\n",
|
|
" .as_marker(color=\"orange\")\n",
|
|
" )\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Distance Matrix"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Before we can find out the best order in which to visit all the sights, we must calculate the pairwise distances between all points. While Google also offers a [Directions API](https://developers.google.com/maps/documentation/directions/start) and a [Distance Matrix API](https://developers.google.com/maps/documentation/distance-matrix/start), we choose to calculate the air distances using the third-party library [geopy <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/geopy/geopy).\n",
|
|
"\n",
|
|
"**Q26**: Familiarize yourself with the [documentation](https://geopy.readthedocs.io/en/stable/) and install [geopy <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/geopy/geopy) with the `pip` command line tool!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"!pip install geopy"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"We use [geopy <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/geopy/geopy) primarily for converting the `latitude`-`longitude` coordinates into a [distance matrix <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Distance_matrix).\n",
|
|
"\n",
|
|
"Because the [earth is not flat <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Flat_Earth), [geopy <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_gh.png\">](https://github.com/geopy/geopy) provides a `great_circle()` function that calculates the so-called [orthodromic distance <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Great-circle_distance) between two places on a sphere."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from geopy.distance import great_circle"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q27**: For quick reference, read the docstring of `great_circle()` and execute the code cells below to calculate the distance between the `arrival` and the `departure`!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"great_circle?"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"tegel = Place(arrival, client=api).sync_from_google()\n",
|
|
"schoenefeld = Place(departure, client=api).sync_from_google()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"great_circle(tegel.location, schoenefeld.location)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"great_circle(tegel.location, schoenefeld.location).km"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"great_circle(tegel.location, schoenefeld.location).meters"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Place` Class (continued): Distance to another `Place`"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q28**: Finish the `distance_to()` method in the `Place` class that takes a `other` argument and returns the distance in meters! Adhere to the given docstring!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Place:\n",
|
|
" \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n",
|
|
"\n",
|
|
" # answers from above\n",
|
|
"\n",
|
|
" # answer to Q28\n",
|
|
" def distance_to(self, other):\n",
|
|
" \"\"\"Calculate the distance to another place in meters.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" other (Place): the other place to calculate the distance to\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" distance (int)\n",
|
|
"\n",
|
|
" Raises:\n",
|
|
" RuntimeError: if one of the places is not synchronized with\n",
|
|
" the Google Maps Geocoding API\n",
|
|
" \"\"\"\n",
|
|
" if not self.place_id or not other.place_id:\n",
|
|
" raise RuntimeError(\"must synchronize places with Google first\")\n",
|
|
" return ..."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q29**: Execute the code cells below to test the new feature!\n",
|
|
"\n",
|
|
"Note: If done right, object-oriented code reads almost like plain English."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"tegel = Place(arrival, client=api).sync_from_google()\n",
|
|
"schoenefeld = Place(departure, client=api).sync_from_google()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"tegel.distance_to(schoenefeld)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q30**: Execute the next code cell to instantiate the `Place`s in `sights` again!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"places = Place.from_addresses(sights, client=api, sync=True)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"places"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Map` Class (continued): Pairwise Distances"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Now, we add a read-only `distances` property on our `Map` class. As we are working with air distances, these are *symmetric* which reduces the number of distances we must calculate.\n",
|
|
"\n",
|
|
"To do so, we use the [combinations() <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html#itertools.combinations) generator function in the [itertools <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html) module in the [standard library <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/index.html). That produces all possible `r`-`tuple`s from an `iterable` argument. `r` is `2` in our case as we are looking at `origin`-`destination` pairs.\n",
|
|
"\n",
|
|
"Let's first look at an easy example of [combinations() <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html#itertools.combinations) to understand how it works: It gives us all the `2`-`tuple`s from a `list` of five `numbers` disregarding the order of the `tuple`s' elements."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import itertools"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"numbers = [1, 2, 3, 4, 5]\n",
|
|
"\n",
|
|
"for x, y in itertools.combinations(numbers, 2):\n",
|
|
" print(x, y)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"`distances` uses the internal `._start`, `._end`, and `._places` attributes and creates a `dict` with the keys consisting of all pairs of `Place`s and the values being their distances in meters. As this operation is rather costly, we cache the distances the first time we calculate them into a hidden instance attribute `._distances`, which must be initialized with `None` in the `.__init__()` method.\n",
|
|
"\n",
|
|
"**Q31**: Finish the `.distances` property as described!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Map:\n",
|
|
" \"\"\"A map with plotting and routing capabilities.\"\"\"\n",
|
|
"\n",
|
|
" # answers from above with a tiny adjustment\n",
|
|
"\n",
|
|
" # answer to Q31\n",
|
|
" ...\n",
|
|
" def distances(self):\n",
|
|
" \"\"\"Return a dict with the pairwise distances of all places.\n",
|
|
"\n",
|
|
" Implementation note: The results of the calculations are cached.\n",
|
|
" \"\"\"\n",
|
|
" if not self._distances:\n",
|
|
" distances = ...\n",
|
|
" all_pairs = itertools.combinations(\n",
|
|
" ...,\n",
|
|
" r=2,\n",
|
|
" )\n",
|
|
" for origin, destination in all_pairs:\n",
|
|
" distance = ...\n",
|
|
" distances[origin, destination] = distance\n",
|
|
" distances[destination, origin] = distance\n",
|
|
" self._distances = ...\n",
|
|
" return ..."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"We pretty print the total distance matrix."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin = Map(\n",
|
|
" \"Berlin\",\n",
|
|
" center=(52.5015154, 13.4066838),\n",
|
|
" start=Place(arrival, client=api).sync_from_google(),\n",
|
|
" end=Place(departure, client=api).sync_from_google(),\n",
|
|
" places=places,\n",
|
|
" initial_zoom=10,\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"pprint(berlin.distances)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"How can we be sure the matrix contains all possible pairs? As we have 9 `sights` plus the `start` and the `end` of the tour, we conclude that there must be `11 * 10 = 110` distances excluding the `0` distances of a `Place` to itself that are not in the distance matrix."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"n_places = len(places) + 2\n",
|
|
"\n",
|
|
"n_places * (n_places - 1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"len(berlin.distances)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Route Optimization"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Let us find the cost minimal order of traveling from the `arrival` airport to the `departure` airport while visiting all the `sights`.\n",
|
|
"\n",
|
|
"This problem can be expressed as finding the shortest so-called [Hamiltonian path <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Hamiltonian_path) from the `start` to `end` on the `Map` (i.e., a path that visits each intermediate node exactly once). With the \"hack\" of assuming the distance of traveling from the `end` to the `start` to be `0` and thereby effectively merging the two airports into a single node, the problem can be viewed as a so-called [traveling salesman problem <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Traveling_salesman_problem) (TSP).\n",
|
|
"\n",
|
|
"The TSP is a hard problem to solve but also well studied in the literature. Assuming symmetric distances, a TSP with $n$ nodes has $\\frac{(n-1)!}{2}$ possible routes. $(n-1)$ because any node can be the `start` / `end` and divided by $2$ as the problem is symmetric.\n",
|
|
"\n",
|
|
"Starting with about $n = 20$, the TSP is almost impossible to solve exactly in a reasonable amount of time. Luckily, we do not have that many `sights` to visit, and so we use a [brute force <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Brute-force_search) approach and simply loop over all possible routes to find the shortest.\n",
|
|
"\n",
|
|
"In the case of our tourist, we \"only\" need to try out `181_440` possible routes because the two airports are effectively one node and $n$ becomes $10$."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import math"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"math.factorial(len(places) + 1 - 1) // 2"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Analyzing the problem a bit further, all we need is a list of [permutations <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_wiki.png\">](https://en.wikipedia.org/wiki/Permutation) of the sights as the two airports are always the first and last location.\n",
|
|
"\n",
|
|
"The [permutations() <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html#itertools.permutations) generator function in the [itertools <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html) module in the [standard library <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/index.html) helps us with the task. Let's see an example to understand how it works."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"numbers = [1, 2, 3]\n",
|
|
"\n",
|
|
"for permutation in itertools.permutations(numbers):\n",
|
|
" print(permutation)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"However, if we use [permutations() <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html#itertools.permutations) as is, we try out *redundant* routes. For example, transferred to our case, the tuples `(1, 2, 3)` and `(3, 2, 1)` represent the *same* route as the distances are symmetric and the tourist could be going in either direction. To obtain the *unique* routes, we use an `if`-clause in a \"hacky\" way by only accepting routes where the first node has a smaller value than the last. Thus, we keep, for example, `(1, 2, 3)` and discard `(3, 2, 1)`."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"for permutation in itertools.permutations(numbers):\n",
|
|
" if permutation[0] < permutation[-1]:\n",
|
|
" print(permutation)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"In order to compare `Place`s as numbers, we would have to implement, among others, the `.__eq__()` special method. Otherwise, we get a `TypeError`."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"Place(arrival, client=api) < Place(departure, client=api)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"As a quick and dirty solution, we use the `.location` property on a `Place` to do the comparison."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"Place(arrival, client=api).location < Place(departure, client=api).location"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"As the code cell below shows, combining [permutations() <img height=\"12\" style=\"display: inline-block\" src=\"../static/link/to_py.png\">](https://docs.python.org/3/library/itertools.html#itertools.permutations) with an `if`-clause results in the correct number of routes to be looped over."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"sum(\n",
|
|
" 1\n",
|
|
" for route in itertools.permutations(places)\n",
|
|
" if route[0].location < route[-1].location\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"To implement the brute force algorithm, we split the logic into two methods.\n",
|
|
"\n",
|
|
"First, we create an `.evaluate()` method that takes a `route` argument that is a sequence of `Place`s and returns the total distance of the route. Internally, this method uses the `.distances` property repeatedly, which is why we built in caching above.\n",
|
|
"\n",
|
|
"**Q32**: Finish the `.evaluate()` method as described!\n",
|
|
"\n",
|
|
"Second, we create a `.brute_force()` method that needs no arguments. It loops over all possible routes to find the shortest. As the `start` and `end` of a route are fixed, we only need to look at `permutation`s of inner nodes. Each `permutation` can then be traversed in a forward and a backward order. `.brute_force()` enables method chaining as well.\n",
|
|
"\n",
|
|
"**Q33**: Finish the `.brute_force()` method as described!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### The `Map` Class (continued): Brute Forcing the TSP"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Map:\n",
|
|
" \"\"\"A map with plotting and routing capabilities.\"\"\"\n",
|
|
"\n",
|
|
" # answers from above\n",
|
|
"\n",
|
|
" # answer to Q32\n",
|
|
" def evaluate(self, route):\n",
|
|
" \"\"\"Calculate the total distance of a route.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" route (sequence of Places): the ordered nodes in a tour\n",
|
|
"\n",
|
|
" Returns:\n",
|
|
" cost (int)\n",
|
|
" \"\"\"\n",
|
|
" cost = ...\n",
|
|
" # Loop over all pairs of consecutive places.\n",
|
|
" origin = ...\n",
|
|
" for destination in ...:\n",
|
|
" cost += self.distances[...]\n",
|
|
" ...\n",
|
|
"\n",
|
|
" return ...\n",
|
|
"\n",
|
|
" # answer to Q33\n",
|
|
" def brute_force(self):\n",
|
|
" \"\"\"Calculate the shortest route by brute force.\n",
|
|
"\n",
|
|
" The route is plotted on the folium.Map.\n",
|
|
" \"\"\"\n",
|
|
" # Assume a very high cost to begin with.\n",
|
|
" min_cost = ...\n",
|
|
" best_route = None\n",
|
|
"\n",
|
|
" # Loop over all permutations of the intermediate nodes to visit.\n",
|
|
" for permutation in ...:\n",
|
|
" # Skip redundant permutations.\n",
|
|
" if ...:\n",
|
|
" ...\n",
|
|
" # Travel through the routes in both directions.\n",
|
|
" for route in (permutation, permutation[::-1]):\n",
|
|
" # Add start and end to the route.\n",
|
|
" route = (..., *route, ...)\n",
|
|
" # Check if a route is cheaper than all routes seen before.\n",
|
|
" cost = ...\n",
|
|
" if ...:\n",
|
|
" min_cost = ...\n",
|
|
" best_route = ...\n",
|
|
"\n",
|
|
" # Plot the route on the map\n",
|
|
" folium.PolyLine(\n",
|
|
" [x.location for x in best_route],\n",
|
|
" color=\"orange\", weight=3, opacity=1\n",
|
|
" ).add_to(self._map)\n",
|
|
"\n",
|
|
" return ..."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"**Q34**: Find the best route for our tourist by executing the code cells below!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin = Map(\n",
|
|
" \"Berlin\",\n",
|
|
" center=(52.4915154, 13.4066838),\n",
|
|
" start=Place(arrival, client=api).sync_from_google(),\n",
|
|
" end=Place(departure, client=api).sync_from_google(),\n",
|
|
" places=places,\n",
|
|
" initial_zoom=12,\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"berlin.brute_force().show()"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"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.2"
|
|
},
|
|
"toc": {
|
|
"base_numbering": 1,
|
|
"nav_menu": {},
|
|
"number_sections": false,
|
|
"sideBar": true,
|
|
"skip_h1_title": true,
|
|
"title_cell": "Table of Contents",
|
|
"title_sidebar": "Contents",
|
|
"toc_cell": false,
|
|
"toc_position": {
|
|
"height": "calc(100% - 180px)",
|
|
"left": "10px",
|
|
"top": "150px",
|
|
"width": "320px"
|
|
},
|
|
"toc_section_display": true,
|
|
"toc_window_display": false
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|