{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 5 — Vectorized Numerical Computing with NumPy\n",
    "\n",
    "**Companion notebook** for the Lesson 5 slides. Rule for the exercises: **no Python\n",
    "`for` loops** over cash flows, ages, or scenarios."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-001",
   "metadata": {},
   "outputs": [],
   "source": [
    "def check(label, got, expected, tol=1e-6):\n",
    "    \"\"\"Compare your answer with the expected value; prints ✅ or ❌ (never raises).\"\"\"\n",
    "    import math\n",
    "    try:\n",
    "        if isinstance(expected, float) or isinstance(got, float):\n",
    "            ok = math.isclose(float(got), float(expected), rel_tol=tol, abs_tol=tol)\n",
    "        elif isinstance(expected, (list, tuple)) and expected and isinstance(expected[0], str):\n",
    "            ok = list(got) == list(expected)\n",
    "        elif hasattr(expected, \"__len__\") and not isinstance(expected, str):\n",
    "            import numpy as np\n",
    "            ok = np.allclose(np.asarray(got, dtype=float), np.asarray(expected, dtype=float), rtol=tol, atol=tol)\n",
    "        else:\n",
    "            ok = got == expected\n",
    "    except Exception:\n",
    "        ok = False\n",
    "    print((\"✅ \" if ok else \"❌ \") + f\"{label}: got {got!r}, expected {expected!r}\")\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. Why arrays? Compare a list comprehension with a vectorized sum\n",
    "(Uncomment the `%timeit` lines to measure on your machine.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "xs = list(range(1_000_000))\n",
    "arr = np.arange(1_000_000, dtype=float)\n",
    "%timeit sum(x * x for x in xs)\n",
    "%timeit (arr * arr).sum()\n",
    "print(arr.itemsize, arr.nbytes)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "## 2. Creating arrays; shape and dtype"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "q = np.array([0.0010, 0.0012, 0.0015, 0.0019, 0.0024, 0.0031])\n",
    "print(q.dtype, q.shape, q.ndim)\n",
    "print(np.arange(1, 6), np.linspace(0, 1, 5), np.full(4, 100.0))\n",
    "m = np.arange(12).reshape(3, 4)\n",
    "m"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "## 3. Indexing, masks, and views vs. copies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(m[1], m[:, 2], m[1, 2])\n",
    "print(q[q > 0.0015], q[[0, 2, 4]])\n",
    "\n",
    "window = q[1:4]          # a VIEW\n",
    "window[0] = 99\n",
    "print(\"after changing the view:\", q)\n",
    "q = np.array([0.0010, 0.0012, 0.0015, 0.0019, 0.0024, 0.0031])\n",
    "print(np.shares_memory(q, q[1:4]), np.shares_memory(q, q[[1, 2, 3]]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## 4. Broadcasting: a table of discount factors with no loop"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "rates = np.array([0.03, 0.05, 0.07])[:, None]   # (3, 1)\n",
    "t = np.arange(1, 6)                             # (5,)\n",
    "D = (1 + rates) ** -t                           # (3, 5)\n",
    "print(D.shape)\n",
    "print(D.round(4))\n",
    "print(\"5-year annuity factors:\", D.sum(axis=1).round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "## 5. Dot products and matrix products"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-011",
   "metadata": {},
   "outputs": [],
   "source": [
    "A = np.array([[1, 2], [3, 4]])\n",
    "B = np.array([[5, 6], [7, 8]])\n",
    "print(np.array([1.0, 2, 3]) @ np.array([4.0, 5, 6]))\n",
    "print(A @ B)\n",
    "print(A * B)          # element-wise — different!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-012",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn 1 — a bond portfolio in one line\n",
    "Rows are bonds, columns are years 1–5. Price all four at a flat 4% with one matrix\n",
    "product."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-013",
   "metadata": {},
   "outputs": [],
   "source": [
    "CF = np.array([[5, 5, 5, 5, 105],\n",
    "               [3, 3, 103, 0, 0],\n",
    "               [0, 0, 0, 0, 100],\n",
    "               [8, 8, 8, 8, 108]], dtype=float)\n",
    "prices = None\n",
    "...  # your code here\n",
    "check(\"bond prices\", prices, [104.4518, 97.2249, 82.1927, 117.8073], tol=1e-4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-014",
   "metadata": {},
   "source": [
    "## Your turn 2 — cohort mortality (Standard Ultimate Survival Model)\n",
    "\n",
    "$p_x = \\exp\\!\\big(-A - B c^x (c-1)/\\ln c\\big)$ with $A = 0.00022$,\n",
    "$B = 2.7\\times10^{-6}$, $c = 1.124$. For ages 60–69 at 5%:\n",
    "\n",
    "* `A_term` — the 10-year term insurance EPV $\\sum_k v^{k+1}\\,{}_kp_{60}\\,q_{60+k}$\n",
    "* `a_due` — the 10-year temporary annuity-due $\\sum_k v^k\\,{}_kp_{60}$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-015",
   "metadata": {},
   "outputs": [],
   "source": [
    "A_, B_, c_ = 0.00022, 2.7e-6, 1.124\n",
    "ages = np.arange(60, 70)\n",
    "A_term = a_due = None\n",
    "...  # your code here\n",
    "check(\"A¹ 60:10\", A_term, 0.042521, tol=1e-5)\n",
    "check(\"ä 60:10\", a_due, 7.955548, tol=1e-6)\n",
    "if A_term is not None and a_due is not None:\n",
    "    print(f\"Level net premium for 100,000: {100_000 * A_term / a_due:,.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-016",
   "metadata": {},
   "source": [
    "## Your turn 3 — multi-asset portfolio returns\n",
    "Simulate 10,000 scenarios with the seeded generator below, then combine them with the\n",
    "weights using `@`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-017",
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(2026)\n",
    "mu = np.array([0.06, 0.03, 0.08])\n",
    "sd = np.array([0.15, 0.05, 0.20])\n",
    "corr = np.array([[1.0, 0.2, 0.7], [0.2, 1.0, 0.1], [0.7, 0.1, 1.0]])\n",
    "cov = np.outer(sd, sd) * corr\n",
    "R = rng.multivariate_normal(mu, cov, size=10_000)\n",
    "w = np.array([0.5, 0.3, 0.2])\n",
    "port = theory_sd = None\n",
    "...  # your code here\n",
    "check(\"simulated mean\", None if port is None else round(port.mean(), 4), 0.0564)\n",
    "check(\"simulated sd\", None if port is None else round(port.std(), 4), 0.1108)\n",
    "check(\"theoretical sd\", theory_sd, 0.1105, tol=1e-3)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
