{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 2. Utility functions and plotting\n", "\n", "This is the first session concerning Python code for the Python 201 course. Thus, the focus will be on brushing up and only introducing a few new smaller topics. \n", "\n", "The primary focus will be on writing well-structured functions and keeping code modular." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Static & Dynamic Code Analysis\n", "\n", "Static code analysis means analysing the code without executing it. \n", "The opposite naturally being dynamic code analysis where the analysis is performed by executing the code on a real or virtual processor.\n", "\n", "These types of analysis have a lot of use cases but can e.g. provide:\n", "\n", "* find common security issues in the code (such as SQL injection vulnerabilities)\n", "* code linting against various standards/guidelines and/or style conventions\n", "* automatic code formating\n", "* autocompletion\n", "* measuring class cohesion\n", "* spell checking\n", "* measure code (cyclomatic) complexity\n", "* type checking\n", "* find unused classes, functions and variables \n", "* graphing imports/dependencies\n", "* run/execution timers\n", "* find program errors\n", "* find memory leaks,\n", "* find race conditions\n", "\n", "In the following are the most important use cases you should know summarised.\n", "\n", "#### Autocompletion\n", "\n", "Autocompletion is a tool constantly trying to predict the rest of what the user is typing. This is a must have and are activated by default in most editors.\n", "For Python *Jedi* is one of the best. It's been heavily tested and seem to have a deeper understanding of Python than its competitors.\n", "The new kid on the block is *Kite* which uses Deep Learning to do multi-line predicts etc.\n", "\n", "#### Linting\n", "\n", "Code linters check that the formating of the code is inline with a selected style. This is a very useful tool that helps the user write more clear and readable code. Especially new users can benefit a lot from this.\n", "\n", "We recommend using *Pycodestyle* (former *PEP8*) that highlights deviations from the well-known PEP8 Style Guide https://www.python.org/dev/peps/pep-0008/.\n", "\n", "#### Type Checking\n", "\n", "Python is a dynamically typed language with all the pros and cons that comes with. However, it is still possible to achive some of the benefints of statically typed language by using type hinting (specify types without impacting runtime) and then use static analysis to detect type errors/inconsistencies. Note that this is primarily relevant for larger Python projects.\n", "\n", "Type hinting:\n", "```python\n", "age: int = 1 # This is how you declare the type of a variable type\n", "\n", "child: bool # You don't need to initialize a variable to annotate it\n", " \n", "if age < 18:\n", " child = True\n", "else:\n", " child = False\n", "\n", "# Built-in types\n", "from typing import List, Set, Dict, Tuple, Optional\n", "\n", "# This is how you annotate a function definition\n", "def f(x: int, s: Sequence[int]) -> List[int]:\n", "\t...\n", "```\n", "\n", "#### Unit testing\n", "\n", "Unit testing means that you predefine a series of code inputs for which you know what the outcome should be. By dynamic code analysis thess tests can be setup to automatically run on every single code commit immidiately informing the user if any of the tests fails.\n", "Unit testing and test converage gets relevant when you have a project many people rely on and you need to ensure correct behaviour during futher development.\n", "We recomment using *pytest*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Code in multiple files\n", "\n", "Once a program grows to more than a few hundred lines it might make sense to separate code into multiple files. This can help to maintain a better overview of the structure of the program by storing connected pieces of logic together.\n", "\n", "Before moving on let's distinguish between two types of Python files:\n", "\n", "- A **module** is a `.py` file that gets imported into another file so the code can be used there\n", "\n", "- A **script** is a `.py` file that is meant to be run directly \n", "\n", "A quick recap of how **modules** imports work in Python, here shown for the math library:\n", "\n", "---\n", "\n", "```python\n", "# Lets you access everything in the math library by dot-notation (e.g math.pi) \n", "import math\n", "\n", "# Lets you use pi directly\n", "from math import pi \n", "\n", "# Lets you use everything in the math library directly\n", "from math import * \n", "```\n", "---\n", "\n", "The last one is not considered good practice, since variables will be untraceable. It can be good for making quick tests though.\n", "\n", "This same principle works for file specific modules as well. So if we create `my_module.py`.\n", "\n", "---\n", "```python\n", "# my_module could be your own python file\n", "import my_module \n", "```\n", "---\n", "\n", "If `my_module` contains a function called `my_func`, you can after the import call it as `my_module.my_func()`.\n", "\n", "Similar to the math module example, we could do\n", "\n", "---\n", "```python\n", "# Lets you use my_func directly \n", "from my_module import my_func\n", "```\n", "---\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### Understanding the `import` search\n", "\n", "When the Python interpreter sees a statement like `import module_name`, Python will search for a file called `module_name.py`. \n", "\n", "It searches in certain directories among which are the **current working directory** and the **directory where you installed Python** *(if it is added to your path)*. \n", "\n", "It's easy to see the directories within which Python searches. Just add the following lines to your code:\n", "\n", "---\n", "```python\n", "# Import module that has information about the Python interpreter\n", "import sys \n", "\n", "# Print a list of directories where interpreter searches\n", "print(sys.path)\n", "```\n", "---\n", "\n", "You can control which directories Python should search. This is as simple as adding a directory path to the list like `sys.path.append(directory_path)`.\n", "\n", "It should be noted that the search is sequential, so if you want to have a new directory searched first you could do `sys.path.insert(0, directory_path)` which would insert `directory_path` as the first element to be searched. \n", "\n", "This can for example be used to make sure that the top level (root) of a project directory is always searched. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Importing executes the file\n", "\n", "It is important to stress that **when importing a file, the actual code inside the file is run.** \n", "\n", "Suppose you have a module `moduleA`\n", "\n", "---\n", "```python\n", "# moduleA.py \n", "print(2+2)\n", "```\n", "---\n", "\n", "and a module `moduleB` \n", "\n", "---\n", "```python\n", "# moduleB.py\n", "import moduleA\n", "\n", "```\n", "---\n", "\n", "If you open `moduleB` in your editor and run the code it will output `4`. This is because importing `moduleA` literally runs the code that resides inside `moduleA`. \n", "\n", "Try to recreate this small example to see it in action." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### Simple use case with multiple files\n", "\n", "One example of this could be\n", "\n", "- One module called `utils.py` containing all *utility* functions that the program uses. This could e.g. be code for reading in, cleaning or manipulating data.\n", "\n", "- A module `plotting.py` containing all code responsible for plotting of results. \n", "\n", "- A script file `main.py` that imports the modules and is used to run the code. \n", "\n", "---\n", "```python\n", "# main.py\n", "\n", "import utils\n", "import plotting\n", "\n", "# Create some example data as lists\n", "x = [i for i in range(100)]\n", "y = [i*i for i in x]\n", "\n", "# Modify the data by means of a function inside utils.py\n", "x_modified, y_modified = utils.func_inside_utils(x, y)\n", "\n", "# Plot the modified lists by means of function inside plotting \n", "plotting.func_inside_plotting(x_modified, y_modified)\n", "```\n", "---\n", "\n", "This approach has some advantages as it\n", "\n", "- creates modularity by grouping related code inside modules and functions\n", "\n", "- makes the code more readable, reusable and maintainable\n", "\n", "- encourages the creator to make the code in `utils` and `plotting` more project independent\n", "\n", "- makes the actual *working file* (`main.py`) more clean since most of the complicated logic sits inside separate modules\n", "\n", "A more [here](https://chrisyeh96.github.io/2017/08/08/definitive-guide-python-imports.html) for a good resource on Python import statements." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running a module as a script\n", "\n", "Even though a **module** is meant to be imported into another file, it's possible to make it runnable as a script. The way to do this is to add the following line, typically at the end of the module level code:\n", "\n", "---\n", "```python\n", "if __name__ == '__main__':\n", " # Code here will only be run if this exact .py file is run directly\n", " # It will not run if it is imported\n", "```\n", "---\n", "\n", "This can be used in various ways, for example for setting up simple runnable test examples in each module. \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Functions with variable number of parameter\n", "\n", "\n", "### Dynamic arguments `*args`\n", "\n", "Python has a built-in feature that enables use of a unlimited number of function parameters." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "()\n", "(2,)\n", "(2, 4)\n", "(2, 4, 8)\n" ] } ], "source": [ "def func(*args):\n", " ''' Print the arguments that were inputted to the function.'''\n", " print(args)\n", " \n", "\n", "# Call the function with various number of arguments\n", "func()\n", "func(2)\n", "func(2, 4)\n", "func(2, 4, 8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Recall that **a standard function with a fixed number of arguments will raise an error** is it does not receive the exact number of arguments that it expects. \n", "\n", "Note that default arguments can be omitted when calling standard functions though." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hi\n" ] } ], "source": [ "def standard_func(string):\n", " ''' Print the string that was inputted '''\n", " print(string)\n", " \n", " \n", "# Run the standard function with a single argument (as it expects)\n", "standard_func('Hi')" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "standard_func() takes 1 positional argument but 2 were given", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m# Run the function with two arguments when it expects only a single argument\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mstandard_func\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'Hi'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'Ho'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mTypeError\u001b[0m: standard_func() takes 1 positional argument but 2 were given" ] } ], "source": [ "# Run the function with two arguments when it expects only a single argument\n", "standard_func('Hi', 'Ho')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As seen, this raises an error.\n", "\n", "The naming `*args` is not forced, but just the standard convention when talking about this behavior. You could call it whatever you want as long as there is an `*` in front. \n", "\n", "In the example below the name `numbers` is used in place of `args`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n", "9\n", "15\n" ] } ], "source": [ "def sum_numbers(*numbers):\n", " ''' Return the sum of all given arguments '''\n", " return sum([*numbers])\n", "\n", "\n", "# Call function with different number of arguments\n", "print(sum_numbers(2))\n", "print(sum_numbers(2, 7))\n", "print(sum_numbers(2, 7, 5, 1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**If you already have a list of values** from any previous code, you can unpack the list while calling the function by using an `*`. " ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "15" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Define a list of values\n", "a = [2, 7, 5, 1]\n", "\n", "# Call function and unpack at the same time\n", "sum_numbers(*a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `*args` feature can be useful for creating flexibility in a function, allowing the caller more options and freedom.\n", "\n", "An good example of an `*args` implementation is the built-in `zip`, which takes in a dynamic amount of iterables and generates values for looping over them simultaneously. In the Python documentation this is written as `zip(*iterables)`, see [here](https://docs.python.org/3/library/functions.html#zip).\n", "\n", "See how `zip` can be used with a varying number of arguments in the demo below." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 Hi\n", "2 Ho\n" ] } ], "source": [ "# Using zip with two arguments\n", "for n, s in zip([1, 2], ['Hi', 'Ho']):\n", " print(n, s)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 Hi Yi\n", "2 Ho Ha\n" ] } ], "source": [ "# Using zip with three arguments\n", "for n, s1, s2 in zip([1, 2], ['Hi', 'Ho'], ['Yi', 'Ha']):\n", " print(n, s1, s2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dynamic keyword arguments `**kwargs`\n", "\n", "The functionality for `*args` can be used with so-called ***keyword arguments***. It works very much the same way as `*args`, except there is a keyword assigned to each argument value. \n", "\n", "The syntax is with double asterisk `**kwargs` and it works as demonstrated below." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'p': 1, 'q': 2}\n", "{'bla': 327, 'blo': ('hi', 'ho')}\n" ] } ], "source": [ "def kw_func(**kwargs):\n", " ''' Print the keyword arguments that were inputted to the function. '''\n", " print(kwargs)\n", " \n", " \n", "# Call the function with various numbers of keyword arguments\n", "kw_func(p=1, q=2)\n", "kw_func(bla=327, blo=('hi', 'ho'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One might say that `*args` refer to lists and `**kwargs` refer to dictionaries.\n", "\n", "Also here the naming `kwargs` is just a standard convention name, short for **keyword arguments**. You can call it whatever you want. As long as there are `**` in front it will create this behavior.\n", "\n", "It might not be very often that `*args` or `**kwargs` are needed for your own functions, but they do have their place and they are used in many functions and methods in third-party libraries. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Some NumPy and SciPy functions\n", "\n", "NumPy and SciPy has a great amount of useful functions to do all sorts of operations. Only a few were mentioned in the Python 101 course. \n", "\n", "The brief description below gives an overview of some functions that might be useful for solving the exercises.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `np.arange`\n", "\n", "`np.arange` is sort of like Python's standard `range(start, stop [, step])`. However, the standard Python version only allows for use of integer values, whereas the NumPy version is more flexible. See a small demonstration below. " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([-2. , -1.5, -1. , -0.5, 0. , 0.5, 1. , 1.5])" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "\n", "# Create and array from -2 to 2 with step 0.5\n", "a = np.arange(-2, 2, 0.5)\n", "a" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice the that start point in included, but not the endpoint.\n", "\n", "A related NumPy function is `np.linspace` which was used in the previous course. It lets the user specify the number of elements for the resulting array rather than the step, as for `np.arange`. \n", "\n", "Docs for [np.arange](https://docs.scipy.org/doc/numpy/reference/generated/numpy.arange.html)\n", "\n", "Docs for [np.linspace](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creation of boolean arrays (aka masks)\n", "\n", "A boolean array, also called a **mask array**, can be created by simply stating a condition on an existing array." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ True, True, True, True, True, False, False, False])" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Using the `a` array defined above\n", "\n", "# Create a mask array for a with a condition\n", "a < 0.2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Multiple conditions are also allowed with a simple syntax. " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([False, False, True, True, True, True, True, False])" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a mask array for a with multiple conditions\n", "(a > -1.4) & (a < 1.4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `np.where`\n", "\n", "This function returns the indices where a boolean array has elements `True`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(array([0, 1, 2, 3, 4], dtype=int64),)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Extract indices where elements in a are less than 2 \n", "np.where(a < 0.2)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(array([2, 3, 4, 5, 6], dtype=int64),)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Repeating from above\n", "a_bool = (a > -1.4) & (a < 1.4)\n", "\n", "# Get indices where the multiple conditions are True\n", "np.where(a_bool)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that the returned value is a tuple. If you want the first element, which is an array, extract it by indexing.\n", "\n", "Docs for [np.where](https://docs.scipy.org/doc/numpy/reference/generated/numpy.where.html)\n", "\n", "[Another explanation](https://thispointer.com/numpy-where-tutorial-examples-python/) (arguably better)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `np.append`\n", "\n", "Appending of an array to another array is demonstrated below. " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 1, 2, 3, 11, 12])" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create two arrays\n", "e = np.array([1, 2, 3])\n", "f = np.array([11, 12])\n", "\n", "# Combine them by appending one to the other\n", "np.append(e, f)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Doc for [np.append](https://docs.scipy.org/doc/numpy/reference/generated/numpy.append.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### NumPy fancy indexing\n", "\n", "NumPy arrays can be indexed by other arrays. This is an extremely useful feature. See the demonstration below." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([-2. , -1.5, -1. , -0.5, 0. , 0.5, 1. , 1.5])" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Given an array (`a` from before)\n", "a" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([3, 4])" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# And another array representing indices\n", "b = np.array([3, 4])\n", "b" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([-0.5, 0. ])" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# a can be indexed by b as\n", "a[b]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This concept can be extended to more complicated use cases.\n", "\n", "NumPy/SciPy [explanation for fancy indexing](https://scipy-lectures.org/intro/numpy/array_object.html#fancy-indexing)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### SciPy `find_peaks`\n", "\n", "The SciPy library has many useful functions for scientific computing. One of them is `find_peaks` which is found in the `signal` module (`scipy.signal`). This means that it can be imported as `from scipy.signal import find_peaks`.\n", "\n", "**Given an array of values it finds all the peaks** by comparing to the value in the two neighboring points. Note that it does not find the valleys!\n", "\n", "It has some useful optional parameters, e.g. setting a minimum height below which no peaks are returned.\n", "\n", "The function returns a tuple with two values. The first is the array indices where peaks were found, the next is a property dictionary with some information about the analysis results. \n", "\n", "Saving a function return to variables can be done by unpacking. If a tuple with two elements are returned, one can save the returned values like this:\n", "\n", "---\n", "```python\n", "# Save variables from a function that returns two variables\n", "a, b = function_call()\n", "\n", "# Or if only the first variables is need in the code\n", "a, _ = function_call()\n", "```\n", "---\n", "\n", "Docs for [find_peaks](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercises\n", "\n", "From solving the exercises in Session 0 you should have a local Git repository with a `README.md` and a `.gitignore`. We encourage to use this repository while working with the exercises in this course to get familiarity with the Git workflow. \n", "\n", "In Session 1 you linked the local repo to a remote one on your GitHub account. You can `push` when you want to sync your local work to the remote.\n", "\n", "The exercises in this session focus on:\n", " \n", "- Git for version control\n", "- `numpy` and `scipy`\n", "- usage of `**kwargs` as a function parameter (and `**args` if you do the bonus exercises)\n", "- code spread across in multiple files\n", "\n", "*In case you do not yet have a local Git repository set up for this course, you can do the exercises from Session 0 before doing these. If you just want to get going with these exercises, you can run `git init` inside the directory where you want to store your code and start.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercises 1\n", "\n", "1. **Create a branch**\n", "\n", " As you're about do a larger chunk of work with a specific topic in mind, create a branch with a descriptive name (e.g. `session2`) and check it out. \n", "\n", " Recall that `git branch ` creates a branch and `git checkout ` checks it out. This can be done in one operation as `git checkout -b `.\n", "\n", "\n", "2. **Create a folder and a module**\n", " \n", " Create a folder for Session 2 and a module called `utils.py` inside it. \n", " \n", " The `utils.py` module will store all the utility functions that can be called upon to do a lot of ground work. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercise 2\n", "\n", "Inside `utils.py` write the function called `extract_interval` which has its skeleton presented below. Read the docstring for details. \n", "\n", "---\n", "```python\n", "def extract_interval(x, x_bounds):\n", " ''' \n", " Return the indices of `x` that are within `x_bounds` with both of values \n", " `x_bounds` being inclusive.\n", " \n", " Parameters\n", " ----------\n", " x : 1D numpy array\n", " x-coordinates sorted in ascending order.\n", " x_bounds : tuple\n", " A two-element tuple defining the interval within which to extract x-indices.\n", " \n", " Returns\n", " -------\n", " 1D numpy array\n", " Array of indices for `x`-values that reside within `x_bounds`.\n", " \n", " Assumptions\n", " ------------\n", " The function assumes that the input array `x` is sorted in\n", " ascending order.\n", " '''\n", " # Your code here!\n", "```\n", "---\n", "\n", "### Some examples to test against:\n", "\n", "```python\n", "# Basic test 1\n", "x1 = np.arange(0, 10)\n", "idx1 = extract_interval(x=x1, x_bounds=(3, 7))\n", "\n", "# Basic test 2\n", "x2 = np.arange(-0.6, 3.8, 0.4)\n", "idx2 = extract_interval(x=x2, x_bounds=(-0.3, 1.5))\n", "\n", "```\n", "---\n", "Results should be `idx1 = [3 4 5 6 7]` and `idx2 = [1 2 3 4 5]`.\n", "\n", "*Place test runs like the above in the bottom of the file in a `if _name__ == '__main__':` block. This way it will not get run if the module is imported from elsewhere.*\n", "\n", "### Commit your changes\n", "\n", "Once you're happy with the function you can commit your changes to create a snapshot of you repository state. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercise 3\n", "\n", "Write a function called `extrema_indices` inside `utils.py` that takes as input an array of y-coordinates and returns the indices of all local extrema (peaks and valleys). Use the `find_peaks` function from `scipy.signal`. \n", "\n", "Be sure to write a good docstring for the function. You can use the same structure as in the previous exercise as guideline.\n", "\n", "*Recall that `find_peaks` only finds the peaks and **not** the valleys, so you have to do a slight workaround to get the valleys.*\n", "\n", "### Testing the code\n", "\n", "Test the code by running `yy` from below through the function\n", "\n", "---\n", "```python\n", "# A dummy graph to test against\n", "xx = np.linspace(1, 20, 40)\n", "yy = np.sin(xx) * 3 * np.cos(xx**2)\n", "```\n", "---\n", "\n", "It should return this array:\n", "\n", "```\n", "array([ 1.83936866, 2.00743698, 1.76511072, 2.68372999, -0.10032392,\n", " 1.99429972, 0.27309389, 2.23407394, 0.12795831, 2.51053515,\n", " 0.2974298 , -2.00646398, -0.63018205, -2.36255017, -2.42571128,\n", " -0.65672365, -0.37642334, -2.60571339, -1.05772914, -1.4625172 ,\n", " -0.7508744 , -2.34019749, -1.50602509])\n", "```\n", "\n", "\n", "### Commit your changes\n", "\n", "Create a commit when you have finished the exercise." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercise 4\n", "\n", "Write the function described in the docstring below.\n", "\n", "```python\n", "def arrays_todict(x_arr, y_arr):\n", " ''' \n", " Return a dictionary with y-values in string form as keys \n", " and x- and y-values as values. \n", " \n", " Parameters\n", " ----------\n", " x_arr : 1D numpy array\n", " Array of x-values\n", " y_arr : 1D numpy array\n", " Array of y-values\n", " \n", " Returns\n", " -------\n", " dict\n", " Dictionary of the form \n", " {'y1': (x1, y1), 'y2': (x2, y2), ..., 'yn': (xn, yn)}\n", " '''\n", " # Your code here!\n", "```\n", "---\n", "\n", "You might want to format the decimals for the keys. See [here](https://nbviewer.jupyter.org/github/Python-Crash-Course/Python101/blob/master/Course%20Notes/Python%20Course%20Notes.ipynb#Printing-and-formatting) for how to do that using f-strings.\n", "\n", "Create a commit when you have tested and finished the exercise.\n", "\n", "### Hint\n", "\n", "*This can be solved by a one-liner with a dictionary comprehension. They work similarly to list comprehensions, but instead of a single value at each element, one must supply a key and a value. See a simple demonstration of a dictionary comprehension below.* " ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'2*0': 0, '2*1': 2, '2*2': 4, '2*3': 6, '2*4': 8}" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Example of dict comprehension\n", "{f'2*{x}': 2*x for x in range(5)}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The built-in `zip` might also be useful. It enables iterating over two iterables simultaneously.\n", "\n", "Docs for [zip](https://docs.python.org/3/library/functions.html#zip). \n", "\n", "*Notice that the Python implementation of `zip` says `zip( *iterables) `. So it uses the ` *args` concept described above.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercise 5\n", "\n", "Create a new module `plotting.py` for storing plotting related code. Inside it, write a function called `annotate_points`. \n", "\n", "---\n", "```python\n", "def annotate_points(points_to_annotate, ax, **kwargs):\n", " '''\n", " Annotate points with corresponding text.\n", " \n", " Parameters\n", " ----------\n", " points_to_annoate : dict\n", " Dictionary with desired annotation text as keys and the (x, y)-\n", " coordinates of the annotation as values.\n", " ax : matplotlib axis object\n", " Axis object on which to plot the annotations. \n", " **kwargs : keyword arguments\n", " Arguments to be forwarded to the ax.annotate call. \n", " '''\n", " # Your code here\n", "```\n", "---\n", "\n", "The purpose of the function for these exercises is to annotate the extreme values which are to be stored inside a dictionary of the form `{'extr_val1': (x1, y1), 'extr_val2': (x2, y2), ...}`.\n", "\n", "For annotating points use `plt.annotate` or `ax.annotate`. They work similarly, but `plt` is for the Matlab-like API and `ax` is for the Object-oriented API of matplotlib.\n", "\n", "Docs for [plt.annotate](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.annotate.html)\n", "\n", "Docs for [ax.annoate](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.axes.Axes.annotate.html)\n", "\n", "Note the intended use of `**kwargs` in the `annotate_points` function. It is meant to just transfer the many possible keyword arguments from your custom function to the matplotlib function. By using this, your own function does not limit the customizability of the underlying plotting code. Try for example to add the argument `color='green'` to the function call when testing.\n", "\n", "### Testing the function\n", "You can test the function by creating a plot and annotating the extreme values. Create some dummy values for a graph yourself or use these ones provided in Exercise 3.\n", "\n", "### Commit you changes\n", "\n", "Commit your changes when you are done." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercise 6\n", "\n", "Create module `main.py` and import `utils` and `plotting` into it.\n", "\n", "This is now a \"clean\" file that has access to all of our custom functions without looking cluttered. \n", "\n", "Inside `main.py` create a graph and use the functions previously created to annotate only extreme values within a certain $x$-interval of your choice.\n", "\n", "You can use the test code from Exercise 3 and 5 if you want." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Some improvements\n", "\n", "The exercises are basically done. If you want you can keep improving the functions. Some good improvements could be:\n", "\n", "- Make `extract_interval` work with multiple intervals by use of `*args`.\n", "\n", "\n", "- Specify the number of decimal points for the dict keys that are returned from the `arrays_to_dict` function. This would be prettier when the strings that are stored as keys are eventually annotated in the plot. \n", "\n", "If you start altering your existing, working code remember to make sure that you have that code stored in a commit (snapshot). This way you can always revert to that state if need be.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# End of exercises\n", "*The cell below is for setting the style of this document. It's not part of the exercises.*" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Apply css theme to notebook\n", "from IPython.display import HTML\n", "HTML(''.format(open('../css/cowi.css').read()))" ] } ], "metadata": { "hide_input": false, "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.7.4" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Table of Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 2 }