From 80893d2d59575639a2a0921617d68e75cc5cfd79 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Mon, 17 Aug 2026 17:49:42 -0700 Subject: [PATCH] docs: add Friedland Chapter 15 DC Insurer monitoring exhibits Recreate Exhibit IV Sheets 2-4 (actual-versus-expected reported) without depending on Chapters 7-10 JSON. Co-authored-by: Cursor --- docs/_toc.yml | 1 + docs/friedland/chapter_15_monitoring.ipynb | 1013 ++++++++++++++++++++ 2 files changed, 1014 insertions(+) create mode 100644 docs/friedland/chapter_15_monitoring.ipynb diff --git a/docs/_toc.yml b/docs/_toc.yml index 6a6e068b..9f3072a6 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -41,6 +41,7 @@ parts: - file: friedland/chapter_8.ipynb - file: friedland/chapter_9.ipynb - file: friedland/chapter_10.ipynb + - file: friedland/chapter_15_monitoring.ipynb - chapters: - file: gallery/index.md - chapters: diff --git a/docs/friedland/chapter_15_monitoring.ipynb b/docs/friedland/chapter_15_monitoring.ipynb new file mode 100644 index 00000000..b0f24183 --- /dev/null +++ b/docs/friedland/chapter_15_monitoring.ipynb @@ -0,0 +1,1013 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ch15m-intro", + "metadata": {}, + "source": [ + "# Chapter 15 - Monitoring Unpaid Claim Estimates\n", + "\n", + "> Deviations of actual development from projected development of claims or\n", + "> claim counts are one of the most useful diagnostic tools for evaluating the\n", + "> accuracy of the unpaid claim estimate.\n", + ">\n", + "> -- Friedland, Chapter 15\n", + "\n", + "The last part of Chapter 15 is not another projection method. It is a\n", + "**roll-forward**: take the ultimates and reporting pattern selected at one\n", + "valuation, and compare actual reported claims in the next period with the\n", + "amount that pattern said should emerge.\n", + "\n", + "This notebook recreates Friedland's **DC Insurer** monitoring exhibits\n", + "(*Exhibit IV, Sheets 2–4*). The quarterly development triangle in Sheet 1 is\n", + "not shipped as a sample — the printed latest diagonals and selected CDFs are\n", + "enough to run the actual-versus-expected tests.\n", + "\n", + "For each accident year, expected reported claims between two valuation dates\n", + "are\n", + "\n", + "$$\n", + "\\frac{\\text{Ultimate}_{t_0} - \\text{Reported}_{t_0}}{1 - p_{t_0}}\n", + "\\times (p_{t_1} - p_{t_0})\n", + "$$\n", + "\n", + "where $p_t$ is the selected percent reported at time $t$, equal to $1 / \\text{CDF}_t$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ch15m-imports", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T00:49:11.447934Z", + "iopub.status.busy": "2026-08-18T00:49:11.447665Z", + "iopub.status.idle": "2026-08-18T00:49:17.530244Z", + "shell.execute_reply": "2026-08-18T00:49:17.529404Z" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import chainladder as cl\n", + "from IPython.display import display\n", + "\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.width\", 1000)\n", + "\n", + "\n", + "def expected_reported(ultimate, reported, pct_from, pct_to):\n", + " \"\"\"Expected emergence between two valuations (Friedland Exhibit IV).\"\"\"\n", + " unreported = 1.0 - pct_from\n", + " emergence = np.where(unreported > 0, (ultimate - reported) / unreported, 0.0)\n", + " return emergence * (pct_to - pct_from)" + ] + }, + { + "cell_type": "markdown", + "id": "ch15m-sheet2-md", + "metadata": {}, + "source": [ + "## Exhibit IV, Sheet 2 — Ultimates at 12/31/2007\n", + "\n", + "DC Insurer selects ultimates with the reported development technique. The\n", + "12/31/2007 latest diagonal and the selected CDFs to ultimate are taken from\n", + "the printed exhibit (values in $000). Age 12 uses a 1.136 CDF (88.0%\n", + "reported); age 24 uses 1.001 (99.9% reported); older years are at 1.000.\n", + "\n", + "`DevelopmentConstant` attaches those CDFs and `Chainladder` produces the\n", + "Sheet 2 ultimates. The text's worked example for accident year 2007 is\n", + "$2{,}463 \\times 1.136 = 2{,}798$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ch15m-sheet2", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T00:49:17.534861Z", + "iopub.status.busy": "2026-08-18T00:49:17.533966Z", + "iopub.status.idle": "2026-08-18T00:49:17.661635Z", + "shell.execute_reply": "2026-08-18T00:49:17.660925Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
AgeReported at 12/31/07CDF to UltimateProjected Ultimate
19971323376.01.0003376.0
19981202788.01.0002788.0
19991081649.01.0001649.0
2000961687.01.0001687.0
2001842088.01.0002088.0
2002722355.01.0002355.0
2003602994.01.0002994.0
2004483412.01.0003412.0
2005362814.01.0002814.0
2006242949.01.0012952.0
2007122463.01.1362798.0
\n", + "
" + ], + "text/plain": [ + " Age Reported at 12/31/07 CDF to Ultimate Projected Ultimate\n", + "1997 132 3376.0 1.000 3376.0\n", + "1998 120 2788.0 1.000 2788.0\n", + "1999 108 1649.0 1.000 1649.0\n", + "2000 96 1687.0 1.000 1687.0\n", + "2001 84 2088.0 1.000 2088.0\n", + "2002 72 2355.0 1.000 2355.0\n", + "2003 60 2994.0 1.000 2994.0\n", + "2004 48 3412.0 1.000 3412.0\n", + "2005 36 2814.0 1.000 2814.0\n", + "2006 24 2949.0 1.001 2952.0\n", + "2007 12 2463.0 1.136 2798.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total projected ultimate: 28,913\n" + ] + } + ], + "source": [ + "years = np.arange(1997, 2008)\n", + "reported_2007 = np.array(\n", + " [3376, 2788, 1649, 1687, 2088, 2355, 2994, 3412, 2814, 2949, 2463],\n", + " dtype=float,\n", + ")\n", + "ages = (2007 - years) * 12 + 12\n", + "cdf_by_age = {12: 1.136, 24: 1.001, **{age: 1.000 for age in ages if age >= 36}}\n", + "\n", + "snapshot = pd.DataFrame(\n", + " {\n", + " \"Accident Year\": years,\n", + " \"Calendar Year\": 2007,\n", + " \"Reported Claims\": reported_2007,\n", + " }\n", + ")\n", + "tri = cl.Triangle(\n", + " snapshot,\n", + " origin=\"Accident Year\",\n", + " development=\"Calendar Year\",\n", + " columns=\"Reported Claims\",\n", + " cumulative=True,\n", + ")\n", + "dev = cl.DevelopmentConstant(patterns=cdf_by_age, style=\"cdf\").fit_transform(tri)\n", + "cl_model = cl.Chainladder().fit(dev)\n", + "\n", + "sheet2 = pd.DataFrame(index=years)\n", + "sheet2[\"Age\"] = ages\n", + "sheet2[\"Reported at 12/31/07\"] = reported_2007\n", + "sheet2[\"CDF to Ultimate\"] = [cdf_by_age[age] for age in ages]\n", + "sheet2[\"Projected Ultimate\"] = np.round(\n", + " cl_model.ultimate_.to_frame(origin_as_datetime=False).iloc[:, 0].values, 0\n", + ")\n", + "display(sheet2)\n", + "print(f\"Total projected ultimate: {sheet2['Projected Ultimate'].sum():,.0f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ch15m-sheet3-md", + "metadata": {}, + "source": [ + "## Exhibit IV, Sheet 3 — Annual monitoring test\n", + "\n", + "One year later, compare calendar-year 2008 actual reported claims with the\n", + "amount implied by the 12/31/2007 ultimates and reporting pattern. The text\n", + "works accident year 2007 as\n", + "\n", + "$$\n", + "\\frac{2{,}798 - 2{,}463}{1 - 0.880} \\times (0.999 - 0.880) = 332\n", + "$$\n", + "\n", + "and accident year 2006 as\n", + "\n", + "$$\n", + "\\frac{2{,}952 - 2{,}949}{1 - 0.999} \\times (1.000 - 0.999) = 3.\n", + "$$\n", + "\n", + "Older years are fully reported, so expected emergence is zero." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ch15m-sheet3", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T00:49:17.665174Z", + "iopub.status.busy": "2026-08-18T00:49:17.664879Z", + "iopub.status.idle": "2026-08-18T00:49:17.690773Z", + "shell.execute_reply": "2026-08-18T00:49:17.689042Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\EthanKang\\AppData\\Local\\Temp\\ipykernel_21808\\827484566.py:13: RuntimeWarning: invalid value encountered in divide\n", + " emergence = np.where(unreported > 0, (ultimate - reported) / unreported, 0.0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Selected Ultimate% Reported 12/31/07% Reported 12/31/08Reported 12/31/07Reported 12/31/08ActualExpectedDifference
19973376.01.0001.0003376.03376.00.00.00.0
19982788.01.0001.0002788.02788.00.00.00.0
19991649.01.0001.0001649.01649.00.00.00.0
20001687.01.0001.0001687.01687.00.00.00.0
20012088.01.0001.0002088.02096.08.00.08.0
20022355.01.0001.0002355.02340.0-15.00.0-15.0
20032994.01.0001.0002994.03007.013.00.013.0
20043412.01.0001.0003412.03392.0-20.00.0-20.0
20052814.01.0001.0002814.02885.071.00.071.0
20062952.00.9991.0002949.03030.081.03.078.0
20072798.00.8800.9992463.02733.0270.0332.0-62.0
\n", + "
" + ], + "text/plain": [ + " Selected Ultimate % Reported 12/31/07 % Reported 12/31/08 Reported 12/31/07 Reported 12/31/08 Actual Expected Difference\n", + "1997 3376.0 1.000 1.000 3376.0 3376.0 0.0 0.0 0.0\n", + "1998 2788.0 1.000 1.000 2788.0 2788.0 0.0 0.0 0.0\n", + "1999 1649.0 1.000 1.000 1649.0 1649.0 0.0 0.0 0.0\n", + "2000 1687.0 1.000 1.000 1687.0 1687.0 0.0 0.0 0.0\n", + "2001 2088.0 1.000 1.000 2088.0 2096.0 8.0 0.0 8.0\n", + "2002 2355.0 1.000 1.000 2355.0 2340.0 -15.0 0.0 -15.0\n", + "2003 2994.0 1.000 1.000 2994.0 3007.0 13.0 0.0 13.0\n", + "2004 3412.0 1.000 1.000 3412.0 3392.0 -20.0 0.0 -20.0\n", + "2005 2814.0 1.000 1.000 2814.0 2885.0 71.0 0.0 71.0\n", + "2006 2952.0 0.999 1.000 2949.0 3030.0 81.0 3.0 78.0\n", + "2007 2798.0 0.880 0.999 2463.0 2733.0 270.0 332.0 -62.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ActualExpectedDifference
Total408.0335.073.0
\n", + "
" + ], + "text/plain": [ + " Actual Expected Difference\n", + "Total 408.0 335.0 73.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pct_2007 = 1.0 / sheet2[\"CDF to Ultimate\"].to_numpy()\n", + "# One year later each origin is 12 months older. Age 12 -> 24 (99.9%),\n", + "# age 24 -> 36 (100%), and mature years stay at 100%.\n", + "cdf_2008 = np.array([cdf_by_age.get(age + 12, 1.000) for age in ages])\n", + "pct_2008 = 1.0 / cdf_2008\n", + "\n", + "reported_2008 = np.array(\n", + " [3376, 2788, 1649, 1687, 2096, 2340, 3007, 3392, 2885, 3030, 2733],\n", + " dtype=float,\n", + ")\n", + "ultimate = sheet2[\"Projected Ultimate\"].to_numpy()\n", + "expected = np.round(expected_reported(ultimate, reported_2007, pct_2007, pct_2008), 0)\n", + "actual = reported_2008 - reported_2007\n", + "\n", + "sheet3 = pd.DataFrame(index=years)\n", + "sheet3[\"Selected Ultimate\"] = ultimate\n", + "sheet3[\"% Reported 12/31/07\"] = np.round(pct_2007, 3)\n", + "sheet3[\"% Reported 12/31/08\"] = np.round(pct_2008, 3)\n", + "sheet3[\"Reported 12/31/07\"] = reported_2007\n", + "sheet3[\"Reported 12/31/08\"] = reported_2008\n", + "sheet3[\"Actual\"] = actual\n", + "sheet3[\"Expected\"] = expected\n", + "sheet3[\"Difference\"] = actual - expected\n", + "display(sheet3)\n", + "display(sheet3[[\"Actual\", \"Expected\", \"Difference\"]].sum().rename(\"Total\").to_frame().T)" + ] + }, + { + "cell_type": "markdown", + "id": "ch15m-sheet4-md", + "metadata": {}, + "source": [ + "## Exhibit IV, Sheet 4 — Monthly monitoring test\n", + "\n", + "DC Insurer has quarterly development factors. Monthly percent-reported values\n", + "are **linear interpolations of the quarterly percent reported**. Between age 12\n", + "(88.0%) and age 15 ($1 / 1.016 \\approx 98.4%$) that gives 91.5% at 13 months\n", + "and 95.0% at 14 months, matching the printed January / February 2008 template.\n", + "\n", + "The same actual-versus-expected formula is applied month by month." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ch15m-sheet4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T00:49:17.694066Z", + "iopub.status.busy": "2026-08-18T00:49:17.693644Z", + "iopub.status.idle": "2026-08-18T00:49:17.724288Z", + "shell.execute_reply": "2026-08-18T00:49:17.723122Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\EthanKang\\AppData\\Local\\Temp\\ipykernel_21808\\827484566.py:13: RuntimeWarning: invalid value encountered in divide\n", + " emergence = np.where(unreported > 0, (ultimate - reported) / unreported, 0.0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Selected Ultimate% Reported 12/31/07% Reported 1/31/08% Reported 2/29/08Reported 12/31/07Reported 1/31/08Reported 2/29/08Actual JanExpected JanDiff JanActual FebExpected FebDiff Feb
19973376.01.0001.0001.0003376.03376.03376.00.00.00.00.00.00.0
19982788.01.0001.0001.0002788.02788.02788.00.00.00.00.00.00.0
19991649.01.0001.0001.0001649.01649.01649.00.00.00.00.00.00.0
20001687.01.0001.0001.0001687.01687.01687.00.00.00.00.00.00.0
20012088.01.0001.0001.0002088.02088.02088.00.00.00.00.00.00.0
20022355.01.0001.0001.0002355.02355.02355.00.00.00.00.00.00.0
20032994.01.0001.0001.0002994.02994.02994.00.00.00.00.00.00.0
20043412.01.0001.0001.0003412.03412.03412.00.00.00.00.00.00.0
20052814.01.0001.0001.0002814.02814.02814.00.00.00.00.00.00.0
20062952.00.9990.9990.9992949.02951.02986.02.00.02.035.00.035.0
20072798.00.8800.9150.9502463.02473.02538.010.097.0-87.065.0132.0-67.0
\n", + "
" + ], + "text/plain": [ + " Selected Ultimate % Reported 12/31/07 % Reported 1/31/08 % Reported 2/29/08 Reported 12/31/07 Reported 1/31/08 Reported 2/29/08 Actual Jan Expected Jan Diff Jan Actual Feb Expected Feb Diff Feb\n", + "1997 3376.0 1.000 1.000 1.000 3376.0 3376.0 3376.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "1998 2788.0 1.000 1.000 1.000 2788.0 2788.0 2788.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "1999 1649.0 1.000 1.000 1.000 1649.0 1649.0 1649.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2000 1687.0 1.000 1.000 1.000 1687.0 1687.0 1687.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2001 2088.0 1.000 1.000 1.000 2088.0 2088.0 2088.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2002 2355.0 1.000 1.000 1.000 2355.0 2355.0 2355.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2003 2994.0 1.000 1.000 1.000 2994.0 2994.0 2994.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2004 3412.0 1.000 1.000 1.000 3412.0 3412.0 3412.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2005 2814.0 1.000 1.000 1.000 2814.0 2814.0 2814.0 0.0 0.0 0.0 0.0 0.0 0.0\n", + "2006 2952.0 0.999 0.999 0.999 2949.0 2951.0 2986.0 2.0 0.0 2.0 35.0 0.0 35.0\n", + "2007 2798.0 0.880 0.915 0.950 2463.0 2473.0 2538.0 10.0 97.0 -87.0 65.0 132.0 -67.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Actual JanExpected JanDiff JanActual FebExpected FebDiff Feb
Total12.097.0-85.0100.0132.0-32.0
\n", + "
" + ], + "text/plain": [ + " Actual Jan Expected Jan Diff Jan Actual Feb Expected Feb Diff Feb\n", + "Total 12.0 97.0 -85.0 100.0 132.0 -32.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "quarterly_cdf = {12: 1.136, 15: 1.016, 24: 1.001, 36: 1.000}\n", + "\n", + "\n", + "def pct_reported_at(age):\n", + " \"\"\"Linearly interpolate percent reported between quarterly CDF ages.\"\"\"\n", + " knots = np.array(sorted(quarterly_cdf))\n", + " pcts = 1.0 / np.array([quarterly_cdf[k] for k in knots])\n", + " if age <= knots[0]:\n", + " return float(pcts[0])\n", + " if age >= knots[-1]:\n", + " return 1.0\n", + " return float(np.interp(age, knots, pcts))\n", + "\n", + "\n", + "pct_jan = np.array([pct_reported_at(age + 1) for age in ages])\n", + "pct_feb = np.array([pct_reported_at(age + 2) for age in ages])\n", + "\n", + "# Printed latest reported at 1/31/08 and 2/29/08 for the two immature years;\n", + "# mature years have no expected emergence, so the January/February actuals\n", + "# are taken from the printed Sheet 4 template where they are non-zero.\n", + "reported_jan = reported_2007.copy()\n", + "reported_feb = reported_2007.copy()\n", + "reported_jan[-1] = 2473 # AY 2007\n", + "reported_feb[-1] = 2538\n", + "reported_jan[-2] = 2951 # AY 2006\n", + "reported_feb[-2] = 2986\n", + "\n", + "expected_jan = np.round(\n", + " expected_reported(ultimate, reported_2007, pct_2007, pct_jan), 0\n", + ")\n", + "expected_feb = np.round(\n", + " expected_reported(ultimate, reported_jan, pct_jan, pct_feb), 0\n", + ")\n", + "actual_jan = reported_jan - reported_2007\n", + "actual_feb = reported_feb - reported_jan\n", + "\n", + "sheet4 = pd.DataFrame(index=years)\n", + "sheet4[\"Selected Ultimate\"] = ultimate\n", + "sheet4[\"% Reported 12/31/07\"] = np.round(pct_2007, 3)\n", + "sheet4[\"% Reported 1/31/08\"] = np.round(pct_jan, 3)\n", + "sheet4[\"% Reported 2/29/08\"] = np.round(pct_feb, 3)\n", + "sheet4[\"Reported 12/31/07\"] = reported_2007\n", + "sheet4[\"Reported 1/31/08\"] = reported_jan\n", + "sheet4[\"Reported 2/29/08\"] = reported_feb\n", + "sheet4[\"Actual Jan\"] = actual_jan\n", + "sheet4[\"Expected Jan\"] = expected_jan\n", + "sheet4[\"Diff Jan\"] = actual_jan - expected_jan\n", + "sheet4[\"Actual Feb\"] = actual_feb\n", + "sheet4[\"Expected Feb\"] = expected_feb\n", + "sheet4[\"Diff Feb\"] = actual_feb - expected_feb\n", + "display(sheet4)\n", + "display(\n", + " sheet4[[\"Actual Jan\", \"Expected Jan\", \"Diff Jan\", \"Actual Feb\", \"Expected Feb\", \"Diff Feb\"]]\n", + " .sum()\n", + " .rename(\"Total\")\n", + " .to_frame()\n", + " .T\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ch15m-recon-md", + "metadata": {}, + "source": [ + "### Reconciliation to Friedland\n", + "\n", + "The two worked examples and the interpolated January / February 2008 percents\n", + "are reconciled to the printed exhibit. Mature-year actuals on Sheet 3 come from\n", + "the printed 12/31/2008 diagonal and are not re-derived." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ch15m-assert", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T00:49:17.728324Z", + "iopub.status.busy": "2026-08-18T00:49:17.727690Z", + "iopub.status.idle": "2026-08-18T00:49:17.735142Z", + "shell.execute_reply": "2026-08-18T00:49:17.734440Z" + } + }, + "outputs": [], + "source": [ + "# Exhibit IV, Sheet 2 — AY 2007 and 2006 ultimates\n", + "assert sheet2.loc[2007, \"Projected Ultimate\"] == 2798\n", + "assert sheet2.loc[2006, \"Projected Ultimate\"] == 2952\n", + "assert sheet2.loc[2007, \"CDF to Ultimate\"] == 1.136\n", + "assert sheet2.loc[2006, \"CDF to Ultimate\"] == 1.001\n", + "\n", + "# Exhibit IV, Sheet 3 — worked examples and total expected emergence\n", + "assert sheet3.loc[2007, \"% Reported 12/31/07\"] == 0.880\n", + "assert sheet3.loc[2007, \"% Reported 12/31/08\"] == 0.999\n", + "assert sheet3.loc[2007, \"Expected\"] == 332\n", + "assert sheet3.loc[2006, \"Expected\"] == 3\n", + "assert sheet3[\"Expected\"].sum() == 335\n", + "\n", + "# Exhibit IV, Sheet 4 — interpolated percent reported for AY 2007\n", + "assert np.isclose(sheet4.loc[2007, \"% Reported 1/31/08\"], 0.915, atol=5e-4)\n", + "assert np.isclose(sheet4.loc[2007, \"% Reported 2/29/08\"], 0.950, atol=5e-4)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}