diff --git a/chainladder/utils/data/_manifest.py b/chainladder/utils/data/_manifest.py
index ca269e1b..c872fd84 100644
--- a/chainladder/utils/data/_manifest.py
+++ b/chainladder/utils/data/_manifest.py
@@ -139,6 +139,13 @@
],
"cumulative": True,
},
+ "friedland_dc_insurer": {
+ "origin": "Accident Year",
+ "development": "Calendar Year",
+ "index": None,
+ "columns": ["Reported Claims"],
+ "cumulative": True,
+ },
"friedland_gl_insurer": {
"origin": "Accident Year",
"development": "Calendar Year",
diff --git a/chainladder/utils/data/friedland_dc_insurer.csv b/chainladder/utils/data/friedland_dc_insurer.csv
new file mode 100644
index 00000000..7969b209
--- /dev/null
+++ b/chainladder/utils/data/friedland_dc_insurer.csv
@@ -0,0 +1,78 @@
+Accident Year,Calendar Year,Reported Claims
+1997,1997,
+1997,1998,
+1997,1999,
+1997,2000,
+1997,2001,
+1997,2002,
+1997,2003,
+1997,2004,
+1997,2005,
+1997,2006,
+1997,2007,3376
+1997,2008,3376
+1998,1998,
+1998,1999,
+1998,2000,
+1998,2001,
+1998,2002,
+1998,2003,
+1998,2004,
+1998,2005,
+1998,2006,
+1998,2007,2788
+1998,2008,2788
+1999,1999,
+1999,2000,
+1999,2001,
+1999,2002,
+1999,2003,
+1999,2004,
+1999,2005,
+1999,2006,
+1999,2007,1649
+1999,2008,1649
+2000,2000,
+2000,2001,
+2000,2002,
+2000,2003,
+2000,2004,
+2000,2005,
+2000,2006,
+2000,2007,1687
+2000,2008,1687
+2001,2001,
+2001,2002,
+2001,2003,
+2001,2004,
+2001,2005,
+2001,2006,
+2001,2007,2088
+2001,2008,2096
+2002,2002,
+2002,2003,
+2002,2004,
+2002,2005,
+2002,2006,
+2002,2007,2355
+2002,2008,2340
+2003,2003,
+2003,2004,
+2003,2005,
+2003,2006,
+2003,2007,2994
+2003,2008,3007
+2004,2004,
+2004,2005,
+2004,2006,
+2004,2007,3412
+2004,2008,3392
+2005,2005,
+2005,2006,
+2005,2007,2814
+2005,2008,2885
+2006,2006,
+2006,2007,2949
+2006,2008,3030
+2007,2007,2463
+2007,2008,2733
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..d423f775
--- /dev/null
+++ b/docs/friedland/chapter_15_monitoring.ipynb
@@ -0,0 +1,979 @@
+{
+ "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 a **roll-forward**: take the ultimates and\n",
+ "reporting pattern selected at one valuation, and compare actual reported\n",
+ "claims in the next period with the amount that pattern said should emerge.\n",
+ "\n",
+ "This notebook recreates Friedland's **DC Insurer** monitoring exhibits\n",
+ "(*Exhibit IV, Sheets 2–4*). The 12/31/2007 and 12/31/2008 diagonals are the\n",
+ "`friedland_dc_insurer` sample. The selected CDFs stop at 36 months (age-to-ult\n",
+ "1.000), so later ages are treated as fully reported.\n",
+ "\n",
+ "Expected emergence in the next calendar period comes from the fitted\n",
+ "`Chainladder` model — `full_triangle_.dev_to_val()` at the later valuation\n",
+ "minus the prior `latest_diagonal` — the same pattern as the gallery Actual vs\n",
+ "Expected example. For each accident year that is the Friedland formula\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 = 1 / \\text{CDF}_t$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "ch15m-imports",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-24T15:44:50.252618Z",
+ "iopub.status.busy": "2026-08-24T15:44:50.252359Z",
+ "iopub.status.idle": "2026-08-24T15:44:55.432141Z",
+ "shell.execute_reply": "2026-08-24T15:44:55.431063Z"
+ }
+ },
+ "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 as_series(tri):\n",
+ " s = tri.to_frame(origin_as_datetime=False).iloc[:, 0]\n",
+ " s.index = [int(getattr(i, \"year\", i)) for i in s.index]\n",
+ " return s"
+ ]
+ },
+ {
+ "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. Age 12\n",
+ "uses a 1.136 CDF (88.0% reported); age 24 uses 1.001 (99.9% reported); age 36\n",
+ "and later are 1.000.\n",
+ "\n",
+ "`DevelopmentConstant` attaches those CDFs to the 12/31/2007 slice of the\n",
+ "sample and `Chainladder` produces the Sheet 2 ultimates. The text's worked\n",
+ "example for accident year 2007 is $2{,}463 \\times 1.136 = 2{,}798$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "ch15m-sheet2",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-24T15:44:55.435912Z",
+ "iopub.status.busy": "2026-08-24T15:44:55.435334Z",
+ "iopub.status.idle": "2026-08-24T15:44:55.522113Z",
+ "shell.execute_reply": "2026-08-24T15:44:55.521257Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Age | \n",
+ " Reported at 12/31/07 | \n",
+ " CDF to Ultimate | \n",
+ " Projected Ultimate | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 1997 | \n",
+ " 132 | \n",
+ " 3376.0 | \n",
+ " 1.000 | \n",
+ " 3376.0 | \n",
+ "
\n",
+ " \n",
+ " | 1998 | \n",
+ " 120 | \n",
+ " 2788.0 | \n",
+ " 1.000 | \n",
+ " 2788.0 | \n",
+ "
\n",
+ " \n",
+ " | 1999 | \n",
+ " 108 | \n",
+ " 1649.0 | \n",
+ " 1.000 | \n",
+ " 1649.0 | \n",
+ "
\n",
+ " \n",
+ " | 2000 | \n",
+ " 96 | \n",
+ " 1687.0 | \n",
+ " 1.000 | \n",
+ " 1687.0 | \n",
+ "
\n",
+ " \n",
+ " | 2001 | \n",
+ " 84 | \n",
+ " 2088.0 | \n",
+ " 1.000 | \n",
+ " 2088.0 | \n",
+ "
\n",
+ " \n",
+ " | 2002 | \n",
+ " 72 | \n",
+ " 2355.0 | \n",
+ " 1.000 | \n",
+ " 2355.0 | \n",
+ "
\n",
+ " \n",
+ " | 2003 | \n",
+ " 60 | \n",
+ " 2994.0 | \n",
+ " 1.000 | \n",
+ " 2994.0 | \n",
+ "
\n",
+ " \n",
+ " | 2004 | \n",
+ " 48 | \n",
+ " 3412.0 | \n",
+ " 1.000 | \n",
+ " 3412.0 | \n",
+ "
\n",
+ " \n",
+ " | 2005 | \n",
+ " 36 | \n",
+ " 2814.0 | \n",
+ " 1.000 | \n",
+ " 2814.0 | \n",
+ "
\n",
+ " \n",
+ " | 2006 | \n",
+ " 24 | \n",
+ " 2949.0 | \n",
+ " 1.001 | \n",
+ " 2952.0 | \n",
+ "
\n",
+ " \n",
+ " | 2007 | \n",
+ " 12 | \n",
+ " 2463.0 | \n",
+ " 1.136 | \n",
+ " 2798.0 | \n",
+ "
\n",
+ " \n",
+ "
\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": [
+ "tri = cl.load_sample(\"friedland_dc_insurer\")\n",
+ "tri_2007 = tri[tri.valuation < \"2008\"]\n",
+ "\n",
+ "ages = [int(age) for age in tri_2007.ddims]\n",
+ "cdf_by_age = {12: 1.136, 24: 1.001, **{age: 1.000 for age in ages if age >= 36}}\n",
+ "\n",
+ "dev = cl.DevelopmentConstant(patterns=cdf_by_age, style=\"cdf\").fit_transform(tri_2007)\n",
+ "model = cl.Chainladder().fit(dev)\n",
+ "\n",
+ "years = [int(year) for year in tri_2007.origin.year]\n",
+ "sheet2 = pd.DataFrame(index=years)\n",
+ "sheet2[\"Age\"] = ages[::-1]\n",
+ "sheet2[\"Reported at 12/31/07\"] = as_series(tri_2007.latest_diagonal)\n",
+ "sheet2[\"CDF to Ultimate\"] = [cdf_by_age[age] for age in sheet2[\"Age\"]]\n",
+ "sheet2[\"Projected Ultimate\"] = np.round(as_series(model.ultimate_), 0)\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 model. Slice `full_triangle_` at the 2008\n",
+ "valuation for the expected cumulative; subtract the 2007 `latest_diagonal`\n",
+ "for expected emergence.\n",
+ "\n",
+ "The text 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-24T15:44:55.526381Z",
+ "iopub.status.busy": "2026-08-24T15:44:55.525656Z",
+ "iopub.status.idle": "2026-08-24T15:44:55.613428Z",
+ "shell.execute_reply": "2026-08-24T15:44:55.612521Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Selected Ultimate | \n",
+ " % Reported 12/31/07 | \n",
+ " % Reported 12/31/08 | \n",
+ " Reported 12/31/07 | \n",
+ " Reported 12/31/08 | \n",
+ " Actual | \n",
+ " Expected | \n",
+ " Difference | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 1997 | \n",
+ " 3376.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 3376.0 | \n",
+ " 3376.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 1998 | \n",
+ " 2788.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2788.0 | \n",
+ " 2788.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 1999 | \n",
+ " 1649.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1649.0 | \n",
+ " 1649.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2000 | \n",
+ " 1687.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1687.0 | \n",
+ " 1687.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2001 | \n",
+ " 2088.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2088.0 | \n",
+ " 2096.0 | \n",
+ " 8.0 | \n",
+ " 0.0 | \n",
+ " 8.0 | \n",
+ "
\n",
+ " \n",
+ " | 2002 | \n",
+ " 2355.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2355.0 | \n",
+ " 2340.0 | \n",
+ " -15.0 | \n",
+ " 0.0 | \n",
+ " -15.0 | \n",
+ "
\n",
+ " \n",
+ " | 2003 | \n",
+ " 2994.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2994.0 | \n",
+ " 3007.0 | \n",
+ " 13.0 | \n",
+ " 0.0 | \n",
+ " 13.0 | \n",
+ "
\n",
+ " \n",
+ " | 2004 | \n",
+ " 3412.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 3412.0 | \n",
+ " 3392.0 | \n",
+ " -20.0 | \n",
+ " 0.0 | \n",
+ " -20.0 | \n",
+ "
\n",
+ " \n",
+ " | 2005 | \n",
+ " 2814.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2814.0 | \n",
+ " 2885.0 | \n",
+ " 71.0 | \n",
+ " 0.0 | \n",
+ " 71.0 | \n",
+ "
\n",
+ " \n",
+ " | 2006 | \n",
+ " 2952.0 | \n",
+ " 0.999 | \n",
+ " 1.000 | \n",
+ " 2949.0 | \n",
+ " 3030.0 | \n",
+ " 81.0 | \n",
+ " 3.0 | \n",
+ " 78.0 | \n",
+ "
\n",
+ " \n",
+ " | 2007 | \n",
+ " 2798.0 | \n",
+ " 0.880 | \n",
+ " 0.999 | \n",
+ " 2463.0 | \n",
+ " 2733.0 | \n",
+ " 270.0 | \n",
+ " 332.0 | \n",
+ " -62.0 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " Actual | \n",
+ " Expected | \n",
+ " Difference | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | Total | \n",
+ " 408.0 | \n",
+ " 335.0 | \n",
+ " 73.0 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Actual Expected Difference\n",
+ "Total 408.0 335.0 73.0"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "expected_cum = model.full_triangle_.dev_to_val()\n",
+ "expected_cum = expected_cum[expected_cum.valuation == tri.valuation_date]\n",
+ "\n",
+ "reported_2007 = as_series(tri_2007.latest_diagonal)\n",
+ "reported_2008 = as_series(tri.latest_diagonal)\n",
+ "expected = np.round(as_series(expected_cum) - reported_2007, 0)\n",
+ "actual = reported_2008 - reported_2007\n",
+ "\n",
+ "pct_2007 = 1.0 / sheet2[\"CDF to Ultimate\"].to_numpy()\n",
+ "cdf_2008 = np.array([cdf_by_age.get(age + 12, 1.000) for age in sheet2[\"Age\"]])\n",
+ "pct_2008 = 1.0 / cdf_2008\n",
+ "\n",
+ "sheet3 = pd.DataFrame(index=years)\n",
+ "sheet3[\"Selected Ultimate\"] = sheet2[\"Projected 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",
+ "Expected monthly emergence uses the fitted `ibnr_` and those interpolated\n",
+ "percents: $\\text{IBNR} \\times (p_{t+1} - p_t) / (1 - p_t)$. The annual triangle\n",
+ "grain cannot hold monthly CDFs, so the interpolation stays on the selected\n",
+ "pattern rather than on `DevelopmentConstant`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "ch15m-sheet4",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-24T15:44:55.616022Z",
+ "iopub.status.busy": "2026-08-24T15:44:55.615702Z",
+ "iopub.status.idle": "2026-08-24T15:44:55.658238Z",
+ "shell.execute_reply": "2026-08-24T15:44:55.657318Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Selected Ultimate | \n",
+ " % Reported 12/31/07 | \n",
+ " % Reported 1/31/08 | \n",
+ " % Reported 2/29/08 | \n",
+ " Reported 12/31/07 | \n",
+ " Reported 1/31/08 | \n",
+ " Reported 2/29/08 | \n",
+ " Actual Jan | \n",
+ " Expected Jan | \n",
+ " Diff Jan | \n",
+ " Actual Feb | \n",
+ " Expected Feb | \n",
+ " Diff Feb | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 1997 | \n",
+ " 3376.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 3376.0 | \n",
+ " 3376.0 | \n",
+ " 3376.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 1998 | \n",
+ " 2788.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2788.0 | \n",
+ " 2788.0 | \n",
+ " 2788.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 1999 | \n",
+ " 1649.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1649.0 | \n",
+ " 1649.0 | \n",
+ " 1649.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2000 | \n",
+ " 1687.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1687.0 | \n",
+ " 1687.0 | \n",
+ " 1687.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2001 | \n",
+ " 2088.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2088.0 | \n",
+ " 2088.0 | \n",
+ " 2088.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2002 | \n",
+ " 2355.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2355.0 | \n",
+ " 2355.0 | \n",
+ " 2355.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2003 | \n",
+ " 2994.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2994.0 | \n",
+ " 2994.0 | \n",
+ " 2994.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2004 | \n",
+ " 3412.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 3412.0 | \n",
+ " 3412.0 | \n",
+ " 3412.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2005 | \n",
+ " 2814.0 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 1.000 | \n",
+ " 2814.0 | \n",
+ " 2814.0 | \n",
+ " 2814.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " | 2006 | \n",
+ " 2952.0 | \n",
+ " 0.999 | \n",
+ " 0.999 | \n",
+ " 0.999 | \n",
+ " 2949.0 | \n",
+ " 2951.0 | \n",
+ " 2986.0 | \n",
+ " 2.0 | \n",
+ " 0.0 | \n",
+ " 2.0 | \n",
+ " 35.0 | \n",
+ " 0.0 | \n",
+ " 35.0 | \n",
+ "
\n",
+ " \n",
+ " | 2007 | \n",
+ " 2798.0 | \n",
+ " 0.880 | \n",
+ " 0.915 | \n",
+ " 0.950 | \n",
+ " 2463.0 | \n",
+ " 2473.0 | \n",
+ " 2538.0 | \n",
+ " 10.0 | \n",
+ " 97.0 | \n",
+ " -87.0 | \n",
+ " 65.0 | \n",
+ " 132.0 | \n",
+ " -67.0 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " Actual Jan | \n",
+ " Expected Jan | \n",
+ " Diff Jan | \n",
+ " Actual Feb | \n",
+ " Expected Feb | \n",
+ " Diff Feb | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | Total | \n",
+ " 12.0 | \n",
+ " 97.0 | \n",
+ " -85.0 | \n",
+ " 100.0 | \n",
+ " 132.0 | \n",
+ " -32.0 | \n",
+ "
\n",
+ " \n",
+ "
\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 sheet2[\"Age\"]])\n",
+ "pct_feb = np.array([pct_reported_at(age + 2) for age in sheet2[\"Age\"]])\n",
+ "\n",
+ "# Printed latest reported at 1/31/08 and 2/29/08 for the two immature years.\n",
+ "reported_jan = reported_2007.copy()\n",
+ "reported_feb = reported_2007.copy()\n",
+ "reported_jan.loc[2007] = 2473\n",
+ "reported_feb.loc[2007] = 2538\n",
+ "reported_jan.loc[2006] = 2951\n",
+ "reported_feb.loc[2006] = 2986\n",
+ "\n",
+ "ibnr = as_series(model.ibnr_)\n",
+ "unreported = 1.0 - pct_2007\n",
+ "scale = np.divide(ibnr.to_numpy(), unreported, out=np.zeros(len(ibnr)), where=unreported > 0)\n",
+ "expected_jan = np.round(scale * (pct_jan - pct_2007), 0)\n",
+ "unreported_jan = 1.0 - pct_jan\n",
+ "remaining = reported_jan.to_numpy() # actual Jan used as the new starting reported\n",
+ "ibnr_jan = sheet2[\"Projected Ultimate\"].to_numpy() - remaining\n",
+ "scale_feb = np.divide(ibnr_jan, unreported_jan, out=np.zeros(len(ibnr)), where=unreported_jan > 0)\n",
+ "expected_feb = np.round(scale_feb * (pct_feb - pct_jan), 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\"] = sheet2[\"Projected 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"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "ch15m-recon",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-24T15:44:55.661060Z",
+ "iopub.status.busy": "2026-08-24T15:44:55.660694Z",
+ "iopub.status.idle": "2026-08-24T15:44:55.667073Z",
+ "shell.execute_reply": "2026-08-24T15:44:55.665570Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Exhibit IV, Sheet 2\n",
+ "assert sheet2.loc[2007, \"Projected Ultimate\"] == 2798\n",
+ "assert sheet2.loc[2006, \"Projected Ultimate\"] == 2952\n",
+ "\n",
+ "# Exhibit IV, Sheet 3\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
+}