{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "767ad1b8",
   "metadata": {},
   "source": [
    "# Python Variables and Lists"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d1bc8d2",
   "metadata": {},
   "source": [
    "## Python variables\n",
    "**Variables** in python are like a labeled box that stores a value so you can use it later in your program.\n",
    "They are created using ``var_name = 1`` for example.\n",
    "\n",
    "The value you give to a variable can have different **types** :\n",
    "* _int_ : Integers are numbers where we ignore what comes after the decimal point.\n",
    "* _float_ : Floating point values are numbers that can have any precision after the decimal point. When you do not need to know what comes after the decimal point, prefer using integers.\n",
    "* _bool_ : Are True/False values. They can only take the values ``True`` and ``False``.\n",
    "* _string_ : Strings are the python words for normal text. Define them using \" or '.\n",
    "* _List_ : List will be explored later on. They are a collection of different values.\n",
    "* And many more !\n",
    "\n",
    "\n",
    "Create two variables a and b and assign some values, then use the inbuilt function ``print()`` to display them, and do some simple operations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4ccb3cf0",
   "metadata": {},
   "outputs": [],
   "source": [
    "a = ...\n",
    "b = ...\n",
    "c = a * b # Multiplication\n",
    "\n",
    "text = \"The result is: \"\n",
    "\n",
    "print(a + b)  # Addition\n",
    "print(c)\n",
    "print(text, a - b)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61a11e6c",
   "metadata": {},
   "source": [
    "## Python Lists\n",
    "\n",
    "Python lists are a collection of values that can have different types.\n",
    "They are created using brackets, and specifying a list of elements : ``my_list = [1,5,6]``.\n",
    "Accessing the list is done by indicating the indices of the value you want to access using brackets.\\\n",
    "Python starts counting from 0 : ``element_3 = my_list[2]``\n",
    "\n",
    "Create a list with 7 values, they can be integers, strings, floats ...\n",
    "Try to access the values 0, 3, 7, -1.\n",
    "\n",
    "You can run some basic functions to get information on the list, such as ``len(my_list)``. Try printing the size of your list.\n",
    "Another usefull function is `my_list.append(value)` which adds `value` to the end of the list.\n",
    "\n",
    "Lists can hold multiple data types in the same time. For example, you can have a list that contains integers, strings, and floats all together.\n",
    "`my_list = [1, \"hello\", 3.14, True, \"world\", 42, None]`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "158ba097",
   "metadata": {},
   "outputs": [],
   "source": [
    "my_list = ...\n",
    "\n",
    "print(my_list[0])  # First element\n",
    "...  # Fourth element\n",
    "...  # Seventh element\n",
    "...  # Last element\n",
    "\n",
    "print(\"Length :\", len(my_list))  # Size of the list"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b421d65",
   "metadata": {},
   "source": [
    "## Python Loops\n",
    "Loops are functions that allow to repeat a certain set of instructions.\n",
    "\n",
    "**For** loops create a variable that will go through a set of values, and the loop will be exited afterwards.\n",
    "\n",
    "**While** loops repeat as long as a certain condition is verified, if this condition cannot be satisfied, this may lead to an infinite loop ! **Conditions** in python are mathematical equations that can be answered by a simple ``True`` or ``False``.\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "302e060a",
   "metadata": {},
   "source": [
    "Create a **For** loop that prints all the values from your list, one after another. To do that - access each element in by its index in a loop. You can used the function ``range(n)`` which creates a list of integers from 0 to n-1. The syntax looks like: \n",
    "\n",
    "```python\n",
    "for i in range(n):\n",
    "    ... # Do something with index i\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2204086a",
   "metadata": {},
   "outputs": [],
   "source": [
    "..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7ef4653",
   "metadata": {},
   "source": [
    "Now, do the same with the **While** loop. Here, you need to again access each element of `my_list` by its index, but you will need to create a variable that will be incremented at each iteration of the loop, and the loop will stop when this variable reaches the size of the list. The syntax is as follows :\n",
    "\n",
    "```python\n",
    "i = 0\n",
    "while i < len(my_list):\n",
    "    ... # Do something with index i\n",
    "    i += 1\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8091ffe9",
   "metadata": {},
   "outputs": [],
   "source": [
    "..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9e9f577",
   "metadata": {},
   "source": [
    "We can also iterate directly over the elements of a list, without using their indices. This is done using the following syntax :\n",
    "```python\n",
    "for element in my_list:\n",
    "    ... # Do something with element\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f23ce5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e304298",
   "metadata": {},
   "source": [
    "Now create **For** loop that returns the total sum of the list.\n",
    "(Since we added some \"none number\" values, you may need to create a new list)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6dd93879",
   "metadata": {},
   "outputs": [],
   "source": [
    "new_list = [1, 5, 3.1, -5, 4.9, 6]\n",
    "\n",
    "i = 0\n",
    "total_sum = 0\n",
    "for i in range(len(new_list)):\n",
    "    ...\n",
    "    print(\"Current sum at index: \", i, total_sum)\n",
    "print(\"Total sum :\", total_sum)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afcdc0a2",
   "metadata": {},
   "source": [
    "As mentioned, python provides some basic functions to help you so you don't have to write basic codes like the sum."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c02d18f1",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Sum using built-in function :\", sum(new_list))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0326db0",
   "metadata": {},
   "source": [
    "Sometimes we need to add lists together, as you would with a matrix.\n",
    "\n",
    "Create two lists and add them together. (Make sure they are made of only numbers !). Does this do what you expect it to do ?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6a5e2797",
   "metadata": {},
   "outputs": [],
   "source": [
    "list_1 = [1, 2, 3]\n",
    "list_2 = [4, 5, 6]\n",
    "\n",
    "print(list_1 + list_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb3ccc44",
   "metadata": {},
   "source": [
    "# Numpy and Numpy Arrays\n",
    "\n",
    "### Python Packages and Numpy\n",
    "Python uses functions to execute many repetitive tasks. The functions that are in the base python installation are called *in-built functions*.\\\n",
    "**Packages** are collections of functions that can be included in your script to facilitate your coding. You can import a package using `import package_name`.\n",
    "\n",
    "\n",
    "**Numpy** is one of the most versatile packages for python. It includes new types of values and new functions to greatly speed up computing speed. Import numpy using `import numpy as np`, meaning you will have access to numpy functions by writing `np.function_name()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e20bad6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1abd8b3f",
   "metadata": {},
   "source": [
    "### Numpy Arrays\n",
    "\n",
    "One of the most useful feature of Numpy are **numpy array**.\n",
    "Numpy arrays are similar to Python Lists, but present an arsenal of useful functions that simplify the use of lists and greatly improve computing speed.\\\n",
    "We will now explore some of these functionalities."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12b1cb12",
   "metadata": {},
   "source": [
    "To create a numpy array multiple techniques exist :\n",
    "- `array = np.array([1,2,3])` creates an array from an existing python list\n",
    "- `array = np.arange(start, stop, step)` creates an array ranging from `start` to `stop` in steps of size `step`\n",
    "- `array = np.zeros(N)` or `array = np.ones(N)` creates a list of size `N` filled with 0 or 1.\n",
    "- And many more functions that you can discover through the [documentation](https://numpy.org/doc/2.3/index.html)\n",
    "\n",
    "To access elements of the list, precise the indices of the element you wish to access. In a 1D list, `list[0]` will give you the first element, `list[4]` will give you the 5th element and `list[-1]` the last one. `list[1:4]` will give you a list with all the elements from indices 1 to 3, `list[:5]` from the first to the 5th element and `list[1:5:2]` every 2 elements between indices 1 and 4.   \n",
    "\n",
    "Use these functions to create a few numpy arrays, and print their values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a99797c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "array = ... # np.array\n",
    "print(array)\n",
    "\n",
    "array = ... # np.arange - numpy equivalent python `range` used in for-loops above\n",
    "print(array)\n",
    "\n",
    "array = ... # np.zeros OR np.ones\n",
    "print(array)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a586832d",
   "metadata": {},
   "outputs": [],
   "source": [
    "my_list = [1,2,3,4.5,10.1,15.2]\n",
    "array = ... # np.array(...)\n",
    " \n",
    "print('First element:', array[0]) # 1st element \n",
    "\n",
    "print('Elements 2 to 4:', array[1:4]) # Elements 2-3, i.e. including the index 1 and excluding the index 3\n",
    "\n",
    "print('Elements up to 3rd (exclusively):', array[:2])\n",
    "print('Elements up to 3rd (inclusively):', array[:3])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb194eaa",
   "metadata": {},
   "source": [
    "Now, let's see how NumPy arrays are different from Python list in terms of operations. Let's repeat the example from above, but now compare the result against arrays."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "list_1 = [1, 2, 3]\n",
    "list_2 = [4, 5, 6]\n",
    "\n",
    "print(list_1 + list_2)\n",
    "\n",
    "array_1 = ... \n",
    "array_2 = ...\n",
    "\n",
    "print(array_1 + array_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11762412",
   "metadata": {},
   "source": [
    "We see that numpy arrays support element-wise operations, meaning that the operation is done for each element of the array. This allows as to get **massive** speed improvements when doing operations on large lists, as we will see in the next exercise.\n",
    "\n",
    "Let's try to calculate the sum of squares of the first 1 million integers using both lists and numpy arrays, and compare the execution time. Timings can be estimated with the \n",
    "`%%timeit` magic command in beginning of the cell in Jupyter Notebooks:\n",
    "\n",
    "```python\n",
    "%%timeit\n",
    "# Your code here\n",
    "```\n",
    "\n",
    "Or using the `timeit` package:\n",
    "\n",
    "```python\n",
    "import timeit\n",
    "\n",
    "start = timeit.default_timer()\n",
    "# Your code here\n",
    "end = timeit.default_timer()\n",
    "print(\"Time taken: \", end - start)\n",
    "```\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "331d9ef3",
   "metadata": {},
   "source": [
    "First, let's do this using python lists. Create a list of the first 1 million integers using `range()`, then use a loop to calculate the sum of squares of all elements in the list. Time the execution of your code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "3bc76fdb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "123 ms ± 2.46 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit # Runs the cell many times to get averaged execution time\n",
    "\n",
    "N = 1000000\n",
    "numbers = list(range(N)) # quick way of generating the list of numbers\n",
    "squares = []\n",
    "for number in numbers:\n",
    "    squares.append(number**2)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "073e734c",
   "metadata": {},
   "source": [
    "Now, let's repeat the same using numpy arrays. Create a numpy array of the first 1 million integers using `np.arange()`, then calculate the sum of squares of all elements in the array. Time the execution of your code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "0535d0a1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "991 μs ± 37.5 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "\n",
    "N = 1000000\n",
    "numbers_array = np.arange(N)\n",
    "squares = numbers_array**2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4618cef2",
   "metadata": {},
   "source": [
    "We get a **MASSIVE** speedup using numpy arrays ! This is because numpy arrays use *vectorized* operations, meaning that the operation is done on the entire array at once, rather than element by element."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c39fb7d6",
   "metadata": {},
   "source": [
    "There are other element-wise operations that are available in for NumPy arrays! Try to use muplication, subtraction, division, exponentiation, and see how they work."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "45eaf90d",
   "metadata": {},
   "outputs": [],
   "source": [
    "array_1 = np.array([1, 2, 3])\n",
    "array_2 = np.array([4, 5, 6])\n",
    "\n",
    "n = 3.5\n",
    "\n",
    "print(\"Multiply array by a number: \")\n",
    "result = ...\n",
    "print(result)  \n",
    "\n",
    "print(\"Divide array by a number: \", ...)\n",
    "result = ...\n",
    "print(result)\n",
    "\n",
    "print(\"Add / Subtract a number: \")\n",
    "result = ...\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84ce3192",
   "metadata": {},
   "source": [
    "We can also do operations between two arrays of the same size, and numpy will do the operation element-wise. Try to add, subtract, multiply, divide two arrays!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d2ef6fff",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Sum of two arrays: \", array_1 + array_2)\n",
    "print(\"Subtraction of two arrays: \", array_1 - array_2)\n",
    "print(\"Product of two arrays: \", array_1 * array_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bfd06d04",
   "metadata": {},
   "source": [
    "We will discuss next week how to start using these methods in statistics and how to represent this data!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "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.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
