{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 7 — Continuous Distributions & Loss Generation\n",
    "\n",
    "**Companion notebook** for the Lesson 7 slides: `scipy.stats`, its parameterization\n",
    "traps, heavy tails, and a synthetic motor portfolio."
   ]
  },
  {
   "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\n",
    "from scipy import stats"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. Frozen distributions: pdf, cdf, sf, ppf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "N = stats.norm(loc=100, scale=15)\n",
    "print(N.pdf(100), N.cdf(130), N.sf(130), N.ppf(0.975))\n",
    "print(N.rvs(size=3, random_state=np.random.default_rng(11)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "## 2. Parameterization traps\n",
    "\n",
    "* Exponential: `scale` = mean = 1/λ\n",
    "* Lognormal: `s = sigma`, `scale = exp(mu)`\n",
    "* Truncated normal: bounds in **standard units**, `(bound - loc) / scale`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "E = stats.expon(scale=5000)\n",
    "print(\"exponential:\", E.mean(), E.cdf(5000), E.ppf(0.5))\n",
    "\n",
    "sigma = 1.0\n",
    "mu = np.log(5000) - sigma**2 / 2\n",
    "L = stats.lognorm(s=sigma, scale=np.exp(mu))\n",
    "print(\"lognormal mean, median:\", L.mean(), L.median())\n",
    "\n",
    "T = stats.truncnorm((18 - 42) / 14, (85 - 42) / 14, loc=42, scale=14)\n",
    "print(\"truncated normal mean:\", T.mean(), \"  WRONG bounds give:\", stats.truncnorm(18, 85, loc=42, scale=14).mean())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "## 3. Light vs. heavy tails (both have mean 5,000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "for k in (2, 5, 10, 20):\n",
    "    print(f\"P(X > {k:>2} × mean): exponential {E.sf(k * 5000):.2e}   lognormal {L.sf(k * 5000):.2e}\")\n",
    "\n",
    "x = L.rvs(size=1_000_000, random_state=np.random.default_rng(8))\n",
    "for d in (5_000, 20_000, 50_000):\n",
    "    print(f\"mean excess over {d:>6,}: {x[x > d].mean() - d:,.0f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## 4. Inverse-transform sampling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "u = np.random.default_rng(5).random(5)\n",
    "print(u.round(4), E.ppf(u).round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn — a driver profile generator\n",
    "\n",
    "Using one generator seeded with 2026, draw **in this order**:\n",
    "\n",
    "1. Ages: truncated normal (mean 42, sd 14, bounds 18–85), rounded **down** to whole years\n",
    "2. Mileage: lognormal with `s=0.5`, `scale=12_000`\n",
    "3. Claim counts: Poisson with rate `0.08 × (mileage/12000)**0.5 × age_factor`, where the\n",
    "   age factor is 1.8 under 25, 1.3 at 70+, else 1.0\n",
    "4. Severities: lognormal `s=1.2`, `scale=4_000`, one per claim; then apply a 500\n",
    "   deductible and a 50,000 limit"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-011",
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(2026)\n",
    "n = 10_000\n",
    "ages = claims = paid = None\n",
    "...  # your code here\n",
    "\n",
    "check(\"mean age\", None if ages is None else round(ages.mean(), 2), 43.06)\n",
    "check(\"total claims\", None if claims is None else int(claims.sum()), 852)\n",
    "check(\"mean paid per claim\", None if paid is None else round(paid.mean(), 2), 7132.35)\n",
    "check(\"loss cost per driver\", None if paid is None else round(paid.sum() / n, 2), 607.68)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-012",
   "metadata": {},
   "source": [
    "**Compare with theory:** the expected paid amount per claim is\n",
    "$E[\\min(X, 50{,}500)] - E[\\min(X, 500)]$. Compute it with `.expect()` and check the\n",
    "simulated mean is within a couple of standard errors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-013",
   "metadata": {},
   "outputs": [],
   "source": [
    "sev = stats.lognorm(s=1.2, scale=4_000)\n",
    "theory = sev.expect(lambda v: np.minimum(v, 50_500)) - sev.expect(lambda v: np.minimum(v, 500))\n",
    "print(f\"theory {theory:,.2f}\")\n",
    "if paid is not None:\n",
    "    print(f\"simulated {paid.mean():,.2f} ± {paid.std() / np.sqrt(len(paid)):,.0f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
