{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Array Computation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "hide-input",
     "thebe-init",
     "runnable"
    ]
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "from pathlib import Path\n",
    "\n",
    "current = Path.cwd()\n",
    "for parent in [current, *current.parents]:\n",
    "    if (parent / '_config.yml').exists():\n",
    "        project_root = parent  # ← Add project root, not chapters\n",
    "        break\n",
    "else:\n",
    "    project_root = Path.cwd().parent.parent\n",
    "\n",
    "sys.path.insert(0, str(project_root))\n",
    "\n",
    "from shared import thinkpython, diagram, jupyturtle"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```{contents}\n",
    ":local:\n",
    ":depth: 2\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "NumPy organizes array operations along axes, masks, and broadcasting rules. {numref}`numpy-array-concepts` summarizes how these concepts interact when reducing or slicing arrays {cite}`Harris_2020`.\n",
    "\n",
    "```{figure} ../../figures/numpy-array-concepts.png\n",
    "---\n",
    "name: numpy-array-concepts\n",
    "alt: NumPy array concepts\n",
    "width: 100%\n",
    "---\n",
    "[NumPy Array Concepts](https://www.nature.com/articles/s41586-020-2649-2)\n",
    "```"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "NumPy is so important in the Pythonic data science world because it optimizes computation with arrays.\n",
    "Computational efficiency on NumPy arrays lies in the use of **vectorized operations**, or vector-based operations, generally implemented through NumPy’s [universal array functions](http://docs.scipy.org/doc/numpy/reference/ufuncs.html) (**ufuncs**), that work on entire arrays at once rather than looping through individual elements. This approach is dramatically faster than traditional Python loops and is fundamental to efficient data science."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we'll explore how NumPy's **universal functions (ufuncs)** enable fast, efficient computation on arrays. You'll learn why vectorization matters, how to use ufuncs effectively, and advanced techniques like broadcasting and boolean indexing.\n",
    "\n",
    "**What You'll Learn**\n",
    "\n",
    "1. **Vectorized vs. Iterative Operations**: Compare performance and see why vectorization matters\n",
    "2. **Universal Functions (ufuncs)**: Explore unary and binary operations for arrays\n",
    "3. **Array Arithmetic**: Perform mathematical operations on arrays\n",
    "4. **Aggregation**: Compute summary statistics efficiently\n",
    "5. **Advanced Features**: Broadcasting, indexing, and boolean logic"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "## Iterative vs Vectorized Operations\n",
    "\n",
    "Vectorized operations in NumPy refer to performing operations on **entire arrays** at once, rather than **iterating** through individual elements using Python loops. \n",
    "\n",
    "Vectorized operations are more efficient, and we will compare NumPy's **vectorized operations** with Python list iteration.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Contender 1: List Iteration\n",
    "Let's take a look at a general iteration (**iterative loop**) function example (note the code is meant to represent a computation for your Python programming learning, not a business context):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use the new numpy random number generator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(seed=42)      ### default_rng() is a Random Number Generator\n",
    "                                            ### it's a function in numpy's random module\n",
    "                                            ### A random seed (or seed state, or just seed) \n",
    "                                            ### is a number (or vector) used to initialize \n",
    "                                            ### a pseudorandom number generator. \n",
    "                                            ### A pseudorandom number generator's number sequence \n",
    "                                            ### is completely determined by the seed: thus, if \n",
    "                                            ### a pseudorandom number generator is later reinitialized \n",
    "                                            ### with the same seed, it will produce the same sequence of numbers.\n",
    "                                            ### https://en.wikipedia.org/wiki/Random_seed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Design a Python function that would loop through a sequence a number of times and does something."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "def compute_reciprocals_iteration(values):\n",
    "    output = np.empty(len(values))          ### numpy.empty() creates an array of size by taking whatever \n",
    "    for i in range(len(values)):                ### \"garbage\" values are present in the allocated memory at the time of creation.\n",
    "        output[i] = 1.0 / values[i]\n",
    "    return output                           ### return the output array"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1.        , 0.14285714, 0.16666667, 0.25      , 0.25      ])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### test the function with a small array of values\n",
    "values = rng.integers(1, 10, size=5)        ### 5 numbers between 1 and 10 (exclusvie)\n",
    "compute_reciprocals_iteration(values)                     ### e.g., array([7, 4, 2, 4, 8])\n",
    "                                            ### when returned, Jupyter evaluates and outputs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```{note}\n",
    "\n",
    "`np.random.default_rng()` is the recommended function for creating a modern, isolated random-number generator object in NumPy.\n",
    "\n",
    "**42** is widely considered the conventional, or \"default,\" seed for NumPy's random number generation for reproducibility. \n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Contender 2: Vectorized Operation (NO LOOP!)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "def compute_reciprocals_numpy(values):\n",
    "    return 1.0 / values             ### no loop!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1.        , 0.14285714, 0.16666667, 0.25      , 0.25      ])"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### test the function with the same array of values\n",
    "compute_reciprocals_numpy(values)   ### compare with compute_reciprocals(values)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Who Wins?\n",
    "Now calling the function with a huge number (**1000000**) of times."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "654 ms ± 6.32 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
      "384 μs ± 2.42 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
     ]
    }
   ],
   "source": [
    "big_array = 1000000\n",
    "values = rng.integers(1, 100, size=big_array)\n",
    "\n",
    "%timeit compute_reciprocals_iteration(values)\n",
    "%timeit compute_reciprocals_numpy(values)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true
   },
   "source": [
    "```{note}\n",
    "- ms (millisecond) is 1/1000 second\n",
    "- μs (mu-second; microsecond) is one millionth (1/1000,000) second (10⁻⁶ s); \n",
    "- ns (nanosecond) is one billionth of a second (10⁻⁹ s)\n",
    "- -n <N>: Specifies the number of loops per run\n",
    "- -r <R>: Specifies the number of times the entire timing experiment is repeated.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**NumPy vectorized operation** is clearly faster than list **iteration**. Why?\n",
    "\n",
    "- Lists store references to objects.\n",
    "- NumPy arrays store raw numbers in memory."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Round 2: `sum()`\n",
    "\n",
    "We have seen the performance of NumPy arrays (vectorized operations) in large-scale looping. Now we'll compare list iteration with NumPy vectorized operations. Let's choose the `sum()` function. (The following sample code is adapted from [geeksforgeeks](https://www.geeksforgeeks.org/numpy/vectorized-operations-in-numpy/))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Iterative sum: 499999500000\n",
      "Time taken by iterative sum: 0.0023260116577148438\n",
      "Vectorized sum: 499999500000\n",
      "Time taken by vectorized sum: 0.00022482872009277344\n",
      "\n",
      "NumPy array sum is 9.47 times faster of list sum.\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "import time\n",
    "\n",
    "#################### list sum() ####################\n",
    "lst = list(range(1000000))   ### create list\n",
    "start_time = time.time()     ### time starts\n",
    "# iterative_sum = sum(range(15000))          ### this is from geeksforgeeks and it's wrong\n",
    "iterative_sum = sum(lst)     ### sum list\n",
    "print(\"\\nIterative sum:\", iterative_sum)\n",
    "print(\"Time taken by iterative sum:\", time.time() - start_time)\n",
    "iterative_time = time.time() - start_time\n",
    "\n",
    "#################### numpy sum() ####################\n",
    "arr = np.arange(1000000)     ### create array\n",
    "start_time = time.time()     ### time starts\n",
    "vectorized_sum = np.sum(arr) ### sum array\n",
    "print(\"Vectorized sum:\", vectorized_sum)\n",
    "print(\"Time taken by vectorized sum:\", time.time() - start_time)\n",
    "vectorized_time = time.time() - start_time\n",
    "\n",
    "print()\n",
    "\n",
    "print(f\"NumPy array sum is {iterative_time / vectorized_time:.2f} times faster of list sum.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Universal Functions\n",
    "\n",
    "NumPy array operations written in a vectorized way compute the entire array ('vector') at once, without Python needing to loop over individual elements are called universal functions (**ufuncs**). Ufuncs is a convenient interface to this statically typed, compiled, vectorized operation."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As a very simple and practical example (as opposed to the conceptual experimentation earlier in this chapter), let's say we would like to add the **elements** of two lists, which is similar to the tasks in the opening cases, only simpler. With Python, we would use: \n",
    "\n",
    "- a `for` loop\n",
    "- the `zip()` function\n",
    "- the `append()` method"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[6, 8, 10, 12]\n"
     ]
    }
   ],
   "source": [
    "x = list(range(1, 5)) \n",
    "y = list(range(5, 9))\n",
    "z = []\n",
    "\n",
    "for i, j in zip(x, y):\n",
    "  z.append(i + j)\n",
    "\n",
    "print(z)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With `NumPy`, vectorized **element-wise** scalar addition is much efficient, and the `+` operator below performs a `ufunc` as well."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 6  8 10 12]\n"
     ]
    }
   ],
   "source": [
    "x = np.arange(1, 5) \n",
    "y = np.arange(5, 9)\n",
    "\n",
    "z = x + y\n",
    "\n",
    "print(z)              "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Array Arithmetic\n",
    "\n",
    "NumPy implements arithmetic operators using **ufuncs**, as shown in the table below. In addition to ufuncs, NumPy ufuncs also utilize Python's native arithmetic **operators**, which can be more intuitive to use. For example, for element-wise operations like addition, you can simply use Python’s arithmetic operators directly on NumPy's array object. This vectorized method shifts the looping process to NumPy’s compiled layer, resulting in significantly faster execution.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Arithmetic ufunc implementation:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Operation      | Operator | ufunc (NumPy)                  | Example  | Vectorized Meaning\n",
    "| -------------- | -------- | ------------------------------ | -------- | ---------------------------------------------- |\n",
    "| Addition       | `+`      | `np.add`                       | `a + b`  | Element-wise addition                          |\n",
    "| Subtraction    | `-`      | `np.subtract`                  | `a - b`  | Element-wise subtraction                       |\n",
    "| Multiplication | `*`      | `np.multiply`                  | `a * b`  | Element-wise multiply (not matrix multiply)    |\n",
    "| Division       | `/`      | `np.true_divide` (`np.divide`) | `a / b`  | Element-wise division                          |\n",
    "| Floor Division | `//`     | `np.floor_divide`              | `a // b` | Element-wise floor (quotient rounded down)     |\n",
    "| Modulo         | `%`      | `np.mod` (`np.remainder`)      | `a % b`  | Element-wise remainder                         |\n",
    "| Power          | `**`     | `np.power`                     | `a ** 2` | Element-wise power (e.g., square each element) |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Examples of using the NumPy arithmetic ufuncs are as follows, and you may use the Python arithmetic operators in place of ufuncs for these vectorized operations, as listed above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "x == [0 1 2 3 4]\n",
      "x + 5  == [5 6 7 8 9]\n",
      "x - 5  == [-5 -4 -3 -2 -1]\n",
      "x * 2  == [0 2 4 6 8]\n",
      "x / 2  == [0.  0.5 1.  1.5 2. ]\n",
      "x // 2 == [0 0 1 1 2]\n"
     ]
    }
   ],
   "source": [
    "### summary: ufunc arithmetic scalor operators\n",
    "\n",
    "x = np.arange(5)\n",
    "print(\"x ==\", x)\n",
    "print(\"x + 5  ==\", x + 5)\n",
    "print(\"x - 5  ==\", x - 5)\n",
    "print(\"x * 2  ==\", x * 2)\n",
    "print(\"x / 2  ==\", x / 2)\n",
    "print(\"x // 2 ==\", x // 2) # floor division"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For array-on-array operations using the Python arithmetic operators. For example, `np.add(arr1, arr2)` is the same as `arr1 + arr2`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 5  7  9 11 13]\n",
      "[ 5  7  9 11 13]\n"
     ]
    }
   ],
   "source": [
    "arr1 = np.arange(5)\n",
    "arr2 = np.arange(5, 10)\n",
    "\n",
    "print(np.add(arr1, arr2))     ### ufunc\n",
    "print(arr1 + arr2)            ### ufunc honors Python arithmetic operators"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now one example for each of the arithmetic ufuncs: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 5  7  9 11 13]\n",
      "[-5 -5 -5 -5 -5]\n",
      "[ 0 -1 -2 -3 -4]\n",
      "[ 0  6 14 24 36]\n",
      "[0.         0.16666667 0.28571429 0.375      0.44444444]\n",
      "[0 0 0 1 1]\n",
      "[0 0 1 1 2]\n",
      "[ 0  1  8 27 64]\n",
      "[0 1 0 1 0]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "arr1 = np.arange(5)           ### 0...4\n",
    "arr2 = np.arange(5, 10)       ### 5...9\n",
    "\n",
    "print(np.add(arr1, arr2))\n",
    "print(np.subtract(arr1, arr2))\n",
    "print(np.negative(arr1))\n",
    "print(np.multiply(arr1, arr2))\n",
    "print(np.divide(arr1, arr2))\n",
    "print(np.floor_divide(arr1, 3))\n",
    "print(np.ceil(np.floor_divide(arr1, 2)))\n",
    "print(np.power(arr1, 3))\n",
    "print(np.mod(arr1, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Divided by Zero\n",
    "\n",
    "Unlike regular Python, you do not receive an error when you divide a number by zero(`ZeroDivisionError: division by zero`). Instead, \n",
    "\n",
    "- `inf`: You receive a **`inf`** and a `RuntimeWarning: divide by zero encountered in divide`.\n",
    "- `nan`: You receive a **`nan`** and a `RuntimeWarning: invalid value encountered in divide`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In NumPy, `np.nan` represents \"Not a Number,\" a special floating-point value defined by the IEEE 754 standard. It signifies missing or undefined numerical data, such as the result of an indeterminate mathematical operation (e.g., dividing zero by zero). So,\n",
    "\n",
    "| Expression | Result | Why                      |\n",
    "| ---------- | ------ | ------------------------ |\n",
    "| `5 / 0`    | `inf`  | Overflow toward infinity |\n",
    "| `0 / 0`    | `nan`  | Indeterminate form       |\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "nan\n",
      "[nan  1.  1.  1.  1.]\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/g4/v24tl8t172g5d7rzsd63y51w0000gp/T/ipykernel_27047/1681753110.py:4: RuntimeWarning: invalid value encountered in scalar divide\n",
      "  print(arr1[0] / arr2[0])\n",
      "/var/folders/g4/v24tl8t172g5d7rzsd63y51w0000gp/T/ipykernel_27047/1681753110.py:5: RuntimeWarning: invalid value encountered in divide\n",
      "  print(arr1 / arr2)\n"
     ]
    }
   ],
   "source": [
    "arr1 = np.arange(5)\n",
    "arr2 = np.arange(5)\n",
    "\n",
    "print(arr1[0] / arr2[0])\n",
    "print(arr1 / arr2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inf\n",
      "[       inf 6.         3.5        2.66666667 2.25      ]\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/g4/v24tl8t172g5d7rzsd63y51w0000gp/T/ipykernel_41319/1933371002.py:4: RuntimeWarning: divide by zero encountered in scalar divide\n",
      "  print(arr2[0] / arr1[0])\n",
      "/var/folders/g4/v24tl8t172g5d7rzsd63y51w0000gp/T/ipykernel_41319/1933371002.py:5: RuntimeWarning: divide by zero encountered in divide\n",
      "  print(arr2 / arr1)\n"
     ]
    }
   ],
   "source": [
    "arr1 = np.arange(5)\n",
    "arr2 = np.arange(5, 10)\n",
    "\n",
    "print(arr2[0] / arr1[0])\n",
    "print(arr2 / arr1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### More math ufuncs\n",
    "\n",
    "Numpy comes with many mathematical ufuncs, which are essentially just mathematical operations you can use to operate on the arrays.\n",
    "\n",
    "- abs()\n",
    "- sqrt()\n",
    "- exp()\n",
    "- sin()\n",
    "- log()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For `abs()`, let us start with Python `abs()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "tags": [
     "raises-exception"
    ]
   },
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "bad operand type for abs(): 'list'",
     "output_type": "error",
     "traceback": [
      "\u001b[31mTypeError\u001b[39m\u001b[31m:\u001b[39m bad operand type for abs(): 'list'\n"
     ]
    }
   ],
   "source": [
    "%%expect TypeError\n",
    "\n",
    "y = [-2, -1, 0, 1, 2]\n",
    "abs(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, with NumPy, as long as it is a NumPy array, ufunc works as expected."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[2 1 0 1 2]\n",
      "[2 1 0 1 2]\n",
      "[2 1 0 1 2]\n"
     ]
    }
   ],
   "source": [
    "x = np.array([-2, -1, 0, 1, 2])\n",
    "\n",
    "print(abs(x))\n",
    "print(np.absolute(x))     ### ufunc\n",
    "print(np.abs(x))          ### alias to ufunc"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For `sqrt()`,"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 0,  1,  2,  3],\n",
       "       [ 4,  5,  6,  7],\n",
       "       [ 8,  9, 10, 11]])"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### make a 2-D array using reshape() method\n",
    "arr = np.arange(12)\n",
    "arr = arr.reshape(3, 4)\n",
    "arr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0.        , 1.        , 1.41421356, 1.73205081],\n",
       "       [2.        , 2.23606798, 2.44948974, 2.64575131],\n",
       "       [2.82842712, 3.        , 3.16227766, 3.31662479]])"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### taking square roots\n",
    "np.sqrt(arr)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Aggregations\n",
    "\n",
    "NumPy aggregation functions allow us to summarize many values into fewer, often a single value. Instead of looping through an array manually, functions like `np.sum()`, `np.mean()`, `np.min()`, and `np.max()` efficiently compute totals, averages, and other summary statistics. These operations are `vectorized` and optimized in `compiled` code, making them much faster than pure Python loops. When working with multi-dimensional arrays, we can also use the axis parameter to control the direction of the aggregation.\n",
    "\n",
    "Aggregation = “Reduce many values into fewer values.”"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Function      | What It Computes   | Example        | Axis Support | Notes                       |\n",
    "| ------------- | ------------------ | -------------- | ------------ | --------------------------- |\n",
    "| `np.sum()`    | Total sum          | `np.sum(a)`    | Yes          | Adds all elements           |\n",
    "| `np.mean()`   | Average            | `np.mean(a)`   | Yes          | Arithmetic mean             |\n",
    "| `np.median()` | Median             | `np.median(a)` | Yes          | Middle value                |\n",
    "| `np.min()`    | Minimum            | `np.min(a)`    | Yes          | Smallest value              |\n",
    "| `np.max()`    | Maximum            | `np.max(a)`    | Yes          | Largest value               |\n",
    "| `np.std()`    | Standard deviation | `np.std(a)`    | Yes          | Spread (default population) |\n",
    "| `np.var()`    | Variance           | `np.var(a)`    | Yes          | Spread squared              |\n",
    "| `np.prod()`   | Product            | `np.prod(a)`   | Yes          | Multiply all elements       |\n",
    "| `np.argmin()` | Index of min       | `np.argmin(a)` | Yes          | Position of smallest value  |\n",
    "| `np.argmax()` | Index of max       | `np.argmax(a)` | Yes          | Position of largest value   |\n",
    "| `np.any()`    | Any True?          | `np.any(a)`    | Yes          | Logical OR across elements  |\n",
    "| `np.all()`    | All True?          | `np.all(a)`    | Yes          | Logical AND across elements |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Axis Support\n",
    "\n",
    "Some NumPy functions allow this axis= parameter. The axis you specify is the axis that gets eliminated.\n",
    "\n",
    "- axis=0 → remove row dimension → result keeps columns\n",
    "- axis=1 → remove column dimension → result keeps rows"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1, 2, 3],\n",
       "       [4, 5, 6]])"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr = np.arange(1, 7).reshape(2, 3)\n",
    "arr"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Call                | Result    | Meaning             |\n",
    "|------------------- |--------- | ------------------- |\n",
    "| `np.sum(a)`         | 21        | Sum of all elements |\n",
    "| `np.sum(a, axis=0)` | `[5 7 9]` | Column sums         |\n",
    "| `np.sum(a, axis=1)` | `[6 15]`  | Row sums            |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[5 7 9]\n",
      "[ 6 15]\n"
     ]
    }
   ],
   "source": [
    "total = np.sum(arr)\n",
    "ax_0 = np.sum(arr, axis=0)      ### sum along the first axis (rows)\n",
    "ax_1 = np.sum(arr, axis=1)      ### sum along the second axis (columns)\n",
    "\n",
    "print(ax_0)\n",
    "print(ax_1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `sum()`\n",
    "\n",
    "There are different syntaxes for summing a vector of a (1-D) NumPy array :\n",
    "\n",
    "- Python function\n",
    "- NumPy method\n",
    "- Array object method"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0 1 2 3 4 5 6 7 8 9]\n",
      "45\n",
      "45\n",
      "45\n"
     ]
    }
   ],
   "source": [
    "arr = np.arange(10)\n",
    "print(arr)\n",
    "\n",
    "print(f\"{sum(arr)}\")        ### sum() is a built-in Python function that takes an iterable as an argument and returns the sum of its elements\n",
    "print(f\"{np.sum(arr)}\")     ### numpy.sum() is a function that takes an array as an argument and returns the sum of its elements\n",
    "print(f\"{arr.sum()}\")       ### array object has a sum() method that calls the numpy.sum() function"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's use the NumPy random number generator. The results are almost the same in simple test cases. \n",
    "\n",
    "*For your interest, computers are not good at floating numbers:\n",
    "```python\n",
    ">>> 0.0001 + 0.0002\n",
    "0.00030000000000000003\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0.77395605 0.43887844 0.85859792 0.69736803 0.09417735 0.97562235\n",
      " 0.7611397  0.78606431 0.12811363 0.45038594 0.37079802 0.92676499\n",
      " 0.64386512 0.82276161 0.4434142  0.22723872 0.55458479 0.06381726\n",
      " 0.82763117 0.6316644  0.75808774 0.35452597 0.97069802 0.89312112\n",
      " 0.7783835  0.19463871 0.466721   0.04380377 0.15428949 0.68304895\n",
      " 0.74476216 0.96750973 0.32582536 0.37045971 0.46955581 0.18947136\n",
      " 0.12992151 0.47570493 0.22690935 0.66981399 0.43715192 0.8326782\n",
      " 0.7002651  0.31236664 0.8322598  0.80476436 0.38747838 0.2883281\n",
      " 0.6824955  0.13975248 0.1999082  0.00736227 0.78692438 0.66485086\n",
      " 0.70516538 0.78072903 0.45891578 0.5687412  0.139797   0.11453007\n",
      " 0.66840296 0.47109621 0.56523611 0.76499886 0.63471832 0.5535794\n",
      " 0.55920716 0.3039501  0.03081783 0.43671739 0.21458467 0.40852864\n",
      " 0.85340307 0.23393949 0.05830274 0.28138389 0.29359376 0.66191651\n",
      " 0.55703215 0.78389821 0.66431354 0.40638686 0.81402038 0.16697292\n",
      " 0.02271207 0.09004786 0.72235935 0.46187723 0.16127178 0.50104478\n",
      " 0.1523121  0.69632038 0.44615628 0.38102123 0.30151209 0.63028259\n",
      " 0.36181261 0.08764992 0.1180059  0.96189766 0.90858069 0.69970713\n",
      " 0.26586996 0.96917638 0.7787509  0.71689019 0.4493615  0.27224156\n",
      " 0.09639096 0.9026024  0.45577629 0.20236336 0.30595662 0.57921957\n",
      " 0.17677278 0.85661428 0.75851953 0.71946296 0.43209304 0.62730884\n",
      " 0.58409797 0.6498466  0.08444432 0.4158074  0.04161417 0.49399082\n",
      " 0.32986121 0.14452419 0.10340297 0.58764457 0.17059297 0.92512012\n",
      " 0.58106114 0.3468698  0.59091549 0.02280387 0.95855921 0.48230344\n",
      " 0.78273523 0.08273    0.48665833 0.49070699 0.93782645 0.57172805\n",
      " 0.4734894  0.26697566 0.331569   0.5206724  0.43891146 0.02161208\n",
      " 0.82629192 0.89616077 0.14024909 0.55403614 0.10857574 0.67224009\n",
      " 0.28123378 0.65942263 0.72699461 0.76864749 0.10774095 0.91601185\n",
      " 0.23021399 0.03741256 0.55485247 0.37092228 0.82978974 0.80825147\n",
      " 0.31713889 0.9528994  0.29091784 0.51505713 0.25596509 0.93604357\n",
      " 0.16460782 0.04491062 0.43509706 0.99237556 0.89167727 0.74860802\n",
      " 0.89079249 0.89344664 0.51885836 0.31592905 0.77201243 0.66166126\n",
      " 0.37365773 0.09446667 0.74678961 0.26246052 0.93681315 0.24097058\n",
      " 0.12275793 0.83111267 0.15328432 0.17926831 0.59938279 0.87456204\n",
      " 0.19643467 0.31032367 0.77740484 0.97182643 0.50074119 0.1438975\n",
      " 0.01393629 0.22965603 0.13182222 0.67765867 0.1218325  0.50632993\n",
      " 0.69426244 0.58111661 0.19977565 0.80412453 0.71540713 0.738984\n",
      " 0.13105775 0.1237538  0.92756255 0.39757819 0.30094869 0.48858405\n",
      " 0.66286421 0.95562326 0.28644623 0.92480843 0.02485949 0.55519804\n",
      " 0.63397511 0.1058974  0.1403396  0.41911432 0.96623191 0.59604255\n",
      " 0.93302322 0.80436092 0.4673816  0.78476345 0.01783678 0.109144\n",
      " 0.82942861 0.79681709 0.23264074 0.53076959 0.60601582 0.86773895\n",
      " 0.60310716 0.41257157 0.37418404 0.42588209 0.65193103 0.86749063\n",
      " 0.45389688 0.24783956 0.23666236 0.74601428 0.81656876 0.10527808\n",
      " 0.06655886 0.59443366 0.14617324 0.82466419 0.31033467 0.14387193\n",
      " 0.92097047 0.16553172 0.28472008 0.1536134  0.11549006 0.02114802\n",
      " 0.05539541 0.17464147 0.05338193 0.59114382 0.68071453 0.39363046\n",
      " 0.3179911  0.50452624 0.87500494 0.85113163 0.04347506 0.18149841\n",
      " 0.23674487 0.24938758 0.57123265 0.41626243 0.04925412 0.37361414\n",
      " 0.52375295 0.1016719  0.83345855 0.05196187 0.92484187 0.09911314\n",
      " 0.84357495 0.90265314 0.97957068 0.80202588 0.77947754 0.64248328\n",
      " 0.77899635 0.13455221 0.53606804 0.51422287 0.85757214 0.46279937\n",
      " 0.3850895  0.63956327 0.26646332 0.13976841 0.47787727 0.41688937\n",
      " 0.23256994 0.36751181 0.36639245 0.32749556 0.37946408 0.68574335\n",
      " 0.29687647 0.94885793 0.91634802 0.48091043 0.32836121 0.53543479\n",
      " 0.84856049 0.65258734 0.80439183 0.53272228 0.63291763 0.28815561\n",
      " 0.73489316 0.20240459 0.69479813 0.86071907 0.13210284 0.61437974\n",
      " 0.09509575 0.72571563 0.08449322 0.93593982 0.13740793 0.95888025\n",
      " 0.80088418 0.593682   0.7826241  0.79511484 0.94602706 0.25338335\n",
      " 0.5900759  0.0950492  0.6161657  0.1712913  0.56495061 0.57243051\n",
      " 0.46598515 0.52263178 0.76392339 0.79924472 0.49215322 0.59959344\n",
      " 0.93123624 0.11973359 0.11710357 0.08770901 0.65786329 0.4186083\n",
      " 0.77432142 0.67123141 0.33363776 0.89836655 0.76253215 0.27053494\n",
      " 0.36419202 0.31443998 0.15761165 0.14778337 0.93612746 0.43790404\n",
      " 0.38331982 0.72968571 0.55299307 0.93613999 0.78030149 0.47936956\n",
      " 0.37635947 0.98663154 0.71776024 0.95119466 0.11847858 0.85053368\n",
      " 0.63707388 0.12192168 0.588258   0.68609637 0.01230269 0.45431796\n",
      " 0.82539951 0.29535903 0.45854808 0.44231413 0.30192739 0.9184419\n",
      " 0.78129404 0.11058841 0.99703466 0.87920002 0.28390844 0.83689658\n",
      " 0.10641953 0.99910473 0.66568474 0.65012502 0.09044073 0.8970334\n",
      " 0.0289995  0.24082806 0.14302188 0.77676794 0.19820423 0.91063823\n",
      " 0.65626904 0.03616271 0.00542983 0.05165792 0.60592518 0.80148181\n",
      " 0.23855282 0.84940884 0.05723194 0.80096385 0.92779543 0.7721084\n",
      " 0.69812078 0.83798022 0.0401513  0.20178211 0.12492368 0.50453099\n",
      " 0.74518813 0.63001184 0.8511311  0.15521299 0.73462109 0.19304149\n",
      " 0.27075875 0.7099047  0.98020478 0.61154361 0.05450031 0.61630897\n",
      " 0.04235055 0.88414571 0.70957829 0.17312785 0.09172101 0.18353323\n",
      " 0.98002718 0.45856064 0.78408095 0.63640834 0.57241315 0.14513025\n",
      " 0.94602445 0.30134263 0.57801722 0.69977594 0.64923316 0.94059441\n",
      " 0.14843899 0.50835274 0.40403439 0.47416873 0.11921753 0.13409461\n",
      " 0.27807555 0.3047046  0.42790321 0.61098755 0.63462912 0.4118109\n",
      " 0.40878311 0.21762853 0.58830625 0.31704091 0.03605983 0.41840004\n",
      " 0.47413268 0.22559287 0.57245793 0.5657719  0.70200218 0.64794848\n",
      " 0.65243306 0.31621415 0.78743222 0.54914438 0.4314182  0.62601248\n",
      " 0.36065733 0.51273924 0.73670569 0.88640289 0.9210572  0.50363293\n",
      " 0.52027511 0.79987041 0.31445069 0.83738236 0.49414165 0.11585672\n",
      " 0.07205915 0.84199321 0.05556792 0.28061144 0.33413004 0.17299445\n",
      " 0.31389337 0.74269257 0.01468284 0.82717342 0.85654802 0.37226157\n",
      " 0.1536129  0.60084041 0.11967256 0.36491936 0.95842918 0.99546447\n",
      " 0.77210489 0.31096151 0.68766505 0.70540637 0.3878417  0.64088863\n",
      " 0.01072764 0.20905766 0.5250883  0.1637513  0.16590687 0.83630429\n",
      " 0.989133   0.55596943 0.83906973 0.99032166 0.14159589 0.44824561\n",
      " 0.39257272 0.08004928 0.75533017 0.43377903 0.46932693 0.15067297\n",
      " 0.18092665 0.90710362 0.04464909 0.23285228 0.29205933 0.49019754\n",
      " 0.58644517 0.49328998 0.08411533 0.24366745 0.84358838 0.6375887\n",
      " 0.64914905 0.67020326 0.76290302 0.05810848 0.36660838 0.53952744\n",
      " 0.33845648 0.84447887 0.48257251 0.76862759 0.85201552 0.50479148\n",
      " 0.90955224 0.58712394 0.8502743  0.3405908  0.49881696 0.53141104\n",
      " 0.10497972 0.39855251 0.91733767 0.63083224 0.17750658 0.33885564\n",
      " 0.19160301 0.02482313 0.92746046 0.44820733 0.30753507 0.59847719\n",
      " 0.00731446 0.27802211 0.70303347 0.63376977 0.98180595 0.62035771\n",
      " 0.47750587 0.76143256 0.90332787 0.72069595 0.96321122 0.78200517\n",
      " 0.86680144 0.11410407 0.7324135  0.4400887  0.5531038  0.65410241\n",
      " 0.96981512 0.98457808 0.28822825 0.7337535  0.74998354 0.34649286\n",
      " 0.12386977 0.04094696 0.77734313 0.48969974 0.98554017 0.46497346\n",
      " 0.97791698 0.411576   0.79368215 0.08481927 0.55546171 0.80205979\n",
      " 0.92470167 0.82258309 0.03697073 0.37270234 0.04869847 0.10928229\n",
      " 0.67530563 0.7132582  0.77372068 0.86545655 0.73943147 0.80087159\n",
      " 0.04896371 0.23453515 0.62189777 0.8581253  0.00450012 0.51462934\n",
      " 0.6772874  0.02960729 0.40135556 0.89563488 0.67161281 0.23765836\n",
      " 0.85278113 0.34803142 0.85334467 0.29894365 0.59032025 0.39694007\n",
      " 0.27482505 0.88655756 0.18759368 0.08481159 0.34192694 0.71763915\n",
      " 0.80743161 0.99874337 0.29636206 0.40794195 0.13682128 0.57487193\n",
      " 0.99758004 0.70088101 0.59521284 0.39236909 0.91529876 0.4969166\n",
      " 0.13436691 0.36537846 0.06716667 0.20197904 0.01766878 0.45327992\n",
      " 0.63454026 0.34329246 0.42038177 0.95920927 0.75196312 0.54085664\n",
      " 0.28454088 0.8969968  0.23509712 0.32534273 0.90906481 0.52954206\n",
      " 0.74231795 0.59074479 0.65343921 0.29938329 0.24137206 0.32249235\n",
      " 0.15544156 0.87431437 0.28324693 0.56148939 0.79197443 0.78382411\n",
      " 0.43838626 0.47625731 0.99470175 0.67459748 0.81463844 0.90255397\n",
      " 0.78758984 0.18517934 0.56217073 0.10189416 0.65292213 0.95534943\n",
      " 0.51273206 0.43297249 0.03584275 0.95977452 0.10300268 0.04107909\n",
      " 0.24606661 0.06553043 0.45511784 0.5160878  0.31256995 0.05096048\n",
      " 0.11160037 0.38450438 0.06052845 0.69818973 0.20702054 0.30207157\n",
      " 0.39412578 0.41660963 0.00166071 0.11207037 0.86276456 0.00123306\n",
      " 0.50816736 0.48950201 0.33309416 0.43132702 0.78058114 0.84120371\n",
      " 0.26034854 0.32249044 0.24248285 0.4798634  0.68325836 0.22825288\n",
      " 0.33073574 0.93038462 0.04856929 0.46076961 0.71155804 0.15045401\n",
      " 0.04737402 0.13820537 0.91882319 0.00925978 0.18832197 0.03128351\n",
      " 0.11062947 0.62014929 0.24163891 0.56921205 0.59019538 0.84943533\n",
      " 0.00474139 0.85336946 0.61911909 0.16274349 0.77293739 0.85549154\n",
      " 0.25426348 0.91879368 0.45445743 0.60342445 0.98464953 0.36153608\n",
      " 0.81315935 0.31839912 0.79921352 0.60073392 0.2163557  0.41402609\n",
      " 0.31763563 0.07810839 0.02983477 0.34647866 0.01903415 0.16548776\n",
      " 0.72518332 0.70809124 0.73858039 0.31715317 0.89001946 0.59383055\n",
      " 0.12602634 0.14371866 0.69309543 0.17294394 0.5072927  0.99177448\n",
      " 0.0040076  0.0165798  0.99308458 0.58463883 0.12690998 0.8973022\n",
      " 0.8803803  0.53620041 0.62179203 0.27318719 0.0505197  0.59456761\n",
      " 0.29475519 0.66362105 0.83624518 0.01816054 0.59571377 0.23128054\n",
      " 0.8738885  0.25366451 0.61077013 0.55354084 0.3961665  0.67762076\n",
      " 0.72576963 0.5669378  0.75842224 0.98341708 0.41927239 0.51458305\n",
      " 0.01246988 0.79602856 0.52023007 0.40796143 0.09407196 0.88965642\n",
      " 0.39500724 0.6825816  0.14937416 0.96154963 0.17844176 0.19954199\n",
      " 0.85898286 0.91258274 0.21214741 0.46980089 0.7334949  0.87836573\n",
      " 0.3790527  0.51701949 0.74171641 0.73113598 0.78296013 0.56993382\n",
      " 0.1046151  0.90397329 0.86557254 0.79809744 0.09986248 0.20439737\n",
      " 0.74360037 0.02314009 0.97922463 0.37714468 0.71931253 0.88757109\n",
      " 0.39462989 0.31913249 0.60874113 0.58098937 0.40913998 0.60177476\n",
      " 0.93538137 0.46764026 0.19674218 0.37720598 0.39410698 0.13110869\n",
      " 0.1628715  0.68456207 0.33947775 0.95489839 0.24359675 0.09892451\n",
      " 0.75350456 0.88103605 0.27811336 0.2020881  0.18576613 0.5220123\n",
      " 0.46841664 0.25943984 0.04517312 0.48149134 0.95933235 0.6525192\n",
      " 0.49550656 0.11069207 0.25243629 0.29477395 0.76480355 0.8767108\n",
      " 0.90164185 0.98456861 0.98236668 0.95299913 0.07182214 0.13779354\n",
      " 0.30446653 0.55289825 0.09697789 0.84579696 0.61675618 0.5423644\n",
      " 0.16531132 0.25325547 0.16031202 0.85193316 0.58422786 0.73512943\n",
      " 0.29603286 0.37125264 0.40485496 0.76001497 0.77235299 0.20681353\n",
      " 0.94153886 0.12065534 0.89612208 0.10039565 0.26453981 0.84647854\n",
      " 0.17949207 0.41356793 0.4498593  0.24525897 0.7102452  0.85133465\n",
      " 0.87457514 0.33932215 0.53085039 0.2484064  0.2447965  0.16125406\n",
      " 0.94001901 0.88798883 0.77735871 0.51766152 0.49060792 0.52974567\n",
      " 0.53656801 0.43456308 0.13175483 0.12566385 0.95224972 0.48203076\n",
      " 0.95320552 0.16357688 0.55441655 0.20771725 0.25318461 0.03005458\n",
      " 0.11895331 0.91684841 0.32153299 0.60812921 0.46501413 0.40045125\n",
      " 0.53188609 0.18723894 0.9886039  0.81830787 0.74161463 0.4687593\n",
      " 0.15287712 0.92066586 0.34138707 0.05010669 0.34240368 0.79445703\n",
      " 0.62270874 0.75083858 0.79363538 0.21195461 0.92378332 0.4379896\n",
      " 0.63901365 0.00230877 0.9933685  0.28089639]\n",
      "\n",
      "Python sum: 497.17783852843127\n",
      "NumPy  sum: 497.17783852843195\n"
     ]
    }
   ],
   "source": [
    "### random number generation\n",
    "rng = np.random.default_rng(seed=42)    ### ref: import random\n",
    "                                        ### random.random()\n",
    "rnd_arr = rng.random(1000)\n",
    "print(rnd_arr)\n",
    "\n",
    "print()\n",
    "\n",
    "### these two methods are equivalent, but numpy.sum() is faster than Python's built-in sum() function for large arrays.\n",
    "print(f\"Python sum: {sum(rnd_arr)}\")\n",
    "print(f\"NumPy  sum: {np.sum(rnd_arr)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Considering performance, NumPy’s compiled code version of the operation is computed much more quickly. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "31.9 ms ± 1.15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n",
      "122 μs ± 407 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n"
     ]
    }
   ],
   "source": [
    "big_array = rng.random(1000000)\n",
    "\n",
    "%timeit sum(big_array)\n",
    "%timeit np.sum(big_array)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `min() & max()`\n",
    "\n",
    "Python has built-in min and max functions, and NumPy has corresponding functions for finding the minimum and maximum values of an array. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(seed=42)    ### create random number generator (rng)\n",
    "\n",
    "big_array = rng.random(1000000)         ### use the rng to generate random numbers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python min/max:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(np.float64(1.2500323287589765e-07), np.float64(0.9999997172035572))"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "min(big_array), max(big_array)          ### note this returns a tuple"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "NumPy min/max:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(np.float64(1.2500323287589765e-07), np.float64(0.9999997172035572))"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "np.min(big_array), np.max(big_array)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There is a different syntax, though, which uses the methods of the array objects: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(np.float64(1.2500323287589765e-07), np.float64(0.9999997172035572))"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "big_array.min(), big_array.max()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Compare performance:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "23.2 ms ± 366 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n",
      "78.5 μs ± 1.38 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n",
      "79.2 μs ± 648 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit min(big_array)\n",
    "%timeit np.min(big_array)\n",
    "%timeit big_array.min()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Based on the performance observations above, we should always use NumPy ufuncs. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `argmin()` and `argmax`\n",
    "\n",
    "`argmin()` and `argmax` help you find non-obvious outlier positions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[10  2 10  4] \n",
      "\n",
      "[[ 1  2  3]\n",
      " [ 4 20  6]]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "### 1D array\n",
    "arr_1d = np.array([1, 2, 3, 4])\n",
    "arr_1d[2] = 10                  ### Update the third element (index 2)\n",
    "print(arr_1d, \"\\n\")\n",
    "\n",
    "### 2D array\n",
    "arr_2d = np.array([[1, 2, 3], [4, 5, 6]])\n",
    "arr_2d[1, 1] = 20               ### Update element at row 1, column 1\n",
    "print(arr_2d)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "1\n",
      "4\n",
      "0\n"
     ]
    }
   ],
   "source": [
    "### numpy.argmax() returns the indices of the maximum values along a specified axis of an array.\n",
    "### numpy.argmin() returns the indices of the minimum values along a specified axis of an array.\n",
    "\n",
    "print(arr_1d.argmax())\n",
    "print(arr_1d.argmin())\n",
    "\n",
    "print(arr_2d.argmax())\n",
    "print(arr_2d.argmin())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 1,  2,  3],\n",
       "       [ 4, 20,  6]])"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr_2d"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Broadcasting\n",
    "\n",
    "[Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html) lets NumPy combine arrays of different shapes without copying data: the smaller array is conceptually 'stretched' to match, computed on the fly rather than materialized in memory. Arithmetic broadcasting (like `x + 100`) returns a new array and leaves the original **unchanged**; but broadcasting during in-place assignment (like `arr[0:5] = 10`) does modify the original array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ 5,  7,  9, 11, 13])"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### same shape addition: default behavior\n",
    "\n",
    "x = np.arange(0, 5)     ### 0, 1, ,2 ,3, 4 \n",
    "y = np.arange(5, 10)    ### 5, 6, ,7, 8, 9\n",
    "x + y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Broadcasting Rules\n",
    "Broadcasting follows specific rules to determine compatibility:\n",
    "\n",
    "- Two dimensions are compatible for broadcasting if they are equal or if one of them is 1.\n",
    "- Arrays are treated as if they have the shape of the maximum dimensions by \"stretching\" the smaller dimension with a size of 1 to match.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Simple broadcasting: value 100 below is **stretched** or **broadcasted** into the array `[0, 1, 2, 3, 4]` and the results are added."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([100, 101, 102, 103, 104])"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### different shape: add a scalar to an 1-D array\n",
    "### broadcasting\n",
    "\n",
    "x + 100"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0, 1, 2, 3, 4])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### x stays the same\n",
    "x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[11 22 33]\n",
      " [14 25 36]]\n"
     ]
    }
   ],
   "source": [
    "### observe what happens when we do addition with arrays; the operation is by vector:\n",
    "\n",
    "a = np.array([[1, 2, 3],\n",
    "              [4, 5, 6]])\n",
    "\n",
    "b = np.array([10, 20, 30])\n",
    "\n",
    "print(a + b)     ### stretch b into the two vectors in a"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here above, we say that b is **broadcasted** over the second dimension to match the shape of a."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### setting a value with index range \n",
    "\n",
    "arr = np.arange(0, 10)\n",
    "arr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([10, 10, 10, 10, 10,  5,  6,  7,  8,  9])"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### udpate array (broadcasting)\n",
    "### operation not saved but array changed\n",
    "\n",
    "arr[0:5]=10\n",
    "\n",
    "### show array again:\n",
    "arr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Basic Broadcasting with Scalar\n",
    "### Given the Series:\n",
    "import pandas as pd\n",
    "\n",
    "sales = pd.Series([100, 150, 200, 120], index=['A', 'B', 'C', 'D'])\n",
    "\n",
    "### Apply a 10% discount to all sales values.\n",
    "### Your code starts here:\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "A     90.0\n",
      "B    135.0\n",
      "C    180.0\n",
      "D    108.0\n",
      "dtype: float64\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "import pandas as pd\n",
    "\n",
    "sales = pd.Series([100, 150, 200, 120], index=['A', 'B', 'C', 'D'])\n",
    "discounted_sales = sales * 0.90\n",
    "print(discounted_sales)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "A     90.0\n",
       "B    135.0\n",
       "C    180.0\n",
       "D    108.0\n",
       "dtype: float64"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### Solution\n",
    "sales * 90/100"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "vscode": {
     "languageId": "plaintext"
    }
   },
   "source": [
    "## Masking\n",
    "\n",
    "NumPy masking is a technique for **selecting** or **filtering** array elements based on **conditions**. Instead of using **indices**, you create a **boolean mask** (an **array of True/False** values) to specify which elements you want to keep, modify, or analyze. In NumPy, **masking** is a method for selecting, modifying, or ignoring elements in an array based on a **boolean condition**.\n",
    "\n",
    "Given an array, there are a host of useful operations you can do with Boolean using:\n",
    "\n",
    "- Comparison operators\n",
    "- Logical operators"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[False False False  True  True  True]\n",
      "[4 5 6]\n"
     ]
    }
   ],
   "source": [
    "arr = np.array([1, 2, 3, 4, 5, 6])   ### create array\n",
    "\n",
    "### Create a mask (boolean array)\n",
    "mask = arr > 3             ### Result: [False, False, False, True, True, True]\n",
    "print(mask)\n",
    "\n",
    "### Apply the mask to select elements\n",
    "result = arr[mask]         # Result: [4, 5, 6]\n",
    "print(result)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Comparison operations with f-string\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "equal: [False False  True False]\n",
      "greater: [False  True False  True]\n"
     ]
    }
   ],
   "source": [
    "arr1 = np.array([True, False, True, False])\n",
    "arr2 = np.array([True, True, False, False])\n",
    "arr3 = np.array([1, 5, 2, 8])\n",
    "arr4 = np.array([3, 2, 2, 7])\n",
    "\n",
    "print(f\"equal: {np.equal(arr3, arr4)}\")\n",
    "print(f\"greater: {np.greater(arr3, arr4)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the terminal, go to your project folder, activate the virtual environment, and go into Python to do the following:\n",
    "\n",
    "\n",
    "```python\n",
    "(.venv) [user]@[host]ː~/workspace/thinkdsm$ python\n",
    "Python 3.13.7 (main, Aug 14 2025, 11:12:11) [Clang 17.0.0 (clang-1700.0.13.3)] on darwin\n",
    "Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.\n",
    ">>> import numpy as np\n",
    ">>> x = np.array([1, 2, 3, 4, 5])\n",
    ">>> x > 3\n",
    "array([False, False, False,  True,  True])\n",
    ">>> x < 3\n",
    "array([ True,  True, False, False, False])\n",
    ">>> x >= 3\n",
    "array([False, False,  True,  True,  True])\n",
    ">>> x <=3\n",
    "array([ True,  True,  True, False, False])\n",
    ">>> x != 3 # not equal\n",
    "array([ True,  True, False,  True,  True])\n",
    ">>> x == 3 # equal\n",
    "array([False, False,  True, False, False])\n",
    ">>>\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Logical operations\n",
    "\n",
    "**Logical operations** on arrays. Note that we use `&`, `|`. The difference with Python `and` and `or` is: `and` and `or` operate on the object as a whole, while `&` and `|` operate on the elements within the object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[False  True  True False]\n",
      "[ True False False  True]\n",
      "[ True  True False False]\n"
     ]
    }
   ],
   "source": [
    "### example of logical operations\n",
    "\n",
    "a = np.array([1, 5, 10, 15])\n",
    "print((a > 3) & (a < 12))           ### array([False,  True,  True, False])\n",
    "print((a < 3) | (a > 12))           ### array([ True, False, False,  True])\n",
    "print(~(a > 5))                     ### array([ True,  True, False, False])\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[ 1  5]\n",
      " [10 15]]\n",
      "\n",
      "[[False  True]\n",
      " [ True False]]\n",
      "\n",
      "[[ True False]\n",
      " [False  True]]\n",
      "\n",
      "[[ True  True]\n",
      " [False False]]\n"
     ]
    }
   ],
   "source": [
    "a = np.array([1, 5, 10, 15]).reshape(2,2)\n",
    "print(a)\n",
    "print()\n",
    "\n",
    "print((a > 3) & (a < 12))           ### array([False,  True,  True, False])\n",
    "print()\n",
    "print((a < 3) | (a > 12))           ### array([ True, False, False,  True])\n",
    "print()\n",
    "print(~(a > 5))                     ### array([ True,  True, False, False])\n",
    "### ~ is bitwise NOT operator, which inverts the boolean values in the array."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### logical_ operators "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "logical_and: [ True False False False]\n",
      "logical_or:  [ True  True  True False]\n",
      "logical_not: [False  True False  True]\n",
      "logical_xor: [False  True  True False]\n",
      "\n",
      "equal: [False False  True False]\n",
      "greater: [False  True False  True]\n"
     ]
    }
   ],
   "source": [
    "arr1 = np.array([True, False, True, False])\n",
    "arr2 = np.array([True, True, False, False])\n",
    "arr3 = np.array([1, 5, 2, 8])\n",
    "arr4 = np.array([3, 2, 2, 7])\n",
    "\n",
    "# Logical operations\n",
    "print(f\"logical_and: {np.logical_and(arr1, arr2)}\")\n",
    "print(f\"logical_or:  {np.logical_or(arr1, arr2)}\")\n",
    "print(f\"logical_not: {np.logical_not(arr1)}\")\n",
    "print(f\"logical_xor: {np.logical_xor(arr1, arr2)}\")     ### XOR (eXclusive OR) is a logical operation \n",
    "                                                        ### that outputs true if and only if its inputs \n",
    "                                                        ### are different. \n",
    "\n",
    "print()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Filtering Values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ 95,  88, 102])"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "temperatures = np.array([68, 72, 95, 58, 88, 102])\n",
    "\n",
    "hot_days = temperatures[temperatures > 85]  # [95, 88, 102]\n",
    "hot_days"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Modifying elements"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 2, 3, 0, 0])"
      ]
     },
     "execution_count": 47,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr = np.array([1, 2, 3, 4, 5])\n",
    "\n",
    "arr[arr > 3] = 0               ### Sets elements > 3 to 0\n",
    "arr"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Counting elements"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3\n"
     ]
    }
   ],
   "source": [
    "arr = np.array([5, 10, 15, 20, 25])\n",
    "\n",
    "count = np.sum(arr > 12)  # Counts elements > 12 (returns 3)\n",
    "print(count)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Fancy Indexing\n",
    "\n",
    "**Fancy** indexing, or **vectorized** indexing, allows you to select entire rows or columns out of order. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[10 40 60]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "arr = np.array([10, 20, 30, 40, 50, 60, 70])\n",
    "indices = np.array([0, 3, 5])    ### indices to select: 0th, 3rd, and 5th elements\n",
    "\n",
    "selected_elements = arr[indices]\n",
    "print(selected_elements)         ### Output: [10 40 60]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original 2D array:\n",
      " [[ 0  1  2  3]\n",
      " [ 4  5  6  7]\n",
      " [ 8  9 10 11]]\n",
      "\n",
      "Selected elements: [1 7 8]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "arr_2d = np.arange(12).reshape(3, 4)     ### the 2-D array\n",
    "print(\"Original 2D array:\\n\", arr_2d)\n",
    "\n",
    "print()\n",
    "\n",
    "### indexing\n",
    "row_indices = np.array([0, 1, 2])\n",
    "col_indices = np.array([1, 3, 0])\n",
    "\n",
    "### Select elements at (0, 1), (1, 3), and (2, 0)\n",
    "selected_elements = arr_2d[row_indices, col_indices]     ### pay attention to the syntax here\n",
    "print(\"Selected elements:\", selected_elements)           ### selected elements: [ 1  7  8]\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Another example of fancy indexing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### set up matrix\n",
    "arr2d = np.zeros((10,10))\n",
    "arr2d"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "### Length of array\n",
    "\n",
    "arr_length = arr2d.shape[1]    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "10"
      ]
     },
     "execution_count": 55,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr_length"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "       [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],\n",
       "       [2., 2., 2., 2., 2., 2., 2., 2., 2., 2.],\n",
       "       [3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],\n",
       "       [4., 4., 4., 4., 4., 4., 4., 4., 4., 4.],\n",
       "       [5., 5., 5., 5., 5., 5., 5., 5., 5., 5.],\n",
       "       [6., 6., 6., 6., 6., 6., 6., 6., 6., 6.],\n",
       "       [7., 7., 7., 7., 7., 7., 7., 7., 7., 7.],\n",
       "       [8., 8., 8., 8., 8., 8., 8., 8., 8., 8.],\n",
       "       [9., 9., 9., 9., 9., 9., 9., 9., 9., 9.]])"
      ]
     },
     "execution_count": 56,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### set up array\n",
    "\n",
    "for i in range(arr_length):\n",
    "    arr2d[i] = i              ### Row 0 gets all 0s, row 1 gets all 1s, etc.\n",
    "    \n",
    "arr2d"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we will do fancy indexing with a list of indices — you can select rows in any order you want, and they'll be returned in that exact order:\n",
    "\n",
    "1. The double brackets **`[[ ]]`** mean \"use a list of indices.\"\n",
    "2. `[2,4,6,8]` specifies which rows to select: row 2, row 4, row 6, row 8.\n",
    "3. Result: A new 4×10 array with those 4 rows stacked together in that order"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[2., 2., 2., 2., 2., 2., 2., 2., 2., 2.],\n",
       "       [4., 4., 4., 4., 4., 4., 4., 4., 4., 4.],\n",
       "       [6., 6., 6., 6., 6., 6., 6., 6., 6., 6.],\n",
       "       [8., 8., 8., 8., 8., 8., 8., 8., 8., 8.]])"
      ]
     },
     "execution_count": 57,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### allows in any order\n",
    "arr2d[[2,4,6,8]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, another fancy indexing:\n",
    "\n",
    "1. Same idea, but notice the rows are in a different order: 6, 4, 2, 7.\n",
    "2. NumPy doesn't care about the order — it just grabs the rows in the sequence you specify.\n",
    "3. Result: Row 6, then row 4, then row 2, then row 7 (in that exact sequence). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[6., 6., 6., 6., 6., 6., 6., 6., 6., 6.],\n",
       "       [4., 4., 4., 4., 4., 4., 4., 4., 4., 4.],\n",
       "       [2., 2., 2., 2., 2., 2., 2., 2., 2., 2.],\n",
       "       [7., 7., 7., 7., 7., 7., 7., 7., 7., 7.]])"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### allows in any order\n",
    "arr2d[[6,4,2,7]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sorting Arrays"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sorting lists\n",
    "\n",
    "- Python **sorted( )** function; returns a copy of sorted **Python list**\n",
    "- Python list **sort( )** method (work on the object, **change in place**)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[1, 1, 2, 3, 4, 5, 6, 9]"
      ]
     },
     "execution_count": 54,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### sorted function on a list\n",
    "\n",
    "lst = [3, 1, 4, 1, 5, 9, 2, 6]\n",
    "\n",
    "sorted(lst)      ### returns a sorted copy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[3, 1, 4, 1, 5, 9, 2, 6]"
      ]
     },
     "execution_count": 52,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "lst              ### original object unchanged"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "### list.sort method: sort in place\n",
    "\n",
    "lst.sort()       ### no return value, sorts \"in place\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[1, 1, 2, 3, 4, 5, 6, 9]"
      ]
     },
     "execution_count": 57,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "lst              ### object changed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['I', 'I', 'M', 'O', 'R', 'S', 'S', 'U']"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### sorting a string \n",
    "\n",
    "state = 'MISSOURI' \n",
    "sorted(state)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'MISSOURI'"
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "state            ### original object not changed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original array: [3 1 4 1 5 9 2 6] <class 'numpy.ndarray'>\n",
      "Sorted array: [np.int64(1), np.int64(1), np.int64(2), np.int64(3), np.int64(4), np.int64(5), np.int64(6), np.int64(9)] <class 'list'>\n"
     ]
    }
   ],
   "source": [
    "### sorted(array) returns a sorted copy of the array as a list, while arr.sort() sorts the array in place.\n",
    "### surprise!\n",
    "\n",
    "arr = np.array([3, 1, 4, 1, 5, 9, 2, 6])\n",
    "print(\"Original array:\", arr, type(arr))\n",
    "\n",
    "sorted_arr = sorted(arr)\n",
    "print(\"Sorted array:\", sorted_arr, type(sorted_arr))    ### a list\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sorting NumPy arrays\n",
    "\n",
    "- `np.sort()`: Both **np.sort()** and Python **sorted( )** functions return a sorted copy of an array; original object **unchanged**.\n",
    "- `argsort()`: argsort returns the indices of the sorted elements.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "arr = np.array([1, 5, 4, 2, 3])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[np.int64(1), np.int64(2), np.int64(3), np.int64(4), np.int64(5)]"
      ]
     },
     "execution_count": 66,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### python sorted( ) function\n",
    "\n",
    "sorted(arr)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 5, 4, 2, 3])"
      ]
     },
     "execution_count": 67,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### arr unchanged\n",
    "\n",
    "arr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 2, 3, 4, 5])"
      ]
     },
     "execution_count": 68,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### sort array in place\n",
    "\n",
    "np.sort(arr)     ### returns sorted "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1, 5, 4, 2, 3])"
      ]
     },
     "execution_count": 69,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr              ### original object not changed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**`argsort`** returns the indices of the sorted elements:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0, 3, 4, 2, 1])"
      ]
     },
     "execution_count": 70,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "np.argsort(arr)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`arr[0]` is the smallest element; `arr[1]` has the largest element."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Quick Reference: Array Computation\n",
    "\n",
    "The examples below summarize array computation tools used in this section.\n",
    "\n",
    "::::{grid} 2\n",
    ":gutter: 2\n",
    "\n",
    ":::{grid-item-card} Math and aggregations\n",
    "```python\n",
    "np.sum(m)\n",
    "np.mean(m)\n",
    "np.min(m); np.max(m)\n",
    "np.std(m); np.var(m)\n",
    "np.sum(m, axis=0)\n",
    "np.sum(m, axis=1)\n",
    "np.cumsum(x)\n",
    "np.clip(x, 0, 3)\n",
    "```\n",
    ":::\n",
    "\n",
    ":::{grid-item-card} Performance and safety\n",
    "```python\n",
    "# prefer vectorized operations over loops\n",
    "y = (x * 2) + 1\n",
    "v = x.view()\n",
    "c = x.copy()\n",
    "np.shares_memory(x, v)\n",
    "np.where(x > 2, 1, 0)\n",
    "np.isclose(0.1 + 0.2, 0.3)\n",
    "x = x.astype(np.float32)\n",
    "```\n",
    ":::\n",
    "\n",
    ":::{grid-item-card} Sorting\n",
    "```python\n",
    "sorted(lst)          # returns sorted copy\n",
    "lst.sort()            # sorts in place\n",
    "np.sort(arr)          # returns sorted copy\n",
    "np.argsort(arr)       # returns sorted indices\n",
    "\n",
    "```\n",
    "::::"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv (3.13.13)",
   "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.13.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
