{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-0000",
   "metadata": {},
   "source": [
    "# NumPy: Fast Arrays\n",
    "\n",
    "Meet the array — NumPy's fast, vectorized alternative to Python lists — and learn creation, indexing, broadcasting, aggregation, and random numbers.\n",
    "\n",
    "*Part of the free [Python for Data Science](https://ramadnsyh.dev/courses/python-for-data-science) course by [Muhammad Ramadiansyah](https://ramadnsyh.dev). This notebook is generated from the interactive lesson — [read it online](https://ramadnsyh.dev/courses/python-for-data-science/numpy).*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0001",
   "metadata": {},
   "source": [
    "Almost everything in data science — pandas, scikit-learn, even deep learning\n",
    "frameworks — is built on top of **NumPy** and its core data structure, the\n",
    "**ndarray**. In this lesson you'll see why arrays crush plain Python lists for\n",
    "numerical work, and learn the handful of operations you'll use every single day."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0002",
   "metadata": {},
   "source": [
    "## Why not just use lists?\n",
    "\n",
    "Python lists are flexible, but that flexibility has a price. Watch what happens\n",
    "when you try to do math with them:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0003",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "nums = [1, 2, 3]\n",
    "\n",
    "# Multiplying a list REPEATS it — it doesn't multiply the numbers!\n",
    "print(nums * 3)\n",
    "\n",
    "# And adding two lists concatenates them:\n",
    "print([1, 2, 3] + [4, 5, 6])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0004",
   "metadata": {},
   "source": [
    "To actually multiply every element you'd need a loop or a list comprehension.\n",
    "NumPy arrays behave the way a mathematician expects — operations apply\n",
    "**elementwise**:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0005",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3])\n",
    "print(a * 3)                       # [3 6 9]\n",
    "print(a + np.array([4, 5, 6]))     # [5 7 9]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0006",
   "metadata": {},
   "source": [
    "This style — operating on whole arrays at once instead of looping — is called\n",
    "**vectorization**, and it isn't just prettier, it's dramatically faster because\n",
    "the loop happens in optimized C code instead of Python. Let's measure it:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0007",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import time\n",
    "\n",
    "n = 1_000_000\n",
    "py_list = list(range(n))\n",
    "np_array = np.arange(n)\n",
    "\n",
    "t0 = time.perf_counter()\n",
    "squared_list = [x ** 2 for x in py_list]\n",
    "t_list = time.perf_counter() - t0\n",
    "\n",
    "t0 = time.perf_counter()\n",
    "squared_array = np_array ** 2\n",
    "t_array = time.perf_counter() - t0\n",
    "\n",
    "print(f\"list comprehension : {t_list*1000:7.1f} ms\")\n",
    "print(f\"numpy vectorized   : {t_array*1000:7.1f} ms\")\n",
    "print(f\"speedup            : {t_list/t_array:.0f}x\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0008",
   "metadata": {},
   "source": [
    "A 10–100x speedup is typical. On millions of rows, that's the difference\n",
    "between \"instant\" and \"coffee break\"."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0009",
   "metadata": {},
   "source": [
    "## Creating arrays\n",
    "\n",
    "You'll create arrays in a few standard ways — from lists, from constructors,\n",
    "or as evenly spaced sequences:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0010",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "print(np.array([1, 2, 3]))        # from a list\n",
    "print(np.zeros(4))                # [0. 0. 0. 0.]\n",
    "print(np.ones((2, 3)))            # 2x3 matrix of ones\n",
    "print(np.arange(0, 10, 2))        # like range(): start, stop, step\n",
    "print(np.linspace(0, 10, 5))      # 5 evenly spaced values from 0 to 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0011",
   "metadata": {},
   "source": [
    "Note the difference between the last two: `arange` takes a **step size**\n",
    "(and excludes the stop), while `linspace` takes a **count** of points\n",
    "(and includes both endpoints). `linspace` is the go-to for plotting smooth curves.\n",
    "\n",
    "Every array carries metadata describing itself:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0012",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "m = np.array([[1, 2, 3], [4, 5, 6]])\n",
    "\n",
    "print(m.shape)    # (2, 3) -> 2 rows, 3 columns\n",
    "print(m.ndim)     # 2 dimensions\n",
    "print(m.dtype)    # int64 (or int32 on some platforms)\n",
    "print(m.size)     # 6 elements total\n",
    "\n",
    "# dtype matters: mixing in a float upgrades everything\n",
    "print(np.array([1, 2, 3.5]).dtype)   # float64"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0013",
   "metadata": {},
   "source": [
    "Unlike lists, an array holds values of **one dtype** — that uniformity is\n",
    "exactly what makes it fast."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0014",
   "metadata": {},
   "source": [
    "## Indexing, slicing, and boolean masks\n",
    "\n",
    "One-dimensional indexing works like lists, but 2-D arrays take a comma-separated\n",
    "`[row, column]` pair:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0015",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "a = np.arange(10, 20)\n",
    "print(a[0], a[-1])       # first and last\n",
    "print(a[2:5])            # slice: [12 13 14]\n",
    "\n",
    "m = np.arange(1, 13).reshape(3, 4)\n",
    "print(m)\n",
    "print(m[1, 2])           # row 1, column 2 -> 7\n",
    "print(m[0])              # entire first row\n",
    "print(m[:, 1])           # entire second column\n",
    "print(m[0:2, 1:3])       # top-middle 2x2 block"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0016",
   "metadata": {},
   "source": [
    "The real superpower is **boolean masking** — filtering with a condition instead\n",
    "of a loop:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0017",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "scores = np.array([72, 95, 48, 88, 61, 99, 55])\n",
    "\n",
    "mask = scores >= 70\n",
    "print(mask)              # array of True/False\n",
    "print(scores[mask])      # only the passing scores\n",
    "print(scores[scores < 60])   # usually written in one line\n",
    "\n",
    "# masks also let you modify in place:\n",
    "scores[scores < 60] = 60\n",
    "print(scores)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0018",
   "metadata": {},
   "source": [
    "Keep this pattern in mind — pandas filtering in the next lessons works exactly\n",
    "the same way."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0019",
   "metadata": {},
   "source": [
    "## Elementwise math and broadcasting\n",
    "\n",
    "All the usual math works elementwise, and NumPy ships fast versions of common\n",
    "functions (`np.sin`, `np.exp`, `np.sqrt`, …):"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0020",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "a = np.array([1.0, 4.0, 9.0])\n",
    "b = np.array([10.0, 20.0, 30.0])\n",
    "\n",
    "print(a + b, a * b, sep=\"\\\\n\")\n",
    "print(np.sqrt(a))\n",
    "\n",
    "# Broadcasting: a scalar \"stretches\" to match the array...\n",
    "print(a * 100)\n",
    "\n",
    "# ...and a 1-D row stretches down a 2-D matrix:\n",
    "m = np.zeros((3, 3))\n",
    "row = np.array([1, 2, 3])\n",
    "print(m + row)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0021",
   "metadata": {},
   "source": [
    "**Broadcasting** is NumPy quietly repeating the smaller operand so shapes line\n",
    "up. It saves you from writing loops to, say, subtract a column mean from every\n",
    "row — a trick you'll use constantly when scaling features for machine learning."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0022",
   "metadata": {},
   "source": [
    "## Aggregations and the axis argument\n",
    "\n",
    "Reducing an array to summary numbers is a one-liner:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0023",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "sales = np.array([\n",
    "    [120, 135, 150],    # store A: Jan, Feb, Mar\n",
    "    [ 80,  95, 110],    # store B\n",
    "])\n",
    "\n",
    "print(sales.sum())            # grand total\n",
    "print(sales.mean())           # overall average\n",
    "print(sales.std())            # spread\n",
    "\n",
    "# axis=0 collapses ROWS (result: one value per column/month)\n",
    "print(sales.sum(axis=0))      # monthly totals across stores\n",
    "# axis=1 collapses COLUMNS (result: one value per row/store)\n",
    "print(sales.sum(axis=1))      # total per store"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0024",
   "metadata": {},
   "source": [
    "> **Remembering axis**\n",
    "> \n",
    "> `axis` names the dimension that **disappears**. `axis=0` collapses the rows\n",
    "> (you get column summaries); `axis=1` collapses the columns (you get row\n",
    "> summaries). When in doubt, check the shape of the result."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0025",
   "metadata": {},
   "source": [
    "## Random numbers, the modern way\n",
    "\n",
    "Simulations, synthetic datasets, and train/test splits all need randomness.\n",
    "The modern API is `np.random.default_rng`, which gives you a **generator**\n",
    "object — pass a seed to make results reproducible:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0026",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(42)\n",
    "\n",
    "print(rng.random(3))                  # uniform floats in [0, 1)\n",
    "print(rng.integers(1, 7, size=5))     # dice rolls (1-6)\n",
    "print(rng.normal(loc=170, scale=8, size=4).round(1))  # ~heights in cm\n",
    "\n",
    "# Same seed -> same \"random\" numbers, every time:\n",
    "rng2 = np.random.default_rng(42)\n",
    "print(rng2.random(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0027",
   "metadata": {},
   "source": [
    "Seeding matters more than it looks: it makes your experiments **reproducible**,\n",
    "so a colleague (or future you) can rerun your notebook and get identical results."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0028",
   "metadata": {},
   "source": [
    "### 🏋️ Exercise — Analyze a week of temperatures\n",
    "\n",
    "30] pulls out the hot readings; mask.sum() counts True values.\",\n",
    "    \"Remember: axis=1 collapses columns, giving one number per row (city).\",\n",
    "  ]}\n",
    "  solution={`\n",
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(7)\n",
    "temps = rng.normal(loc=[[31], [24]], scale=2.5, size=(2, 7)).round(1)\n",
    "cities = [\"Jakarta\", \"Bandung\"]\n",
    "\n",
    "print(temps)\n",
    "print(\"Mean per city   :\", temps.mean(axis=1).round(2))\n",
    "print(\"Hottest per city:\", temps.max(axis=1))\n",
    "print(\"Days above 30C  :\", (temps > 30).sum(axis=1))\n",
    "\n",
    "overall = temps.mean()\n",
    "print(\"Above overall avg:\", temps[temps > overall])\n",
    "`}\n",
    ">\n",
    "Simulate one week (7 days) of daily temperatures for **two cities** as a\n",
    "`(2, 7)` array using `np.random.default_rng` — give Jakarta a mean around 31 °C\n",
    "and Bandung around 24 °C. Then compute: (1) each city's average temperature,\n",
    "(2) each city's hottest day, (3) how many days each city was above 30 °C, and\n",
    "(4) all readings above the overall average, using a boolean mask."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0029",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Your solution here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0030",
   "metadata": {},
   "source": [
    "<details><summary>✅ Show solution</summary>\n",
    "\n",
    "```python\n",
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(7)\n",
    "temps = rng.normal(loc=[[31], [24]], scale=2.5, size=(2, 7)).round(1)\n",
    "cities = [\"Jakarta\", \"Bandung\"]\n",
    "\n",
    "print(temps)\n",
    "print(\"Mean per city   :\", temps.mean(axis=1).round(2))\n",
    "print(\"Hottest per city:\", temps.max(axis=1))\n",
    "print(\"Days above 30C  :\", (temps > 30).sum(axis=1))\n",
    "\n",
    "overall = temps.mean()\n",
    "print(\"Above overall avg:\", temps[temps > overall])\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0031",
   "metadata": {},
   "source": [
    "Next up: pandas — where NumPy arrays get labels, column names, and a whole\n",
    "toolkit for real-world tabular data."
   ]
  }
 ]
}