diff --git a/01_elements/01_exercises_solved.ipynb b/01_elements/01_exercises_solved.ipynb new file mode 100644 index 0000000..f7ec2fa --- /dev/null +++ b/01_elements/01_exercises_solved.ipynb @@ -0,0 +1,204 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/01_elements/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/01_elements/00_content.ipynb) of Chapter 1.\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": [ + "## Printing Output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: *Concatenate* `greeting` and `audience` below with the `+` operator and print out the resulting message `\"Hello World\"` with only *one* call of the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function!\n", + "\n", + "Hint: You may have to \"add\" a space character in between `greeting` and `audience`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "greeting = \"Hello\"\n", + "audience = \"World\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello World\n" + ] + } + ], + "source": [ + "print(greeting + \" \" + audience)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: How is your answer to **Q1** an example of the concept of **operator overloading**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Read the documentation on the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function! How can you print the above message *without* concatenating `greeting` and `audience` first in *one* call of [print() ](https://docs.python.org/3/library/functions.html#print)?\n", + "\n", + "Hint: The `*objects` in the documentation implies that we can put several *expressions* (i.e., variables) separated by commas within the same call of the [print() ](https://docs.python.org/3/library/functions.html#print) function." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello World\n" + ] + } + ], + "source": [ + "print(greeting, audience)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What does the `sep=\" \"` mean in the documentation on the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function? Adjust and use it to print out the three names referenced by `first`, `second`, and `third` on *one* line separated by *commas* with only *one* call of the [print() ](https://docs.python.org/3/library/functions.html#print) function!" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "first = \"Anthony\"\n", + "second = \"Berta\"\n", + "third = \"Christian\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Anthony, Berta, Christian\n" + ] + } + ], + "source": [ + "print(first, second, third, sep=\", \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Lastly, what does the `end=\"\\n\"` mean in the documentation? Adjust and use it within the `for`-loop to print the numbers `1` through `10` on *one* line with only *one* call of the [print() ](https://docs.python.org/3/library/functions.html#print) function!" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2 3 4 5 6 7 8 9 10 " + ] + } + ], + "source": [ + "for number in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:\n", + " print(number, end=\" \")" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/02_exercises_solved.ipynb b/01_elements/02_exercises_solved.ipynb new file mode 100644 index 0000000..f4c5f78 --- /dev/null +++ b/01_elements/02_exercises_solved.ipynb @@ -0,0 +1,294 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/01_elements/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/01_elements/00_content.ipynb) of Chapter 1.\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": [ + "## Simple `for`-loops" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`for`-loops are extremely versatile in Python. That is different from many other programming languages.\n", + "\n", + "As shown in the first example in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/01_elements/00_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List), we can create a `list` like `numbers` and loop over the numbers in it on a one-by-one basis." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Fill in the *condition* of the `if` statement such that only numbers divisible by `3` are printed! Adjust the call of the [print() ](https://docs.python.org/3/library/functions.html#print) function such that the `for`-loop prints out all the numbers on *one* line of output!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3 12 6 9 " + ] + } + ], + "source": [ + "for number in numbers:\n", + " if number % 3 == 0:\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of looping over an *existing* object referenced by a variable like `numbers`, we may also create a *new* object within the `for` statement and loop over it directly. For example, below we write out the `list` object as a *literal*.\n", + "\n", + "Generically, the objects contained in a `list` objects are referred to as its **elements**. We reflect that in the name of the *target* variable `element` that is assigned a different number in every iteration of the `for`-loop. While we could use *any* syntactically valid name, it is best to choose one that makes sense in the context (e.g., `number` in `numbers`).\n", + "\n", + "**Q2**: Fill in the condition of the `if` statement such that only numbers consisting of *one* digit are printed out! As before, print out all the numbers on *one* line of output!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 8 5 3 2 6 9 1 4 " + ] + } + ], + "source": [ + "for element in [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]:\n", + " if element // 10 == 0:\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 8 5 3 2 6 9 1 4 " + ] + } + ], + "source": [ + "# Alternative\n", + "for element in [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]:\n", + " if element < 10:\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An easy way to loop over a `list` object in a sorted manner, is to wrap it with the built-in [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function.\n", + "\n", + "**Q3**: Fill in the condition of the `if` statement such that only odd numbers are printed out! Put all the numbers on *one* line of output!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 3 5 7 9 11 " + ] + } + ], + "source": [ + "for number in sorted(numbers):\n", + " if number % 2 == 1:\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Whenever we want to loop over numbers representing a [series ](https://en.wikipedia.org/wiki/Series_%28mathematics%29) in the mathematical sense (i.e., a rule to calculate the next number from its predecessor), we may be able to use the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in.\n", + "\n", + "For example, to loop over the whole numbers from `0` to `9` (both including) in order, we could write them out in a `list` like below.\n", + "\n", + "**Q4**: Fill in the call to the [print() ](https://docs.python.org/3/library/functions.html#print) function such that all the numbers are printed on *one* line ouf output!" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 5 6 7 8 9 " + ] + } + ], + "source": [ + "for number in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Read the documentation on the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in! It may be used with either one, two, or three expressions \"passed\" in. What do `start`, `stop`, and `step` mean? Fill in the calls to [range() ](https://docs.python.org/3/library/functions.html#func-range) and [print() ](https://docs.python.org/3/library/functions.html#print) to mimic the output of **Q4**!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 5 6 7 8 9 " + ] + } + ], + "source": [ + "for number in range(10):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Fill in the calls to [range() ](https://docs.python.org/3/library/functions.html#func-range) and [print() ](https://docs.python.org/3/library/functions.html#print) to print out *all* numbers from `1` to `10` (both including)!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2 3 4 5 6 7 8 9 10 " + ] + } + ], + "source": [ + "for number in range(1, 11):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Fill in the calls to [range() ](https://docs.python.org/3/library/functions.html#func-range) and [print() ](https://docs.python.org/3/library/functions.html#print) to print out the *even* numbers from `1` to `10` (both including)! Do *not* use an `if` statement to accomplish this!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 4 6 8 10 " + ] + } + ], + "source": [ + "for number in range(2, 11, 2):\n", + " print(number, end=\" \")" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/04_exercises_solved.ipynb b/01_elements/04_exercises_solved.ipynb new file mode 100644 index 0000000..19e2963 --- /dev/null +++ b/01_elements/04_exercises_solved.ipynb @@ -0,0 +1,278 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/01_elements/04_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/01_elements/03_content.ipynb) Chapter 1.\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": [ + "## Python as a Calculator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [volume of a sphere ](https://en.wikipedia.org/wiki/Sphere) is defined as $\\frac{4}{3} * \\pi * r^3$.\n", + "\n", + "**Q1**: Calculate it for `r = 2.88` and approximate $\\pi$ with `pi = 3.14`!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "pi = 3.14\n", + "r = 2.88" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100.01055743999999" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(4 / 3) * pi * r ** 3" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "While Python may be used as a calculator, it behaves a bit differently compared to calculator apps that phones or computers come with and that we are accustomed to.\n", + "\n", + "A major difference is that Python \"forgets\" intermediate results that are not assigned to variables. On the contrary, the calculators we work with outside of programming always keep the last result and allow us to use it as the first input for the next calculation.\n", + "\n", + "One way to keep on working with intermediate results in Python is to write the entire calculation as just *one* big expression that is composed of many sub-expressions representing the individual steps in our overall calculation.\n", + "\n", + "**Q2.1**: Given `a` and `b` like below, subtract the smaller `a` from the larger `b`, divide the difference by `9`, and raise the result to the power of `2`! Use operators that preserve the `int` type of the final result! The entire calculations *must* be placed within *one* code cell.\n", + "\n", + "Hint: You may need to group sub-expressions with parentheses `(` and `)`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "a = 42\n", + "b = 87" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "((b - a) // 9) ** 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below contains nothing but a single underscore `_`. In both, a Python command-line prompt and Jupyter notebooks, the variable `_` is automatically updated and always references the object to which the *last* expression executed evaluated to.\n", + "\n", + "**Q2.2**: Execute the code cell below! It should evaluate to the *same* result as the previous code cell (i.e., your answer to **Q2.1** assuming you go through this notebook in order)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2.3**: Implement the same overall calculation as in your answer to **Q2.1** in several independent steps (i.e., code cells)! Use only *one* operator per code cell!\n", + "\n", + "Hint: You should need *two* more code cells after the `b - a` one immediately below. If you *need* to use parentheses, you must be doing something wrong." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b - a" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_ // 9" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_ ** 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.1**: When answering the questions above, you should have used only **expressions** in the code cells. What are expressions syntactically?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.2**: The code cells that provide the numbers to work with contain **statements** that are *not* expressions. What are statements? How are they different from expressions?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/02_functions/01_exercises_solved.ipynb b/02_functions/01_exercises_solved.ipynb new file mode 100644 index 0000000..609377a --- /dev/null +++ b/02_functions/01_exercises_solved.ipynb @@ -0,0 +1,343 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/02_functions/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 2: Functions & Modularization (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/02_functions/00_content.ipynb) of Chapter 2.\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": [ + "## Volume of a Sphere" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: The [volume of a sphere ](https://en.wikipedia.org/wiki/Sphere) is defined as $\\frac{4}{3} * \\pi * r^3$. Calculate this value for $r=10.0$ and round it to 10 digits after the comma.\n", + "\n", + "Hints:\n", + "- use an appropriate approximation for $\\pi$\n", + "- you may use the [standard library ](https://docs.python.org/3/library/index.html) to do so if you have already looked at the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/02_functions/02_content.ipynb) of Chapter 2." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "r = 10.0" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4188.7902047864" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "round((4 / 3) * math.pi * r ** 3, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Encapsulate the logic into a function `sphere_volume()` that takes one *positional* argument `radius` and one *keyword-only* argument `digits` defaulting to `5`. The volume should be returned as a `float` object under *all* circumstances." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def sphere_volume(radius, *, digits=5):\n", + " \"\"\"Calculate the volume of a sphere.\n", + "\n", + " Args:\n", + " radius (float): radius of the sphere\n", + " digits (optional, int): number of digits\n", + " for rounding the resulting volume\n", + "\n", + " Returns:\n", + " volume (float)\n", + " \"\"\"\n", + " return round((4 / 3) * math.pi * radius ** 3, digits)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Evaluate the function with `radius = 100.0` and 1, 5, 10, 15, and 20 digits respectively." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "radius = 100.0" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4188790.2" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sphere_volume(radius, digits=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4188790.20479" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sphere_volume(radius) # or sphere_volume(radius, digits=5)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4188790.2047863905" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sphere_volume(radius, digits=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4188790.2047863905" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sphere_volume(radius, digits=15)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4188790.2047863905" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sphere_volume(radius, digits=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What observation do you make?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Using the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in, write a `for`-loop and calculate the volume of a sphere with `radius = 42.0` for all `digits` from `1` through `20`. Print out each volume on a separate line.\n", + "\n", + "Note: This is the first task where you need to use the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "radius = 42.0" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 310339.1\n", + "2 310339.09\n", + "3 310339.089\n", + "4 310339.0887\n", + "5 310339.08869\n", + "6 310339.088692\n", + "7 310339.0886922\n", + "8 310339.08869221\n", + "9 310339.088692214\n", + "10 310339.0886922141\n", + "11 310339.0886922141\n", + "12 310339.0886922141\n", + "13 310339.0886922141\n", + "14 310339.0886922141\n", + "15 310339.0886922141\n", + "16 310339.0886922141\n", + "17 310339.0886922141\n", + "18 310339.0886922141\n", + "19 310339.0886922141\n", + "20 310339.0886922141\n" + ] + } + ], + "source": [ + "for digits in range(1, 21):\n", + " print(digits, sphere_volume(radius, digits=digits))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: What lesson do you learn about the `float` type?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/03_conditionals/01_exercises_solved.ipynb b/03_conditionals/01_exercises_solved.ipynb new file mode 100644 index 0000000..1ae912a --- /dev/null +++ b/03_conditionals/01_exercises_solved.ipynb @@ -0,0 +1,283 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/03_conditionals/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 3: Conditionals & Exceptions (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/03_conditionals/00_content.ipynb).\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": [ + "## Discounting Customer Orders" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Write a function `discounted_price()` that takes the positional arguments `unit_price` (of type `float`) and `quantity` (of type `int`) and implements a discount scheme for a line item in a customer order as follows:\n", + "\n", + "- if the unit price is over 100 dollars, grant 10% relative discount\n", + "- if a customer orders more than 10 items, one in every five items is for free\n", + "\n", + "Only one of the two discounts is granted, whichever is better for the customer.\n", + "\n", + "The function should then return the overall price for the line item. Do not forget to round appropriately." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def discounted_price(unit_price, quantity):\n", + " \"\"\"Calculate the price of a line item in an order.\n", + "\n", + " Args:\n", + " unit_price (float): price of one ordered item\n", + " quantity (int): number of items ordered\n", + "\n", + " Returns:\n", + " line_item_price (float)\n", + " \"\"\"\n", + " # One could implement type casting\n", + " # to ensure correct input data.\n", + " unit_price = float(unit_price)\n", + " quantity = int(quantity)\n", + "\n", + " # Calculate the line item revenue if only\n", + " # the first discount scheme is applied.\n", + " if unit_price <= 100:\n", + " scheme_a = unit_price * quantity\n", + " else:\n", + " scheme_a = 0.9 * unit_price * quantity\n", + "\n", + " # Calculate the line item revenue if only\n", + " # the second discount scheme is applied.\n", + " # \"One in every five\" means we need to figure out\n", + " # how many full groups of five are contained.\n", + " if quantity <= 10:\n", + " scheme_b = unit_price * quantity\n", + " else:\n", + " groups_of_five = quantity // 5\n", + " scheme_b = unit_price * (quantity - groups_of_five)\n", + "\n", + " # Choose the better option for the customer.\n", + " return round(min(scheme_a, scheme_b), 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Calculate the final price for the following line items of an order:\n", + "- $7$ smartphones @ $99.00$ USD\n", + "- $3$ workstations @ $999.00$ USD\n", + "- $19$ GPUs @ $879.95$ USD\n", + "- $14$ Raspberry Pis @ $35.00$ USD" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "693.0" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "discounted_price(99, 7)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2697.3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "discounted_price(999, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "14079.2" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "discounted_price(879.95, 19)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "420.0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "discounted_price(35, 14)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Calculate the last two line items with order quantities of $20$ and $15$. What do you observe?" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "14079.2" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "discounted_price(879.95, 20)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "420.0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "discounted_price(35, 15)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Looking at the `if`-`else`-logic in the function, why do you think the four example line items in **Q2** were chosen as they were?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/03_conditionals/02_exercises_solved.ipynb b/03_conditionals/02_exercises_solved.ipynb new file mode 100644 index 0000000..72a61a5 --- /dev/null +++ b/03_conditionals/02_exercises_solved.ipynb @@ -0,0 +1,171 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/03_conditionals/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 3: Conditionals & Exceptions (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/03_conditionals/00_content.ipynb).\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": [ + "## Fizz Buzz" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The kids game [Fizz Buzz ](https://en.wikipedia.org/wiki/Fizz_buzz) is said to be often used in job interviews for entry-level positions. However, opinions vary as to how good of a test it is (cf., [source ](https://news.ycombinator.com/item?id=16446774)).\n", + "\n", + "In its simplest form, a group of people starts counting upwards in an alternating fashion. Whenever a number is divisible by $3$, the person must say \"Fizz\" instead of the number. The same holds for numbers divisible by $5$ when the person must say \"Buzz.\" If a number is divisible by both numbers, one must say \"FizzBuzz.\" Probably, this game would also make a good drinking game with the \"right\" beverages." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: First, create a list `numbers` with the numbers from 1 through 100. You could type all numbers manually, but there is, of course, a smarter way. The built-in [range() ](https://docs.python.org/3/library/functions.html#func-range) may be useful here. Read how it works in the documentation. To make the output of [range() ](https://docs.python.org/3/library/functions.html#func-range) a `list` object, you have to wrap it with the [list() ](https://docs.python.org/3/library/functions.html#func-list) built-in (i.e., `list(range(...))`)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "numbers = list(range(1, 101))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Loop over the `numbers` list and *replace* numbers for which one of the two (or both) conditions apply with text strings `\"Fizz\"`, `\"Buzz\"`, or `\"FizzBuzz\"` using the indexing operator `[]` and the assignment statement `=`.\n", + "\n", + "In [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/01_elements/03_content.ipynb#Who-am-I?-And-how-many?), we saw that Python starts indexing with `0` as the first element. Keep that in mind.\n", + "\n", + "So in each iteration of the `for`-loop, you have to determine an `index` variable as well as check the actual `number` for its divisors.\n", + "\n", + "Hint: the order of the conditions is important!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "for number in numbers:\n", + " # We know that numbers goes from 1 through 100\n", + " # so the index is just \"number minus 1\".\n", + " index = number - 1\n", + "\n", + " if (number % 3 == 0) and (number % 5 == 0):\n", + " numbers[index] = \"FizzBuzz\"\n", + " elif number % 3 == 0:\n", + " numbers[index] = \"Fizz\"\n", + " elif number % 5 == 0:\n", + " numbers[index] = \"Buzz\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Alternative: use the enumerate() built-in.\n", + "\n", + "numbers = list(range(1, 101))\n", + "\n", + "for index, number in enumerate(numbers):\n", + " # Use `number % 15 == 0` instead\n", + " # and re-order the clauses.\n", + " if number % 15 == 0:\n", + " numbers[index] = \"FizzBuzz\"\n", + " elif number % 5 == 0:\n", + " numbers[index] = \"Buzz\"\n", + " elif number % 3 == 0:\n", + " numbers[index] = \"Fizz\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Create a loop that prints out either the number or any of the Fizz Buzz substitutes in `numbers`! Do it in such a way that the output is concise!" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz " + ] + } + ], + "source": [ + "for number in numbers:\n", + " print(number, end=\" \")" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/04_iteration/01_exercises.ipynb b/04_iteration/01_exercises.ipynb index cf4b0ce..08fe309 100644 --- a/04_iteration/01_exercises.ipynb +++ b/04_iteration/01_exercises.ipynb @@ -479,7 +479,7 @@ " if _offset is not None:\n", " count += _offset\n", "\n", - " # answer to Q18\n", + " # answer to Q13\n", " hanoi_ordered(..., _offset=_offset)\n", " ...\n", " hanoi_ordered(..., _offset=count)" diff --git a/04_iteration/01_exercises_solved.ipynb b/04_iteration/01_exercises_solved.ipynb new file mode 100644 index 0000000..f65abe3 --- /dev/null +++ b/04_iteration/01_exercises_solved.ipynb @@ -0,0 +1,831 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/04_iteration/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 4: Recursion & Looping (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/04_iteration/00_content.ipynb) of Chapter 4.\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": [ + "## Towers of Hanoi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A popular example of a problem that is solved by recursion art the **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)**.\n", + "\n", + "In its basic version, a tower consisting of, for example, four disks with increasing radii, is placed on the left-most of **three** adjacent spots. In the following, we refer to the number of disks as $n$, so here $n = 4$.\n", + "\n", + "The task is to move the entire tower to the right-most spot whereby **two rules** must be obeyed:\n", + "\n", + "1. Disks can only be moved individually, and\n", + "2. a disk with a larger radius must *never* be placed on a disk with a smaller one.\n", + "\n", + "Although the **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** are a **classic** example, introduced by the mathematician [Édouard Lucas ](https://en.wikipedia.org/wiki/%C3%89douard_Lucas) already in 1883, it is still **actively** researched as this scholarly [article](https://www.worldscientific.com/doi/abs/10.1142/S1793830919300017?journalCode=dmaa&) published in January 2019 shows.\n", + "\n", + "Despite being so easy to formulate, the game is quite hard to solve.\n", + "\n", + "Below is an interactive illustration of the solution with the minimal number of moves for $n = 4$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Watch the following video by [MIT](https://www.mit.edu/)'s professor [Richard Larson](https://idss.mit.edu/staff/richard-larson/) for a comprehensive introduction.\n", + "\n", + "The [MIT Blossoms Initiative](https://blossoms.mit.edu/) is primarily aimed at high school students and does not have any prerequisites.\n", + "\n", + "The video consists of three segments, the last of which is *not* necessary to have watched to solve the tasks below. So, watch the video until 37:55." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"UuIneNBbscc\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Video Review Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Explain for the $n = 3$ case why it can be solved as a **recursion**!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: How does the number of minimal moves needed to solve a problem with three spots and $n$ disks grow as a function of $n$? How does this relate to the answer to **Q1**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: The **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** problem is of **exponential growth**. What does that mean? What does that imply for large $n$?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: The video introduces the recursive relationship $Sol(4, 1, 3) = Sol(3, 1, 2) ~ \\bigoplus ~ Sol(1, 1, 3) ~ \\bigoplus ~ Sol(3, 2, 3)$. The $\\bigoplus$ is to be interpreted as some sort of \"plus\" operation. How does this \"plus\" operation work? How does this way of expressing the problem relate to the answer to **Q1**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Naive Translation to Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As most likely the first couple of tries will result in *semantic* errors, it is advisable to have some sort of **visualization tool** for the program's output: For example, an online version of the game can be found [here](https://www.mathsisfun.com/games/towerofhanoi.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first **generalize** the mathematical relationship from above and then introduce the variable names used in our `sol()` implementation below.\n", + "\n", + "Unsurprisingly, the recursive relationship in the video may be generalized into:\n", + "\n", + "$Sol(n, o, d) = Sol(n-1, o, i) ~ \\bigoplus ~ Sol(1, o, d) ~ \\bigoplus ~ Sol(n-1, i, d)$\n", + "\n", + "$Sol(\\cdot)$ takes three \"arguments\" $n$, $o$, and $d$ and is defined with *three* references to itself that take modified versions of $n$, $o$, and $d$ in different orders. The middle reference, Sol(1, o, d), constitutes the \"end\" of the recursive definition: It is the problem of solving Towers of Hanoi for a \"tower\" of only one disk.\n", + "\n", + "While the first \"argument\" of $Sol(\\cdot)$ is a number that we refer to as `n_disks` below, the second and third \"arguments\" are merely **labels** for the spots, and we refer to the **roles** they take in a given problem as `origin` and `destination` below. Instead of labeling individual spots with the numbers `1`, `2`, and `3` as in the video, we may also call them `\"left\"`, `\"center\"`, and `\"right\"`. Both ways are equally correct! So, only the first \"argument\" of $Sol(\\cdot)$ is really a number!\n", + "\n", + "As an example, the notation $Sol(4, 1, 3)$ from above can then be \"translated\" into Python as either the function call `sol(4, 1, 3)` or `sol(4, \"left\", \"right\")`. This describes the problem of moving a tower consisting of `n_disks=4` disks from either the `origin=1` spot to the `destination=3` spot or from the `origin=\"left\"` spot to the `destination=\"right\"` spot.\n", + "\n", + "To adhere to the rules, an `intermediate` spot $i$ is needed. In `sol()` below, this is a temporary variable within a function call and *not* a parameter of the function itself." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In summary, to move a tower consisting of `n_disks` (= $n$) disks from an `origin` (= $o$) to a `destination` (= $d$), three steps must be executed:\n", + "\n", + "1. Move the tower's topmost `n_disks - 1` (= $n - 1$) disks from the `origin` (= $o$) to an `intermediate` (= $i$) spot (= **Sub-Problem 1**),\n", + "2. move the remaining and largest disk from the `origin` (= $o$) to the `destination` (= $d$), and\n", + "3. move the `n_disks - 1` (= $n - 1$) disks from the `intermediate` (= $i$) spot to the `destination` (= $d$) spot (= **Sub-Problem 2**).\n", + "\n", + "The two sub-problems themselves are solved via the same *recursive* logic." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Write your answers to **Q5** to **Q7** into the skeleton of `sol()` below.\n", + "\n", + "`sol()` takes three arguments `n_disks`, `origin`, and `destination` that mirror $n$, $o$, and $d$ above.\n", + "\n", + "For now, assume that all arguments to `sol()` are `int` objects! We generalize this into real labels further below in the `hanoi()` function.\n", + "\n", + "Once completed, `sol()` should *print* out all the moves in the correct order. For example, *print* `\"1 -> 3\"` to mean \"Move the top-most `n_disks - 1` disks from spot `1` to spot `3`.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def sol(n_disks, origin, destination):\n", + " \"\"\"A naive implementation of Towers of Hanoi.\n", + "\n", + " This function prints out the moves to solve a Towers of Hanoi problem.\n", + "\n", + " Args:\n", + " n_disks (int): number of disks in the tower\n", + " origin (int): spot of the tower at the start; 1, 2, or 3\n", + " destination (int): spot of the tower at the end; 1, 2, or 3\n", + " \"\"\"\n", + " # answer to Q5\n", + " if n_disks <= 0:\n", + " return\n", + "\n", + " # answer to Q6\n", + " if origin == 1 and destination == 2:\n", + " intermediate = 3\n", + " elif origin == 1 and destination == 3:\n", + " intermediate = 2\n", + " elif origin == 2 and destination == 1:\n", + " intermediate = 3\n", + " elif origin == 2 and destination == 3:\n", + " intermediate = 1\n", + " elif origin == 3 and destination == 1:\n", + " intermediate = 2\n", + " else: # origin == 3 and destination == 2\n", + " intermediate = 1\n", + "\n", + " # answer to Q7\n", + " sol(n_disks - 1, origin, intermediate)\n", + " print(origin, \"->\", destination)\n", + " sol(n_disks - 1, intermediate, destination)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What is the `n_disks` argument when the function reaches its **base case**? Check for the base case with a simple `if` statement and return from the function using the **early exit** pattern!\n", + "\n", + "Hint: The base case in the Python implementation may be slightly different than the one shown in the generalized mathematical relationship above!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: If not in the base case, `sol()` determines the `intermediate` spot given concrete `origin` and `destination` arguments. For example, if called with `origin=1` and `destination=2`, `intermediate` must be `3`.\n", + "\n", + "Add *one* compound `if` statement to `sol()` that has a branch for *every* possible `origin`-`destination`-pair that assigns the correct temporary spot to a variable `intermediate`.\n", + "\n", + "Hint: How many 2-tuples of 3 elements can there be if the order matters?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: `sol()` calls itself *two* more times with the correct 2-tuples chosen from the three available spots `origin`, `intermediate`, and `destination`.\n", + "\n", + "*In between* the two recursive function calls, use [print() ](https://docs.python.org/3/library/functions.html#print) to print out from where to where the \"remaining and largest\" disk has to be moved!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Execute the code cells below and confirm that the moves are correct!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 -> 3\n" + ] + } + ], + "source": [ + "sol(1, 1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 -> 2\n", + "1 -> 3\n", + "2 -> 3\n" + ] + } + ], + "source": [ + "sol(2, 1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 -> 3\n", + "1 -> 2\n", + "3 -> 2\n", + "1 -> 3\n", + "2 -> 1\n", + "2 -> 3\n", + "1 -> 3\n" + ] + } + ], + "source": [ + "sol(3, 1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 -> 2\n", + "1 -> 3\n", + "2 -> 3\n", + "1 -> 2\n", + "3 -> 1\n", + "3 -> 2\n", + "1 -> 2\n", + "1 -> 3\n", + "2 -> 3\n", + "2 -> 1\n", + "3 -> 1\n", + "2 -> 3\n", + "1 -> 2\n", + "1 -> 3\n", + "2 -> 3\n" + ] + } + ], + "source": [ + "sol(4, 1, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pythonic Refactoring" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The previous `sol()` implementation does the job, but the conditional statement is unnecessarily tedious. \n", + "\n", + "Let's create a concise `hanoi()` function that, in addition to a positional `n_disks` argument, takes three keyword-only arguments `origin`, `intermediate`, and `destination` with default values `\"left\"`, `\"center\"`, and `\"right\"`.\n", + "\n", + "Write your answers to **Q9** and **Q10** into the subsequent code cell and finalize `hanoi()`!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def hanoi(n_disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\"):\n", + " \"\"\"A Pythonic implementation of Towers of Hanoi.\n", + "\n", + " This function prints out the moves to solve a Towers of Hanoi problem.\n", + "\n", + " Args:\n", + " n_disks (int): number of disks in the tower\n", + " origin (str, optional): label for the spot of the tower at the start\n", + " intermediate (str, optional): label for the intermediate spot\n", + " destination (str, optional): label for the spot of the tower at the end\n", + " \"\"\"\n", + " # answer to Q9\n", + " if n_disks <= 0:\n", + " return\n", + "\n", + " # answer to Q10\n", + " hanoi(\n", + " n_disks - 1,\n", + " origin=origin,\n", + " intermediate=destination,\n", + " destination=intermediate,\n", + " )\n", + " print(origin, \"->\", destination)\n", + " hanoi(\n", + " n_disks - 1,\n", + " origin=intermediate,\n", + " intermediate=origin,\n", + " destination=destination,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Copy the base case from `sol()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: Instead of conditional logic, `hanoi()` calls itself *two* times with the *three* arguments `origin`, `intermediate`, and `destination` passed on in a *different* order.\n", + "\n", + "Figure out how the arguments are passed on in the two recursive `hanoi()` calls and finish `hanoi()`.\n", + "\n", + "Hint: Do not forget to use [print() ](https://docs.python.org/3/library/functions.html#print) to print out the moves!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Execute the code cells below and confirm that the moves are correct!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "left -> right\n" + ] + } + ], + "source": [ + "hanoi(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "left -> center\n", + "left -> right\n", + "center -> right\n" + ] + } + ], + "source": [ + "hanoi(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "left -> right\n", + "left -> center\n", + "right -> center\n", + "left -> right\n", + "center -> left\n", + "center -> right\n", + "left -> right\n" + ] + } + ], + "source": [ + "hanoi(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "left -> center\n", + "left -> right\n", + "center -> right\n", + "left -> center\n", + "right -> left\n", + "right -> center\n", + "left -> center\n", + "left -> right\n", + "center -> right\n", + "center -> left\n", + "right -> left\n", + "center -> right\n", + "left -> center\n", + "left -> right\n", + "center -> right\n" + ] + } + ], + "source": [ + "hanoi(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We could, of course, also use *numeric* labels for the three steps like so." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 -> 3\n", + "1 -> 2\n", + "3 -> 2\n", + "1 -> 3\n", + "2 -> 1\n", + "2 -> 3\n", + "1 -> 3\n" + ] + } + ], + "source": [ + "hanoi(3, origin=1, intermediate=2, destination=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Passing a Value \"down\" the Recursion Tree" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above `hanoi()` prints the optimal solution's moves in the correct order but fails to label each move with an order number. This is built in the `hanoi_ordered()` function below by passing on a \"private\" `_offset` argument \"down\" the recursion tree. The leading underscore `_` in the parameter name indicates that it is *not* to be used by the caller of the function. That is also why the parameter is *not* mentioned in the docstring.\n", + "\n", + "Write your answers to **Q12** and **Q13** into the subsequent code cell and finalize `hanoi_ordered()`! As the logic gets a bit \"involved,\" `hanoi_ordered()` below is almost finished." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def hanoi_ordered(n_disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\", _offset=None):\n", + " \"\"\"A Pythonic implementation of Towers of Hanoi.\n", + "\n", + " This function prints out the moves to solve a Towers of Hanoi problem.\n", + " Each move is labeled with an order number.\n", + "\n", + " Args:\n", + " n_disks (int): number of disks in the tower\n", + " origin (str, optional): label for the spot of the tower at the start\n", + " intermediate (str, optional): label for the intermediate spot\n", + " destination (str, optional): label for the spot of the tower at the end\n", + " \"\"\"\n", + " # answer to Q12\n", + " if n_disks <= 0:\n", + " return\n", + "\n", + " total = (2 ** n_disks - 1)\n", + " half = (2 ** (n_disks - 1) - 1)\n", + " count = total - half\n", + "\n", + " if _offset is not None:\n", + " count += _offset\n", + "\n", + " # answer to Q13\n", + " hanoi_ordered(\n", + " n_disks - 1,\n", + " origin=origin,\n", + " intermediate=destination,\n", + " destination=intermediate,\n", + " _offset=_offset,\n", + " )\n", + " print(count, origin, \"->\", destination)\n", + " hanoi_ordered(\n", + " n_disks - 1,\n", + " origin=intermediate,\n", + " intermediate=origin,\n", + " destination=destination,\n", + " _offset=count,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Copy the base case from the original `hanoi()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: Complete the two recursive function calls with the same arguments as in `hanoi()`! Do *not* change the already filled in `offset` arguments!\n", + "\n", + "Then, adjust the use of [print() ](https://docs.python.org/3/library/functions.html#print) from above to print out the moves with their order number!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Execute the code cells below and confirm that the order numbers are correct!" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 left -> right\n" + ] + } + ], + "source": [ + "hanoi_ordered(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 left -> center\n", + "2 left -> right\n", + "3 center -> right\n" + ] + } + ], + "source": [ + "hanoi_ordered(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 left -> right\n", + "2 left -> center\n", + "3 right -> center\n", + "4 left -> right\n", + "5 center -> left\n", + "6 center -> right\n", + "7 left -> right\n" + ] + } + ], + "source": [ + "hanoi_ordered(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 left -> center\n", + "2 left -> right\n", + "3 center -> right\n", + "4 left -> center\n", + "5 right -> left\n", + "6 right -> center\n", + "7 left -> center\n", + "8 left -> right\n", + "9 center -> right\n", + "10 center -> left\n", + "11 right -> left\n", + "12 center -> right\n", + "13 left -> center\n", + "14 left -> right\n", + "15 center -> right\n" + ] + } + ], + "source": [ + "hanoi_ordered(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, it is to be mentioned that for problem instances with a small `n_disks` argument, it is easier to collect all the moves first in a `list` object and then add the order number with the [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) built-in." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Open Question" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q15**: Conducting your own research on the internet, what can you say about generalizing the **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** problem to a setting with *more than three* landing spots?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/04_iteration/04_exercises_solved.ipynb b/04_iteration/04_exercises_solved.ipynb new file mode 100644 index 0000000..83014a5 --- /dev/null +++ b/04_iteration/04_exercises_solved.ipynb @@ -0,0 +1,547 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/04_iteration/04_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 4: Recursion & Looping (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [third part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/04_iteration/03_content.ipynb) of Chapter 4.\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": [ + "## Throwing Dice" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this exercise, you will model the throwing of dice within the context of a guessing game similar to the one shown in the \"*Example: Guessing a Coin Toss*\" section in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/04_iteration/03_content.ipynb#Example:-Guessing-a-Coin-Toss).\n", + "\n", + "As the game involves randomness, we import the [random ](https://docs.python.org/3/library/random.html) module from the [standard library ](https://docs.python.org/3/library/index.html). To follow best practices, we set the random seed as well." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A die has six sides that we labeled with integers `1` to `6` in this exercise. For a fair die, the probability for each side is the same.\n", + "\n", + "**Q1**: Model a `fair_die` as a `list` object!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "fair_die = [1, 2, 3, 4, 5, 6]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What function from the [random ](https://docs.python.org/3/library/random.html) module that we have seen already is useful for modeling a single throw of the `fair_die`? Write a simple expression (i.e., one function call) that draws one of the equally likely sides! Execute the cell a couple of times to \"see\" the probability distribution!" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.choice(fair_die)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's check if the `fair_die` is indeed fair. To do so, we create a little numerical experiment and throw the `fair_die` `100000` times. We track the six different outcomes in a `list` object called `throws` that we initialize with all `0`s for each outcome.\n", + "\n", + "**Q3**: Complete the `for`-loop below such that it runs `100000` times! In the body, use your answer to **Q2** to simulate a single throw of the `fair_die` and update the corresponding count in `throws`!\n", + "\n", + "Hints: You need to use the indexing operator `[]` and calculate an `index` in each iteration of the loop. Do do not actually need the target variable provided by the `for`-loop and may want to indicate that with an underscore `_`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[16689, 16554, 16470, 16936, 16486, 16865]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "throws = [0, 0, 0, 0, 0, 0]\n", + "\n", + "for _ in range(100000):\n", + " throw = random.choice(fair_die)\n", + " index = throw - 1\n", + " throws[index] += 1\n", + "\n", + "throws" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`throws` contains the simulation results as absolute counts.\n", + "\n", + "**Q4**: Complete the `for`-loop below to convert the counts in `throws` to relative frequencies stored in a `list` called `frequencies`! Round the frequencies to three decimals with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function!\n", + "\n", + "Hints: Initialize `frequencies` just as `throws` above. How many iterations does the `for`-loop have? `6` or `100000`? You may want to obtain an `index` variable with the [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) built-in." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.167, 0.166, 0.165, 0.169, 0.165, 0.169]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "frequencies = [0, 0, 0, 0, 0, 0]\n", + "\n", + "for index, counts in enumerate(throws):\n", + " frequencies[index] = round(counts / 100000, 3)\n", + "\n", + "frequencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: How could we adapt the `list` object used above to model an `unfair_die` where `1` is as likely as `2`, `2` is twice as likely as `3`, and `3` is twice as likely as `4`, `5`, or `6`, who are all equally likely?" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "unfair_die = [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 5, 6]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Copy your solution to **Q2** for the `unfair_die`! Execute the cell a couple of times to \"see\" the probability distribution!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.choice(unfair_die)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Copy and adapt your solutions to **Q3** and **Q4** to calculate the `frequencies` for the `unfair_die`!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.309, 0.307, 0.154, 0.077, 0.076, 0.078]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "throws = [0, 0, 0, 0, 0, 0]\n", + "frequencies = [0, 0, 0, 0, 0, 0]\n", + "\n", + "for _ in range(100000):\n", + " throw = random.choice(unfair_die)\n", + " index = throw - 1\n", + " throws[index] += 1\n", + "\n", + "for index, counts in enumerate(throws):\n", + " frequencies[index] = round(counts / 100000, 3)\n", + "\n", + "frequencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: The built-in [input() ](https://docs.python.org/3/library/functions.html#input) allows us to ask the user to enter a `guess`. What is the data type of the object returned by [input() ](https://docs.python.org/3/library/functions.html#input)? Assume the user enters the `guess` as a number (i.e., \"1\", \"2\", ...) and not as a text (e.g., \"one\")." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess the side of the die: 1\n" + ] + } + ], + "source": [ + "guess = input(\"Guess the side of the die: \")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "guess" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "str" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(guess)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Use a built-in constructor to cast `guess` as an `int` object!\n", + "\n", + "Hint: Simply wrap `guess` or `input(\"Guess the side of the die: \")` with the constructor you choose." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(guess)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: What type of error is raised if `guess` cannot be cast as an `int` object?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Write a `try` statement that catches the type of error (i.e., your answer to **Q10**) raised if the user's input cannot be cast as an `int` object! Print out some nice error message notifying the user of the bad input!" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess the side of the die: random\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Make sure to enter your guess correctly!\n" + ] + } + ], + "source": [ + "try:\n", + " guess = int(input(\"Guess the side of the die: \"))\n", + "except ValueError:\n", + " print(\"Make sure to enter your guess correctly!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Write a function `get_guess()` that takes a user's input and checks if it is a valid side of the die! The function should *return* either an `int` object between `1` and `6` or `None` if the user enters something invalid.\n", + "\n", + "Hints: You may want to re-use the `try` statement from **Q11**. Instead of printing out an error message, you can also `return` directly from the `except`-clause (i.e., early exit) with `None`. So, the user can make *two* kinds of input errors and maybe you want to model that with two *distinct* `return None` statements. Also, you may want to allow the user to enter leading and trailing whitespace that gets removed without an error message." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "def get_guess():\n", + " \"\"\"Process the user's input.\n", + " \n", + " Returns:\n", + " guess (int / NoneType): either 1, 2, 3, 4, 5 or 6\n", + " if the input can be parsed and None otherwise\n", + " \"\"\"\n", + " guess = input(\"Guess the side of the die: \")\n", + "\n", + " # Check if the user entered an integer.\n", + " try:\n", + " guess = int(guess.strip())\n", + " except ValueError:\n", + " return None\n", + "\n", + " # Check if the user entered a valid side.\n", + " if 1 <= guess <= 6:\n", + " return guess\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13** Test your function for all *three* cases!" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess the side of the die: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_guess()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Write an *indefinite* loop where in each iteration a `fair_die` is thrown and the user makes a guess! Print out an error message if the user does not enter something that can be understood as a number between `1` and `6`! The game should continue until the user makes a correct guess." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess the side of the die: 1\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Yes, it was 1\n" + ] + } + ], + "source": [ + "while True:\n", + " guess = get_guess()\n", + " result = random.choice(fair_die)\n", + "\n", + " if guess is None:\n", + " print(\"Make sure to enter your guess correctly!\")\n", + " elif guess == result:\n", + " print(\"Yes, it was\", result)\n", + " break\n", + " else:\n", + " print(\"Ooops, it was\", result)" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/06_text/01_exercises_solved.ipynb b/06_text/01_exercises_solved.ipynb new file mode 100644 index 0000000..c26d685 --- /dev/null +++ b/06_text/01_exercises_solved.ipynb @@ -0,0 +1,1225 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/06_text/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 6: Text & Bytes (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/06_text/00_content.ipynb) of Chapter 6.\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": [ + "## Detecting Palindromes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Palindromes ](https://en.wikipedia.org/wiki/Palindrome) are sequences of characters that read the same backward as forward. Examples are first names like \"Hannah\" or \"Otto,\" words like \"radar\" or \"level,\" or sentences like \"Was it a car or a cat I saw?\"\n", + "\n", + "In this exercise, you implement various functions that check if the given arguments are palindromes or not. We start with an iterative implementation and end with a recursive one.\n", + "\n", + "Conceptually, the first function, `unpythonic_palindrome()`, is similar to the \"*Is the square of a number in `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]` greater than `100`?*\" example in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/04_iteration/03_content.ipynb#Example:-Is-the-square-of-a-number-in-[7,-11,-8,-5,-3,-12,-2,-6,-9,-10,-1,-4]-greater-than-100?): It assumes that the `text` argument is a palindrome (i.e., it initializes `is_palindrom` to `True`) and then checks in a `for`-loop if a pair of corresponding characters, `forward` and `backward`, contradicts that.\n", + "\n", + "**Q1**: How many iterations are needed in the `for`-loop? Take into account that `text` may contain an even or odd number of characters! Inside `unpythonic_palindrome()` below, write an expression whose result is assigned to `chars_to_check`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer > " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: `forward_index` is the index going from left to right. How can we calculate `backward_index`, the index going from right to left, for a given `forward_index`? Write an expression whose result is assigned to `backward_index`! Then, use the indexing operator `[]` to obtain the two characters, `forward` and `backward`, from `text`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Finish `unpythonic_palindrome()` below! Add code that adjusts `text` such that the function is case insensitive if the `ignore_case` argument is `True`! Make sure that the function returns once the first pair of corresponding characters does not match!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def unpythonic_palindrome(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answer to Q3\n", + " is_palindrome = True\n", + " if ignore_case:\n", + " text = text.casefold()\n", + " # answer to Q1\n", + " chars_to_check = len(text) // 2\n", + "\n", + " for forward_index in range(chars_to_check):\n", + " # answer to Q2\n", + " backward_index = -forward_index - 1\n", + " forward = text[forward_index]\n", + " backward = text[backward_index]\n", + "\n", + " print(forward, \"and\", backward) # added for didactical purposes\n", + "\n", + " # answer to Q3\n", + " if forward != backward:\n", + " is_palindrome = False\n", + " break\n", + "\n", + " return is_palindrome" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Ensure that `unpythonic_palindrome()` works for the provided test cases (i.e., the `assert` statements do *not* raise an `AssertionError`)! Also, for each of the test cases, provide a brief explanation of what makes them *unique*!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n and n\n", + "o and o\n" + ] + } + ], + "source": [ + "assert unpythonic_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 1 >" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h and h\n", + "a and a\n", + "n and n\n" + ] + } + ], + "source": [ + "assert unpythonic_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 2 >" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "H and h\n" + ] + } + ], + "source": [ + "assert unpythonic_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 3 >" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "r and r\n", + "a and a\n" + ] + } + ], + "source": [ + "assert unpythonic_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 4 >" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h and a\n" + ] + } + ], + "source": [ + "assert unpythonic_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 5 >" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "w and w\n", + "a and a\n", + "r and s\n" + ] + } + ], + "source": [ + "assert unpythonic_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 6 >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`unpythonic_palindrome()` is considered *not* Pythonic as it uses index variables to implement the looping logic. Instead, we should simply loop over an *iterable* object to work with its elements one by one.\n", + "\n", + "**Q5**: Copy your solutions to the previous questions into `almost_pythonic_palindrome()` below!\n", + "\n", + "**Q6**: The [reversed() ](https://docs.python.org/3/library/functions.html#reversed) and [zip() ](https://docs.python.org/3/library/functions.html#zip) built-ins allow us to loop over the same `text` argument *in parallel* in both forward *and* backward order. Finish the `for` statement's header line to do just that!\n", + "\n", + "Hint: You may need to slice the `text` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def almost_pythonic_palindrome(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " is_palindrome = True\n", + " if ignore_case:\n", + " text = text.casefold()\n", + " chars_to_check = len(text) // 2\n", + "\n", + " # answer to Q6\n", + " for forward, backward in zip(text[:chars_to_check], reversed(text)):\n", + "\n", + " print(forward, \"and\", backward) # added for didactical purposes\n", + "\n", + " # answers from above\n", + " if forward != backward:\n", + " is_palindrome = False\n", + " break\n", + "\n", + " return is_palindrome" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Verify that the test cases work as before!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n and n\n", + "o and o\n" + ] + } + ], + "source": [ + "assert almost_pythonic_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h and h\n", + "a and a\n", + "n and n\n" + ] + } + ], + "source": [ + "assert almost_pythonic_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "H and h\n" + ] + } + ], + "source": [ + "assert almost_pythonic_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "r and r\n", + "a and a\n" + ] + } + ], + "source": [ + "assert almost_pythonic_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h and a\n" + ] + } + ], + "source": [ + "assert almost_pythonic_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "w and w\n", + "a and a\n", + "r and s\n" + ] + } + ], + "source": [ + "assert almost_pythonic_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: `almost_pythonic_palindrome()` above may be made more Pythonic by removing the variable `is_palindrome` with the *early exit* pattern. Make the corresponding changes!" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "def pythonic_palindrome(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " if ignore_case:\n", + " text = text.casefold()\n", + " chars_to_check = len(text) // 2\n", + "\n", + " for forward, backward in zip(text[:chars_to_check], reversed(text)):\n", + "\n", + " print(forward, \"and\", backward) # added for didactical purposes\n", + "\n", + " if forward != backward:\n", + " # answer to Q8\n", + " return False\n", + "\n", + " # answer to Q8\n", + " return True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Verify that the test cases still work!" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n and n\n", + "o and o\n" + ] + } + ], + "source": [ + "assert pythonic_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h and h\n", + "a and a\n", + "n and n\n" + ] + } + ], + "source": [ + "assert pythonic_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "H and h\n" + ] + } + ], + "source": [ + "assert pythonic_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "r and r\n", + "a and a\n" + ] + } + ], + "source": [ + "assert pythonic_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "h and a\n" + ] + } + ], + "source": [ + "assert pythonic_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "w and w\n", + "a and a\n", + "r and s\n" + ] + } + ], + "source": [ + "assert pythonic_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: `pythonic_palindrome()` is *not* able to check numeric palindromes. In addition to the string method that implements the case insensitivity and that essentially causes the `AttributeError`, what *abstract behaviors* are numeric data types, such as the `int` type in the example below, missing that would also cause runtime errors? " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'int' object has no attribute 'casefold'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpythonic_palindrome\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m12321\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mpythonic_palindrome\u001b[0;34m(text, ignore_case)\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0;31m# answers from above\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mignore_case\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 13\u001b[0;31m \u001b[0mtext\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcasefold\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 14\u001b[0m \u001b[0mchars_to_check\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtext\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m//\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'int' object has no attribute 'casefold'" + ] + } + ], + "source": [ + "pythonic_palindrome(12321)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Copy your code from `pythonic_palindrome()` above into `palindrome_ducks()` below and make the latter conform to *duck typing*!\n", + "\n", + "Hints: You may want to use the [str() ](https://docs.python.org/3/library/functions.html#func-str) built-in. You only need to add *one* short line of code." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "def palindrome_ducks(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answer to Q11\n", + " text = str(text)\n", + " # answers from above\n", + " if ignore_case:\n", + " text = text.casefold()\n", + " chars_to_check = len(text) // 2\n", + "\n", + " for forward, backward in zip(text[:chars_to_check], reversed(text)):\n", + " if forward != backward:\n", + " return False\n", + "\n", + " return True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Verify that the two new test cases work as well!" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "assert palindrome_ducks(12321) is True" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "assert palindrome_ducks(12345) is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`palindrome_ducks()` can *not* process palindromes that consist of more than one word." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "palindrome_ducks(\"Never odd or even.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "palindrome_ducks(\"Eva, can I stab bats in a cave?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "palindrome_ducks(\"A man, a plan, a canal - Panama.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "palindrome_ducks(\"A Santa lived as a devil at NASA!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "palindrome_ducks(\"\"\"\n", + " Dennis, Nell, Edna, Leon, Nedra, Anita, Rolf, Nora, Alice, Carol, Leo, Jane,\n", + " Reed, Dena, Dale, Basil, Rae, Penny, Lana, Dave, Denny, Lena, Ida, Bernadette,\n", + " Ben, Ray, Lila, Nina, Jo, Ira, Mara, Sara, Mario, Jan, Ina, Lily, Arne, Bette,\n", + " Dan, Reba, Diane, Lynn, Ed, Eva, Dana, Lynne, Pearl, Isabel, Ada, Ned, Dee,\n", + " Rena, Joel, Lora, Cecil, Aaron, Flora, Tina, Arden, Noel, and Ellen sinned.\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: Implement the final iterative version `is_a_palindrome()` below. Copy your solution from `palindrome_ducks()` above and add code that removes the \"special\" characters (and symbols) from the longer example palindromes above so that they are effectively ignored! Note that this processing should only be done if the `ignore_symbols` argument is set to `True`.\n", + "\n", + "Hints: Use the [replace() ](https://docs.python.org/3/library/stdtypes.html#str.replace) method on the `str` type to achieve that. You may want to do so within another `for`-loop." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "def is_a_palindrome(text, *, ignore_case=True, ignore_symbols=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; may be multiple words\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + " ignore_symbols (bool): If special characters like \".\" or \"?\" and others\n", + " are ignored; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " text = str(text)\n", + " if ignore_case:\n", + " text = text.casefold()\n", + " # answer to Q13\n", + " if ignore_symbols:\n", + " for char in [\" \", \"\\n\", \",\", \".\", \"!\", \"?\", \"-\"]:\n", + " text = text.replace(char, \"\")\n", + " # answers from above\n", + " chars_to_check = len(text) // 2\n", + "\n", + " for forward, backward in zip(text[:chars_to_check], reversed(text)):\n", + " if forward != backward:\n", + " return False\n", + "\n", + " return True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Verify that all test cases below work!" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(12321) is True" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(12345) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Never odd or even.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Never odd or even.\", ignore_symbols=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Eva, can I stab bats in a cave?\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"A man, a plan, a canal - Panama.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"A Santa lived as a devil at NASA!\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"\"\"\n", + " Dennis, Nell, Edna, Leon, Nedra, Anita, Rolf, Nora, Alice, Carol, Leo, Jane,\n", + " Reed, Dena, Dale, Basil, Rae, Penny, Lana, Dave, Denny, Lena, Ida, Bernadette,\n", + " Ben, Ray, Lila, Nina, Jo, Ira, Mara, Sara, Mario, Jan, Ina, Lily, Arne, Bette,\n", + " Dan, Reba, Diane, Lynn, Ed, Eva, Dana, Lynne, Pearl, Isabel, Ada, Ned, Dee,\n", + " Rena, Joel, Lora, Cecil, Aaron, Flora, Tina, Arden, Noel, and Ellen sinned.\n", + "\"\"\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's look at a *recursive* formulation in `recursive_palindrome()` below.\n", + "\n", + "**Q15**: Copy the code from `is_a_palindrome()` that implements the duck typing, the case insensitivity, and the removal of special characters!\n", + "\n", + "The recursion becomes apparent if we remove the *first* and the *last* character from a given `text`: `text` can only be a palindrome if the two removed characters are the same *and* the remaining substring is a palindrome itself! So, the word `\"noon\"` has only *one* recursive call while `\"radar\"` has *two*.\n", + "\n", + "Further, `recursive_palindrome()` has *two* base cases of which only *one* is reached for a given `text`: First, if `recursive_palindrome()` is called with either an empty `\"\"` or a `text` argument with `len(text) == 1`, and, second, if the two removed characters are *not* the same.\n", + "\n", + "**Q16**: Implement the two base cases in `recursive_palindrome()`! Use the *early exit* pattern!\n", + "\n", + "**Q17**: Add the recursive call to `recursive_palindrome()` with a substring of `text`! Pass in the `ignore_case` and `ignore_symbols` arguments as `False`! This avoids unnecessary computations in the recursive calls. Why is that the case?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "def recursive_palindrome(text, *, ignore_case=True, ignore_symbols=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; may be multiple words\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + " ignore_symbols (bool): If special characters like \".\" or \"?\" and others\n", + " are ignored; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " text = str(text)\n", + " if ignore_case:\n", + " text = text.casefold()\n", + " if ignore_symbols:\n", + " for char in [\" \", \"\\n\", \",\", \".\", \"!\", \"?\", \"-\"]:\n", + " text = text.replace(char, \"\")\n", + "\n", + " # answer to Q16\n", + " if len(text) <= 1:\n", + " return True\n", + " elif text[0] != text[-1]:\n", + " return False\n", + "\n", + " # answer to Q17\n", + " return recursive_palindrome(text[1:-1], ignore_case=False, ignore_symbols=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q18**: Lastly, verify that `recursive_palindrome()` passes all the test cases below!" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(12321) is True" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(12345) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Never odd or even.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Never odd or even.\", ignore_symbols=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Eva, can I stab bats in a cave?\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"A man, a plan, a canal - Panama.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"A Santa lived as a devil at NASA!\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"\"\"\n", + " Dennis, Nell, Edna, Leon, Nedra, Anita, Rolf, Nora, Alice, Carol, Leo, Jane,\n", + " Reed, Dena, Dale, Basil, Rae, Penny, Lana, Dave, Denny, Lena, Ida, Bernadette,\n", + " Ben, Ray, Lila, Nina, Jo, Ira, Mara, Sara, Mario, Jan, Ina, Lily, Arne, Bette,\n", + " Dan, Reba, Diane, Lynn, Ed, Eva, Dana, Lynne, Pearl, Isabel, Ada, Ned, Dee,\n", + " Rena, Joel, Lora, Cecil, Aaron, Flora, Tina, Arden, Noel, and Ellen sinned.\n", + "\"\"\") is True" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/07_sequences/02_exercises_solved.ipynb b/07_sequences/02_exercises_solved.ipynb new file mode 100644 index 0000000..0df655d --- /dev/null +++ b/07_sequences/02_exercises_solved.ipynb @@ -0,0 +1,301 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/07_sequences/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 7: Sequential Data (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/07_sequences/01_content.ipynb) of Chapter 7.\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": [ + "## Working with Lists" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Write a function `nested_sum()` that takes a `list` object as its argument, which contains other `list` objects with numbers, and adds up the numbers! Use `nested_numbers` below to test your function!\n", + "\n", + "Hint: You need at least one `for`-loop." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "nested_numbers = [[1, 2, 3], [4], [5], [6, 7], [8], [9]]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def nested_sum(list_of_lists):\n", + " \"\"\"Add up numbers in nested lists.\n", + " \n", + " Args:\n", + " list_of_lists (list): A list containing the lists with the numbers\n", + " \n", + " Returns:\n", + " sum (int or float)\n", + " \"\"\"\n", + " total = 0\n", + " for inner_list in list_of_lists:\n", + " total += sum(inner_list)\n", + "\n", + " return total" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_sum(nested_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Generalize `nested_sum()` into a function `mixed_sum()` that can process a \"mixed\" `list` object, which contains numbers and other `list` objects with numbers! Use `mixed_numbers` below for testing!\n", + "\n", + "Hints: Use the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function to check how an element is to be processed." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "mixed_numbers = [[1, 2, 3], 4, 5, [6, 7], 8, [9]]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def mixed_sum(list_of_lists_or_numbers):\n", + " \"\"\"Add up numbers in nested lists.\n", + " \n", + " Args:\n", + " list_of_lists_or_numbers (list): A list containing both numbers and\n", + " lists with numbers\n", + " \n", + " Returns:\n", + " sum (int or float)\n", + " \"\"\"\n", + " total = 0\n", + " for inner_list in list_of_lists_or_numbers:\n", + " if isinstance(inner_list, abc.Sequence):\n", + " total += sum(inner_list)\n", + " else:\n", + " total += inner_list\n", + "\n", + " return total" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mixed_sum(mixed_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.1**: Write a function `cum_sum()` that takes a `list` object with numbers as its argument and returns a *new* `list` object with the **cumulative sums** of these numbers! So, `sum_up` below, `[1, 2, 3, 4, 5]`, should return `[1, 3, 6, 10, 15]`.\n", + "\n", + "Hint: The idea behind is similar to the [cumulative distribution function ](https://en.wikipedia.org/wiki/Cumulative_distribution_function) from statistics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "sum_up = [1, 2, 3, 4, 5]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def cum_sum(numbers):\n", + " \"\"\"Create the cumulative sums for some numbers.\n", + "\n", + " Args:\n", + " numbers (list): A list with numbers for that the cumulative sums\n", + " are calculated\n", + " \n", + " Returns:\n", + " cum_sums (list): A list with all the cumulative sums\n", + " \"\"\"\n", + " total = 0\n", + " cumulative = []\n", + "\n", + " for number in numbers:\n", + " total += number\n", + " cumulative.append(total)\n", + "\n", + " return cumulative" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 3, 6, 10, 15]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cum_sum(sum_up)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.2**: We should always make sure that our functions also work in corner cases. What happens if your implementation of `cum_sum()` is called with an empty list `[]`? Make sure it handles that case *without* crashing! What would be a good return value in this corner case?\n", + "\n", + "Hint: It is possible to write this without any extra input validation." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cum_sum([])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/07_sequences/04_exercises_solved.ipynb b/07_sequences/04_exercises_solved.ipynb new file mode 100644 index 0000000..769e46d --- /dev/null +++ b/07_sequences/04_exercises_solved.ipynb @@ -0,0 +1,969 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/07_sequences/04_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 7: Sequential Data (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [third part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/07_sequences/03_content.ipynb) of Chapter 7.\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": [ + "## Packing & Unpacking with Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the \"*Function Definitions & Calls*\" section in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/07_sequences/03_content.ipynb#Function-Definitions-&-Calls), we define the following function `product()`. In this exercise, you will improve it by making it more \"user-friendly.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = args[0]\n", + "\n", + " for arg in args[1:]:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `*` in the function's header line *packs* all *positional* arguments passed to `product()` into one *iterable* called `args`.\n", + "\n", + "**Q1**: What is the data type of `args` within the function's body?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because of the packing, we may call `product()` with an abitrary number of *positional* arguments: The product of just `42` remains `42`, while `2`, `5`, and `10` multiplied together result in `100`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, \"abitrary\" does not mean that we can pass *no* argument. If we do so, we get an `IndexError`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "ename": "IndexError", + "evalue": "tuple index out of range", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(*args)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\"\"\"Multiply all arguments.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0marg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mIndexError\u001b[0m: tuple index out of range" + ] + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What line in the body of `product()` causes this exception? What is the exact problem?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/07_sequences/00_content.ipynb#Function-Definitions-&-Calls), we also pass a `list` object, like `one_hundred`, to `product()`, and *no* exception is raised." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "one_hundred = [2, 5, 10]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[2, 5, 10]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: What is wrong with that? What *kind* of error (cf., [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/01_elements/00_content.ipynb#Formal-vs.-Natural-Languages)) is that conceptually? Describe precisely what happens to the passed in `one_hundred` in every line within `product()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, one solution is to *unpack* `one_hundred` with the `*` symbol. We look at another solution further below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(*one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's continue with the issue when calling `product()` *without* any argument.\n", + "\n", + "This revised version of `product()` avoids the `IndexError` from before." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = None\n", + "\n", + " for arg in args:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Describe why no error occurs by going over every line in `product()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unfortunately, the new version cannot process any arguments we pass in any more." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "unsupported operand type(s) for *=: 'NoneType' and 'int'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m42\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(*args)\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0marg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m*=\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for *=: 'NoneType' and 'int'" + ] + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "unsupported operand type(s) for *=: 'NoneType' and 'int'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m5\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m10\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(*args)\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0marg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m*=\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for *=: 'NoneType' and 'int'" + ] + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What line causes troubles now? What is the exact problem?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Replace the `None` in `product()` above with something reasonable that does *not* cause exceptions! Ensure that `product(42)` and `product(2, 5, 10)` return a correct result.\n", + "\n", + "Hints: It is ok if `product()` returns a result *different* from the `None` above. Look at the documentation of the built-in [sum() ](https://docs.python.org/3/library/functions.html#sum) function for some inspiration." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = 1\n", + "\n", + " for arg in args:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, calling `product()` without any arguments returns what we would best describe as a *default* or *start* value. To be \"philosophical,\" what is the product of *no* numbers? We know that the product of *one* number is just the number itself, but what could be a reasonable result when multiplying *no* numbers? The answer is what you use as the initial value of `result` above, and there is only *one* way to make `product(42)` and `product(2, 5, 10)` work." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Rewrite `product()` so that it takes a *keyword-only* argument `start`, defaulting to the above *default* or *start* value, and use `start` internally instead of `result`!\n", + "\n", + "Hint: Remember that a *keyword-only* argument is any parameter specified in a function's header line after the first and only `*` (cf., [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/02_functions/00_content.ipynb#Keyword-only-Arguments))." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, start=1):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " for arg in args:\n", + " start *= arg\n", + "\n", + " return start" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can call `product()` with a truly arbitrary number of *positional* arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Without any *positional* arguments but only the *keyword* argument `start`, for example, `start=0`, we can adjust the answer to the \"philosophical\" problem of multiplying *no* numbers. Because of the *keyword-only* syntax, there is *no* way to pass in a `start` number *without* naming it." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(start=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We could use `start` to inject a multiplier, for example, to double the outcomes." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "84" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42, start=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10, start=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is still one issue left: Because of the function's name, a user of `product()` may assume that it is ok to pass a *collection* of numbers, like `one_hundred`, which are then multiplied." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[2, 5, 10]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: What is a **collection**? How is that different from a **sequence**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Rewrite the latest version of `product()` to check if the *only* positional argument is a *collection* type! If so, its elements are multiplied together. Otherwise, the logic remains the same.\n", + "\n", + "Hints: Use the built-in [len() ](https://docs.python.org/3/library/functions.html#len) and [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) functions to check if there is only *one* positional argument and if it is a *collection* type. Use the *abstract base class* `Collection` from the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module in the [standard library ](https://docs.python.org/3/library/index.html). You may want to *re-assign* `args` inside the body." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, start=1):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " if len(args) == 1 and isinstance(args[0], abc.Collection):\n", + " args = args[0]\n", + "\n", + " for arg in args:\n", + " start *= arg\n", + "\n", + " return start" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All *five* code cells below now return correct results. We may unpack `one_hundred` or not." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(one_hundred)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(*one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Side Note**: Above, we make `product()` work with a single *collection* type argument instead of a *sequence* type to keep it more generic: For example, we can pass in a `set` object, like `{2, 5, 10}` below, and `product()` continues to work correctly. The `set` type is introducted in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/09_mappings/00_content.ipynb#The-set-Type), and one essential difference to the `list` type is that objects of type `set` have *no* order regarding their elements. So, even though `[2, 5, 10]` and `{2, 5, 10}` look almost the same, the order implied in the literal notation gets lost in memory!" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product([2, 5, 10]) # the argument is a collection that is also a sequence" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product({2, 5, 10}) # the argument is a collection that is NOT a sequence" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance({2, 5, 10}, abc.Sequence) # sets are NO sequences" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's continue to improve `product()` and make it more Pythonic. It is always a good idea to mimic the behavior of built-ins when writing our own functions. And, [sum() ](https://docs.python.org/3/library/functions.html#sum), for example, raises a `TypeError` if called *without* any arguments. It does *not* return the \"philosophical\" answer to adding *no* numbers, which would be `0`." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "sum() takes at least 1 positional argument (0 given)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m: sum() takes at least 1 positional argument (0 given)" + ] + } + ], + "source": [ + "sum()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: Adapt the latest version of `product()` to also raise a `TypeError` if called *without* any *positional* arguments!" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, start=1):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " if not args:\n", + " raise TypeError(\"product expected at least 1 arguments, got 0\")\n", + " elif len(args) == 1 and isinstance(args[0], abc.Collection):\n", + " args = args[0]\n", + "\n", + " for arg in args:\n", + " start *= arg\n", + "\n", + " return start" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "product expected at least 1 arguments, got 0", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(start, *args)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\"\"\"Multiply all arguments.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"product expected at least 1 arguments, got 0\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mabc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mCollection\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0margs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: product expected at least 1 arguments, got 0" + ] + } + ], + "source": [ + "product()" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/08_mfr/02_exercises_solved.ipynb b/08_mfr/02_exercises_solved.ipynb new file mode 100644 index 0000000..50eee02 --- /dev/null +++ b/08_mfr/02_exercises_solved.ipynb @@ -0,0 +1,1044 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/08_mfr/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/08_mfr/00_content.ipynb) and [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/08_mfr/01_content.ipynb) part of Chapter 8.\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": [ + "## Removing Outliers in Streaming Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's say we are given a `list` object with random integers like `sample` below, and we want to calculate some basic statistics on them." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "sample = [\n", + " 45, 46, 40, 49, 36, 53, 49, 42, 25, 40, 39, 36, 38, 40, 40, 52, 36, 52, 40, 41,\n", + " 35, 29, 48, 43, 42, 30, 29, 33, 55, 33, 38, 50, 39, 56, 52, 28, 37, 56, 45, 37,\n", + " 41, 41, 37, 30, 51, 32, 23, 40, 53, 40, 45, 39, 99, 42, 34, 42, 34, 39, 39, 53,\n", + " 43, 37, 46, 36, 45, 42, 32, 38, 57, 34, 36, 44, 47, 51, 46, 39, 28, 40, 35, 46,\n", + " 41, 51, 41, 23, 46, 40, 40, 51, 50, 32, 47, 36, 38, 29, 32, 53, 34, 43, 39, 41,\n", + " 40, 34, 44, 40, 41, 43, 47, 57, 50, 42, 38, 25, 45, 41, 58, 37, 45, 55, 44, 53,\n", + " 82, 31, 45, 33, 32, 39, 46, 48, 42, 47, 40, 45, 51, 35, 31, 46, 40, 44, 61, 57,\n", + " 40, 36, 35, 55, 40, 56, 36, 35, 86, 36, 51, 40, 54, 50, 49, 36, 41, 37, 48, 41,\n", + " 42, 44, 40, 43, 51, 47, 46, 50, 40, 23, 40, 39, 28, 38, 42, 46, 46, 42, 46, 31,\n", + " 32, 40, 48, 27, 40, 40, 30, 32, 25, 31, 30, 43, 44, 29, 45, 41, 63, 32, 33, 58,\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: `list` objects are **sequences**. What *four* behaviors do they always come with?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Write a function `mean()` that calculates the simple arithmetic mean of a given `sequence` with numbers!\n", + "\n", + "Hints: You can solve this task with [built-in functions ](https://docs.python.org/3/library/functions.html) only. A `for`-loop is *not* needed." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def mean(sequence):\n", + " return sum(sequence) / len(sequence)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "sample_mean = mean(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_mean" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Write a function `std()` that calculates the [standard deviation ](https://en.wikipedia.org/wiki/Standard_deviation) of a `sequence` of numbers! Integrate your `mean()` version from before and the [sqrt() ](https://docs.python.org/3/library/math.html#math.sqrt) function from the [math ](https://docs.python.org/3/library/math.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provided to you below. Make sure `std()` calls `mean()` only *once* internally! Repeated calls to `mean()` would be a waste of computational resources.\n", + "\n", + "Hints: Parts of the code are probably too long to fit within the suggested 79 characters per line. So, use *temporary variables* inside your function. Instead of a `for`-loop, you may want to use a `list` comprehension or, even better, a memoryless `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sqrt" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def std(sequence):\n", + " seq_mean = mean(sequence)\n", + " squared_deviations = ((n - seq_mean) ** 2 for n in sequence)\n", + " return sqrt(sum(squared_deviations) / len(sequence))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "sample_std = std(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9.870663604844408" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_std" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Complete `standardize()` below that takes a `sequence` of numbers and returns a `list` object with the **[z-scores ](https://en.wikipedia.org/wiki/Standard_score)** of these numbers! A z-score is calculated by subtracting the mean and dividing by the standard deviation. Re-use `mean()` and `std()` from before. Again, ensure that `standardize()` calls `mean()` and `std()` only *once*! Further, round all z-scores with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function and pass on the keyword-only argument `digits` to it.\n", + "\n", + "Hint: You may want to use a `list` comprehension instead of a `for`-loop." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def standardize(sequence, *, digits=3):\n", + " seq_mean = mean(sequence)\n", + " seq_std = std(sequence)\n", + " return [round((n - seq_mean) / seq_std, digits) for n in sequence]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores = standardize(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [pprint() ](https://docs.python.org/3/library/pprint.html#pprint.pprint) function from the [pprint ](https://docs.python.org/3/library/pprint.html) module in the [standard library ](https://docs.python.org/3/library/index.html) allows us to \"pretty print\" long `list` objects compactly." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.304, 0.405, -0.203, 0.709, -0.608, 1.114, 0.709, 0.0, -1.722, -0.203, -0.304,\n", + " -0.608, -0.405, -0.203, -0.203, 1.013, -0.608, 1.013, -0.203, -0.101, -0.709,\n", + " -1.317, 0.608, 0.101, 0.0, -1.216, -1.317, -0.912, 1.317, -0.912, -0.405, 0.81,\n", + " -0.304, 1.418, 1.013, -1.418, -0.507, 1.418, 0.304, -0.507, -0.101, -0.101,\n", + " -0.507, -1.216, 0.912, -1.013, -1.925, -0.203, 1.114, -0.203, 0.304, -0.304,\n", + " 5.775, 0.0, -0.81, 0.0, -0.81, -0.304, -0.304, 1.114, 0.101, -0.507, 0.405,\n", + " -0.608, 0.304, 0.0, -1.013, -0.405, 1.52, -0.81, -0.608, 0.203, 0.507, 0.912,\n", + " 0.405, -0.304, -1.418, -0.203, -0.709, 0.405, -0.101, 0.912, -0.101, -1.925,\n", + " 0.405, -0.203, -0.203, 0.912, 0.81, -1.013, 0.507, -0.608, -0.405, -1.317,\n", + " -1.013, 1.114, -0.81, 0.101, -0.304, -0.101, -0.203, -0.81, 0.203, -0.203,\n", + " -0.101, 0.101, 0.507, 1.52, 0.81, 0.0, -0.405, -1.722, 0.304, -0.101, 1.621,\n", + " -0.507, 0.304, 1.317, 0.203, 1.114, 4.052, -1.114, 0.304, -0.912, -1.013,\n", + " -0.304, 0.405, 0.608, 0.0, 0.507, -0.203, 0.304, 0.912, -0.709, -1.114, 0.405,\n", + " -0.203, 0.203, 1.925, 1.52, -0.203, -0.608, -0.709, 1.317, -0.203, 1.418,\n", + " -0.608, -0.709, 4.458, -0.608, 0.912, -0.203, 1.216, 0.81, 0.709, -0.608,\n", + " -0.101, -0.507, 0.608, -0.101, 0.0, 0.203, -0.203, 0.101, 0.912, 0.507, 0.405,\n", + " 0.81, -0.203, -1.925, -0.203, -0.304, -1.418, -0.405, 0.0, 0.405, 0.405, 0.0,\n", + " 0.405, -1.114, -1.013, -0.203, 0.608, -1.52, -0.203, -0.203, -1.216, -1.013,\n", + " -1.722, -1.114, -1.216, 0.101, 0.203, -1.317, 0.304, -0.101, 2.128, -1.013,\n", + " -0.912, 1.621]\n" + ] + } + ], + "source": [ + "pprint(z_scores, compact=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We know that `standardize()` works correctly if the resulting z-scores' mean and standard deviation approach `0` and `1` for a long enough `sequence`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-2.500000000001501e-05, 0.9999935971670018)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mean(z_scores), std(z_scores)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Even though `standardize()` calls `mean()` and `std()` only once each, `mean()` is called *twice*! That is so because `std()` internally also re-uses `mean()`!\n", + "\n", + "**Q5.1**: Rewrite `std()` to take an optional keyword-only argument `seq_mean`, defaulting to `None`. If provided, `seq_mean` is used instead of the result of calling `mean()`. Otherwise, the latter is called.\n", + "\n", + "Hint: You must check if `seq_mean` is still the default value." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "def std(sequence, *, seq_mean=None):\n", + " if seq_mean is None:\n", + " seq_mean = mean(sequence)\n", + " squared_deviations = ((n - seq_mean) ** 2 for n in sequence)\n", + " return sqrt(sum(squared_deviations) / len(sequence))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`std()` continues to work as before." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "sample_std = std(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9.870663604844408" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_std" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5.2**: Now, rewrite `standardize()` to pass on the return value of `mean()` to `std()`! In summary, `standardize()` calculates the z-scores for the numbers in the `sequence` with as few computational steps as possible." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def standardize(sequence, *, digits=3):\n", + " seq_mean = mean(sequence)\n", + " seq_std = std(sequence, seq_mean=seq_mean)\n", + " return [round((n - seq_mean) / seq_std, digits) for n in sequence]" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores = standardize(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-2.500000000001501e-05, 0.9999935971670018)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mean(z_scores), std(z_scores)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: With both `sample` and `z_scores` being materialized `list` objects, we can loop over pairs consisting of a number from `sample` and its corresponding z-score. Write a `for`-loop that prints out all the \"outliers,\" as which we define numbers with an absolute z-score above `1.96`. There are *four* of them in the `sample`.\n", + "\n", + "Hint: Use the [abs() ](https://docs.python.org/3/library/functions.html#abs) and [zip() ](https://docs.python.org/3/library/functions.html#zip) built-ins." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "99 5.775\n", + "82 4.052\n", + "86 4.458\n", + "63 2.128\n" + ] + } + ], + "source": [ + "for number, z_score in zip(sample, z_scores):\n", + " if abs(z_score) > 1.96:\n", + " print(number, z_score)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We provide a `stream` module with a `data` object that models an *infinite* **stream** of data (cf., the [*stream.py* ](https://github.com/webartifex/intro-to-python/blob/develop/08_mfr/stream.py) file in the repository)." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "from stream import data" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`data` is of type `generator` and has *no* length." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "generator" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "object of type 'generator' has no len()", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m: object of type 'generator' has no len()" + ] + } + ], + "source": [ + "len(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So, the only thing we can do with it is to pass it to the built-in [next() ](https://docs.python.org/3/library/functions.html#next) function and go over the numbers it streams one by one." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: What happens if you call `mean()` with `data` as the argument? What is the problem?\n", + "\n", + "Hints: If you try it out, you may have to press the \"Stop\" button in the toolbar at the top. Your computer should *not* crash, but you will *have to* restart this Jupyter notebook with \"Kernel\" > \"Restart\" and import `data` again." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Write a function `take_sample()` that takes an `iterable` as its argument, like `data`, and creates a *materialized* `list` object out of its first `n` elements, defaulting to `1_000`!\n", + "\n", + "Hints: [next() ](https://docs.python.org/3/library/functions.html#next) and the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in may be helpful. You may want to use a `list` comprehension instead of a `for`-loop and write a one-liner. Audacious students may want to look at [isclice() ](https://docs.python.org/3/library/itertools.html#itertools.islice) in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](https://docs.python.org/3/library/index.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "def take_sample(iterator, *, n=1_000):\n", + " return [next(iterator) for _ in range(n)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We take a `new_sample` from the stream of `data`, and its statistics are similar to the initial `sample`." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "new_sample = take_sample(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1000" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(new_sample)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42.319" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mean(new_sample)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10.172966086643575" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "std(new_sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Convert `standardize()` into a *new* function `standardized()` that implements the *same* logic but works on a possibly *infinite* stream of data, provided as an `iterable`, instead of a *finite* `sequence`.\n", + "\n", + "To calculate a z-score, we need the stream's overall mean and standard deviation, and that is *impossible* to calculate if we do not know how long the stream is, and, in particular, if it is *infinite*. So, `standardized()` first takes a sample from the `iterable` internally, and uses the sample's mean and standard deviation to calculate the z-scores.\n", + "\n", + "Hint: `standardized()` *must* return a `generator` object. So, use a `generator` expression as the return value; unless you know about the `yield` statement already (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement))." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "def standardized(iterable, *, digits=3):\n", + " sample = take_sample(iterable)\n", + " seq_mean = mean(sample)\n", + " seq_std = std(sample, seq_mean=seq_mean)\n", + " return (round((n - seq_mean) / seq_std, digits) for n in iterable)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`standardized()` works almost like `standardize()` except that we use it with [next() ](https://docs.python.org/3/library/functions.html#next) to obtain the z-scores one by one." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores = standardized(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + ". at 0x7f68441b1e40>" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "z_scores" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "generator" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(z_scores)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "-0.648" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(z_scores)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.1**: `standardized()` allows us to go over an *infinite* stream of z-scores. What we want to do instead is to loop over the stream's raw numbers and skip the outliers. In the remainder of this exercise, you look at the parts that make up the `skip_outliers()` function below to achieve precisely that.\n", + "\n", + "The first steps in `skip_outliers()` are the same as in `standardized()`: We take a `sample` from the stream of `data` and calculate its statistics." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "sample = take_sample(data)\n", + "seq_mean = mean(sample)\n", + "seq_std = std(sample, seq_mean=seq_mean)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.2**: Just as in `standardized()`, write a `generator` expression that produces z-scores one by one! However, instead of just generating a z-score, the resulting `generator` object should produce `tuple` objects consisting of a \"raw\" number from `data` and its z-score.\n", + "\n", + "Hint: Look at the revisited \"*Averaging Even Numbers*\" example in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/07_sequences/01_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List-%28revisited%29) for some inspiration, which also contains a `generator` expression producing `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "standardizer = ((n, (n - seq_mean) / seq_std) for n in data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`standardizer` should produce `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(50, 0.7995860272154574)" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(standardizer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.3**: Write another `generator` expression that loops over `standardizer`. It contains an `if`-clause that keeps only numbers with an absolute z-score below the `threshold_z`. If you fancy, use `tuple` unpacking." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "threshold_z = 1.96" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "no_outliers = (n for n, z in standardizer if abs(z) < threshold_z)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`no_outliers` should produce `int` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "38" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(no_outliers)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.4**: Lastly, put everything together in the `skip_outliers()` function! Make sure you refer to `iterable` inside the function and not the global `data`." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "def skip_outliers(iterable, *, threshold_z=1.96):\n", + " sample = take_sample(iterable)\n", + " seq_mean = mean(sample)\n", + " seq_std = std(sample, seq_mean=seq_mean)\n", + " standardizer = ((n, (n - seq_mean) / seq_std) for n in iterable)\n", + " no_outliers = (n for n, z in standardizer if abs(z) < threshold_z)\n", + " return no_outliers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can create a `generator` object and loop over the `data` in the stream with outliers skipped. Instead of the default `1.96`, we use a `threshold_z` of only `0.05`: That filters out all numbers except `42`." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "skipper = skip_outliers(data, threshold_z=0.05)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + ". at 0x7f684423e120>" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "skipper" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "generator" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(skipper)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(skipper)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: You implemented the functions `mean()`, `std()`, `standardize()`, `standardized()`, and `skip_outliers()`. Which of them are **eager**, and which are **lazy**? How do these two concepts relate to **finite** and **infinite** data?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/08_mfr/03_exercises_solved.ipynb b/08_mfr/03_exercises_solved.ipynb new file mode 100644 index 0000000..ea95b56 --- /dev/null +++ b/08_mfr/03_exercises_solved.ipynb @@ -0,0 +1,638 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/08_mfr/03_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/08_mfr/00_content.ipynb) and [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/08_mfr/01_content.ipynb) part of Chapter 8.\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": [ + "## Packing & Unpacking with Functions (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Copy your solution to **Q10** from the \"*Packing & Unpacking with Functions*\" exercise in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/07_sequences/04_exercises.ipynb) into the code cell below!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, start=1):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " if not args:\n", + " raise TypeError(\"product expected at least 1 arguments, got 0\")\n", + " elif len(args) == 1 and isinstance(args[0], abc.Collection):\n", + " args = args[0]\n", + "\n", + " for arg in args:\n", + " start *= arg\n", + "\n", + " return start" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Verify that all test cases below work (i.e., the `assert` statements must *not* raise an `AssertionError`)!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(42) == 42" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(2, 5, 10) == 100" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(2, 5, 10, start=2) == 200" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "one_hundred = [2, 5, 10]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(one_hundred) == 100" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(*one_hundred) == 100" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Verify that `product()` raises a `TypeError` when called without any arguments!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "product expected at least 1 arguments, got 0", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(start, *args)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\"\"\"Multiply all arguments.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"product expected at least 1 arguments, got 0\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mabc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mCollection\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0margs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: product expected at least 1 arguments, got 0" + ] + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This implementation of `product()` is convenient to use, in particular, because we can pass it any *collection* object with or without *unpacking* it.\n", + "\n", + "However, `product()` suffers from one last flaw: We cannot pass it a **stream** of data, as modeled, for example, with a `generator` object that produces elements on a one-by-one basis.\n", + "\n", + "**Q4**: Click through the following code cells and observe what they do!\n", + "\n", + "The [*stream.py* ](https://github.com/webartifex/intro-to-python/blob/develop/08_mfr/stream.py) module in the book's repository provides a `make_finite_stream()` function. It is a *factory* function creating objects of type `generator` that we use to model *streaming* data." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from stream import make_finite_stream" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "data = make_finite_stream()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "generator" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`generator` objects are good for only *one* thing: Giving us the \"next\" element in a series of possibly *infinitely* many objects. While the `data` object is finite (i.e., execute the next code cell until you see a `StopIteration` exception), ..." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "40" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "... it has *no* concept of a \"length:\" The built-in [len() ](https://docs.python.org/3/library/functions.html#len) function raises a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "object of type 'generator' has no len()", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m: object of type 'generator' has no len()" + ] + } + ], + "source": [ + "len(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can use the built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor to *materialize* all elements. However, in a real-world scenario, these may *not* fit into our machine's memory! If you get an empty `list` object below, you have to create a *new* `data` object above again." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[25, 40, 49, 36, 53, 49]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To be more realistic, `make_finite_stream()` creates `generator` objects producing a varying number of elements." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[42, 32, 33, 47, 60, 48, 39, 30, 48, 55, 40, 43]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(make_finite_stream())" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[49, 31, 43, 45, 40, 52, 43, 29, 33, 55, 33, 38, 50, 39]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(make_finite_stream())" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[56, 40, 49, 42, 49, 49, 62]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(make_finite_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see what happens if we pass a `generator` object, as created by `make_finite_stream()`, instead of a materialized *collection*, like `one_hundred`, to `product()`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "unsupported operand type(s) for *=: 'int' and 'generator'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmake_finite_stream\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(start, *args)\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0marg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m \u001b[0mstart\u001b[0m \u001b[0;34m*=\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 10\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mstart\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for *=: 'int' and 'generator'" + ] + } + ], + "source": [ + "product(make_finite_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What line causes the `TypeError`? What line is really the problem in `product()`? Hint: These may be different lines. Describe what happens on each line in the function's body until the exception is raised!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Adapt `product()` one last time to make it work with `generator` objects, or more generallz *iterators*, as well!\n", + "\n", + "Hints: This task is as easy as replacing `Collection` with something else. Which of the three behaviors of *collections* do `generator` objects also exhibit? You may want to look at the documentations on the built-in [max() ](https://docs.python.org/3/library/functions.html#max), [min() ](https://docs.python.org/3/library/functions.html#min), and [sum() ](https://docs.python.org/3/library/functions.html#sum) functions: What kind of argument do they take?" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, start=1):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " if not args:\n", + " raise TypeError(\"product expected at least 1 arguments, got 0\")\n", + " elif len(args) == 1 and isinstance(args[0], abc.Iterable):\n", + " args = args[0]\n", + "\n", + " for arg in args:\n", + " start *= arg\n", + "\n", + " return start" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The final version of `product()` behaves like built-ins in edge cases (i.e., `sum()` also raises a `TypeError` when called without arguments), ..." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "product expected at least 1 arguments, got 0", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mproduct\u001b[0;34m(start, *args)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\"\"\"Multiply all arguments.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"product expected at least 1 arguments, got 0\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mabc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mIterable\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0margs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: product expected at least 1 arguments, got 0" + ] + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "... works with the arguments passed either separately as *positional* arguments, *packed* together into a single *collection* argument, or *unpacked*, ..." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product([2, 5, 10])" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(*[2, 5, 10])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "... and can handle *streaming* data with *indefinite* \"length.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "185492401920000" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(make_finite_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In real-world projects, the data science practitioner must decide if it is worthwhile to make a function usable in various different forms as we do in this exercise. This may be over-engineered.\n", + "\n", + "Yet, two lessons are important to take away:\n", + "- It is a good idea to *mimic* the behavior of *built-ins* when accepting arguments, and\n", + "- make functions capable of working with *streaming* data." + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/09_mappings/01_exercises_solved.ipynb b/09_mappings/01_exercises_solved.ipynb new file mode 100644 index 0000000..19bd98a --- /dev/null +++ b/09_mappings/01_exercises_solved.ipynb @@ -0,0 +1,689 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/09_mappings/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 9: Mappings & Sets (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/09_mappings/00_content.ipynb) of Chapter 9.\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": [ + "## Working with Nested Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's write some code to analyze the historic soccer game [Brazil vs. Germany ](https://en.wikipedia.org/wiki/Brazil_v_Germany_%282014_FIFA_World_Cup%29) during the 2014 World Cup.\n", + "\n", + "Below, `players` consists of two nested `dict` objects, one for each team, that hold `tuple` objects (i.e., records) with information on the players. Besides the jersey number, name, and position, each `tuple` objects contains a `list` object with the times when the player scored." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "players = {\n", + " \"Brazil\": [\n", + " (12, \"JĂºlio CĂ©sar\", \"Goalkeeper\", []),\n", + " (4, \"David Luiz\", \"Defender\", []),\n", + " (6, \"Marcelo\", \"Defender\", []),\n", + " (13, \"Dante\", \"Defender\", []),\n", + " (23, \"Maicon\", \"Defender\", []),\n", + " (5, \"Fernandinho\", \"Midfielder\", []),\n", + " (7, \"Hulk\", \"Midfielder\", []),\n", + " (8, \"Paulinho\", \"Midfielder\", []),\n", + " (11, \"Oscar\", \"Midfielder\", [90]),\n", + " (16, \"Ramires\", \"Midfielder\", []),\n", + " (17, \"Luiz Gustavo\", \"Midfielder\", []),\n", + " (19, \"Willian\", \"Midfielder\", []),\n", + " (9, \"Fred\", \"Striker\", []),\n", + " ],\n", + " \"Germany\": [\n", + " (1, \"Manuel Neuer\", \"Goalkeeper\", []),\n", + " (4, \"Benedikt Höwedes\", \"Defender\", []),\n", + " (5, \"Mats Hummels\", \"Defender\", []),\n", + " (16, \"Philipp Lahm\", \"Defender\", []),\n", + " (17, \"Per Mertesacker\", \"Defender\", []),\n", + " (20, \"JĂ©rĂ´me Boateng\", \"Defender\", []),\n", + " (6, \"Sami Khedira\", \"Midfielder\", [29]),\n", + " (7, \"Bastian Schweinsteiger\", \"Midfielder\", []),\n", + " (8, \"Mesut Ă–zil\", \"Midfielder\", []),\n", + " (13, \"Thomas MĂ¼ller\", \"Midfielder\", [11]),\n", + " (14, \"Julian Draxler\", \"Midfielder\", []),\n", + " (18, \"Toni Kroos\", \"Midfielder\", [24, 26]),\n", + " (9, \"AndrĂ© SchĂ¼rrle\", \"Striker\", [69, 79]),\n", + " (11, \"Miroslav Klose\", \"Striker\", [23]),\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Write a dictionary comprehension to derive a new `dict` object, called `brazilian_players`, that maps a Brazilian player's name to his position!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "brazilian_players = {name: position for _, name, position, _ in players[\"Brazil\"]}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'JĂºlio CĂ©sar': 'Goalkeeper',\n", + " 'David Luiz': 'Defender',\n", + " 'Marcelo': 'Defender',\n", + " 'Dante': 'Defender',\n", + " 'Maicon': 'Defender',\n", + " 'Fernandinho': 'Midfielder',\n", + " 'Hulk': 'Midfielder',\n", + " 'Paulinho': 'Midfielder',\n", + " 'Oscar': 'Midfielder',\n", + " 'Ramires': 'Midfielder',\n", + " 'Luiz Gustavo': 'Midfielder',\n", + " 'Willian': 'Midfielder',\n", + " 'Fred': 'Striker'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brazilian_players" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Generalize the code fragment into a `get_players()` function: Passed a `team` name, it returns a `dict` object like `brazilian_players`. Verify that the function works for the German team as well!" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def get_players(team):\n", + " \"\"\"Creates a dictionary mapping the players' names to their position.\"\"\"\n", + " return {name: position for _, name, position, _ in players[team]}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Manuel Neuer': 'Goalkeeper',\n", + " 'Benedikt Höwedes': 'Defender',\n", + " 'Mats Hummels': 'Defender',\n", + " 'Philipp Lahm': 'Defender',\n", + " 'Per Mertesacker': 'Defender',\n", + " 'JĂ©rĂ´me Boateng': 'Defender',\n", + " 'Sami Khedira': 'Midfielder',\n", + " 'Bastian Schweinsteiger': 'Midfielder',\n", + " 'Mesut Ă–zil': 'Midfielder',\n", + " 'Thomas MĂ¼ller': 'Midfielder',\n", + " 'Julian Draxler': 'Midfielder',\n", + " 'Toni Kroos': 'Midfielder',\n", + " 'AndrĂ© SchĂ¼rrle': 'Striker',\n", + " 'Miroslav Klose': 'Striker'}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_players(\"Germany\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Often, we are given a `dict` object like the one returned from `get_players()`: Its main characteristic is that it maps a large set of unique keys (i.e., the players' names) onto a smaller set of non-unique values (i.e., the positions).\n", + "\n", + "**Q3**: Create a generic `invert()` function that swaps the keys and values of a `mapping` argument passed to it and returns them in a *new* `dict` object! Ensure that *no* key gets lost! Verify your implementation with the `brazilian_players` dictionary!\n", + "\n", + "Hints: Think of this as a grouping operation. The *new* values are `list` or `tuple` objects that hold the original keys. You may want to use either the [defaultdict ](https://docs.python.org/3/library/collections.html#collections.defaultdict) type from the [collections ](https://docs.python.org/3/library/collections.html) module in the [standard library ](https://docs.python.org/3/library/index.html) or the [.setdefault() ](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method on the ordinary `dict` type." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def invert(mapping):\n", + " \"\"\"Invert the keys and values of a mapping argument.\"\"\"\n", + " answer = {}\n", + " for key, value in mapping.items():\n", + " if value not in answer:\n", + " answer[value] = [key]\n", + " else:\n", + " answer[value].append(key)\n", + " return answer\n", + "\n", + "# Alternative 1\n", + "def invert(mapping):\n", + " \"\"\"Invert the keys and values of a mapping argument.\"\"\"\n", + " answer = {}\n", + " for key, value in mapping.items():\n", + " answer.setdefault(value, []).append(key)\n", + " return answer\n", + "\n", + "# Alternative 2\n", + "from collections import defaultdict\n", + "\n", + "def invert(mapping):\n", + " \"\"\"Invert the keys and values of a mapping argument.\"\"\"\n", + " answer = defaultdict(list)\n", + " for key, value in mapping.items():\n", + " answer[value].append(key)\n", + " return dict(answer)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Goalkeeper': ['JĂºlio CĂ©sar'],\n", + " 'Defender': ['David Luiz', 'Marcelo', 'Dante', 'Maicon'],\n", + " 'Midfielder': ['Fernandinho',\n", + " 'Hulk',\n", + " 'Paulinho',\n", + " 'Oscar',\n", + " 'Ramires',\n", + " 'Luiz Gustavo',\n", + " 'Willian'],\n", + " 'Striker': ['Fred']}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "invert(brazilian_players)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Write a `score_at_minute()` function: It takes two arguments, `team` and `minute`, and returns the number of goals the `team` has scored up until this time in the game.\n", + "\n", + "Hints: The function may reference the global `players` for simplicity. Earn bonus points if you can write this in a one-line expression using some *reduction* function and a `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def score_at_minute(team, minute):\n", + " \"\"\"Determine the number of goals scored by a team until a given minute.\"\"\"\n", + " score = 0\n", + " for _, _, _, mins in players[team]:\n", + " for m in mins:\n", + " if m <= minute:\n", + " score += 1\n", + " return score\n", + "\n", + "# Alternative: with a one-line expression.\n", + "def score_at_minute(team, minute):\n", + " \"\"\"Determine the number of goals scored by a team until a given minute.\"\"\"\n", + " return sum(1 for _, _, _, mins in players[team] for m in mins if m <= minute)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The score at half time was:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "score_at_minute(\"Brazil\", 45)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "score_at_minute(\"Germany\", 45)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The final score was:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "score_at_minute(\"Brazil\", 90)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "score_at_minute(\"Germany\", 90)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Write a `goals_by_player()` function that takes an argument like the global `players`, and returns a `dict` object mapping the players to the number of goals they scored!\n", + "\n", + "Hints: Do *not* \"hard code\" the names of the teams! Earn bonus points if you can solve it in a one-line `dict` comprehension." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def goals_by_player(players):\n", + " \"\"\"Create a dictionary mapping the players' names to the number of goals.\"\"\"\n", + " scorers = {}\n", + " for team in players.keys():\n", + " for _, name, _, goals in players[team]:\n", + " scorers[name] = len(goals)\n", + " return scorers\n", + "\n", + "# Alternative: with a one-line expression.\n", + "def goals_by_player(players):\n", + " \"\"\"Create a dictionary mapping the players' names to the number of goals.\"\"\"\n", + " return {n: len(g) for t in players for _, n, _, g in players[t]}" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'JĂºlio CĂ©sar': 0,\n", + " 'David Luiz': 0,\n", + " 'Marcelo': 0,\n", + " 'Dante': 0,\n", + " 'Maicon': 0,\n", + " 'Fernandinho': 0,\n", + " 'Hulk': 0,\n", + " 'Paulinho': 0,\n", + " 'Oscar': 1,\n", + " 'Ramires': 0,\n", + " 'Luiz Gustavo': 0,\n", + " 'Willian': 0,\n", + " 'Fred': 0,\n", + " 'Manuel Neuer': 0,\n", + " 'Benedikt Höwedes': 0,\n", + " 'Mats Hummels': 0,\n", + " 'Philipp Lahm': 0,\n", + " 'Per Mertesacker': 0,\n", + " 'JĂ©rĂ´me Boateng': 0,\n", + " 'Sami Khedira': 1,\n", + " 'Bastian Schweinsteiger': 0,\n", + " 'Mesut Ă–zil': 0,\n", + " 'Thomas MĂ¼ller': 1,\n", + " 'Julian Draxler': 0,\n", + " 'Toni Kroos': 2,\n", + " 'AndrĂ© SchĂ¼rrle': 2,\n", + " 'Miroslav Klose': 1}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "goals_by_player(players)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Write a `dict` comprehension to filter out the players who did *not* score from the preceding result.\n", + "\n", + "Hints: Reference the `goals_by_player()` function from before." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Oscar': 1,\n", + " 'Sami Khedira': 1,\n", + " 'Thomas MĂ¼ller': 1,\n", + " 'Toni Kroos': 2,\n", + " 'AndrĂ© SchĂ¼rrle': 2,\n", + " 'Miroslav Klose': 1}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{k: v for k, v in goals_by_player(players).items() if v > 0}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'AndrĂ© SchĂ¼rrle',\n", + " 'Miroslav Klose',\n", + " 'Oscar',\n", + " 'Sami Khedira',\n", + " 'Thomas MĂ¼ller',\n", + " 'Toni Kroos'}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# As a set comprehension.\n", + "{k for k, v in goals_by_player(players).items() if v > 0}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Write a `all_goals()` function that takes one argument like the global `players` and returns a `list` object containing $2$-element `tuple` objects where the first element is the minute a player scored and the second his name! The list should be sorted by the time.\n", + "\n", + "Hints: You may want to use either the built-in [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function or the `list` type's [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) method. Earn bonus points if you can write a one-line expression with a `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def all_goals(players):\n", + " \"\"\"Create a time table of the individual goals.\"\"\"\n", + " answer = []\n", + " for team in players.keys():\n", + " for _, name, _, minutes in players[team]:\n", + " for minute in minutes:\n", + " answer.append((minute, name))\n", + " return sorted(answer)\n", + "\n", + "# Alternative: with a one-line expression.\n", + "def all_goals(players):\n", + " \"\"\"Create a time table of the individual goals.\"\"\"\n", + " return sorted((m, n) for t in players for _, n, _, mins in players[t] for m in mins)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(11, 'Thomas MĂ¼ller'),\n", + " (23, 'Miroslav Klose'),\n", + " (24, 'Toni Kroos'),\n", + " (26, 'Toni Kroos'),\n", + " (29, 'Sami Khedira'),\n", + " (69, 'AndrĂ© SchĂ¼rrle'),\n", + " (79, 'AndrĂ© SchĂ¼rrle'),\n", + " (90, 'Oscar')]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_goals(players)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Lastly, write a `summary()` function that takes one argument like the global `players` and prints out a concise report of the goals, the score at the half, and the final result.\n", + "\n", + "Hints: Use the `all_goals()` and `score_at_minute()` functions from before.\n", + "\n", + "The output should look similar to this:\n", + "```\n", + "12' Gerd MĂ¼ller scores\n", + "...\n", + "HALFTIME: TeamA 1 TeamB 2\n", + "77' Ronaldo scores\n", + "...\n", + "FINAL: TeamA 1 TeamB 3\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "def summary(players):\n", + " \"\"\"Create a written summary of the game.\"\"\"\n", + " # Create two lists with the goals of either half.\n", + " goals = all_goals(players)\n", + " first_half_goals = [(m, n) for m, n in goals if m <= 45]\n", + " second_half_goals = [(m, n) for m, n in goals if m > 45]\n", + "\n", + " # Print the goals of the first half.\n", + " for minute, name in first_half_goals:\n", + " print(f\"{minute}' {name} scores\")\n", + "\n", + " # Print the half time score.\n", + " print(\"HALFTIME:\", end= \" \")\n", + " for team in players.keys():\n", + " score = score_at_minute(team, 45)\n", + " print(f\"{team} {score}\", end=\" \")\n", + " print(\"\")\n", + "\n", + " # Print the goals of the second half.\n", + " for minute, name in second_half_goals:\n", + " print(f\"{minute}' {name} scores\")\n", + "\n", + " # Print the final score.\n", + " print(\"FINAL:\", end=\" \")\n", + " for team in players.keys():\n", + " score = score_at_minute(team, 90)\n", + " print(f\"{team} {score}\", end=\" \")\n", + " print(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "11' Thomas MĂ¼ller scores\n", + "23' Miroslav Klose scores\n", + "24' Toni Kroos scores\n", + "26' Toni Kroos scores\n", + "29' Sami Khedira scores\n", + "HALFTIME: Brazil 0 Germany 5 \n", + "69' AndrĂ© SchĂ¼rrle scores\n", + "79' AndrĂ© SchĂ¼rrle scores\n", + "90' Oscar scores\n", + "FINAL: Brazil 1 Germany 7 \n" + ] + } + ], + "source": [ + "summary(players)" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/09_mappings/03_exercises_solved.ipynb b/09_mappings/03_exercises_solved.ipynb new file mode 100644 index 0000000..4021550 --- /dev/null +++ b/09_mappings/03_exercises_solved.ipynb @@ -0,0 +1,292 @@ +{ + "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 ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/09_mappings/03_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 9: Mappings & Sets (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/09_mappings/02_content.ipynb) of Chapter 9.\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": [ + "## Memoization without Side Effects" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is considered *bad practice* to make a function and thereby its correctness dependent on a program's *global state*: For example, in the \"*Easy at second Glance: Fibonacci Numbers*\" section in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/09_mappings/02_content.ipynb#\"Easy-at-second-Glance\"-Example:-Fibonacci-Numbers--%28revisited%29), we use a global `memo` to store the Fibonacci numbers that have already been calculated.\n", + "\n", + "That `memo` dictionary could be \"manipulated.\" More often than not, such things happen by accident: Imagine we wrote two independent recursive functions that both rely on memoization to solve different problems, and, unintentionally, we made both work with the *same* global `memo`. As a result, we would observe \"random\" bugs depending on the order in which we executed these functions. Such bugs are hard to track down in practice.\n", + "\n", + "A common remedy is to avoid global state and pass intermediate results \"down\" the recursion tree in a \"hidden\" argument. By convention, we prefix parameter names with a single leading underscore `_`, such as with `_memo` below, to indicate that the caller of our `fibonacci()` function *must not* use it. Also, we make `_memo` a *keyword-only* argument to force ourselves to always explicitly name it in a function call. Because it is an **implementation detail**, the `_memo` parameter is *not* mentioned in the docstring.\n", + "\n", + "Your task is to complete this version of `fibonacci()` so that the function works *without* any **side effects** in the global scope." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### \"Easy at third Glance\" Example: [Fibonacci Numbers ](https://en.wikipedia.org/wiki/Fibonacci_number)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def fibonacci(i, *, debug=False, _memo=None):\n", + " \"\"\"Calculate the ith Fibonacci number.\n", + "\n", + " Args:\n", + " i (int): index of the Fibonacci number to calculate\n", + " debug (bool): show non-cached calls; defaults to False\n", + "\n", + " Returns:\n", + " ith_fibonacci (int)\n", + " \"\"\"\n", + " # answer to Q1\n", + " if _memo is None:\n", + " _memo = {\n", + " 0: 0,\n", + " 1: 1,\n", + " }\n", + "\n", + " # answer to Q2\n", + " if i in _memo:\n", + " return _memo[i]\n", + "\n", + " if debug: # added for didactical purposes\n", + " print(f\"fibonacci({i}) is calculated\")\n", + "\n", + " # answer to Q3\n", + " recurse = (\n", + " fibonacci(i - 1, debug=debug, _memo=_memo)\n", + " + fibonacci(i - 2, debug=debug, _memo=_memo)\n", + " )\n", + " # answer to Q4\n", + " _memo[i] = recurse\n", + " return recurse" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Q1**: When `fibonacci()` is initially called, `_memo` is set to `None`. So, there is *no* `dict` object yet. Implement the *two* base cases in the first `if` statement!\n", + "\n", + "Hints: All you need to do is create a *new* `dict` object with the results for `i=0` and `i=1`. This object is then passed on in the recursive function calls. Use the `is` operator in the `if` statement." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: When `fibonacci()` is called for non-base cases (i.e., `i > 1`), it first checks if the result is already in the `_memo`. Implement that step in the second `if` statement!\n", + "\n", + "Hint: Use the early exit pattern." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: If `fibonacci()` is called for an `i` argument whose result is not yet in the `_memo`, it must calculate it with the usual recursive function calls. Fill in the arguments to the two recursive `fibonacci()` calls!\n", + "\n", + "Hint: You must pass on the hidden `_memo`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Lastly, after the two recursive calls have returned, `fibonacci()` must store the `recurse` result for the given `i` in the `_memo` *before* returning it. Implement that logic!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What happens to the hidden `_memo` after the initial call to `fibonacci()` returned? How many hidden `_memo` objects exist in memory during the entire computation?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because `fibonacci()` is now independent of the *global state*, the same eleven recursive function calls are made each time! So, this `fibonacci()` is a **pure** function, meaning it has *no* side effects.\n", + "\n", + "**Q6**: Execute the following code cell a couple of times to observe that!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fibonacci(12) is calculated\n", + "fibonacci(11) is calculated\n", + "fibonacci(10) is calculated\n", + "fibonacci(9) is calculated\n", + "fibonacci(8) is calculated\n", + "fibonacci(7) is calculated\n", + "fibonacci(6) is calculated\n", + "fibonacci(5) is calculated\n", + "fibonacci(4) is calculated\n", + "fibonacci(3) is calculated\n", + "fibonacci(2) is calculated\n" + ] + }, + { + "data": { + "text/plain": [ + "144" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12, debug=True) # = 13th Fibonacci number -> 11 recursive calls necessary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The runtime of `fibonacci()` is now stable: There is no message that \"an intermediate result is being cached\" as in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/09_mappings/02_content.ipynb#\"Easy-at-second-Glance\"-Example:-Fibonacci-Numbers--%28revisited%29).\n", + "\n", + "**Q7**: Execute the following code cells a couple of times to observe that!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "287 µs ± 40.1 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1\n", + "fibonacci(99)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.45 ms ± 1.18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1\n", + "fibonacci(999)" + ] + } + ], + "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.8.6" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/11_classes/01_exercises_solved.ipynb b/11_classes/01_exercises_solved.ipynb new file mode 100644 index 0000000..3b4334f --- /dev/null +++ b/11_classes/01_exercises_solved.ipynb @@ -0,0 +1,2534 @@ +{ + "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 ](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 ](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 ](https://en.wikipedia.org/wiki/Traveling_salesman_problem)**.\n", + "\n", + "Imagine that a tourist lands at Berlin's [Tegel Airport ](https://en.wikipedia.org/wiki/Berlin_Tegel_Airport) in the morning and has his \"connecting\" flight from [Schönefeld Airport ](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": 1, + "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 ](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": 2, + "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 ](https://github.com/googlemaps).\n", + "\n", + "**Q3**: Familiarize yourself with the [googlemaps ](https://github.com/googlemaps/google-maps-services-python) package! Then, install it with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: googlemaps in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (4.4.2)\n", + "Requirement already satisfied: requests<3.0,>=2.20.0 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from googlemaps) (2.24.0)\n", + "Requirement already satisfied: idna<3,>=2.5 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (2.10)\n", + "Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (1.25.11)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (2020.6.20)\n", + "Requirement already satisfied: chardet<4,>=3.0.2 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (3.0.4)\n" + ] + } + ], + "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": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import googlemaps" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "api = googlemaps.Client(key=key)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "api" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "googlemaps.client.Client" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['base_url',\n", + " 'channel',\n", + " 'clear_experience_id',\n", + " 'client_id',\n", + " 'client_secret',\n", + " 'directions',\n", + " 'distance_matrix',\n", + " 'elevation',\n", + " 'elevation_along_path',\n", + " 'find_place',\n", + " 'geocode',\n", + " 'geolocate',\n", + " 'get_experience_id',\n", + " 'key',\n", + " 'nearest_roads',\n", + " 'place',\n", + " 'places',\n", + " 'places_autocomplete',\n", + " 'places_autocomplete_query',\n", + " 'places_nearby',\n", + " 'places_photo',\n", + " 'queries_per_second',\n", + " 'requests_kwargs',\n", + " 'retry_over_query_limit',\n", + " 'retry_timeout',\n", + " 'reverse_geocode',\n", + " 'sent_times',\n", + " 'session',\n", + " 'set_experience_id',\n", + " 'snap_to_roads',\n", + " 'snapped_speed_limits',\n", + " 'speed_limits',\n", + " 'static_map',\n", + " 'timeout',\n", + " 'timezone']" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'address_components': [{'long_name': 'Pariser Platz',\n", + " 'short_name': 'Pariser Platz',\n", + " 'types': ['route']},\n", + " {'long_name': 'Mitte',\n", + " 'short_name': 'Mitte',\n", + " 'types': ['political', 'sublocality', 'sublocality_level_1']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['locality', 'political']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['administrative_area_level_1', 'political']},\n", + " {'long_name': 'Germany',\n", + " 'short_name': 'DE',\n", + " 'types': ['country', 'political']},\n", + " {'long_name': '10117', 'short_name': '10117', 'types': ['postal_code']}],\n", + " 'formatted_address': 'Pariser Platz, 10117 Berlin, Germany',\n", + " 'geometry': {'location': {'lat': 52.5162746, 'lng': 13.3777041},\n", + " 'location_type': 'GEOMETRIC_CENTER',\n", + " 'viewport': {'northeast': {'lat': 52.51762358029149,\n", + " 'lng': 13.3790530802915},\n", + " 'southwest': {'lat': 52.5149256197085, 'lng': 13.3763551197085}}},\n", + " 'place_id': 'ChIJiQnyVcZRqEcRY0xnhE77uyY',\n", + " 'plus_code': {'compound_code': 'G98H+G3 Berlin, Germany',\n", + " 'global_code': '9F4MG98H+G3'},\n", + " 'types': ['establishment', 'point_of_interest', 'tourist_attraction']}]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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() ](https://docs.python.org/3/library/pprint.html#pprint.pprint) function in the [pprint ](https://docs.python.org/3/library/pprint.html) module in the [standard library ](https://docs.python.org/3/library/index.html)!" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "response = api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = response[0]" + ] + }, + { + "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": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'address_components': [{'long_name': 'Pariser Platz',\n", + " 'short_name': 'Pariser Platz',\n", + " 'types': ['route']},\n", + " {'long_name': 'Mitte',\n", + " 'short_name': 'Mitte',\n", + " 'types': ['political',\n", + " 'sublocality',\n", + " 'sublocality_level_1']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['locality', 'political']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['administrative_area_level_1', 'political']},\n", + " {'long_name': 'Germany',\n", + " 'short_name': 'DE',\n", + " 'types': ['country', 'political']},\n", + " {'long_name': '10117',\n", + " 'short_name': '10117',\n", + " 'types': ['postal_code']}],\n", + " 'formatted_address': 'Pariser Platz, 10117 Berlin, Germany',\n", + " 'geometry': {'location': {'lat': 52.5162746, 'lng': 13.3777041},\n", + " 'location_type': 'GEOMETRIC_CENTER',\n", + " 'viewport': {'northeast': {'lat': 52.51762358029149,\n", + " 'lng': 13.3790530802915},\n", + " 'southwest': {'lat': 52.5149256197085,\n", + " 'lng': 13.3763551197085}}},\n", + " 'place_id': 'ChIJiQnyVcZRqEcRY0xnhE77uyY',\n", + " 'plus_code': {'compound_code': 'G98H+G3 Berlin, Germany',\n", + " 'global_code': '9F4MG98H+G3'},\n", + " 'types': ['establishment', 'point_of_interest', 'tourist_attraction']}\n" + ] + } + ], + "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": 14, + "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", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\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 = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " # answer to Q10\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Verify that the instantiating a `Place` object works!" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Pariser Platz, 10117 Berlin, Germany'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.address" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'ChIJiQnyVcZRqEcRY0xnhE77uyY'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.place_id" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(52.5162746, 13.3777041)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 21, + "metadata": { + "code_folding": [] + }, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\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", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\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", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude\n", + "\n", + " # answer to Q14\n", + " @classmethod\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 = cls(address, client=client)\n", + " if sync:\n", + " place.sync_from_google()\n", + " places.append(place)\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": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Place.from_addresses(sights, client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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 ](https://github.com/python-visualization/folium) to achieve that.\n", + "\n", + "**Q16**: Familiarize yourself with [folium ](https://github.com/python-visualization/folium) and install it with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: folium in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (0.11.0)\n", + "Requirement already satisfied: requests in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (2.24.0)\n", + "Requirement already satisfied: jinja2>=2.9 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (2.11.2)\n", + "Requirement already satisfied: branca>=0.3.0 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (0.4.1)\n", + "Requirement already satisfied: numpy in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (1.19.2)\n", + "Requirement already satisfied: idna<3,>=2.5 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (2.10)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (2020.6.20)\n", + "Requirement already satisfied: chardet<4,>=3.0.2 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (3.0.4)\n", + "Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (1.25.11)\n", + "Requirement already satisfied: MarkupSafe>=0.23 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from jinja2>=2.9->folium) (1.1.1)\n" + ] + } + ], + "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": 25, + "metadata": {}, + "outputs": [], + "source": [ + "import folium" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = folium.Map(location=(52.513186, 13.3944349), zoom_start=14)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "folium.folium.Map" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "berlin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to put something on the map, [folium ](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, `\"Brandenburger Tor
(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": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mfolium\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mMarker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlocation\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpopup\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtooltip\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0micon\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdraggable\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m \n", + "Create a simple stock Leaflet marker on the map, with optional\n", + "popup text or Vincent visualization.\n", + "\n", + "Parameters\n", + "----------\n", + "location: tuple or list\n", + " Latitude and Longitude of Marker (Northing, Easting)\n", + "popup: string or folium.Popup, default None\n", + " Label for the Marker; either an escaped HTML string to initialize\n", + " folium.Popup or a folium.Popup instance.\n", + "tooltip: str or folium.Tooltip, default None\n", + " Display a text when hovering over the object.\n", + "icon: Icon plugin\n", + " the Icon plugin to use to render the marker.\n", + "draggable: bool, default False\n", + " Set to True to be able to drag the marker around the map.\n", + "\n", + "Returns\n", + "-------\n", + "Marker names and HTML in obj.template_vars\n", + "\n", + "Examples\n", + "--------\n", + ">>> Marker(location=[45.5, -122.3], popup='Portland, OR')\n", + ">>> Marker(location=[45.5, -122.3], popup=Popup('Portland, OR'))\n", + "# If the popup label has characters that need to be escaped in HTML\n", + ">>> Marker(location=[45.5, -122.3],\n", + "... popup=Popup('Mom & Pop Arrow Shop >>', parse_html=True))\n", + "\u001b[0;31mFile:\u001b[0m ~/repos/intro-to-python/.venv/lib/python3.8/site-packages/folium/map.py\n", + "\u001b[0;31mType:\u001b[0m type\n", + "\u001b[0;31mSubclasses:\u001b[0m Circle, CircleMarker, RegularPolygonMarker\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "folium.Marker?" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "m = folium.Marker(\n", + " location=brandenburg_gate.location,\n", + " popup=f\"{brandenburg_gate.name}
({brandenburg_gate.address})\",\n", + " tooltip=brandenburg_gate.name,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "folium.map.Marker" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.add_to(berlin)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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 ](https://github.com/python-visualization/folium)'s `Icon` type to control the color of the marker." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\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", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\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", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude\n", + "\n", + " @classmethod\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 = cls(address, client=client)\n", + " if sync:\n", + " place.sync_from_google()\n", + " places.append(place)\n", + " return places\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=self.location,\n", + " popup=f\"{self.name}
({self.address})\",\n", + " tooltip=self.name,\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": 35, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "must synchronize with Google first", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mbrandenburg_gate\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mas_marker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mas_marker\u001b[0;34m(self, color)\u001b[0m\n\u001b[1;32m 69\u001b[0m \"\"\"\n\u001b[1;32m 70\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplace_id\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 71\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"must synchronize with Google first\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 72\u001b[0m return folium.Marker(\n\u001b[1;32m 73\u001b[0m \u001b[0mlocation\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlocation\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mRuntimeError\u001b[0m: must synchronize with Google first" + ] + } + ], + "source": [ + "brandenburg_gate.as_marker()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 38, + "metadata": {}, + "outputs": [], + "source": [ + "places = Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Map` Class" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "subslide" + } + }, + "source": [ + "To make [folium ](https://github.com/python-visualization/folium)'s `Map` class work even better with our `Place` instances, we write our own `Map` class wrapping [folium ](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": 40, + "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", + " self._start = start\n", + " self._end = end\n", + " self._places = places\n", + " self._map = folium.Map(location=center, zoom_start=initial_zoom)\n", + "\n", + " # Add markers to the map.\n", + " self.add_marker(start.as_marker(color=\"green\"))\n", + " self.add_marker(end.as_marker(color=\"red\"))\n", + " for place in places:\n", + " self.add_marker(place.as_marker())\n", + "\n", + " def __repr__(self):\n", + " return f\"\"\n", + "\n", + " # answer to Q24\n", + " def show(self):\n", + " \"\"\"Return a folium.Map representation of the map.\"\"\"\n", + " return self._map\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", + " marker.add_to(self._map)\n", + " return self" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's put all the sights, the two airports, and three more places, the [Bundeskanzleramt ](https://en.wikipedia.org/wiki/German_Chancellery), the [Olympic Stadium ](https://en.wikipedia.org/wiki/Olympiastadion_%28Berlin%29), and the [East Side Gallery ](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": 41, + "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": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "berlin" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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 ](https://github.com/geopy/geopy).\n", + "\n", + "**Q26**: Familiarize yourself with the [documentation](https://geopy.readthedocs.io/en/stable/) and install [geopy ](https://github.com/geopy/geopy) with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: geopy in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (2.0.0)\n", + "Requirement already satisfied: geographiclib<2,>=1.49 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from geopy) (1.50)\n" + ] + } + ], + "source": [ + "!pip install geopy" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use [geopy ](https://github.com/geopy/geopy) primarily for converting the `latitude`-`longitude` coordinates into a [distance matrix ](https://en.wikipedia.org/wiki/Distance_matrix).\n", + "\n", + "Because the [earth is not flat ](https://en.wikipedia.org/wiki/Flat_Earth), [geopy ](https://github.com/geopy/geopy) provides a `great_circle()` function that calculates the so-called [orthodromic distance ](https://en.wikipedia.org/wiki/Great-circle_distance) between two places on a sphere." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "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": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mInit signature:\u001b[0m \u001b[0mgreat_circle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m \n", + "Use spherical geometry to calculate the surface distance between two\n", + "points.\n", + "\n", + "Set which radius of the earth to use by specifying a ``radius`` keyword\n", + "argument. It must be in kilometers. The default is to use the module\n", + "constant `EARTH_RADIUS`, which uses the average great-circle radius.\n", + "\n", + "Example::\n", + "\n", + " >>> from geopy.distance import great_circle\n", + " >>> newport_ri = (41.49008, -71.312796)\n", + " >>> cleveland_oh = (41.499498, -81.695391)\n", + " >>> print(great_circle(newport_ri, cleveland_oh).miles)\n", + " 536.997990696\n", + "\u001b[0;31mFile:\u001b[0m ~/repos/intro-to-python/.venv/lib/python3.8/site-packages/geopy/distance.py\n", + "\u001b[0;31mType:\u001b[0m ABCMeta\n", + "\u001b[0;31mSubclasses:\u001b[0m \n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "great_circle?" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "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": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Distance(25.370448366418135)" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "great_circle(tegel.location, schoenefeld.location)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25.370448366418135" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "great_circle(tegel.location, schoenefeld.location).km" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25370.448366418135" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 51, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\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", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\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", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude\n", + "\n", + " @classmethod\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 = cls(address, client=client)\n", + " if sync:\n", + " place.sync_from_google()\n", + " places.append(place)\n", + " return places\n", + "\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=self.location,\n", + " popup=f\"{self.name}
({self.address})\",\n", + " tooltip=self.name,\n", + " icon=folium.Icon(color=color),\n", + " )\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 int(great_circle(self.location, other.location).meters)" + ] + }, + { + "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": 52, + "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": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25370" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 54, + "metadata": {}, + "outputs": [], + "source": [ + "places = Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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() ](https://docs.python.org/3/library/itertools.html#itertools.combinations) generator function in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](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() ](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": 56, + "metadata": {}, + "outputs": [], + "source": [ + "import itertools" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2\n", + "1 3\n", + "1 4\n", + "1 5\n", + "2 3\n", + "2 4\n", + "2 5\n", + "3 4\n", + "3 5\n", + "4 5\n" + ] + } + ], + "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": 58, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answers from above with a tiny adjustment\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", + " self._start = start\n", + " self._end = end\n", + " self._places = places\n", + " self._map = folium.Map(location=center, zoom_start=initial_zoom)\n", + " self._distances = None\n", + "\n", + " self.add_marker(start.as_marker(color=\"green\"))\n", + " self.add_marker(end.as_marker(color=\"red\"))\n", + " for place in places:\n", + " self.add_marker(place.as_marker())\n", + "\n", + " def __repr__(self):\n", + " return f\"\"\n", + "\n", + " def show(self):\n", + " \"\"\"Return a folium.Map representation of the map.\"\"\"\n", + " return self._map\n", + "\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", + " marker.add_to(self._map)\n", + " return self\n", + "\n", + " # answer to Q31\n", + " @property\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", + " [self._start, self._end] + self._places,\n", + " r=2,\n", + " )\n", + " for origin, destination in all_pairs:\n", + " distance = origin.distance_to(destination)\n", + " distances[origin, destination] = distance\n", + " distances[destination, origin] = distance\n", + " self._distances = distances\n", + " return self._distances" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We pretty print the total distance matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "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": 60, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{(, ): 7671,\n", + " (, ): 4654,\n", + " (, ): 5589,\n", + " (, ): 1821,\n", + " (, ): 9386,\n", + " (, ): 25370,\n", + " (, ): 9386,\n", + " (, ): 25370,\n", + " (, ): 4654,\n", + " (, ): 5878,\n", + " (, ): 2839,\n", + " (, ): 5112,\n", + " (, ): 8955,\n", + " (, ): 7430,\n", + " (, ): 7981,\n", + " (, ): 11020,\n", + " (, ): 7671,\n", + " (, ): 280,\n", + " (, ): 11020,\n", + " (, ): 6454,\n", + " (, ): 7981,\n", + " (, ): 6454,\n", + " (, ): 9266,\n", + " (, ): 7430,\n", + " (, ): 12042,\n", + " (, ): 9266,\n", + " (, ): 17695,\n", + " (, ): 12042,\n", + " (, ): 18133,\n", + " (, ): 17695,\n", + " (, ): 16861,\n", + " (, ): 18133,\n", + " (, ): 15199,\n", + " (, ): 16861,\n", + " (, ): 20057,\n", + " (, ): 15199,\n", + " (, ): 18944,\n", + " (, ): 20057,\n", + " (, ): 18411,\n", + " (, ): 18944,\n", + " (, ): 18230,\n", + " (, ): 18411,\n", + " (, ): 13382,\n", + " (, ): 18230,\n", + " (, ): 2491,\n", + " (, ): 13382,\n", + " (, ): 2240,\n", + " (, ): 2491,\n", + " (, ): 2577,\n", + " (, ): 2240,\n", + " (, ): 2368,\n", + " (, ): 2577,\n", + " (, ): 4354,\n", + " (, ): 2368,\n", + " (, ): 2539,\n", + " (, ): 4354,\n", + " (, ): 642,\n", + " (, ): 2539,\n", + " (, ): 5237,\n", + " (, ): 642,\n", + " (, ): 1304,\n", + " (, ): 5237,\n", + " (, ): 3353,\n", + " (, ): 1304,\n", + " (, ): 3383,\n", + " (, ): 3353,\n", + " (, ): 1876,\n", + " (, ): 3383,\n", + " (, ): 1876,\n", + " (, ): 2848,\n", + " (, ): 280,\n", + " (, ): 4831,\n", + " (, ): 2848,\n", + " (, ): 2110,\n", + " (, ): 4831,\n", + " (, ): 3984,\n", + " (, ): 2110,\n", + " (, ): 2837,\n", + " (, ): 3984,\n", + " (, ): 1571,\n", + " (, ): 2837,\n", + " (, ): 2811,\n", + " (, ): 1571,\n", + " (, ): 3648,\n", + " (, ): 2811,\n", + " (, ): 4930,\n", + " (, ): 3648,\n", + " (, ): 4928,\n", + " (, ): 4930,\n", + " (, ): 3590,\n", + " (, ): 4928,\n", + " (, ): 3185,\n", + " (, ): 3590,\n", + " (, ): 2851,\n", + " (, ): 3185,\n", + " (, ): 4749,\n", + " (, ): 2851,\n", + " (, ): 3219,\n", + " (, ): 4749,\n", + " (, ): 1838,\n", + " (, ): 3219,\n", + " (, ): 7451,\n", + " (, ): 1838,\n", + " (, ): 1821,\n", + " (, ): 7451,\n", + " (, ): 5589,\n", + " (, ): 5112,\n", + " (, ): 2839,\n", + " (, ): 5878,\n", + " (, ): 8955}\n" + ] + } + ], + "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": 61, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "110" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_places = len(places) + 2\n", + "\n", + "n_places * (n_places - 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "110" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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 ](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 ](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 ](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": 63, + "metadata": {}, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "181440" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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 ](https://en.wikipedia.org/wiki/Permutation) of the sights as the two airports are always the first and last location.\n", + "\n", + "The [permutations() ](https://docs.python.org/3/library/itertools.html#itertools.permutations) generator function in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](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": 65, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1, 2, 3)\n", + "(1, 3, 2)\n", + "(2, 1, 3)\n", + "(2, 3, 1)\n", + "(3, 1, 2)\n", + "(3, 2, 1)\n" + ] + } + ], + "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() ](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": 66, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1, 2, 3)\n", + "(1, 3, 2)\n", + "(2, 1, 3)\n" + ] + } + ], + "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": 67, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'<' not supported between instances of 'Place' and 'Place'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mPlace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marrival\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclient\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mapi\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mPlace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdeparture\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclient\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mapi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m: '<' not supported between instances of 'Place' and 'Place'" + ] + } + ], + "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": 68, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Place(arrival, client=api).location < Place(departure, client=api).location" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As the code cell below shows, combining [permutations() ](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": 69, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "181440" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 70, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answers from above\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", + " self._start = start\n", + " self._end = end\n", + " self._places = places\n", + " self._map = folium.Map(location=center, zoom_start=initial_zoom)\n", + " self._distances = None\n", + "\n", + " self.add_marker(start.as_marker(color=\"green\"))\n", + " self.add_marker(end.as_marker(color=\"red\"))\n", + " for place in places:\n", + " self.add_marker(place.as_marker())\n", + "\n", + " def __repr__(self):\n", + " return f\"\"\n", + "\n", + " def show(self):\n", + " \"\"\"Return a folium.Map representation of the map.\"\"\"\n", + " return self._map\n", + "\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", + " marker.add_to(self._map)\n", + " return self\n", + "\n", + " @property\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", + " [self._start, self._end] + self._places, r=2,\n", + " )\n", + " for origin, destination in all_pairs:\n", + " distance = origin.distance_to(destination)\n", + " distances[origin, destination] = distance\n", + " distances[destination, origin] = distance\n", + " self._distances = distances\n", + "\n", + " return self._distances\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 = 0\n", + " # Loop over all pairs of consecutive places.\n", + " origin = route[0]\n", + " for destination in route[1:]:\n", + " cost += self.distances[origin, destination]\n", + " origin = destination\n", + "\n", + " return cost\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 = 999_999_999\n", + " best_route = None\n", + "\n", + " # Loop over all permutations of the intermediate nodes to visit.\n", + " for permutation in itertools.permutations(self._places):\n", + " # Skip redundant permutations.\n", + " if permutation[0].location > permutation[-1].location:\n", + " continue\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 = (self._start, *route, self._end)\n", + " # Check if a route is cheaper than all routes seen before.\n", + " cost = self.evaluate(route)\n", + " if cost < min_cost:\n", + " min_cost = cost\n", + " best_route = 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 self" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q34**: Find the best route for our tourist by executing the code cells below!" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "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": 72, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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.8.6" + }, + "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 +} diff --git a/poetry.lock b/poetry.lock index e50d7f0..23df10c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,6 +85,17 @@ packaging = "*" six = ">=1.9.0" webencodings = "*" +[[package]] +name = "branca" +version = "0.4.1" +description = "Generate complex HTML+JS pages with Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +jinja2 = "*" + [[package]] name = "certifi" version = "2020.6.20" @@ -179,6 +190,62 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "folium" +version = "0.11.0" +description = "Make beautiful maps with Leaflet.js & Python" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +branca = ">=0.3.0" +jinja2 = ">=2.9" +numpy = "*" +requests = "*" + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "geographiclib" +version = "1.50" +description = "The geodesic routines from GeographicLib" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "geopy" +version = "2.0.0" +description = "Python Geocoding Toolbox" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +geographiclib = ">=1.49,<2" + +[package.extras] +aiohttp = ["aiohttp"] +dev = ["async-generator", "flake8 (>=3.6.0,<3.7.0)", "isort (>=4.3.4,<4.4.0)", "coverage", "pytest-aiohttp", "pytest (>=3.10)", "readme-renderer", "sphinx", "sphinx-rtd-theme (>=0.4.0)"] +dev-docs = ["readme-renderer", "sphinx", "sphinx-rtd-theme (>=0.4.0)"] +dev-lint = ["async-generator", "flake8 (>=3.6.0,<3.7.0)", "isort (>=4.3.4,<4.4.0)"] +dev-test = ["async-generator", "coverage", "pytest-aiohttp", "pytest (>=3.10)"] +requests = ["urllib3 (>=1.24.2)", "requests (>=2.16.2)"] +timezone = ["pytz"] + +[[package]] +name = "googlemaps" +version = "4.4.2" +description = "Python client library for Google Maps Platform" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +requests = ">=2.20.0,<3.0" + [[package]] name = "identify" version = "1.5.6" @@ -218,7 +285,7 @@ test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose"] [[package]] name = "ipython" -version = "7.18.1" +version = "7.19.0" description = "IPython: Productive Interactive Computing" category = "main" optional = false @@ -634,7 +701,7 @@ tox_to_nox = ["jinja2", "tox"] [[package]] name = "numpy" -version = "1.19.2" +version = "1.19.3" description = "NumPy is the fundamental package for array computing with Python." category = "main" optional = false @@ -692,7 +759,7 @@ python-versions = "*" [[package]] name = "pre-commit" -version = "2.7.1" +version = "2.8.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -839,7 +906,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] name = "rise" -version = "5.7.0" +version = "5.7.1" description = "Reveal.js - Jupyter/IPython Slideshow Extension" category = "dev" optional = false @@ -890,15 +957,15 @@ test = ["pathlib2"] [[package]] name = "toml" -version = "0.10.1" +version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tornado" -version = "6.0.4" +version = "6.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." category = "main" optional = false @@ -986,7 +1053,7 @@ tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "p [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "0483228a1bbf92c52f5045db05de80b951dbd7302db756ca6f65cd56b482b814" +content-hash = "d195ae57b5a36e1ddf290a2daafc9f99544c7a19b15e966b011cfee8435ad2b1" [metadata.files] appdirs = [ @@ -1035,6 +1102,10 @@ bleach = [ {file = "bleach-3.2.1-py2.py3-none-any.whl", hash = "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd"}, {file = "bleach-3.2.1.tar.gz", hash = "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080"}, ] +branca = [ + {file = "branca-0.4.1-py3-none-any.whl", hash = "sha256:84eb4a2cc2c6f988c7ed07523de18c9867baeac3539a24cb3b66c255399bb1c5"}, + {file = "branca-0.4.1.tar.gz", hash = "sha256:8a9df7811a4d845ffaddad1030075cf26157c91d0be10b4f800ef1c8b3caedb8"}, +] certifi = [ {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, @@ -1112,6 +1183,21 @@ filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] +folium = [ + {file = "folium-0.11.0-py2.py3-none-any.whl", hash = "sha256:8a819fe19791411fc7c7202ae597787e7b4b5189b23f43756931d2ff73a48f4a"}, + {file = "folium-0.11.0.tar.gz", hash = "sha256:540789abc21872469e52c59ac3962c61259a8df557feadd6514eb23eb0a64ca7"}, +] +geographiclib = [ + {file = "geographiclib-1.50-py3-none-any.whl", hash = "sha256:51cfa698e7183792bce27d8fb63ac8e83689cd8170a730bf35e1a5c5bf8849b9"}, + {file = "geographiclib-1.50.tar.gz", hash = "sha256:12bd46ee7ec25b291ea139b17aa991e7ef373e21abd053949b75c0e9ca55c632"}, +] +geopy = [ + {file = "geopy-2.0.0-py3-none-any.whl", hash = "sha256:fdc596bab67d9f4828f43bf9e97c4e0a1d1518b7c2357485cef0e1c3cf470220"}, + {file = "geopy-2.0.0.tar.gz", hash = "sha256:b88e189f0d2e0051da45b6311d5b2ced59afaf1378eb27ebb57eaf37c166a03b"}, +] +googlemaps = [ + {file = "googlemaps-4.4.2.tar.gz", hash = "sha256:8e13cbecc9b5b0462d53a78074c166753027be285db0c9fff70f5b40241ce500"}, +] identify = [ {file = "identify-1.5.6-py2.py3-none-any.whl", hash = "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e"}, {file = "identify-1.5.6.tar.gz", hash = "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421"}, @@ -1125,8 +1211,8 @@ ipykernel = [ {file = "ipykernel-5.3.4.tar.gz", hash = "sha256:9b2652af1607986a1b231c62302d070bc0534f564c393a5d9d130db9abbbe89d"}, ] ipython = [ - {file = "ipython-7.18.1-py3-none-any.whl", hash = "sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8"}, - {file = "ipython-7.18.1.tar.gz", hash = "sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"}, + {file = "ipython-7.19.0-py3-none-any.whl", hash = "sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f"}, + {file = "ipython-7.19.0.tar.gz", hash = "sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -1292,32 +1378,40 @@ nox = [ {file = "nox-2020.8.22.tar.gz", hash = "sha256:efa5adcf1134012f96bcd0a496ccebd4c9e9da53a831888a2a779462440eebcf"}, ] numpy = [ - {file = "numpy-1.19.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b594f76771bc7fc8a044c5ba303427ee67c17a09b36e1fa32bde82f5c419d17a"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e6ddbdc5113628f15de7e4911c02aed74a4ccff531842c583e5032f6e5a179bd"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3733640466733441295b0d6d3dcbf8e1ffa7e897d4d82903169529fd3386919a"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:4339741994c775396e1a274dba3609c69ab0f16056c1077f18979bec2a2c2e6e"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c6646314291d8f5ea900a7ea9c4261f834b5b62159ba2abe3836f4fa6705526"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7118f0a9f2f617f921ec7d278d981244ba83c85eea197be7c5a4f84af80a9c3c"}, - {file = "numpy-1.19.2-cp36-cp36m-win32.whl", hash = "sha256:9a3001248b9231ed73894c773142658bab914645261275f675d86c290c37f66d"}, - {file = "numpy-1.19.2-cp36-cp36m-win_amd64.whl", hash = "sha256:967c92435f0b3ba37a4257c48b8715b76741410467e2bdb1097e8391fccfae15"}, - {file = "numpy-1.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d526fa58ae4aead839161535d59ea9565863bb0b0bdb3cc63214613fb16aced4"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eb25c381d168daf351147713f49c626030dcff7a393d5caa62515d415a6071d8"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:62139af94728d22350a571b7c82795b9d59be77fc162414ada6c8b6a10ef5d02"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0c66da1d202c52051625e55a249da35b31f65a81cb56e4c69af0dfb8fb0125bf"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:2117536e968abb7357d34d754e3733b0d7113d4c9f1d921f21a3d96dec5ff716"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54045b198aebf41bf6bf4088012777c1d11703bf74461d70cd350c0af2182e45"}, - {file = "numpy-1.19.2-cp37-cp37m-win32.whl", hash = "sha256:aba1d5daf1144b956bc87ffb87966791f5e9f3e1f6fab3d7f581db1f5b598f7a"}, - {file = "numpy-1.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:addaa551b298052c16885fc70408d3848d4e2e7352de4e7a1e13e691abc734c1"}, - {file = "numpy-1.19.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:58d66a6b3b55178a1f8a5fe98df26ace76260a70de694d99577ddeab7eaa9a9d"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:59f3d687faea7a4f7f93bd9665e5b102f32f3fa28514f15b126f099b7997203d"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cebd4f4e64cfe87f2039e4725781f6326a61f095bc77b3716502bed812b385a9"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c35a01777f81e7333bcf276b605f39c872e28295441c265cd0c860f4b40148c1"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d7ac33585e1f09e7345aa902c281bd777fdb792432d27fca857f39b70e5dd31c"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:04c7d4ebc5ff93d9822075ddb1751ff392a4375e5885299445fcebf877f179d5"}, - {file = "numpy-1.19.2-cp38-cp38-win32.whl", hash = "sha256:51ee93e1fac3fe08ef54ff1c7f329db64d8a9c5557e6c8e908be9497ac76374b"}, - {file = "numpy-1.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:1669ec8e42f169ff715a904c9b2105b6640f3f2a4c4c2cb4920ae8b2785dac65"}, - {file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"}, - {file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"}, + {file = "numpy-1.19.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:942d2cdcb362739908c26ce8dd88db6e139d3fa829dd7452dd9ff02cba6b58b2"}, + {file = "numpy-1.19.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:efd656893171bbf1331beca4ec9f2e74358fc732a2084f664fd149cc4b3441d2"}, + {file = "numpy-1.19.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1a307bdd3dd444b1d0daa356b5f4c7de2e24d63bdc33ea13ff718b8ec4c6a268"}, + {file = "numpy-1.19.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9d08d84bb4128abb9fbd9f073e5c69f70e5dab991a9c42e5b4081ea5b01b5db0"}, + {file = "numpy-1.19.3-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7197ee0a25629ed782c7bd01871ee40702ffeef35bc48004bc2fdcc71e29ba9d"}, + {file = "numpy-1.19.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:8edc4d687a74d0a5f8b9b26532e860f4f85f56c400b3a98899fc44acb5e27add"}, + {file = "numpy-1.19.3-cp36-cp36m-win32.whl", hash = "sha256:522053b731e11329dd52d258ddf7de5288cae7418b55e4b7d32f0b7e31787e9d"}, + {file = "numpy-1.19.3-cp36-cp36m-win_amd64.whl", hash = "sha256:eefc13863bf01583a85e8c1121a901cc7cb8f059b960c4eba30901e2e6aba95f"}, + {file = "numpy-1.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ff88bcf1872b79002569c63fe26cd2cda614e573c553c4d5b814fb5eb3d2822"}, + {file = "numpy-1.19.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e080087148fd70469aade2abfeadee194357defd759f9b59b349c6192aba994c"}, + {file = "numpy-1.19.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:50f68ebc439821b826823a8da6caa79cd080dee2a6d5ab9f1163465a060495ed"}, + {file = "numpy-1.19.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b9074d062d30c2779d8af587924f178a539edde5285d961d2dfbecbac9c4c931"}, + {file = "numpy-1.19.3-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:463792a249a81b9eb2b63676347f996d3f0082c2666fd0604f4180d2e5445996"}, + {file = "numpy-1.19.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:ea6171d2d8d648dee717457d0f75db49ad8c2f13100680e284d7becf3dc311a6"}, + {file = "numpy-1.19.3-cp37-cp37m-win32.whl", hash = "sha256:0ee77786eebbfa37f2141fd106b549d37c89207a0d01d8852fde1c82e9bfc0e7"}, + {file = "numpy-1.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:271139653e8b7a046d11a78c0d33bafbddd5c443a5b9119618d0652a4eb3a09f"}, + {file = "numpy-1.19.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e983cbabe10a8989333684c98fdc5dd2f28b236216981e0c26ed359aaa676772"}, + {file = "numpy-1.19.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d78294f1c20f366cde8a75167f822538a7252b6e8b9d6dbfb3bdab34e7c1929e"}, + {file = "numpy-1.19.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:199bebc296bd8a5fc31c16f256ac873dd4d5b4928dfd50e6c4995570fc71a8f3"}, + {file = "numpy-1.19.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:dffed17848e8b968d8d3692604e61881aa6ef1f8074c99e81647ac84f6038535"}, + {file = "numpy-1.19.3-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5ea4401ada0d3988c263df85feb33818dc995abc85b8125f6ccb762009e7bc68"}, + {file = "numpy-1.19.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:604d2e5a31482a3ad2c88206efd43d6fcf666ada1f3188fd779b4917e49b7a98"}, + {file = "numpy-1.19.3-cp38-cp38-win32.whl", hash = "sha256:a2daea1cba83210c620e359de2861316f49cc7aea8e9a6979d6cb2ddab6dda8c"}, + {file = "numpy-1.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:dfdc8b53aa9838b9d44ed785431ca47aa3efaa51d0d5dd9c412ab5247151a7c4"}, + {file = "numpy-1.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f7f56b5e85b08774939622b7d45a5d00ff511466522c44fc0756ac7692c00f2"}, + {file = "numpy-1.19.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8802d23e4895e0c65e418abe67cdf518aa5cbb976d97f42fd591f921d6dffad0"}, + {file = "numpy-1.19.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c4aa79993f5d856765819a3651117520e41ac3f89c3fc1cb6dee11aa562df6da"}, + {file = "numpy-1.19.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:51e8d2ae7c7e985c7bebf218e56f72fa93c900ad0c8a7d9fbbbf362f45710f69"}, + {file = "numpy-1.19.3-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:50d3513469acf5b2c0406e822d3f314d7ac5788c2b438c24e5dd54d5a81ef522"}, + {file = "numpy-1.19.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:741d95eb2b505bb7a99fbf4be05fa69f466e240c2b4f2d3ddead4f1b5f82a5a5"}, + {file = "numpy-1.19.3-cp39-cp39-win32.whl", hash = "sha256:1ea7e859f16e72ab81ef20aae69216cfea870676347510da9244805ff9670170"}, + {file = "numpy-1.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:83af653bb92d1e248ccf5fdb05ccc934c14b936bcfe9b917dc180d3f00250ac6"}, + {file = "numpy-1.19.3-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:9a0669787ba8c9d3bb5de5d9429208882fb47764aa79123af25c5edc4f5966b9"}, + {file = "numpy-1.19.3.zip", hash = "sha256:35bf5316af8dc7c7db1ad45bec603e5fb28671beb98ebd1d65e8059efcfd3b72"}, ] packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, @@ -1339,8 +1433,8 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pre-commit = [ - {file = "pre_commit-2.7.1-py2.py3-none-any.whl", hash = "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a"}, - {file = "pre_commit-2.7.1.tar.gz", hash = "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"}, + {file = "pre_commit-2.8.2-py2.py3-none-any.whl", hash = "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315"}, + {file = "pre_commit-2.8.2.tar.gz", hash = "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6"}, ] prometheus-client = [ {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, @@ -1451,8 +1545,8 @@ requests = [ {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, ] rise = [ - {file = "rise-5.7.0-py2.py3-none-any.whl", hash = "sha256:b500c7c3f7b09c8194a66feeffd60ead6da0132d9fa18a2bb7352587778ef482"}, - {file = "rise-5.7.0.tar.gz", hash = "sha256:6c00721189e0b457ca40ab4eb0abef8edbba6c71bc04d7f04ad813a214ddea74"}, + {file = "rise-5.7.1-py2.py3-none-any.whl", hash = "sha256:df8ce9f0e575d334b27ff40a1f91a4c78d9f7b4995858bb81185ceeaf98eae3a"}, + {file = "rise-5.7.1.tar.gz", hash = "sha256:641db777cb907bf5e6dc053098d7fd213813fa9a946542e52b900eb7095289a6"}, ] send2trash = [ {file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"}, @@ -1471,19 +1565,51 @@ testpath = [ {file = "testpath-0.4.4.tar.gz", hash = "sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e"}, ] toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tornado = [ - {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, - {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, - {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, - {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, - {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, - {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, - {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, - {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, - {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, + {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, + {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, + {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, + {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, + {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, + {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, + {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, + {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, + {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, + {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, + {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, + {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, + {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, + {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, + {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, + {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, + {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, + {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] traitlets = [ {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, diff --git a/pyproject.toml b/pyproject.toml index aa92bb6..805d7a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,11 @@ python = "^3.8" jupyterlab = "^2.2.8" numpy = "^1.19.2" +# Chapter 11: "A Traveling Salesman Problem" exercises +folium = "^0.11.0" +geopy = "^2.0.0" +googlemaps = "^4.4.2" + [tool.poetry.dev-dependencies] # Task runners nox = "^2020.8.22"