{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2d6971a4",
   "metadata": {},
   "source": [
    "# Module 2 — Indexing, Slicing & Views\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a4a7e1d",
   "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_2_indexing_views.ipynb)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4b3387db",
   "metadata": {},
   "source": [
    "## Welcome\n",
    "\n",
    "Indexing is how you *select* data out of an array, and it's where NumPy has\n",
    "the most surprises for people coming from Python lists — especially around\n",
    "whether what you get back is a **view** (shares memory with the original)\n",
    "or a **copy** (independent). Getting this wrong is one of the most common\n",
    "sources of silent bugs in real NumPy code, so this module slows down and\n",
    "builds careful intuition before moving on.\n",
    "\n",
    "**Learning goals**\n",
    "- Index and slice 1-D and multi-D arrays, including negative indices and steps\n",
    "- Build and combine boolean masks to filter data\n",
    "- Use fancy (integer-array) indexing\n",
    "- Know exactly when an operation returns a view vs. a copy — and why it matters\n",
    "- Use `np.newaxis` and `...` (Ellipsis) to index higher-dimensional arrays\n",
    "\n",
    "**Setup** — run this first.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8c3ea26",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "print(np.__version__)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6dbbfb39",
   "metadata": {},
   "source": [
    "## Lesson 1 — Basic Indexing & Slicing\n",
    "\n",
    "Array indexing extends Python's `list[start:stop:step]` syntax to any\n",
    "number of dimensions, with one index/slice per axis, separated by commas:\n",
    "`arr[row_slice, col_slice]`. Negative indices count from the end, and a\n",
    "negative step reverses direction.\n",
    "\n",
    "| Expression | Meaning |\n",
    "|---|---|\n",
    "| `arr[2]` | element/row at index 2 |\n",
    "| `arr[-1]` | last element/row |\n",
    "| `arr[1:4]` | indices 1, 2, 3 |\n",
    "| `arr[::2]` | every other element |\n",
    "| `arr[::-1]` | reversed |\n",
    "| `arr[1:3, 0:2]` | rows 1-2, columns 0-1 |\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fde9b47e",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.arange(10)\n",
    "print(arr[2], arr[-1], arr[1:4], arr[::2], arr[::-1])\n",
    "\n",
    "grid = np.arange(16).reshape(4, 4)\n",
    "print(grid)\n",
    "print(grid[1:3, 0:2])   # rows 1-2, cols 0-1\n",
    "print(grid[:, 2])       # every row, column 2\n",
    "print(grid[-1, :])      # last row\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b96006d1",
   "metadata": {},
   "source": [
    "### 💪 Exercise 1.1: Border of a grid\n",
    "\n",
    "Write `border(grid)` that takes a 2-D array and returns its last row\n",
    "reversed — i.e. `grid[-1][::-1]`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04028cbb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def border(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "caa64e16",
   "metadata": {},
   "outputs": [],
   "source": [
    "g = np.arange(9).reshape(3, 3)\n",
    "result = border(g)\n",
    "print(\"border(np.arange(9).reshape(3, 3)) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([8, 7, 6])\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": "fee85d1d",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Index the last row with `grid[-1]`, then reverse it with `[::-1]`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae3e086c",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def border(grid):\n",
    "    return grid[-1][::-1]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbb7f990",
   "metadata": {},
   "source": [
    "### 💪 Exercise 1.2: Every other row and column\n",
    "\n",
    "Write `downsample(grid)` that returns every other row AND every other\n",
    "column of a 2-D array, starting from index 0 (i.e. `grid[0::2, 0::2]`).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0dcf2e6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def downsample(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81e5d1d0",
   "metadata": {},
   "outputs": [],
   "source": [
    "g = np.arange(16).reshape(4, 4)\n",
    "result = downsample(g)\n",
    "print(\"downsample(np.arange(16).reshape(4, 4)) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([[0, 2], [8, 10]])\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": "cf2db579",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Apply a step-2 slice to both axes at once: `grid[0::2, 0::2]`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aaac277e",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def downsample(grid):\n",
    "    return grid[0::2, 0::2]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75abdf6a",
   "metadata": {},
   "source": [
    "## Lesson 2 — Boolean Indexing\n",
    "\n",
    "Comparing an array to a value produces a **boolean mask** — an array of\n",
    "`True`/`False` the same shape as the original. Indexing with that mask\n",
    "returns only the elements where it's `True`. To combine conditions, you\n",
    "must use the bitwise operators `&`, `|`, `~` (NOT Python's `and`/`or`/`not`\n",
    "— those don't work elementwise and will raise an error on arrays with more\n",
    "than one element). `np.where(condition, a, b)` is the vectorized\n",
    "if/else: pick from `a` where `condition` is True, else from `b`.\n",
    "\n",
    "Two more things come up constantly once you're working with masks. First,\n",
    "`True`/`False` behave as `1`/`0` in arithmetic, so `mask.sum()` counts how\n",
    "many elements are `True` — a quick way to count matches without reaching\n",
    "for the full aggregation toolkit (Module 4 covers `.sum()`, `.mean()`, and\n",
    "friends in depth). Second, indexing itself never mutates anything, but if\n",
    "you're about to change values and want to leave the original untouched,\n",
    "call `.copy()` first to get an independent array — Lesson 4 covers views\n",
    "vs. copies in full.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8374d65a",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.array([3, -1, 7, -5, 2, 0, 9])\n",
    "mask = arr > 0\n",
    "print(mask)\n",
    "print(arr[mask])                      # positive values only\n",
    "print(arr[(arr > 0) & (arr < 8)])     # combine with & , not \"and\"\n",
    "\n",
    "clipped = np.where(arr < 0, 0, arr)   # replace negatives with 0\n",
    "print(clipped)\n",
    "\n",
    "arr[arr < 0] = 0                      # assign through a mask, in place\n",
    "print(arr)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55f0f680",
   "metadata": {},
   "source": [
    "### 💪 Exercise 2.1: Clip to a range\n",
    "\n",
    "Write `clip_range(arr, low, high)` that returns a **new** array where every\n",
    "value below `low` is replaced by `low`, and every value above `high` is\n",
    "replaced by `high` (values already in range are unchanged). Do not use\n",
    "`np.clip` — build it from boolean masks and `np.where` (or boolean\n",
    "assignment on a copy) to practice the mechanics.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b1d0679",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clip_range(arr, low, high):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7bc8bbfd",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = clip_range(np.array([-5, 0, 3, 10, 15]), 0, 10)\n",
    "print(\"clip_range(np.array([-5, 0, 3, 10, 15]), 0, 10) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([0, 0, 3, 10, 10])\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": "a5102de2",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Make a copy first (`out = arr.copy()`), then assign through two masks: `out[out < low] = low` and `out[out > high] = high`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b10e128",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def clip_range(arr, low, high):\n",
    "    out = arr.copy()\n",
    "    out[out < low] = low\n",
    "    out[out > high] = high\n",
    "    return out\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c19e5ab8",
   "metadata": {},
   "source": [
    "### 💪 Exercise 2.2: Count values in a band\n",
    "\n",
    "Write `count_in_band(arr, low, high)` that returns how many elements of\n",
    "`arr` satisfy `low <= x <= high`, as a plain Python `int`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65330eb5",
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_in_band(arr, low, high):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6497ba5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = count_in_band(np.array([1, 5, 8, 12, 20]), 5, 12)\n",
    "print(\"count_in_band(np.array([1, 5, 8, 12, 20]), 5, 12) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = 3\n",
    "_passed = result == expected and isinstance(result, int)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\", expected, \"(as a plain Python int)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f401f0b",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Combine two comparisons with `&`, then `.sum()` the boolean mask — `True` counts as 1. Wrap the result in `int(...)`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6eee6f16",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def count_in_band(arr, low, high):\n",
    "    mask = (arr >= low) & (arr <= high)\n",
    "    return int(mask.sum())\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41aa5062",
   "metadata": {},
   "source": [
    "## Lesson 3 — Fancy Indexing\n",
    "\n",
    "\"Fancy indexing\" means indexing with an array (or list) of integers instead\n",
    "of a single index or a slice. It lets you pick out an arbitrary, possibly\n",
    "repeated, possibly reordered set of elements/rows/columns in one shot. You\n",
    "can combine a fancy index on one axis with a slice on another. Unlike basic\n",
    "slicing, **fancy indexing always returns a copy**, never a view.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3bd40e1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.array([10, 20, 30, 40, 50])\n",
    "print(arr[[0, 2, 4]])        # pick specific elements\n",
    "print(arr[[3, 3, 1]])        # repeats allowed, any order\n",
    "\n",
    "grid = np.arange(12).reshape(4, 3)\n",
    "print(grid[[0, 2]])          # rows 0 and 2\n",
    "print(grid[[0, 2], :2])      # rows 0 and 2, first two columns\n",
    "print(grid[[0, 1, 2], [0, 1, 2]])  # (0,0), (1,1), (2,2) -- paired coordinates!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80ad6d2e",
   "metadata": {},
   "source": [
    "### 💪 Exercise 3.1: Reorder rows\n",
    "\n",
    "Write `reorder_rows(grid, order)` that takes a 2-D array and a list of row\n",
    "indices `order`, and returns a new array with the rows rearranged into that\n",
    "order (rows may repeat or be dropped).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "939f9c08",
   "metadata": {},
   "outputs": [],
   "source": [
    "def reorder_rows(grid, order):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "845919cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "g = np.arange(9).reshape(3, 3)\n",
    "result = reorder_rows(g, [2, 0, 0])\n",
    "print(\"reorder_rows(np.arange(9).reshape(3, 3), [2, 0, 0]) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([[6, 7, 8], [0, 1, 2], [0, 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": "aafc54e3",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Fancy-index the row axis directly: `grid[order]`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db858fc9",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def reorder_rows(grid, order):\n",
    "    return grid[order]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48d45f01",
   "metadata": {},
   "source": [
    "### 💪 Exercise 3.2: Diagonal via coordinates\n",
    "\n",
    "Write `pick_diagonal(grid)` that returns the main diagonal of a square 2-D\n",
    "array using **paired fancy indexing** (`grid[rows, cols]`), not\n",
    "`np.diagonal` or `np.diag`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b18331b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def pick_diagonal(grid):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ba9d3ed5",
   "metadata": {},
   "outputs": [],
   "source": [
    "g = np.arange(9).reshape(3, 3)\n",
    "result = pick_diagonal(g)\n",
    "print(\"pick_diagonal(np.arange(9).reshape(3, 3)) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([0, 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": "098cc2f6",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Build row and column index arrays with `np.arange(len(grid))` and use them both: `grid[idx, idx]`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3404845",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def pick_diagonal(grid):\n",
    "    idx = np.arange(len(grid))\n",
    "    return grid[idx, idx]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7db04a4f",
   "metadata": {},
   "source": [
    "## Lesson 4 — Views vs. Copies\n",
    "\n",
    "This is the single most important mental model in this module:\n",
    "\n",
    "| Operation | View or copy? |\n",
    "|---|---|\n",
    "| Basic slicing (`arr[1:4]`, `grid[:, 0]`) | **View** |\n",
    "| Boolean indexing (`arr[mask]`) | **Copy** |\n",
    "| Fancy indexing (`arr[[0, 2]]`) | **Copy** |\n",
    "| `.reshape()` (when possible) | **View** |\n",
    "| `.ravel()` | View when possible, else copy |\n",
    "| `.flatten()` | Always a **copy** |\n",
    "\n",
    "A view shares the same underlying memory as the array it came from — `arr2\n",
    "= arr[1:4]` doesn't copy any data, it just creates a new array object that\n",
    "*looks at* part of `arr`'s buffer. **Mutating a view mutates the original.**\n",
    "Check `arr2.base is arr` (or `arr2.base is not None`) to tell if something\n",
    "is a view — though `.base` chains can get indirect (a view of a view), so\n",
    "`np.shares_memory(a, b)` is the more robust way to ask \"do these two arrays\n",
    "point at the same underlying memory?\" directly, without worrying about how\n",
    "many `.base` hops apart they are. Call `.copy()` whenever you need a truly\n",
    "independent array.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d2de776e",
   "metadata": {},
   "outputs": [],
   "source": [
    "original = np.array([1, 2, 3, 4, 5])\n",
    "view = original[1:4]\n",
    "print(\"view.base is original:\", view.base is original)\n",
    "\n",
    "view[0] = 999                 # mutating the VIEW...\n",
    "print(\"original:\", original)  # ...mutates the ORIGINAL too!\n",
    "\n",
    "safe_copy = original[1:4].copy()\n",
    "safe_copy[0] = -1\n",
    "print(\"original after mutating the copy:\", original)   # unaffected\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "469c56c1",
   "metadata": {},
   "source": [
    "### 💪 Exercise 4.1: A function that must not mutate its input\n",
    "\n",
    "Write `double_first_n(arr, n)` that returns a **new** array equal to `arr`\n",
    "but with its first `n` elements doubled. Calling this function must leave\n",
    "the original `arr` completely unchanged — a common bug is slicing (which\n",
    "gives a view) and then mutating in place.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "efbc2e8e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def double_first_n(arr, n):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "94130c60",
   "metadata": {},
   "outputs": [],
   "source": [
    "original = np.array([1, 2, 3, 4, 5])\n",
    "result = double_first_n(original, 3)\n",
    "print(\"double_first_n(np.array([1, 2, 3, 4, 5]), 3) ->\")\n",
    "print(result)\n",
    "print()\n",
    "print(\"original array after the call ->\", original)\n",
    "\n",
    "expected = np.array([2, 4, 6, 4, 5])\n",
    "original_unchanged = np.array_equal(original, np.array([1, 2, 3, 4, 5]))\n",
    "_passed = np.array_equal(result, expected) and original_unchanged\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected result:\", expected, \"- and the original should still read [1 2 3 4 5]\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c6c4445",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Start from `arr.copy()`, not from `arr` directly or a slice of it — then mutate the copy freely.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a989118d",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def double_first_n(arr, n):\n",
    "    out = arr.copy()\n",
    "    out[:n] *= 2\n",
    "    return out\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8868d856",
   "metadata": {},
   "source": [
    "### 💪 Exercise 4.2: Is it a view?\n",
    "\n",
    "Write `is_view_of(sub, original)` that returns `True` if `sub` is a view\n",
    "into `original`'s memory (i.e. shares its buffer), and `False` if `sub` is\n",
    "an independent copy. Hint: this should work for both the array returned by\n",
    "slicing AND the array itself (`original.base is None`, but it IS the data\n",
    "— treat that case as \"not a copy\", i.e. return `True` only when `sub` is\n",
    "genuinely backed by `original`'s memory).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9397a7b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_view_of(sub, original):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "658513bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "original = np.arange(10)\n",
    "view = original[2:5]\n",
    "copy = original[2:5].copy()\n",
    "fancy = original[[2, 3, 4]]\n",
    "\n",
    "r_view = is_view_of(view, original)\n",
    "r_copy = is_view_of(copy, original)\n",
    "r_fancy = is_view_of(fancy, original)\n",
    "print(\"is_view_of(original[2:5], original) ->\", r_view)\n",
    "print(\"is_view_of(original[2:5].copy(), original) ->\", r_copy)\n",
    "print(\"is_view_of(original[[2, 3, 4]], original) ->\", r_fancy)\n",
    "\n",
    "_passed = (r_view == True) and (r_copy == False) and (r_fancy == False)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected: True, False, False\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f8ec55d",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`np.shares_memory(sub, original)` answers exactly this question and handles all the edge cases correctly — no need to reinvent it with `.base`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4785b44d",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def is_view_of(sub, original):\n",
    "    return bool(np.shares_memory(sub, original))\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1343448b",
   "metadata": {},
   "source": [
    "## Lesson 5 — `np.newaxis` and `...` (Ellipsis)\n",
    "\n",
    "`np.newaxis` (an alias for `None`) inserts a new length-1 axis at the\n",
    "position you place it — the standard way to turn a 1-D array into an\n",
    "explicit row or column vector, which matters a lot once you start\n",
    "broadcasting (Module 3). `...` (Ellipsis) means \"as many full slices `:` as\n",
    "needed to cover the remaining axes\" — handy for indexing deep into a\n",
    "high-dimensional array without writing a `:` for every axis in between.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84afa491",
   "metadata": {},
   "outputs": [],
   "source": [
    "v = np.array([1, 2, 3])\n",
    "print(v.shape)                       # (3,)\n",
    "print(v[:, np.newaxis].shape)        # (3, 1) -- column vector\n",
    "print(v[np.newaxis, :].shape)        # (1, 3) -- row vector\n",
    "\n",
    "cube = np.arange(24).reshape(2, 3, 4)\n",
    "print(cube[..., 0].shape)            # same as cube[:, :, 0] -> (2, 3)\n",
    "print(cube[0, ...].shape)            # same as cube[0, :, :] -> (3, 4)\n",
    "\n",
    "grid = np.zeros((3, 3))\n",
    "grid[1, :] = [7, 8, 9]               # assign into a slice\n",
    "print(grid)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c258bfd1",
   "metadata": {},
   "source": [
    "### 💪 Exercise 5.1: To column vector\n",
    "\n",
    "Write `to_column(v)` that takes a 1-D array of shape `(n,)` and returns it\n",
    "reshaped to a column vector of shape `(n, 1)` using `np.newaxis` (not\n",
    "`.reshape`).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39219aef",
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_column(v):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9053057",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = to_column(np.array([1, 2, 3]))\n",
    "print(\"to_column(np.array([1, 2, 3])) ->\")\n",
    "print(result)\n",
    "print(\"shape:\", result.shape)\n",
    "\n",
    "_passed = result.shape == (3, 1) and np.array_equal(result.ravel(), np.array([1, 2, 3]))\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected shape (3, 1) containing [1, 2, 3]\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21a614ee",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`v[:, np.newaxis]` adds a new axis at position 1.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0a11218",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def to_column(v):\n",
    "    return v[:, np.newaxis]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6af55fe",
   "metadata": {},
   "source": [
    "### 💪 Exercise 5.2: First channel of any stack of images\n",
    "\n",
    "Write `first_channel(images)` that takes an array of shape\n",
    "`(n_images, height, width, n_channels)` (or any number of leading axes\n",
    "followed by a channel axis last) and returns just channel 0 for every\n",
    "image, using `...` so your function works regardless of how many\n",
    "leading axes there are.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "264191c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def first_channel(images):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c38f746",
   "metadata": {},
   "outputs": [],
   "source": [
    "imgs = np.arange(2 * 4 * 4 * 3).reshape(2, 4, 4, 3)\n",
    "result = first_channel(imgs)\n",
    "print(\"first_channel(imgs) with imgs.shape ==\", imgs.shape, \"->\")\n",
    "print(result)\n",
    "\n",
    "expected = imgs[:, :, :, 0]\n",
    "_passed = result.shape == (2, 4, 4) and np.array_equal(result, expected)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b40d9924",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`images[..., 0]` takes channel 0 regardless of how many axes come before it.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40f96bb5",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def first_channel(images):\n",
    "    return images[..., 0]\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37d04fb3",
   "metadata": {},
   "source": [
    "## 🎯 Key Takeaways\n",
    "\n",
    "- Index one axis at a time with `arr[rows, cols]`; negative indices and\n",
    "  step slices (`::2`, `::-1`) work on every axis.\n",
    "- Boolean masks (`arr > 0`) filter data; combine conditions with `&`/`|`/`~`,\n",
    "  never Python's `and`/`or`/`not`. `np.where` is the vectorized if/else.\n",
    "- Fancy indexing (`arr[[0, 2, 4]]`) lets you pick, repeat, and reorder\n",
    "  elements arbitrarily — and always returns a copy.\n",
    "- **Basic slicing returns a view; boolean and fancy indexing return a\n",
    "  copy.** Mutating a view mutates the original array. Use `.copy()` when\n",
    "  you need independence, and `np.shares_memory` to check.\n",
    "- `np.newaxis` inserts a length-1 axis (crucial for broadcasting); `...`\n",
    "  stands in for \"all the remaining axes.\"\n",
    "\n",
    "Next up: **Module 3 — Array Operations & Broadcasting**, where the shapes\n",
    "you've been building here start combining with each other automatically.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
