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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \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": [ + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \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": [ + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \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 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 +}