{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 2 — Collections, Iteration & Mathematical Series\n",
    "\n",
    "**Companion notebook** for the Lesson 2 slides. Run cells top to bottom; replace the\n",
    "`...` in **Your turn** cells, then run the `check()` cells."
   ]
  },
  {
   "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}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. Lists and slicing\n",
    "\n",
    "Indexes start at 0; negative indexes count from the end. A slice `[start:stop:step]`\n",
    "includes `start` and **excludes** `stop`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "q = [0.0010, 0.0012, 0.0015, 0.0019, 0.0024, 0.0031]   # q_x for ages 40..45\n",
    "print(q[0], q[-1], len(q))\n",
    "print(q[1:3], q[:2], q[3:], q[::2], q[::-1])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "### Assignment never copies\n",
    "Two names can point at the **same** list. Copy explicitly when you need independence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "base = [0.01, 0.02]\n",
    "stressed = base            # same object!\n",
    "stressed.append(0.03)\n",
    "print(\"base after append via 'stressed':\", base)\n",
    "\n",
    "stressed = base.copy()     # a real copy\n",
    "stressed.append(0.99)\n",
    "print(\"base after copying first:\", base)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "## 2. Tuples and unpacking"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "policy = (\"P001\", 34, \"F\")\n",
    "policy_id, age, sex = policy\n",
    "print(policy_id, age, sex)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## 3. Dictionaries and sets\n",
    "\n",
    "Look values up by key. `.get(key, default)` avoids a `KeyError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "rates = {\"TERM\": 1.24, \"WHOLE_LIFE\": 14.50, \"UL\": 9.10}\n",
    "print(rates[\"TERM\"], rates.get(\"ANNUITY\"), rates.get(\"ANNUITY\", 0.0), \"UL\" in rates)\n",
    "for product, rate in rates.items():\n",
    "    print(f\"{product:<11}{rate:>6.2f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-010",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_force_q1 = {\"P001\", \"P002\", \"P003\"}\n",
    "in_force_q2 = {\"P002\", \"P003\", \"P004\"}\n",
    "print(\"both:\", sorted(in_force_q1 & in_force_q2))\n",
    "print(\"exits:\", sorted(in_force_q1 - in_force_q2), \" new:\", sorted(in_force_q2 - in_force_q1))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-011",
   "metadata": {},
   "source": [
    "### Why lookups in sets and dicts are fast\n",
    "A list is searched item by item; a set hashes the key and jumps straight to it.\n",
    "`%timeit` measures it (your numbers will differ, the gap won't)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-012",
   "metadata": {},
   "outputs": [],
   "source": [
    "ids_list = [f\"P{i:06d}\" for i in range(1_000_000)]\n",
    "ids_set = set(ids_list)\n",
    "%timeit \"P999999\" in ids_list\n",
    "%timeit \"P999999\" in ids_set\n",
    "print(\"P999999\" in ids_list, \"P999999\" in ids_set)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-013",
   "metadata": {},
   "source": [
    "Uncomment the two `%timeit` lines above to compare the speeds yourself.\n",
    "\n",
    "## 4. `for` loops, `range`, `enumerate`, `zip`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-014",
   "metadata": {},
   "outputs": [],
   "source": [
    "cash_flows = [50, 50, 50, 50, 1050]\n",
    "i = 0.04\n",
    "pv = 0.0\n",
    "for t, cf in enumerate(cash_flows, start=1):\n",
    "    pv += cf * (1 + i) ** -t\n",
    "print(f\"{pv:,.2f}\")\n",
    "print(list(range(1, 11, 3)), list(range(10, 0, -2)))\n",
    "for age, qx in zip(range(40, 46), q):\n",
    "    print(age, qx)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-015",
   "metadata": {},
   "source": [
    "## 5. `while` loops need a tolerance *and* a guard"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-016",
   "metadata": {},
   "outputs": [],
   "source": [
    "r, tol, max_iter = 0.9, 1e-10, 10_000\n",
    "total, term, k = 0.0, 1.0, 0\n",
    "while term > tol:\n",
    "    total += term\n",
    "    term *= r\n",
    "    k += 1\n",
    "    if k >= max_iter:\n",
    "        raise RuntimeError(\"did not converge\")\n",
    "print(k, total, 1 / (1 - r))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-017",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn 1 — perpetuity-immediate by summation\n",
    "\n",
    "Sum $v^k$ for $k = 1, 2, \\dots$ until a term drops below `1e-8`, at $i = 5\\%$.\n",
    "Theory says the answer is $1/i = 20$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-018",
   "metadata": {},
   "outputs": [],
   "source": [
    "def perpetuity_immediate(i, tol=1e-8, max_iter=100_000):\n",
    "    \"\"\"Sum of v^k, k = 1, 2, ... until the term is below tol.\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "check(\"perpetuity at 5%\", perpetuity_immediate(0.05), 19.99999979456978, tol=1e-12)\n",
    "check(\"close to 1/i\", perpetuity_immediate(0.05), 20.0, tol=1e-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-019",
   "metadata": {},
   "source": [
    "## Your turn 2 — Horner's method\n",
    "\n",
    "Evaluate $c_0 + c_1 x + c_2 x^2 + \\dots + c_n x^n$ with one multiply and one add per\n",
    "coefficient: loop over the coefficients **in reverse**, `result = result * x + c`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-020",
   "metadata": {},
   "outputs": [],
   "source": [
    "def horner(coeffs, x):\n",
    "    \"\"\"Evaluate the polynomial with coefficients c_0..c_n at x.\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "check(\"bond PV at 4%\", horner([0, 50, 50, 50, 50, 1050], 1 / 1.04), 1044.5182233101614)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-021",
   "metadata": {},
   "source": [
    "## Your turn 3 — a 30-year mortgage\n",
    "\n",
    "200,000 borrowed at 6% nominal, paid monthly for 360 months.\n",
    "\n",
    "1. Use `horner` to compute the annuity factor $a_{\\overline{360}|}$ at the monthly rate.\n",
    "2. The level payment is the loan divided by that factor.\n",
    "3. Roll the balance forward 12 months: interest = balance × monthly rate;\n",
    "   principal = payment − interest."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-022",
   "metadata": {},
   "outputs": [],
   "source": [
    "loan, j, n = 200_000, 0.06 / 12, 360\n",
    "annuity_factor = None\n",
    "monthly_payment = None\n",
    "balance_after_12 = None\n",
    "...  # your code here\n",
    "\n",
    "check(\"annuity factor\", annuity_factor, 166.79161439233826)\n",
    "check(\"monthly payment\", monthly_payment, 1199.1010503054836)\n",
    "check(\"balance after 12 months\", balance_after_12, 197543.98, tol=1e-7)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
