{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cd533a4f",
   "metadata": {},
   "source": [
    "# Module 1 — NumPy Fundamentals & Array Creation\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8b4de70",
   "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_1_fundamentals.ipynb)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb89d889",
   "metadata": {},
   "source": [
    "## Welcome\n",
    "\n",
    "This module lays the foundation for the whole course: what an `ndarray`\n",
    "actually is, how to create one in every common way, how to inspect it, and\n",
    "how to reshape and combine arrays. Every later module builds on these\n",
    "mechanics, so take your time with the exercises here.\n",
    "\n",
    "**Learning goals**\n",
    "- Explain why NumPy arrays outperform plain Python lists for numeric work\n",
    "- Create arrays with `np.array`, `np.zeros`/`ones`/`full`/`eye`, `np.arange`, `np.linspace`\n",
    "- Read an array's `shape`, `ndim`, `size`, `dtype`, `itemsize`\n",
    "- Cast between dtypes with `.astype`\n",
    "- Reshape, flatten, and transpose arrays\n",
    "- Combine and split arrays along an axis\n",
    "\n",
    "**Setup** — run this first.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "54665b13",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "print(np.__version__)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f34c18b",
   "metadata": {},
   "source": [
    "## Lesson 1 — Why NumPy? `ndarray` vs. Python `list`\n",
    "\n",
    "A Python `list` is a flexible, general-purpose container: each element is a\n",
    "separate Python object, and the list itself is just an array of pointers to\n",
    "those objects. That flexibility has a cost — looping over a list in pure\n",
    "Python re-enters the interpreter for every single element.\n",
    "\n",
    "A NumPy `ndarray` is a fixed-type, contiguous block of memory. Because every\n",
    "element has the same dtype and a known size, NumPy can hand the whole loop\n",
    "off to compiled C code — this is called **vectorization**, and it's the\n",
    "single most important idea in this course.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "099d02d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Same logical operation, two very different implementations.\n",
    "a = [1, 2, 3, 4, 5]\n",
    "b = [10, 20, 30, 40, 50]\n",
    "\n",
    "# Python list: no \"+\" between lists means elementwise addition — you need a loop.\n",
    "elementwise_sum_list = [x + y for x, y in zip(a, b)]\n",
    "print(\"list comprehension:\", elementwise_sum_list)\n",
    "\n",
    "# NumPy array: \"+\" IS elementwise addition, done in compiled code.\n",
    "arr_a = np.array(a)\n",
    "arr_b = np.array(b)\n",
    "print(\"vectorized:        \", arr_a + arr_b)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83f2b0d5",
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "big_a = list(range(2_000_000))\n",
    "big_b = list(range(2_000_000))\n",
    "\n",
    "start = time.perf_counter()\n",
    "_ = [x + y for x, y in zip(big_a, big_b)]\n",
    "print(f\"Python loop:   {time.perf_counter() - start:.4f}s\")\n",
    "\n",
    "np_a = np.array(big_a)\n",
    "np_b = np.array(big_b)\n",
    "\n",
    "start = time.perf_counter()\n",
    "_ = np_a + np_b\n",
    "print(f\"NumPy vectorized: {time.perf_counter() - start:.4f}s\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7bf9d856",
   "metadata": {},
   "source": [
    "One more comparison you'll use constantly — including in this course's own\n",
    "\"check\" cells, starting with the very next exercise: `==` between two arrays\n",
    "compares **elementwise** and gives you back an array of booleans, not a\n",
    "single answer. When you want one `True`/`False` for \"are these two arrays\n",
    "the same?\", use `np.array_equal(a, b)` instead — it checks that both arrays\n",
    "have the same shape and that every element matches.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26423392",
   "metadata": {},
   "outputs": [],
   "source": [
    "a = np.array([1, 2, 3])\n",
    "b = np.array([1, 2, 3])\n",
    "\n",
    "print(a == b)                  # elementwise: array([ True,  True,  True])\n",
    "print(np.array_equal(a, b))    # one answer: True\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a1a726e",
   "metadata": {},
   "source": [
    "### 💪 Exercise 1.1: No loops allowed\n",
    "\n",
    "Write `elementwise_product(a, b)` that takes two same-length Python `list`s\n",
    "of numbers and returns a NumPy array containing their elementwise product.\n",
    "You may **not** use a `for` loop, `while` loop, or list/generator\n",
    "comprehension anywhere in your implementation — convert to arrays and let\n",
    "NumPy do the work.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d671b12e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def elementwise_product(a, b):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebac0693",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = elementwise_product([1, 2, 3], [4, 5, 6])\n",
    "print(\"elementwise_product([1, 2, 3], [4, 5, 6]) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([4, 10, 18])\n",
    "_passed = (\n",
    "    isinstance(result, np.ndarray)\n",
    "    and np.array_equal(result, expected)\n",
    "    and np.array_equal(elementwise_product([0, -1], [5, 5]), np.array([0, -5]))\n",
    ")\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7850297",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Convert both lists to arrays with `np.array(...)`, then use the `*` operator.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9f8af1c",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def elementwise_product(a, b):\n",
    "    return np.array(a) * np.array(b)\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "274dbf4f",
   "metadata": {},
   "source": [
    "## Lesson 2 — Creating Arrays\n",
    "\n",
    "There are three common families of array-creation functions:\n",
    "\n",
    "| Family | Functions | Use case |\n",
    "|---|---|---|\n",
    "| From existing data | `np.array`, `np.asarray` | You already have a list/tuple of values |\n",
    "| Filled with a constant | `np.zeros`, `np.ones`, `np.full`, `np.eye` | You need a placeholder of a given shape |\n",
    "| Evenly spaced ranges | `np.arange`, `np.linspace` | You need a sequence of numbers |\n",
    "\n",
    "`np.arange(start, stop, step)` works like Python's `range` but returns an\n",
    "array and accepts floats. `np.linspace(start, stop, num)` instead fixes the\n",
    "**count** of points (inclusive of both ends by default), which is usually\n",
    "what you want for plotting.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "979a1e3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(np.zeros((2, 3)))\n",
    "print()\n",
    "print(np.full((2, 2), fill_value=7))\n",
    "print()\n",
    "print(np.eye(3))\n",
    "print()\n",
    "print(np.arange(0, 10, 2))       # stop is exclusive\n",
    "print(np.linspace(0, 1, 5))      # stop is inclusive\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae1029af",
   "metadata": {},
   "source": [
    "A quick preview before the next exercise — Module 2 covers indexing and\n",
    "slicing in full depth, but you'll need a taste of it now. Slicing an array\n",
    "works just like slicing a Python list — `arr[start:stop:step]` — except it\n",
    "extends naturally to multiple axes by separating them with a comma. So\n",
    "`arr[::2, ::2]` means \"every other row, every other column\": `::2` alone\n",
    "means \"the whole axis, stepping by 2.\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19f0c3c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = np.arange(16).reshape(4, 4)\n",
    "print(grid)\n",
    "print()\n",
    "print(grid[::2, ::2])   # every other row, every other column\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13aa1920",
   "metadata": {},
   "source": [
    "### 💪 Exercise 2.1: Build a checkerboard\n",
    "\n",
    "Write `checkerboard(n)` that returns an `(n, n)` array of `int` dtype\n",
    "containing a checkerboard pattern of `0`s and `1`s, where position `(0, 0)`\n",
    "is `1`. For `n=4` the result should be:\n",
    "\n",
    "```\n",
    "[[1 0 1 0]\n",
    " [0 1 0 1]\n",
    " [1 0 1 0]\n",
    " [0 1 0 1]]\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebc60f81",
   "metadata": {},
   "outputs": [],
   "source": [
    "def checkerboard(n):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "850a2696",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = checkerboard(4)\n",
    "print(\"checkerboard(4) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([[1, 0, 1, 0],\n",
    "                      [0, 1, 0, 1],\n",
    "                      [1, 0, 1, 0],\n",
    "                      [0, 1, 0, 1]])\n",
    "_passed = np.array_equal(result, expected) and checkerboard(2).tolist() == [[1, 0], [0, 1]]\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17338ed8",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Build an `(n, n)` array of zeros with `np.zeros`, then use slicing with a step of 2 on rows and columns — `board[0::2, 0::2]` and `board[1::2, 1::2]` — to set the right cells to 1.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ecb11fd3",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def checkerboard(n):\n",
    "    board = np.zeros((n, n), dtype=int)\n",
    "    board[0::2, 0::2] = 1\n",
    "    board[1::2, 1::2] = 1\n",
    "    return board\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "83ce20f1",
   "metadata": {},
   "source": [
    "## Lesson 3 — Array Attributes & Dtypes\n",
    "\n",
    "Every array carries metadata describing its layout:\n",
    "\n",
    "- `.shape` — a tuple with the size along each axis\n",
    "- `.ndim` — number of axes (`len(shape)`)\n",
    "- `.size` — total number of elements\n",
    "- `.dtype` — the element type (`int64`, `float64`, `bool`, ...)\n",
    "- `.itemsize` — bytes per element\n",
    "\n",
    "NumPy infers a dtype from your data, but mixing an `int` with a `float`\n",
    "upcasts the whole array to `float64` — there is no per-element typing.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2bbcebe7",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.array([[1, 2, 3], [4, 5, 6]])\n",
    "print(\"shape:   \", arr.shape)\n",
    "print(\"ndim:    \", arr.ndim)\n",
    "print(\"size:    \", arr.size)\n",
    "print(\"dtype:   \", arr.dtype)\n",
    "print(\"itemsize:\", arr.itemsize, \"bytes\")\n",
    "\n",
    "mixed = np.array([1, 2, 3.5])   # one float -> whole array becomes float64\n",
    "print(mixed.dtype)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e5823a5",
   "metadata": {},
   "source": [
    "### 💪 Exercise 3.1: Describe an array\n",
    "\n",
    "Write `describe(arr)` that returns a dict with keys `\"shape\"`, `\"ndim\"`,\n",
    "`\"size\"`, and `\"dtype_name\"` (the dtype as a string, e.g. `\"int64\"`) for the\n",
    "given array.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "093eb1a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def describe(arr):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6fa346f1",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = describe(np.zeros((3, 4)))\n",
    "print(\"describe(np.zeros((3, 4))) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = {\"shape\": (3, 4), \"ndim\": 2, \"size\": 12, \"dtype_name\": \"float64\"}\n",
    "_passed = (\n",
    "    result.get(\"shape\") == expected[\"shape\"]\n",
    "    and result.get(\"ndim\") == expected[\"ndim\"]\n",
    "    and result.get(\"size\") == expected[\"size\"]\n",
    "    and \"float\" in str(result.get(\"dtype_name\"))\n",
    ")\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7a9cfde",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`str(arr.dtype)` gives you the dtype name as a string.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d0e005a",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def describe(arr):\n",
    "    return {\n",
    "        \"shape\": arr.shape,\n",
    "        \"ndim\": arr.ndim,\n",
    "        \"size\": arr.size,\n",
    "        \"dtype_name\": str(arr.dtype),\n",
    "    }\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5f145be",
   "metadata": {},
   "source": [
    "One more pair of tools before the next exercise. `np.floor(x)` rounds each\n",
    "element down to the nearest integer (elementwise) — `np.ceil` and\n",
    "`np.round` work the same way for rounding up / to the nearest. And\n",
    "`np.all(bool_array)` collapses a whole boolean array down to a single\n",
    "`True`/`False`: `True` only if **every** element is `True`. `np.any` is the\n",
    "mirror image — `True` if **at least one** element is `True`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "71eb9f8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(np.floor(np.array([1.2, 2.9, -0.5])))     # [ 1.  2. -1.]\n",
    "print(np.all(np.array([True, True, False])))     # False — not all True\n",
    "print(np.any(np.array([True, True, False])))     # True — at least one True\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c75fb84",
   "metadata": {},
   "source": [
    "### 💪 Exercise 3.2: Safe integer casting\n",
    "\n",
    "Write `to_int_safe(arr)` that casts a float array to `int64` using\n",
    "`.astype`, but raises a `ValueError` (with any message) if doing so would\n",
    "silently truncate a non-integer value (e.g. casting `2.5` to `2`).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8734174e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_int_safe(arr):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9fa59269",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = to_int_safe(np.array([1.0, 2.0, 3.0]))\n",
    "print(\"to_int_safe(np.array([1.0, 2.0, 3.0])) ->\")\n",
    "print(result, \"dtype:\", result.dtype)\n",
    "\n",
    "ok_basic = result.dtype == np.int64 and np.array_equal(result, [1, 2, 3])\n",
    "\n",
    "try:\n",
    "    to_int_safe(np.array([1.0, 2.5]))\n",
    "    raised_value_error = False\n",
    "except ValueError:\n",
    "    raised_value_error = True\n",
    "except Exception:\n",
    "    raised_value_error = False\n",
    "\n",
    "print()\n",
    "print(\"to_int_safe(np.array([1.0, 2.5])) ->\",\n",
    "      \"raised ValueError\" if raised_value_error else \"did NOT raise ValueError\")\n",
    "\n",
    "_passed = ok_basic and raised_value_error\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected: [1 2 3] with dtype int64, and a ValueError for [1.0, 2.5]\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5ae845c",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "Compare `arr` to `np.floor(arr)` elementwise with `==`, then wrap that in `np.all(...)` to get a single True/False before deciding whether to cast.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b323f0de",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def to_int_safe(arr):\n",
    "    if not np.all(arr == np.floor(arr)):\n",
    "        raise ValueError(\"array contains non-integer values\")\n",
    "    return arr.astype(np.int64)\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bafc96b1",
   "metadata": {},
   "source": [
    "## Lesson 4 — Reshaping & Dimensions\n",
    "\n",
    "`.reshape(new_shape)` returns a view with the same data laid out under a\n",
    "different shape (total element count must match — use `-1` to let NumPy\n",
    "infer one dimension). `.ravel()` flattens to 1-D (a view when possible),\n",
    "`.flatten()` always returns a copy. `.T` (or `np.transpose`) swaps axes, and\n",
    "`np.newaxis` inserts a new length-1 axis, which is the standard way to turn\n",
    "a 1-D array into a row or column vector.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9550bbc",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.arange(12)\n",
    "print(arr.reshape(3, 4))\n",
    "print(arr.reshape(3, -1).shape)   # -1 means \"figure this dimension out\"\n",
    "\n",
    "col = arr[:4][:, np.newaxis]      # turn a 1-D array into a column vector\n",
    "print(col.shape)\n",
    "print(arr.reshape(3, 4).T)        # transpose\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7534f4fa",
   "metadata": {},
   "source": [
    "### 💪 Exercise 4.1: Flatten and pair up\n",
    "\n",
    "Write `to_pairs(arr)` that takes an array of any shape with an even number\n",
    "of elements and returns it reshaped into shape `(-1, 2)` — i.e. a sequence\n",
    "of `[a, b]` pairs, read in row-major (C) order.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e2601772",
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_pairs(arr):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb307b37",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = to_pairs(np.array([[1, 2, 3], [4, 5, 6]]))\n",
    "print(\"to_pairs(np.array([[1, 2, 3], [4, 5, 6]])) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([[1, 2], [3, 4], [5, 6]])\n",
    "_passed = np.array_equal(result, expected) and to_pairs(np.arange(4)).shape == (2, 2)\n",
    "\n",
    "if not _passed:\n",
    "    print()\n",
    "    print(\"Expected:\")\n",
    "    print(expected)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f784f49",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`.ravel()` first to get a flat 1-D sequence, then `.reshape(-1, 2)`.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76194440",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def to_pairs(arr):\n",
    "    return arr.ravel().reshape(-1, 2)\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5e4e18a",
   "metadata": {},
   "source": [
    "## Lesson 5 — Combining & Splitting Arrays\n",
    "\n",
    "- `np.concatenate([a, b], axis=...)` joins arrays along an existing axis\n",
    "  (shapes must match on every other axis).\n",
    "- `np.vstack` / `np.hstack` are convenience wrappers for concatenating along\n",
    "  axis 0 / axis 1.\n",
    "- `np.stack([a, b], axis=...)` joins arrays along a **new** axis instead —\n",
    "  use this when you want to go from several `(n,)` arrays to one `(2, n)`\n",
    "  or `(n, 2)` array.\n",
    "- `np.split(arr, n, axis=...)` divides an array into `n` equal pieces along\n",
    "  an axis.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a09029e",
   "metadata": {},
   "outputs": [],
   "source": [
    "a = np.array([1, 2, 3])\n",
    "b = np.array([4, 5, 6])\n",
    "\n",
    "print(np.concatenate([a, b]))          # (6,)\n",
    "print(np.stack([a, b]))                # (2, 3) — new axis 0\n",
    "print(np.stack([a, b], axis=1))        # (3, 2) — new axis 1\n",
    "\n",
    "m = np.arange(8).reshape(2, 4)\n",
    "left, right = np.split(m, 2, axis=1)\n",
    "print(left)\n",
    "print(right)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58071f9d",
   "metadata": {},
   "source": [
    "### 💪 Exercise 5.1: Interleave two signals\n",
    "\n",
    "Write `interleave(a, b)` that takes two 1-D arrays of the same length `n`\n",
    "and returns a single 1-D array of length `2n` where values alternate:\n",
    "`[a[0], b[0], a[1], b[1], ...]`. Do this using `np.stack` and `reshape` —\n",
    "no Python loop.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f38cae82",
   "metadata": {},
   "outputs": [],
   "source": [
    "def interleave(a, b):\n",
    "    # YOUR CODE HERE\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "adfd4077",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = interleave(np.array([1, 3, 5]), np.array([2, 4, 6]))\n",
    "print(\"interleave(np.array([1, 3, 5]), np.array([2, 4, 6])) ->\")\n",
    "print(result)\n",
    "\n",
    "expected = np.array([1, 2, 3, 4, 5, 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": "d1514f8c",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>💡 Hint (click to expand)</summary>\n",
    "\n",
    "`np.stack([a, b], axis=1)` gives you an `(n, 2)` array of pairs — flatten that.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2621df24",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>✅ Solution (click to expand)</summary>\n",
    "\n",
    "```python\n",
    "def interleave(a, b):\n",
    "    return np.stack([a, b], axis=1).ravel()\n",
    "```\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1482399",
   "metadata": {},
   "source": [
    "## 🎯 Key Takeaways\n",
    "\n",
    "- NumPy arrays beat Python lists for numeric work because they're\n",
    "  contiguous, fixed-type, and support **vectorized** operations in compiled\n",
    "  code.\n",
    "- Pick the right creation function for the job: `np.array` from existing\n",
    "  data, `np.zeros`/`ones`/`full`/`eye` for placeholders, `np.arange`/\n",
    "  `np.linspace` for ranges.\n",
    "- `.shape`, `.ndim`, `.size`, `.dtype` tell you everything about an array's\n",
    "  layout; `.astype` converts between dtypes (and can silently lose data).\n",
    "- `.reshape`, `.ravel`, `.flatten`, `.T`, and `np.newaxis` rearrange data\n",
    "  without changing its values.\n",
    "- `np.concatenate`/`vstack`/`hstack` join along an existing axis;\n",
    "  `np.stack` creates a new one.\n",
    "\n",
    "Next up: **Module 2 — Indexing, Slicing & Views**, where you'll learn how\n",
    "to select exactly the data you want — and which selections give you a view\n",
    "vs. a copy.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
