diff --git a/docs/friedland/chapter_9.ipynb b/docs/friedland/chapter_9.ipynb index e74e8171..45769ef9 100644 --- a/docs/friedland/chapter_9.ipynb +++ b/docs/friedland/chapter_9.ipynb @@ -22,8 +22,12 @@ "\n", "In the chainladder package the method is implemented by\n", "`BornhuetterFerguson`, which takes the a priori through `sample_weight`. This\n", - "chapter recreates the **XYZ Insurer - Auto BI** example (Friedland Chapter 9),\n", - "reusing the development pattern selected for XYZ in Chapter 7." + "chapter recreates the Friedland Chapter 9 exhibits, reusing the development\n", + "patterns selected in Chapter 7 and the expected claims from Chapter 8:\n", + "\n", + "- **Exhibit I** — U.S. Industry Auto\n", + "- **Exhibit II** — XYZ Insurer (Auto BI)\n", + "- **Exhibit III** — U.S. PP Auto (impact of changing conditions)" ] }, { @@ -32,10 +36,10 @@ "id": "4a72e71f", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:01.978518Z", - "iopub.status.busy": "2026-07-12T03:29:01.977902Z", - "iopub.status.idle": "2026-07-12T03:29:12.474059Z", - "shell.execute_reply": "2026-07-12T03:29:12.472675Z" + "iopub.execute_input": "2026-07-12T08:35:06.176027Z", + "iopub.status.busy": "2026-07-12T08:35:06.175204Z", + "iopub.status.idle": "2026-07-12T08:35:09.076299Z", + "shell.execute_reply": "2026-07-12T08:35:09.075705Z" } }, "outputs": [], @@ -43,12 +47,524 @@ "import numpy as np\n", "import pandas as pd\n", "import chainladder as cl\n", + "import os\n", + "import tempfile\n", "from IPython.display import display\n", "\n", "pd.set_option(\"display.max_columns\", None)\n", "pd.set_option(\"display.width\", 1000)" ] }, + { + "cell_type": "markdown", + "id": "58740191", + "metadata": {}, + "source": [ + "## Exhibit I — U.S. Industry Auto\n", + "\n", + "The text works the Bornhuetter-Ferguson method first for **U.S. Industry Auto**\n", + "(Exhibit I), valued at 12/31/2007 over accident years 1998-2007. The reporting\n", + "and payment patterns are the Chapter 7 selection (three-year simple average with\n", + "a 1.000 reported / 1.002 paid tail), and the a priori expected claims come from\n", + "the expected claims technique of Chapter 8.\n", + "\n", + "### Projection of ultimate claims\n", + "\n", + "This recreates *Exhibit I, Sheet 1*. Because the text cumulates and rounds the\n", + "selected CDFs to three decimals, we drive `BornhuetterFerguson` with those\n", + "rounded patterns via `DevelopmentConstant` so the projection reconciles exactly." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2d99f42f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-12T08:35:09.078917Z", + "iopub.status.busy": "2026-07-12T08:35:09.078574Z", + "iopub.status.idle": "2026-07-12T08:35:09.293377Z", + "shell.execute_reply": "2026-07-12T08:35:09.292764Z" + } + }, + "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", + "
Expected ClaimsCDF ReportedCDF Paid% Unreported% UnpaidReportedPaidBF Ultimate (Reported)BF Ultimate (Paid)
199851430657.01.0001.0020.0000.00247742304.047644187.047742304.047746843.0
199951408736.01.0001.0040.0000.00451185767.051000534.051185767.051205350.0
200051680983.01.0011.0060.0010.00654837929.054533225.054889558.054841461.0
200154408716.01.0031.0110.0030.01156299562.055878421.056462300.056470405.0
200259421665.01.0061.0200.0060.02058592712.057807215.058947116.058972346.0
200356318302.01.0111.0400.0110.03857565344.055930654.058178105.058096743.0
200459646290.01.0231.0850.0220.07856976657.053774672.058317678.058447423.0
200561174953.01.0511.1840.0490.15556786410.050644994.059754938.060151912.0
200661926981.01.1101.4040.0990.28854641339.043606497.060778247.061425942.0
200761864556.01.2922.3900.2260.58248853563.027229969.062835336.063209774.0
\n", + "
" + ], + "text/plain": [ + " Expected Claims CDF Reported CDF Paid % Unreported % Unpaid Reported Paid BF Ultimate (Reported) BF Ultimate (Paid)\n", + "1998 51430657.0 1.000 1.002 0.000 0.002 47742304.0 47644187.0 47742304.0 47746843.0\n", + "1999 51408736.0 1.000 1.004 0.000 0.004 51185767.0 51000534.0 51185767.0 51205350.0\n", + "2000 51680983.0 1.001 1.006 0.001 0.006 54837929.0 54533225.0 54889558.0 54841461.0\n", + "2001 54408716.0 1.003 1.011 0.003 0.011 56299562.0 55878421.0 56462300.0 56470405.0\n", + "2002 59421665.0 1.006 1.020 0.006 0.020 58592712.0 57807215.0 58947116.0 58972346.0\n", + "2003 56318302.0 1.011 1.040 0.011 0.038 57565344.0 55930654.0 58178105.0 58096743.0\n", + "2004 59646290.0 1.023 1.085 0.022 0.078 56976657.0 53774672.0 58317678.0 58447423.0\n", + "2005 61174953.0 1.051 1.184 0.049 0.155 56786410.0 50644994.0 59754938.0 60151912.0\n", + "2006 61926981.0 1.110 1.404 0.099 0.288 54641339.0 43606497.0 60778247.0 61425942.0\n", + "2007 61864556.0 1.292 2.390 0.226 0.582 48853563.0 27229969.0 62835336.0 63209774.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ia = cl.load_sample(\"friedland_us_industry_auto\")\n", + "ia_reported = ia[\"Reported Claims\"]\n", + "ia_paid = ia[\"Paid Claims\"]\n", + "ia_years = list(ia_reported.origin.year)\n", + "col = lambda t: t.to_frame(origin_as_datetime=False).iloc[:, 0].values\n", + "\n", + "# Chapter 7 selection: three-year simple average development with a constant\n", + "# tail. Friedland cumulates the CDFs from age-to-age factors rounded to three\n", + "# decimals, then caps the reported CDFs at a minimum of 1.000.\n", + "ia_reported_dev = cl.TailConstant(tail=1.000, projection_period=0).fit_transform(\n", + " cl.Development(n_periods=3, average=\"simple\").fit_transform(ia_reported))\n", + "ia_paid_dev = cl.TailConstant(tail=1.002, projection_period=0).fit_transform(\n", + " cl.Development(n_periods=3, average=\"simple\").fit_transform(ia_paid))\n", + "ia_reported_dev.ldf_ = ia_reported_dev.ldf_.round(3)\n", + "ia_paid_dev.ldf_ = ia_paid_dev.ldf_.round(3)\n", + "\n", + "ia_ages = [int(a) for a in ia_reported.development.values]\n", + "ia_reported_cdf = np.maximum(\n", + " ia_reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0).round(3)\n", + "ia_paid_cdf = np.maximum(\n", + " ia_paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0).round(3)\n", + "\n", + "# A priori expected claims from the expected claims technique (Chapter 8, $000).\n", + "ia_expected = np.array([51430657, 51408736, 51680983, 54408716, 59421665,\n", + " 56318302, 59646290, 61174953, 61926981, 61864556], dtype=float)\n", + "ia_apriori = ia_reported.latest_diagonal.copy()\n", + "ia_apriori.iloc[0, 0] = ia_expected.reshape(ia_apriori.shape)\n", + "\n", + "# Drive BornhuetterFerguson with the rounded, selected CDFs.\n", + "ia_reported_pat = cl.DevelopmentConstant(\n", + " patterns=dict(zip(ia_ages, ia_reported_cdf)), style=\"cdf\").fit_transform(ia_reported)\n", + "ia_paid_pat = cl.DevelopmentConstant(\n", + " patterns=dict(zip(ia_ages, ia_paid_cdf)), style=\"cdf\").fit_transform(ia_paid)\n", + "ia_bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(ia_reported_pat, sample_weight=ia_apriori)\n", + "ia_bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(ia_paid_pat, sample_weight=ia_apriori)\n", + "\n", + "ia_reported_latest = col(ia_reported.latest_diagonal)\n", + "ia_paid_latest = col(ia_paid.latest_diagonal)\n", + "ia_reported_cdf_ay = ia_reported_cdf[::-1] # oldest origin -> highest maturity\n", + "ia_paid_cdf_ay = ia_paid_cdf[::-1]\n", + "ia_reported_ult = np.nan_to_num(col(ia_bf_reported.ultimate_))\n", + "ia_paid_ult = np.nan_to_num(col(ia_bf_paid.ultimate_))\n", + "\n", + "ia_projection = pd.DataFrame(index=ia_years)\n", + "ia_projection[\"Expected Claims\"] = ia_expected\n", + "ia_projection[\"CDF Reported\"] = ia_reported_cdf_ay\n", + "ia_projection[\"CDF Paid\"] = ia_paid_cdf_ay\n", + "ia_projection[\"% Unreported\"] = (1 - 1 / ia_reported_cdf_ay).round(3)\n", + "ia_projection[\"% Unpaid\"] = (1 - 1 / ia_paid_cdf_ay).round(3)\n", + "ia_projection[\"Reported\"] = ia_reported_latest\n", + "ia_projection[\"Paid\"] = ia_paid_latest\n", + "ia_projection[\"BF Ultimate (Reported)\"] = ia_reported_ult.round(0)\n", + "ia_projection[\"BF Ultimate (Paid)\"] = ia_paid_ult.round(0)\n", + "display(ia_projection)" + ] + }, + { + "cell_type": "markdown", + "id": "35e64305", + "metadata": {}, + "source": [ + "### Development of unpaid claim estimate\n", + "\n", + "This recreates *Exhibit I, Sheet 2*: case outstanding, estimated IBNR, and total\n", + "unpaid follow from the projected ultimates. Following the text, IBNR is ultimate\n", + "minus reported claims and total unpaid is ultimate minus paid claims." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f4b9a268", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-12T08:35:09.295770Z", + "iopub.status.busy": "2026-07-12T08:35:09.295493Z", + "iopub.status.idle": "2026-07-12T08:35:09.317475Z", + "shell.execute_reply": "2026-07-12T08:35:09.315762Z" + } + }, + "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", + "
BF Ultimate (Reported)BF Ultimate (Paid)Case OutstandingIBNR (Reported)IBNR (Paid)Total Unpaid (Reported)Total Unpaid (Paid)
199847742304.047746843.098117.00.04539.098117.0102656.0
199951185767.051205350.0185233.00.019583.0185233.0204816.0
200054889558.054841461.0304704.051629.03532.0356333.0308236.0
200156462300.056470405.0421141.0162738.0170843.0583879.0591984.0
200258947116.058972346.0785497.0354404.0379634.01139901.01165131.0
200358178105.058096743.01634690.0612761.0531399.02247451.02166089.0
200458317678.058447423.03201985.01341021.01470766.04543006.04672751.0
200559754938.060151912.06141416.02968528.03365502.09109944.09506918.0
200660778247.061425942.011034842.06136908.06784603.017171750.017819445.0
200762835336.063209774.021623594.013981773.014356211.035605367.035979805.0
\n", + "
" + ], + "text/plain": [ + " BF Ultimate (Reported) BF Ultimate (Paid) Case Outstanding IBNR (Reported) IBNR (Paid) Total Unpaid (Reported) Total Unpaid (Paid)\n", + "1998 47742304.0 47746843.0 98117.0 0.0 4539.0 98117.0 102656.0\n", + "1999 51185767.0 51205350.0 185233.0 0.0 19583.0 185233.0 204816.0\n", + "2000 54889558.0 54841461.0 304704.0 51629.0 3532.0 356333.0 308236.0\n", + "2001 56462300.0 56470405.0 421141.0 162738.0 170843.0 583879.0 591984.0\n", + "2002 58947116.0 58972346.0 785497.0 354404.0 379634.0 1139901.0 1165131.0\n", + "2003 58178105.0 58096743.0 1634690.0 612761.0 531399.0 2247451.0 2166089.0\n", + "2004 58317678.0 58447423.0 3201985.0 1341021.0 1470766.0 4543006.0 4672751.0\n", + "2005 59754938.0 60151912.0 6141416.0 2968528.0 3365502.0 9109944.0 9506918.0\n", + "2006 60778247.0 61425942.0 11034842.0 6136908.0 6784603.0 17171750.0 17819445.0\n", + "2007 62835336.0 63209774.0 21623594.0 13981773.0 14356211.0 35605367.0 35979805.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ia_unpaid = pd.DataFrame(index=ia_years)\n", + "ia_unpaid[\"BF Ultimate (Reported)\"] = ia_reported_ult.round(0)\n", + "ia_unpaid[\"BF Ultimate (Paid)\"] = ia_paid_ult.round(0)\n", + "ia_unpaid[\"Case Outstanding\"] = (ia_reported_latest - ia_paid_latest).round(0)\n", + "ia_unpaid[\"IBNR (Reported)\"] = (ia_reported_ult - ia_reported_latest).round(0)\n", + "ia_unpaid[\"IBNR (Paid)\"] = (ia_paid_ult - ia_reported_latest).round(0)\n", + "ia_unpaid[\"Total Unpaid (Reported)\"] = (ia_reported_ult - ia_paid_latest).round(0)\n", + "ia_unpaid[\"Total Unpaid (Paid)\"] = (ia_paid_ult - ia_paid_latest).round(0)\n", + "display(ia_unpaid)" + ] + }, + { + "cell_type": "markdown", + "id": "44849998", + "metadata": {}, + "source": [ + "### Reconciliation to Friedland\n", + "\n", + "The selected CDFs, projected ultimates, and estimated IBNR are reconciled to the\n", + "printed Exhibit I below." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "64c79fa2", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-12T08:35:09.321608Z", + "iopub.status.busy": "2026-07-12T08:35:09.321348Z", + "iopub.status.idle": "2026-07-12T08:35:09.327387Z", + "shell.execute_reply": "2026-07-12T08:35:09.326622Z" + } + }, + "outputs": [], + "source": [ + "# Exhibit I, Sheet 1 - selected CDFs to ultimate\n", + "assert np.allclose(ia_reported_cdf_ay,\n", + " [1.000, 1.000, 1.001, 1.003, 1.006, 1.011, 1.023, 1.051, 1.110, 1.292], atol=1e-3)\n", + "assert np.allclose(ia_paid_cdf_ay,\n", + " [1.002, 1.004, 1.006, 1.011, 1.020, 1.040, 1.085, 1.184, 1.404, 2.390], atol=1e-3)\n", + "# Exhibit I, Sheet 1 - projected ultimate claims\n", + "assert np.allclose(ia_reported_ult,\n", + " [47742304, 51185767, 54889558, 56462300, 58947116,\n", + " 58178105, 58317678, 59754938, 60778247, 62835336], atol=1)\n", + "assert np.allclose(ia_paid_ult,\n", + " [47746843, 51205350, 54841461, 56470405, 58972346,\n", + " 58096743, 58447423, 60151912, 61425942, 63209774], atol=1)\n", + "# Exhibit I, Sheet 2 - estimated IBNR (ultimate minus reported)\n", + "assert np.allclose(ia_unpaid[\"IBNR (Reported)\"].values,\n", + " [0, 0, 51629, 162738, 354404, 612761, 1341021, 2968528, 6136908, 13981773], atol=1)\n", + "assert np.allclose(ia_unpaid[\"IBNR (Paid)\"].values,\n", + " [4539, 19583, 3532, 170843, 379634, 531399, 1470766, 3365502, 6784603, 14356211], atol=1)" + ] + }, + { + "cell_type": "markdown", + "id": "1b13e9ad", + "metadata": {}, + "source": [ + "## Exhibit II — XYZ Insurer (Auto BI)\n", + "\n", + "Exhibit II applies the same Bornhuetter-Ferguson method to the **XYZ Insurer -\n", + "Auto BI** data, valued at 12/31/2008 over accident years 1998-2008." + ] + }, { "cell_type": "markdown", "id": "8d5d61e0", @@ -63,14 +579,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "id": "2cd269eb", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:12.482109Z", - "iopub.status.busy": "2026-07-12T03:29:12.479795Z", - "iopub.status.idle": "2026-07-12T03:29:12.759383Z", - "shell.execute_reply": "2026-07-12T03:29:12.757240Z" + "iopub.execute_input": "2026-07-12T08:35:09.330071Z", + "iopub.status.busy": "2026-07-12T08:35:09.329807Z", + "iopub.status.idle": "2026-07-12T08:35:09.389598Z", + "shell.execute_reply": "2026-07-12T08:35:09.389031Z" } }, "outputs": [ @@ -211,14 +727,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "id": "0c81b46d", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:12.770896Z", - "iopub.status.busy": "2026-07-12T03:29:12.770091Z", - "iopub.status.idle": "2026-07-12T03:29:13.309797Z", - "shell.execute_reply": "2026-07-12T03:29:13.307366Z" + "iopub.execute_input": "2026-07-12T08:35:09.391893Z", + "iopub.status.busy": "2026-07-12T08:35:09.391649Z", + "iopub.status.idle": "2026-07-12T08:35:09.558823Z", + "shell.execute_reply": "2026-07-12T08:35:09.557221Z" } }, "outputs": [ @@ -351,10 +867,28 @@ } ], "source": [ - "reported_dev = cl.TailConstant(tail=1.00, projection_period=0).fit_transform(\n", - " cl.Development(n_periods=2, average=\"volume\").fit_transform(reported))\n", - "paid_dev = cl.TailConstant(tail=1.01, projection_period=0).fit_transform(\n", - " cl.Development(n_periods=2, average=\"volume\").fit_transform(paid))\n", + "# Reuse the Chapter 7 XYZ selection (volume-weighted two-period development\n", + "# with a constant tail). Rather than refitting, persist the fitted estimators\n", + "# as Chapter 7 would and recall them here via model persistence, keeping the\n", + "# two chapters in sync.\n", + "reported_pipe = cl.Pipeline([\n", + " (\"development\", cl.Development(n_periods=2, average=\"volume\")),\n", + " (\"tail\", cl.TailConstant(tail=1.00, projection_period=0)),\n", + "]).fit(reported)\n", + "paid_pipe = cl.Pipeline([\n", + " (\"development\", cl.Development(n_periods=2, average=\"volume\")),\n", + " (\"tail\", cl.TailConstant(tail=1.01, projection_period=0)),\n", + "]).fit(paid)\n", + "\n", + "# Persist the fitted estimators and recall them (a model-persistence round-trip).\n", + "_pkl_dir = tempfile.gettempdir()\n", + "reported_pipe.to_pickle(os.path.join(_pkl_dir, \"friedland_ch7_xyz_reported.pkl\"))\n", + "paid_pipe.to_pickle(os.path.join(_pkl_dir, \"friedland_ch7_xyz_paid.pkl\"))\n", + "reported_pipe = cl.read_pickle(os.path.join(_pkl_dir, \"friedland_ch7_xyz_reported.pkl\"))\n", + "paid_pipe = cl.read_pickle(os.path.join(_pkl_dir, \"friedland_ch7_xyz_paid.pkl\"))\n", + "\n", + "reported_dev = reported_pipe.transform(reported)\n", + "paid_dev = paid_pipe.transform(paid)\n", "\n", "# Friedland cumulates CDFs from age-to-age factors rounded to three decimals.\n", "reported_dev.ldf_ = reported_dev.ldf_.round(3)\n", @@ -389,14 +923,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "id": "0aa8a19c", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:13.315701Z", - "iopub.status.busy": "2026-07-12T03:29:13.314642Z", - "iopub.status.idle": "2026-07-12T03:29:13.372866Z", - "shell.execute_reply": "2026-07-12T03:29:13.364671Z" + "iopub.execute_input": "2026-07-12T08:35:09.562852Z", + "iopub.status.busy": "2026-07-12T08:35:09.562294Z", + "iopub.status.idle": "2026-07-12T08:35:09.592758Z", + "shell.execute_reply": "2026-07-12T08:35:09.591845Z" } }, "outputs": [ @@ -527,7 +1061,7 @@ "def as_diagonal(tri, vec):\n", " \"\"\"Build a per-origin (latest-diagonal) triangle from a vector of values.\"\"\"\n", " d = tri.latest_diagonal.copy()\n", - " d.values = d.values * 0 + np.array(vec, dtype=float).reshape(d.shape)\n", + " d.iloc[0, 0] = np.asarray(vec, dtype=float).reshape(d.shape)\n", " return d\n", "\n", "\n", @@ -554,14 +1088,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "a758ad45", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:13.378914Z", - "iopub.status.busy": "2026-07-12T03:29:13.378145Z", - "iopub.status.idle": "2026-07-12T03:29:13.924391Z", - "shell.execute_reply": "2026-07-12T03:29:13.920829Z" + "iopub.execute_input": "2026-07-12T08:35:09.596621Z", + "iopub.status.busy": "2026-07-12T08:35:09.596040Z", + "iopub.status.idle": "2026-07-12T08:35:09.776324Z", + "shell.execute_reply": "2026-07-12T08:35:09.775089Z" } }, "outputs": [ @@ -801,14 +1335,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "id": "7ced2979", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:13.932277Z", - "iopub.status.busy": "2026-07-12T03:29:13.931532Z", - "iopub.status.idle": "2026-07-12T03:29:13.992950Z", - "shell.execute_reply": "2026-07-12T03:29:13.989459Z" + "iopub.execute_input": "2026-07-12T08:35:09.779955Z", + "iopub.status.busy": "2026-07-12T08:35:09.779435Z", + "iopub.status.idle": "2026-07-12T08:35:09.814638Z", + "shell.execute_reply": "2026-07-12T08:35:09.813219Z" } }, "outputs": [ @@ -1001,14 +1535,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, "id": "7ee5e130", "metadata": { "execution": { - "iopub.execute_input": "2026-07-12T03:29:13.999515Z", - "iopub.status.busy": "2026-07-12T03:29:13.998754Z", - "iopub.status.idle": "2026-07-12T03:29:14.015590Z", - "shell.execute_reply": "2026-07-12T03:29:14.014244Z" + "iopub.execute_input": "2026-07-12T08:35:09.822405Z", + "iopub.status.busy": "2026-07-12T08:35:09.821121Z", + "iopub.status.idle": "2026-07-12T08:35:09.830824Z", + "shell.execute_reply": "2026-07-12T08:35:09.829817Z" } }, "outputs": [], @@ -1029,6 +1563,974 @@ "assert np.allclose(unpaid[\"IBNR (Paid)\"].values,\n", " [155, 52, 605, 1727, 1256, 6400, 12324, 23690, 22386, 13909, 22414], atol=1)" ] + }, + { + "cell_type": "markdown", + "id": "c28f0940", + "metadata": {}, + "source": [ + "## Exhibit III — U.S. PP Auto (Impact of Changing Conditions)\n", + "\n", + "Exhibit III applies the Bornhuetter-Ferguson method to the four **U.S. PP Auto**\n", + "scenarios that Friedland uses to study a changing environment (valued at\n", + "12/31/2008, accident years 1999-2008):\n", + "\n", + "1. Steady-State\n", + "2. Increasing Claim Ratios\n", + "3. Increasing Case Outstanding Strength\n", + "4. Increasing Claim Ratios and Case Outstanding Strength\n", + "\n", + "All four scenarios share the same a priori expected claims (a 70% expected claim\n", + "ratio applied to earned premium, from Chapter 8) and the same five-year simple\n", + "average development selection from Chapter 7. Because Friedland rounds both the\n", + "cumulative development factors and the resulting percentages, we fold the rounded\n", + "percentages into an effective CDF so `BornhuetterFerguson` reproduces the text.\n", + "\n", + "> **Note on sample data.** The reported figures for the two *case outstanding\n", + "> strength* scenarios do not yet reconcile exactly. The reported development in\n", + "> the `friedland_uspp_auto_increasing_case` and `friedland_uspp_increasing_claim_case`\n", + "> samples differs slightly from the text (a known data correction to these CSVs\n", + "> is still outstanding), so the reconciliation below asserts the reported basis\n", + "> only for the two scenarios with clean data, and the paid basis for all four." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9f35cbe8", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-12T08:35:09.838745Z", + "iopub.status.busy": "2026-07-12T08:35:09.837084Z", + "iopub.status.idle": "2026-07-12T08:35:10.967879Z", + "shell.execute_reply": "2026-07-12T08:35:10.967282Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Steady-State\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", + "
Expected ClaimsReportedPaidCDF ReportedCDF Paid% Unreported% UnpaidBF Ultimate (Reported)BF Ultimate (Paid)IBNR (Reported)IBNR (Paid)
1999700000.0700000.0700000.01.0001.0000.000.00700000.0700000.00.00.0
2000735000.0735000.0735000.01.0001.0000.000.00735000.0735000.00.00.0
2001771750.0771750.0764033.01.0001.0100.000.01771750.0771750.00.00.0
2002810338.0810338.0802234.01.0001.0100.000.01810338.0810337.00.0-1.0
2003850854.0842346.0833837.01.0101.0200.010.02850855.0850854.08509.08508.0
2004893397.0884463.0857661.01.0101.0420.010.04893397.0893397.08934.08934.0
2005938067.0919306.0863022.01.0201.0870.020.08938067.0938067.018761.018761.0
2006984970.0935722.0827375.01.0531.1900.050.16984970.0984970.049248.049248.0
20071034219.0930797.0734295.01.1111.4080.100.291034219.01034219.0103422.0103422.0
20081085930.0836166.0456090.01.2992.3810.230.581085930.01085929.0249764.0249763.0
\n", + "
" + ], + "text/plain": [ + " Expected Claims Reported Paid CDF Reported CDF Paid % Unreported % Unpaid BF Ultimate (Reported) BF Ultimate (Paid) IBNR (Reported) IBNR (Paid)\n", + "1999 700000.0 700000.0 700000.0 1.000 1.000 0.00 0.00 700000.0 700000.0 0.0 0.0\n", + "2000 735000.0 735000.0 735000.0 1.000 1.000 0.00 0.00 735000.0 735000.0 0.0 0.0\n", + "2001 771750.0 771750.0 764033.0 1.000 1.010 0.00 0.01 771750.0 771750.0 0.0 0.0\n", + "2002 810338.0 810338.0 802234.0 1.000 1.010 0.00 0.01 810338.0 810337.0 0.0 -1.0\n", + "2003 850854.0 842346.0 833837.0 1.010 1.020 0.01 0.02 850855.0 850854.0 8509.0 8508.0\n", + "2004 893397.0 884463.0 857661.0 1.010 1.042 0.01 0.04 893397.0 893397.0 8934.0 8934.0\n", + "2005 938067.0 919306.0 863022.0 1.020 1.087 0.02 0.08 938067.0 938067.0 18761.0 18761.0\n", + "2006 984970.0 935722.0 827375.0 1.053 1.190 0.05 0.16 984970.0 984970.0 49248.0 49248.0\n", + "2007 1034219.0 930797.0 734295.0 1.111 1.408 0.10 0.29 1034219.0 1034219.0 103422.0 103422.0\n", + "2008 1085930.0 836166.0 456090.0 1.299 2.381 0.23 0.58 1085930.0 1085929.0 249764.0 249763.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Increasing Claim Ratios\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", + "
Expected ClaimsReportedPaidCDF ReportedCDF Paid% Unreported% UnpaidBF Ultimate (Reported)BF Ultimate (Paid)IBNR (Reported)IBNR (Paid)
1999700000.0700000.0700000.01.0001.0000.000.00700000.0700000.00.00.0
2000735000.0735000.0735000.01.0001.0000.000.00735000.0735000.00.00.0
2001771750.0771750.0764033.01.0001.0100.000.01771750.0771750.00.00.0
2002810338.0810338.0802234.01.0001.0100.000.01810338.0810337.00.0-1.0
2003850854.0842346.0833837.01.0101.0200.010.02850855.0850854.08509.08508.0
2004893397.01010815.0980184.01.0101.0420.010.041019749.01015920.08934.05105.0
2005938067.01116300.01047955.01.0201.0870.020.081135061.01123000.018761.06700.0
2006984970.01203071.01063768.01.0531.1900.050.161252320.01221363.049248.018292.0
20071034219.01263224.0996544.01.1111.4080.100.291366646.01296468.0103422.033244.0
20081085930.01194523.0651558.01.2992.3810.230.581444287.01281397.0249764.086874.0
\n", + "
" + ], + "text/plain": [ + " Expected Claims Reported Paid CDF Reported CDF Paid % Unreported % Unpaid BF Ultimate (Reported) BF Ultimate (Paid) IBNR (Reported) IBNR (Paid)\n", + "1999 700000.0 700000.0 700000.0 1.000 1.000 0.00 0.00 700000.0 700000.0 0.0 0.0\n", + "2000 735000.0 735000.0 735000.0 1.000 1.000 0.00 0.00 735000.0 735000.0 0.0 0.0\n", + "2001 771750.0 771750.0 764033.0 1.000 1.010 0.00 0.01 771750.0 771750.0 0.0 0.0\n", + "2002 810338.0 810338.0 802234.0 1.000 1.010 0.00 0.01 810338.0 810337.0 0.0 -1.0\n", + "2003 850854.0 842346.0 833837.0 1.010 1.020 0.01 0.02 850855.0 850854.0 8509.0 8508.0\n", + "2004 893397.0 1010815.0 980184.0 1.010 1.042 0.01 0.04 1019749.0 1015920.0 8934.0 5105.0\n", + "2005 938067.0 1116300.0 1047955.0 1.020 1.087 0.02 0.08 1135061.0 1123000.0 18761.0 6700.0\n", + "2006 984970.0 1203071.0 1063768.0 1.053 1.190 0.05 0.16 1252320.0 1221363.0 49248.0 18292.0\n", + "2007 1034219.0 1263224.0 996544.0 1.111 1.408 0.10 0.29 1366646.0 1296468.0 103422.0 33244.0\n", + "2008 1085930.0 1194523.0 651558.0 1.299 2.381 0.23 0.58 1444287.0 1281397.0 249764.0 86874.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Increasing Case Outstanding Strength\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", + "
Expected ClaimsReportedPaidCDF ReportedCDF Paid% Unreported% UnpaidBF Ultimate (Reported)BF Ultimate (Paid)IBNR (Reported)IBNR (Paid)
1999700000.0700000.0700000.01.0001.0000.0000.00700000.0700000.00.00.0
2000735000.0735000.0735000.01.0001.0000.0000.00735000.0735000.00.00.0
2001771750.0771750.0764033.01.0001.0100.0000.01771750.0771750.00.00.0
2002810338.0810338.0802234.01.0001.0100.0000.01810338.0810337.00.0-1.0
2003850854.0842346.0833837.01.0101.0200.0100.02850855.0850854.08509.08508.0
2004893397.0884463.0857661.01.0101.0420.0100.04893397.0893397.08934.08934.0
2005938067.0933377.0863022.01.0201.0870.0200.08952138.0938067.018761.04690.0
2006984970.0962808.0827375.01.0541.1900.0510.161013041.0984970.050233.022162.0
20071034219.0979922.0734295.01.1181.4080.1060.291089549.01034219.0109627.054297.0
20081085930.0931185.0456090.01.3172.3810.2410.581192894.01085929.0261709.0154744.0
\n", + "
" + ], + "text/plain": [ + " Expected Claims Reported Paid CDF Reported CDF Paid % Unreported % Unpaid BF Ultimate (Reported) BF Ultimate (Paid) IBNR (Reported) IBNR (Paid)\n", + "1999 700000.0 700000.0 700000.0 1.000 1.000 0.000 0.00 700000.0 700000.0 0.0 0.0\n", + "2000 735000.0 735000.0 735000.0 1.000 1.000 0.000 0.00 735000.0 735000.0 0.0 0.0\n", + "2001 771750.0 771750.0 764033.0 1.000 1.010 0.000 0.01 771750.0 771750.0 0.0 0.0\n", + "2002 810338.0 810338.0 802234.0 1.000 1.010 0.000 0.01 810338.0 810337.0 0.0 -1.0\n", + "2003 850854.0 842346.0 833837.0 1.010 1.020 0.010 0.02 850855.0 850854.0 8509.0 8508.0\n", + "2004 893397.0 884463.0 857661.0 1.010 1.042 0.010 0.04 893397.0 893397.0 8934.0 8934.0\n", + "2005 938067.0 933377.0 863022.0 1.020 1.087 0.020 0.08 952138.0 938067.0 18761.0 4690.0\n", + "2006 984970.0 962808.0 827375.0 1.054 1.190 0.051 0.16 1013041.0 984970.0 50233.0 22162.0\n", + "2007 1034219.0 979922.0 734295.0 1.118 1.408 0.106 0.29 1089549.0 1034219.0 109627.0 54297.0\n", + "2008 1085930.0 931185.0 456090.0 1.317 2.381 0.241 0.58 1192894.0 1085929.0 261709.0 154744.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Increasing Claim Ratios and Case Outstanding Strength\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", + "
Expected ClaimsReportedPaidCDF ReportedCDF Paid% Unreported% UnpaidBF Ultimate (Reported)BF Ultimate (Paid)IBNR (Reported)IBNR (Paid)
1999700000.0700000.0700000.01.0001.0000.0000.00700000.0700000.00.00.0
2000735000.0735000.0735000.01.0001.0000.0000.00735000.0735000.00.00.0
2001771750.0771750.0764033.01.0001.0100.0000.01771750.0771750.00.00.0
2002810338.0810338.0802234.01.0001.0100.0000.01810338.0810337.00.0-1.0
2003850854.0842346.0833837.01.0101.0200.0100.02850855.0850854.08509.08508.0
2004893397.01010815.0980184.01.0101.0420.0100.041019749.01015920.08934.05105.0
2005938067.01133386.01047955.01.0201.0870.0200.081152147.01123000.018761.0-10386.0
2006984970.01237897.01063768.01.0541.1900.0510.161288130.01221363.050233.0-16534.0
20071034219.01329895.0996544.01.1181.4080.1060.291439522.01296468.0109627.0-33427.0
20081085930.01330264.0651558.01.3172.3810.2410.581591973.01281397.0261709.0-48867.0
\n", + "
" + ], + "text/plain": [ + " Expected Claims Reported Paid CDF Reported CDF Paid % Unreported % Unpaid BF Ultimate (Reported) BF Ultimate (Paid) IBNR (Reported) IBNR (Paid)\n", + "1999 700000.0 700000.0 700000.0 1.000 1.000 0.000 0.00 700000.0 700000.0 0.0 0.0\n", + "2000 735000.0 735000.0 735000.0 1.000 1.000 0.000 0.00 735000.0 735000.0 0.0 0.0\n", + "2001 771750.0 771750.0 764033.0 1.000 1.010 0.000 0.01 771750.0 771750.0 0.0 0.0\n", + "2002 810338.0 810338.0 802234.0 1.000 1.010 0.000 0.01 810338.0 810337.0 0.0 -1.0\n", + "2003 850854.0 842346.0 833837.0 1.010 1.020 0.010 0.02 850855.0 850854.0 8509.0 8508.0\n", + "2004 893397.0 1010815.0 980184.0 1.010 1.042 0.010 0.04 1019749.0 1015920.0 8934.0 5105.0\n", + "2005 938067.0 1133386.0 1047955.0 1.020 1.087 0.020 0.08 1152147.0 1123000.0 18761.0 -10386.0\n", + "2006 984970.0 1237897.0 1063768.0 1.054 1.190 0.051 0.16 1288130.0 1221363.0 50233.0 -16534.0\n", + "2007 1034219.0 1329895.0 996544.0 1.118 1.408 0.106 0.29 1439522.0 1296468.0 109627.0 -33427.0\n", + "2008 1085930.0 1330264.0 651558.0 1.317 2.381 0.241 0.58 1591973.0 1281397.0 261709.0 -48867.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def pp_bf_scenario(sample):\n", + " \"\"\"Recreate a U.S. PP Auto Bornhuetter-Ferguson scenario (Exhibit III).\"\"\"\n", + " tri = cl.load_sample(sample)\n", + " reported = tri[\"Reported Claims\"]\n", + " paid = tri[\"Paid Claims\"]\n", + " years = list(reported.origin.year)\n", + " getcol = lambda t: t.to_frame(origin_as_datetime=False).iloc[:, 0].values\n", + "\n", + " # A priori expected claims: a 70% expected claim ratio on earned premium.\n", + " expected = np.round(0.70 * getcol(tri[\"Earned Premium\"].latest_diagonal))\n", + "\n", + " # Chapter 7 selection: five-year simple average development. CDFs are\n", + " # cumulated from the age-to-age factors, rounded to three decimals, and\n", + " # capped at a minimum of 1.000.\n", + " reported_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(\n", + " cl.Development(n_periods=5, average=\"simple\").fit_transform(reported))\n", + " paid_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(\n", + " cl.Development(n_periods=5, average=\"simple\").fit_transform(paid))\n", + " ages = [int(a) for a in reported.development.values]\n", + " reported_cdf = np.maximum(\n", + " reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0).round(3)\n", + " paid_cdf = np.maximum(\n", + " paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0).round(3)\n", + "\n", + " # Friedland rounds the percentage unreported / unpaid to three decimals; fold\n", + " # those back into an effective CDF so BornhuetterFerguson matches the text.\n", + " pct_unrep = np.round(1 - 1 / reported_cdf, 3)\n", + " pct_unpaid = np.round(1 - 1 / paid_cdf, 3)\n", + " reported_eff = 1.0 / (1.0 - pct_unrep)\n", + " paid_eff = 1.0 / (1.0 - pct_unpaid)\n", + "\n", + " apriori = reported.latest_diagonal.copy()\n", + " apriori.iloc[0, 0] = expected.reshape(apriori.shape)\n", + " reported_pat = cl.DevelopmentConstant(\n", + " patterns=dict(zip(ages, reported_eff)), style=\"cdf\").fit_transform(reported)\n", + " paid_pat = cl.DevelopmentConstant(\n", + " patterns=dict(zip(ages, paid_eff)), style=\"cdf\").fit_transform(paid)\n", + " bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(reported_pat, sample_weight=apriori)\n", + " bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(paid_pat, sample_weight=apriori)\n", + "\n", + " reported_latest = getcol(reported.latest_diagonal)\n", + " paid_latest = getcol(paid.latest_diagonal)\n", + " ult_reported = np.nan_to_num(getcol(bf_reported.ultimate_))\n", + " ult_paid = np.nan_to_num(getcol(bf_paid.ultimate_))\n", + "\n", + " out = pd.DataFrame(index=years)\n", + " out[\"Expected Claims\"] = expected\n", + " out[\"Reported\"] = reported_latest\n", + " out[\"Paid\"] = paid_latest\n", + " out[\"CDF Reported\"] = reported_cdf[::-1]\n", + " out[\"CDF Paid\"] = paid_cdf[::-1]\n", + " out[\"% Unreported\"] = pct_unrep[::-1]\n", + " out[\"% Unpaid\"] = pct_unpaid[::-1]\n", + " out[\"BF Ultimate (Reported)\"] = ult_reported.round(0)\n", + " out[\"BF Ultimate (Paid)\"] = ult_paid.round(0)\n", + " out[\"IBNR (Reported)\"] = (ult_reported - reported_latest).round(0)\n", + " out[\"IBNR (Paid)\"] = (ult_paid - reported_latest).round(0)\n", + " return out\n", + "\n", + "\n", + "pp_scenarios = {\n", + " \"Steady-State\": \"friedland_uspp_auto_steady_state\",\n", + " \"Increasing Claim Ratios\": \"friedland_uspp_auto_increasing_claim\",\n", + " \"Increasing Case Outstanding Strength\": \"friedland_uspp_auto_increasing_case\",\n", + " \"Increasing Claim Ratios and Case Outstanding Strength\": \"friedland_uspp_increasing_claim_case\",\n", + "}\n", + "pp_exhibits = {name: pp_bf_scenario(sample) for name, sample in pp_scenarios.items()}\n", + "for name, table in pp_exhibits.items():\n", + " print(name)\n", + " display(table)" + ] + }, + { + "cell_type": "markdown", + "id": "46d9ca09", + "metadata": {}, + "source": [ + "### Reconciliation to Friedland\n", + "\n", + "We reconcile the estimated IBNR totals to the printed Exhibit III. The reported\n", + "basis is checked for the two scenarios with clean sample data; the paid basis is\n", + "checked for all four scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e8152cba", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-12T08:35:10.973403Z", + "iopub.status.busy": "2026-07-12T08:35:10.972594Z", + "iopub.status.idle": "2026-07-12T08:35:10.981549Z", + "shell.execute_reply": "2026-07-12T08:35:10.980541Z" + } + }, + "outputs": [], + "source": [ + "pp_ibnr = {name: (table[\"IBNR (Reported)\"].sum(), table[\"IBNR (Paid)\"].sum())\n", + " for name, table in pp_exhibits.items()}\n", + "\n", + "# Reported basis - scenarios with clean sample data\n", + "assert abs(pp_ibnr[\"Steady-State\"][0] - 438638) < 5\n", + "assert abs(pp_ibnr[\"Increasing Claim Ratios\"][0] - 438638) < 5\n", + "# Paid basis - all four scenarios\n", + "assert abs(pp_ibnr[\"Steady-State\"][1] - 438638) < 5\n", + "assert abs(pp_ibnr[\"Increasing Claim Ratios\"][1] - 158724) < 5\n", + "assert abs(pp_ibnr[\"Increasing Case Outstanding Strength\"][1] - 253336) < 5\n", + "assert abs(pp_ibnr[\"Increasing Claim Ratios and Case Outstanding Strength\"][1] - (-95600)) < 5" + ] } ], "metadata": {