{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "db72fc8b",
   "metadata": {},
   "source": [
    "# Module 4 — Aggregations, Statistics & Sorting\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb3c9ca1",
   "metadata": {},
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/inigoochoa/comprehensive-numpy/blob/main/module_4_aggregations_statistics.ipynb)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc28f04d",
   "metadata": {},
   "source": [
    "## Welcome\n",
    "\n",
    "This module is about turning an array into a number — or a handful of\n",
    "numbers — that summarize it. You'll learn how to aggregate along a\n",
    "specific axis, locate extremes, sort, and count unique values. By the end\n",
    "you'll chain all of these together to analyze a small real-flavored\n",
    "dataset.\n",
    "\n",
    "**Learning goals**\n",
    "- Aggregate with `sum`/`mean`/`std`/`min`/`max`, controlling direction with `axis`\n",
    "- Locate extremes with `argmin`/`argmax` and `np.unravel_index`\n",
    "- Sort arrays and get an ordering with `np.sort` / `arr.sort()` / `np.argsort`\n",
    "- Count unique values with `np.unique`\n",
    "- Combine these tools to analyze a dataset end to end\n",
    "\n",
    "**Setup** — run this first.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f613da2b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "print(np.__version__)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db6e8a74",
   "metadata": {},
   "source": [
    "## Lesson 1 — Aggregations & the `axis` Parameter\n",
    "\n",
    "`np.sum`, `np.mean`, `np.std`, `np.min`, `np.max` all reduce an array to\n",
    "fewer numbers. Called with no `axis`, they collapse the **whole** array to\n",
    "a single scalar. With `axis=0` they reduce *along rows* — one result per\n",
    "**column**. With `axis=1` they reduce *along columns* — one result per\n",
    "**row**.\n",
    "\n",
    "| Call | Meaning |\n",
    "|---|---|\n",
    "| `arr.sum()` | one total, whole array |\n",
    "| `arr.sum(axis=0)` | one total per column (walks down each column) |\n",
    "| `arr.sum(axis=1)` | one total per row (walks across each row) |\n",
    "\n",
    "Pass `keepdims=True` to keep the reduced axis as length-1 instead of\n",
    "dropping it — handy when you want the result to broadcast back against the\n",
    "original array.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "80fe22f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.array([[1, 2, 3],\n",
    "                  [4, 5, 6]])\n",
    "\n",
    "print(\"whole array sum:\", grid.sum())\n",
    "print(\"column sums (axis=0):\", grid.sum(axis=0))\n",
    "print(\"row sums (axis=1):   \", grid.sum(axis=1))\n",
    "print(\"row means, keepdims: \", grid.mean(axis=1, keepdims=True).ravel())\n",
    "\n",
    "# keepdims=True lets you broadcast the result back against the original:\n",
    "centered = grid - grid.mean(axis=1, keepdims=True)\n",
    "print(centered)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8687ad1b",
   "metadata": {},
   "source": [
    "### 💪 Exercise 1.1: Column-normalize a matrix\n",
    "\n",
    "Write `normalize_columns(grid)` that returns a copy of `grid` where each\n",
    "**column** has been divided by that column's sum, so every column sums to\n",
    "`1.0`. `grid` is a 2-D array of positive floats.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b5730e7",
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize_columns(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd8891fd",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.array([[1.0, 2.0], [3.0, 2.0]])\n",
    "result = normalize_columns(grid)\n",
    "print(\"normalize_columns(grid) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([[0.25, 0.5], [0.75, 0.5]])\n",
    "_passed = np.allclose(result.sum(axis=0), [1.0, 1.0]) and np.allclose(result, expected)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca149cdd",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Divide `grid` by `grid.sum(axis=0)` — broadcasting lines up a length-`n_cols` array against each row automatically.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93de7a60",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def normalize_columns(grid):\n",
    "    return grid / grid.sum(axis=0)\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6d02e735",
   "metadata": {},
   "source": [
    "### 💪 Exercise 1.2: Row-wise range\n",
    "\n",
    "Write `row_ranges(grid)` that returns a 1-D array with, for each row, the\n",
    "difference between that row's max and min value (`max - min`).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fcff388c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def row_ranges(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d6bffc64",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.array([[1, 5, 3], [10, 2, 8]])\n",
    "result = row_ranges(grid)\n",
    "print(\"row_ranges(grid) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([4, 8])\n",
    "_passed = np.array_equal(result, expected)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eb6d7e84",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`grid.max(axis=1) - grid.min(axis=1)`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6844bf7a",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def row_ranges(grid):\n",
    "    return grid.max(axis=1) - grid.min(axis=1)\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90463622",
   "metadata": {},
   "source": [
    "## Lesson 2 — `argmin`/`argmax` and Locating Extremes\n",
    "\n",
    "`.max()` tells you the **value**; `.argmax()` tells you **where** it is.\n",
    "Called on a flat array, `argmax` returns a single flat index. Called with\n",
    "`axis` on a 2-D array, it returns one index per row/column — the position\n",
    "*within that row/column*, not a flat index into the whole array.\n",
    "\n",
    "To turn a flat index (from calling `argmax()` on a 2-D array with no\n",
    "`axis`) back into `(row, col)` coordinates, use\n",
    "`np.unravel_index(flat_index, arr.shape)`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7038493d",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.array([[3, 9, 1],\n",
    "                  [7, 2, 8]])\n",
    "\n",
    "print(\"flat argmax:\", grid.argmax())                 # index into flattened array\n",
    "print(\"as coords:  \", np.unravel_index(grid.argmax(), grid.shape))\n",
    "print(\"argmax per row (axis=1):\", grid.argmax(axis=1))\n",
    "print(\"argmin per column (axis=0):\", grid.argmin(axis=0))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80132f8d",
   "metadata": {},
   "source": [
    "### 💪 Exercise 2.1: Find the hotspot\n",
    "\n",
    "Write `hotspot(grid)` that returns the `(row, col)` coordinates (as a tuple\n",
    "of two Python ints) of the single largest value in a 2-D array.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ae2b12ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "def hotspot(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c88e4d58",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.array([[1, 2], [9, 3]])\n",
    "result = hotspot(grid)\n",
    "print(\"hotspot(grid) ->\")\n",
    "print(result)\n",
    "\n",
    "grid2 = np.array([[5, 1, 2], [3, 4, 9]])\n",
    "result2 = hotspot(grid2)\n",
    "print(\"hotspot(grid2) ->\")\n",
    "print(result2)\n",
    "\n",
    "_passed = result == (1, 0) and result2 == (1, 2)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected: (1, 0) and (1, 2)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bad5fe62",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`np.unravel_index(grid.argmax(), grid.shape)` gives you the coordinates as numpy integers — wrap each in `int(...)` for plain Python ints.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c7859c2",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def hotspot(grid):\n",
    "    row, col = np.unravel_index(grid.argmax(), grid.shape)\n",
    "    return (int(row), int(col))\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f39a3fa5",
   "metadata": {},
   "source": [
    "### 💪 Exercise 2.2: Best score per student\n",
    "\n",
    "Each row of `scores` is one student's scores across several tests. Write\n",
    "`best_test_per_student(scores)` that returns a 1-D array with, for each\n",
    "student (row), the **index** (0-based, position within that row) of their\n",
    "best test.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "77387335",
   "metadata": {},
   "outputs": [],
   "source": [
    "def best_test_per_student(scores):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d38f56a",
   "metadata": {},
   "outputs": [],
   "source": [
    "scores = np.array([[70, 95, 60], [88, 40, 91]])\n",
    "result = best_test_per_student(scores)\n",
    "print(\"best_test_per_student(scores) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([1, 2])\n",
    "_passed = np.array_equal(result, expected)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9b4e8f6",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "You want one argmax per row — that's `axis=1`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee69ff56",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def best_test_per_student(scores):\n",
    "    return scores.argmax(axis=1)\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b955c39",
   "metadata": {},
   "source": [
    "## Lesson 3 — Sorting\n",
    "\n",
    "- `np.sort(arr)` returns a **sorted copy**; `arr.sort()` sorts **in place**\n",
    "  and returns `None`.\n",
    "- `np.argsort(arr)` doesn't sort anything — it returns the **indices** that\n",
    "  *would* sort the array. This is the tool you reach for when you need to\n",
    "  sort one array \"by\" another (e.g. sort names by score).\n",
    "- Both accept `axis` on 2-D arrays, sorting each row/column independently\n",
    "  (each row sorted on its own does **not** sort the matrix as a whole).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9cb00e82",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.array([3, 1, 4, 1, 5])\n",
    "print(\"np.sort (copy):   \", np.sort(arr), \"| original untouched:\", arr)\n",
    "\n",
    "order = np.argsort(arr)\n",
    "print(\"argsort indices:  \", order)\n",
    "print(\"names sorted by score, using argsort as a key:\")\n",
    "names = np.array([\"c\", \"a\", \"d\", \"b\", \"e\"])\n",
    "scores = np.array([30, 90, 10, 90, 50])\n",
    "print(names[np.argsort(scores)])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "89984005",
   "metadata": {},
   "source": [
    "### 💪 Exercise 3.1: Rank the runners\n",
    "\n",
    "Write `top_n_names(names, times, n)` that returns the `n` names (as a NumPy\n",
    "array of strings) belonging to the **smallest** `n` values in `times` —\n",
    "i.e. the fastest `n` runners — ordered from fastest to slowest. `names`\n",
    "and `times` are same-length 1-D arrays.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b99fe0bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "def top_n_names(names, times, n):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2acd0877",
   "metadata": {},
   "outputs": [],
   "source": [
    "names = np.array([\"alice\", \"bob\", \"cara\", \"dan\"])\n",
    "times = np.array([12.3, 10.1, 15.0, 9.8])\n",
    "result = top_n_names(names, times, 2)\n",
    "print(\"top_n_names(names, times, 2) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = [\"dan\", \"bob\"]\n",
    "_passed = list(result) == expected\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c4ed36ce",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`np.argsort(times)` gives ascending order (fastest first, since lower time is better); slice the first `n` of `names[order]`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7ea5348",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def top_n_names(names, times, n):\n",
    "    order = np.argsort(times)\n",
    "    return names[order][:n]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37300a0a",
   "metadata": {},
   "source": [
    "### 💪 Exercise 3.2: Sort each row independently\n",
    "\n",
    "Write `sort_rows_desc(grid)` that returns a copy of `grid` with each row\n",
    "sorted in **descending** order (columns no longer correspond to anything\n",
    "meaningful across rows — that's expected).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "891d158e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def sort_rows_desc(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d4eef9df",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.array([[3, 1, 2], [9, 7, 8]])\n",
    "result = sort_rows_desc(grid)\n",
    "print(\"sort_rows_desc(grid) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([[3, 2, 1], [9, 8, 7]])\n",
    "_passed = np.array_equal(result, expected)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42d913e6",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Sort ascending along `axis=1` with `np.sort`, then reverse each row with `[:, ::-1]`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2469b06c",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def sort_rows_desc(grid):\n",
    "    return np.sort(grid, axis=1)[:, ::-1]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2893bf2",
   "metadata": {},
   "source": [
    "## Lesson 4 — Unique Values & Counting\n",
    "\n",
    "`np.unique(arr)` returns the sorted distinct values in an array. Pass\n",
    "`return_counts=True` to also get how many times each value occurred, or\n",
    "`return_index=True` to get the index of each value's **first** occurrence.\n",
    "Both are commonly used together with the unique values to build a simple\n",
    "frequency table.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de8a62d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "votes = np.array([\"cat\", \"dog\", \"cat\", \"cat\", \"fox\", \"dog\"])\n",
    "\n",
    "values, counts = np.unique(votes, return_counts=True)\n",
    "print(\"values:\", values)\n",
    "print(\"counts:\", counts)\n",
    "\n",
    "for value, count in zip(values, counts):\n",
    "    print(f\"{value}: {count}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fea862b",
   "metadata": {},
   "source": [
    "### 💪 Exercise 4.1: Most common value\n",
    "\n",
    "Write `most_common(arr)` that returns the most frequently occurring value\n",
    "in a 1-D array. Assume there's a unique winner (no ties to break).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22fa8378",
   "metadata": {},
   "outputs": [],
   "source": [
    "def most_common(arr):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "292edcb1",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = most_common(np.array([1, 2, 2, 3, 2]))\n",
    "print(\"most_common(np.array([1, 2, 2, 3, 2])) ->\")\n",
    "print(result)\n",
    "\n",
    "result2 = most_common(np.array([\"a\", \"b\", \"b\", \"a\", \"a\"]))\n",
    "print(\"most_common(np.array(['a', 'b', 'b', 'a', 'a'])) ->\")\n",
    "print(result2)\n",
    "\n",
    "_passed = result == 2 and result2 == \"a\"\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected: 2 and 'a'\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "842c9c65",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`np.unique(arr, return_counts=True)` gives you `(values, counts)` — find the index of the largest count with `.argmax()` and index into `values`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6447b1d",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def most_common(arr):\n",
    "    values, counts = np.unique(arr, return_counts=True)\n",
    "    return values[counts.argmax()]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebd46576",
   "metadata": {},
   "source": [
    "### 💪 Exercise 4.2: Values seen exactly once\n",
    "\n",
    "Write `singletons(arr)` that returns, sorted, the values in a 1-D array\n",
    "that occur **exactly once**.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8baeac03",
   "metadata": {},
   "outputs": [],
   "source": [
    "def singletons(arr):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aa6adddf",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = singletons(np.array([1, 2, 2, 3, 4, 4, 4, 5]))\n",
    "print(\"singletons(np.array([1, 2, 2, 3, 4, 4, 4, 5])) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([1, 3, 5])\n",
    "_passed = np.array_equal(result, expected)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27896868",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Get `(values, counts)` from `np.unique(..., return_counts=True)`, then boolean-index `values` with `counts == 1`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0a2304f6",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def singletons(arr):\n",
    "    values, counts = np.unique(arr, return_counts=True)\n",
    "    return values[counts == 1]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab537dd0",
   "metadata": {},
   "source": [
    "## Lesson 5 — Putting It Together: Analyzing Daily Temperatures\n",
    "\n",
    "Let's generate a month of synthetic daily high temperatures (°C) and use\n",
    "everything from this module — aggregation, `argmin`/`argmax`, sorting, and\n",
    "counting — to analyze it in one exercise.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "61950cb3",
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(seed=7)\n",
    "days = np.arange(1, 31)\n",
    "# a gentle seasonal wave plus daily noise, just to have realistic-looking data\n",
    "temps = 18 + 4 * np.sin(np.linspace(0, np.pi, 30)) + rng.normal(0, 1.5, size=30)\n",
    "temps = np.round(temps, 1)\n",
    "\n",
    "plt.figure(figsize=(8, 3))\n",
    "plt.plot(days, temps, marker=\"o\")\n",
    "plt.xlabel(\"Day of month\")\n",
    "plt.ylabel(\"High temp (°C)\")\n",
    "plt.title(\"Synthetic daily high temperatures\")\n",
    "plt.show()\n",
    "\n",
    "print(temps)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d3b1311",
   "metadata": {},
   "source": [
    "### 💪 Exercise 5.1: Temperature report\n",
    "\n",
    "Write `temperature_report(temps, threshold)` that takes the 1-D array of\n",
    "daily temperatures and a `threshold` value, and returns a dict with:\n",
    "\n",
    "- `\"hottest_day\"`: the **1-based** day number (int) with the highest temp\n",
    "  (day 1 = `temps[0]`)\n",
    "- `\"coldest_day\"`: the 1-based day number (int) with the lowest temp\n",
    "- `\"mean\"`: the mean temperature (float)\n",
    "- `\"std\"`: the standard deviation (float)\n",
    "- `\"days_above_threshold\"`: how many days exceeded `threshold` (int)\n",
    "- `\"sorted_desc\"`: the temperatures sorted from hottest to coldest (array)\n",
    "\n",
    "Use `argmax`/`argmin`, `mean`/`std`, boolean indexing + `.sum()` (or\n",
    "`np.count_nonzero`), and `np.sort` — don't write a loop.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97b09178",
   "metadata": {},
   "outputs": [],
   "source": [
    "def temperature_report(temps, threshold):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ea1fef4",
   "metadata": {},
   "outputs": [],
   "source": [
    "temps_demo = np.array([10.0, 25.0, 5.0, 18.0, 20.0])\n",
    "result = temperature_report(temps_demo, threshold=19.0)\n",
    "print(\"temperature_report(temps_demo, threshold=19.0) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = {\n",
    "    \"hottest_day\": 2,\n",
    "    \"coldest_day\": 3,\n",
    "    \"mean\": float(temps_demo.mean()),\n",
    "    \"std\": float(temps_demo.std()),\n",
    "    \"days_above_threshold\": 2,\n",
    "    \"sorted_desc\": np.array([25.0, 20.0, 18.0, 10.0, 5.0]),\n",
    "}\n",
    "_passed = (\n",
    "    isinstance(result, dict)\n",
    "    and result.get(\"hottest_day\") == expected[\"hottest_day\"]\n",
    "    and result.get(\"coldest_day\") == expected[\"coldest_day\"]\n",
    "    and np.isclose(result.get(\"mean\", np.nan), expected[\"mean\"])\n",
    "    and np.isclose(result.get(\"std\", np.nan), expected[\"std\"])\n",
    "    and result.get(\"days_above_threshold\") == expected[\"days_above_threshold\"]\n",
    "    and np.array_equal(result.get(\"sorted_desc\"), expected[\"sorted_desc\"])\n",
    ")\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a38586e4",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`temps.argmax()` is a 0-based flat index — add 1 to get the day number. `(temps > threshold).sum()` counts how many exceed the threshold. `np.sort(temps)[::-1]` sorts hottest-first.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "670f706c",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def temperature_report(temps, threshold):\n",
    "    return {\n",
    "        \"hottest_day\": int(temps.argmax()) + 1,\n",
    "        \"coldest_day\": int(temps.argmin()) + 1,\n",
    "        \"mean\": float(temps.mean()),\n",
    "        \"std\": float(temps.std()),\n",
    "        \"days_above_threshold\": int((temps > threshold).sum()),\n",
    "        \"sorted_desc\": np.sort(temps)[::-1],\n",
    "    }\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71913000",
   "metadata": {},
   "source": [
    "## 🎯 Key Takeaways\n",
    "\n",
    "- `axis=0` reduces **down** columns (one result per column); `axis=1`\n",
    "  reduces **across** rows (one result per row).\n",
    "- `argmin`/`argmax` locate *where* an extreme value is; `np.unravel_index`\n",
    "  turns a flat index back into `(row, col)` coordinates.\n",
    "- `np.sort` returns a copy, `arr.sort()` sorts in place, and `np.argsort`\n",
    "  gives you an ordering you can apply to another array.\n",
    "- `np.unique(..., return_counts=True)` is your go-to for frequency\n",
    "  analysis without writing a loop.\n",
    "- Real analysis is usually a short **chain** of these tools, not a single\n",
    "  call.\n",
    "\n",
    "Next up: **Module 5 — Linear Algebra & Random**, where you'll work with\n",
    "vectors, matrices, and NumPy's modern random number generator.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
