{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 8 (Part B) — Capstone: End-to-End Rating Engine & Report\n",
    "\n",
    "**Companion notebook** for the capstone. It rehearses the two pieces that are new —\n",
    "segment indications for the whole book, and generating a Reveal.js deck from a Jinja2\n",
    "template — and checks your numbers against the answer key.\n",
    "\n",
    "The template engine is a separate package: run `uv add jinja2` in your project first."
   ]
  },
  {
   "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 geopandas as gpd\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from jinja2 import Template\n",
    "\n",
    "abt = pd.read_parquet(\"../data/processed/abt.parquet\")\n",
    "places = gpd.read_file(\"../data/geo/nc_geo.gpkg\", layer=\"places\")\n",
    "coast = gpd.read_file(\"../data/geo/nc_geo.gpkg\", layer=\"coastline\").union_all()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. Territories (Lesson 6, condensed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "pts = gpd.GeoDataFrame(abt[[\"vehicle_id\"]], geometry=gpd.points_from_xy(abt[\"longitude\"], abt[\"latitude\"]),\n",
    "                       crs=\"EPSG:4326\").to_crs(places.crs)\n",
    "hits = gpd.sjoin(pts, places[[\"geometry\"]], how=\"left\", predicate=\"within\")\n",
    "urban = hits.drop_duplicates(\"vehicle_id\").set_index(\"vehicle_id\")[\"index_right\"].notna()\n",
    "abt[\"urban\"] = urban.loc[abt[\"vehicle_id\"]].to_numpy()\n",
    "abt[\"coast_km\"] = pts.geometry.distance(coast).to_numpy() / 1000\n",
    "abt[\"territory\"] = np.select([abt[\"coast_km\"] < 25, abt[\"urban\"]], [\"Coastal\", \"Urban inland\"], \"Rural inland\")\n",
    "abt[\"territory\"].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn 1 — segment indications for the whole book\n",
    "\n",
    "For territory × age band: car years, claims, incurred, earned premium, loss ratio;\n",
    "then trended LR (4% a year for 1.5 years), indicated change against a 68% target,\n",
    "credibility $Z=\\min(1,\\sqrt{\\text{claims}/1082})$, and the proposed change\n",
    "$= Z \\times$ indicated, capped at ±15%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "seg = None\n",
    "...  # your code here\n",
    "\n",
    "if seg is not None:\n",
    "    overall = (seg[\"earned_premium\"] * seg[\"proposed_change\"]).sum() / seg[\"earned_premium\"].sum()\n",
    "    check(\"segments\", len(seg), 18)\n",
    "    check(\"segments above target\", int((seg[\"loss_ratio\"] > 0.68).sum()), 11)\n",
    "    check(\"premium-weighted proposed change\", round(overall, 4), 0.0783)\n",
    "else:\n",
    "    check(\"premium-weighted proposed change\", None, 0.0783)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "## 2. Generating slides with Jinja2\n",
    "`{{ ... }}` inserts a value; `{% for %}` repeats. Autoescaping (on in the real\n",
    "pipeline) stops text in your data being read as HTML."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "template = Template('''\n",
    "<section>\n",
    "  <h2>Loss ratio by territory</h2>\n",
    "  <table>\n",
    "  {% for t in territory %}\n",
    "    <tr><td>{{ t.territory }}</td><td>{{ \"{:.1%}\".format(t.loss_ratio) }}</td></tr>\n",
    "  {% endfor %}\n",
    "  </table>\n",
    "</section>''')\n",
    "\n",
    "by_territory = abt.groupby(\"territory\").agg(incurred=(\"incurred\", \"sum\"), premium=(\"earned_premium\", \"sum\"))\n",
    "by_territory[\"loss_ratio\"] = by_territory[\"incurred\"] / by_territory[\"premium\"]\n",
    "html = template.render(territory=by_territory.reset_index().to_dict(\"records\"))\n",
    "print(html)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## Your turn 2 — a KPI slide\n",
    "\n",
    "Write a template for a slide showing the portfolio's car years (no decimals, with a\n",
    "thousands separator) and loss ratio (one decimal, as a percentage), then render it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "kpis = {\"car_years\": abt[\"exposure\"].sum(), \"loss_ratio\": abt[\"incurred\"].sum() / abt[\"earned_premium\"].sum()}\n",
    "kpi_html = \"\"\n",
    "...  # your code here\n",
    "print(kpi_html)\n",
    "check(\"car years shown\", \"20,101\" in kpi_html, True)\n",
    "check(\"loss ratio shown\", \"74.6%\" in kpi_html, True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "**Next:** wrap these into the `calculate` and `report` stages of your pipeline, and\n",
    "check the full answer key in the capstone slides."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
