diff --git a/docs/_toc.yml b/docs/_toc.yml index 87f3af4d..b7e069eb 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -35,6 +35,7 @@ parts: sections: - file: friedland/chapter_6.rst - file: friedland/chapter_7.rst + - file: friedland/chapter_9.ipynb - chapters: - file: gallery/index.md - chapters: diff --git a/docs/friedland/chapter_9.ipynb b/docs/friedland/chapter_9.ipynb new file mode 100644 index 00000000..e74e8171 --- /dev/null +++ b/docs/friedland/chapter_9.ipynb @@ -0,0 +1,1055 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b33bad90", + "metadata": {}, + "source": [ + "# Chapter 9 - Bornhuetter-Ferguson Technique\n", + "\n", + "> The Bornhuetter-Ferguson technique is essentially a blend of the development\n", + "> technique and the expected claims technique.\n", + ">\n", + "> -- Friedland, Chapter 9\n", + "\n", + "The Bornhuetter-Ferguson (BF) method splits ultimate claims into the claims\n", + "already reported (or paid) plus the *expected* unreported (or unpaid) claims. The\n", + "expected piece is an a priori estimate of ultimate claims (from the expected\n", + "claims technique of Chapter 8), scaled by the percentage still to emerge that is\n", + "implied by the development pattern:\n", + "\n", + "$$\\text{Ultimate} = \\text{Actual} + \\text{Expected Claims} \\times \\left(1 - \\frac{1}{\\text{CDF}}\\right)$$\n", + "\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." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "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" + } + }, + "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)" + ] + }, + { + "cell_type": "markdown", + "id": "8d5d61e0", + "metadata": {}, + "source": [ + "## The data\n", + "\n", + "The XYZ Insurer Auto BI reported and paid triangles run from accident year 1998\n", + "to 2008. Their most recent diagonal (12/31/2008) is the actual claims the BF\n", + "method builds on." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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" + } + }, + "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", + "
ReportedPaid
199815822.015822.0
199925107.024817.0
200037246.036782.0
200138798.038519.0
200248169.044437.0
200344373.039320.0
200470288.052811.0
200570655.040026.0
200648804.022819.0
200731732.011865.0
200818632.03409.0
\n", + "
" + ], + "text/plain": [ + " Reported Paid\n", + "1998 15822.0 15822.0\n", + "1999 25107.0 24817.0\n", + "2000 37246.0 36782.0\n", + "2001 38798.0 38519.0\n", + "2002 48169.0 44437.0\n", + "2003 44373.0 39320.0\n", + "2004 70288.0 52811.0\n", + "2005 70655.0 40026.0\n", + "2006 48804.0 22819.0\n", + "2007 31732.0 11865.0\n", + "2008 18632.0 3409.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tri = cl.load_sample(\"friedland_xyz_auto_bi\")\n", + "reported = tri[\"Reported Claims\"]\n", + "paid = tri[\"Paid Claims\"]\n", + "years = list(reported.origin.year)\n", + "\n", + "col = lambda t: t.to_frame(origin_as_datetime=False).iloc[:, 0].values\n", + "reported_latest = col(reported.latest_diagonal)\n", + "paid_latest = col(paid.latest_diagonal)\n", + "\n", + "claims = pd.DataFrame(index=years)\n", + "claims[\"Reported\"] = reported_latest\n", + "claims[\"Paid\"] = paid_latest\n", + "display(claims)" + ] + }, + { + "cell_type": "markdown", + "id": "e58c06e4", + "metadata": {}, + "source": [ + "## Development patterns\n", + "\n", + "The BF method reuses the development pattern selected for XYZ in Chapter 7: a\n", + "volume-weighted two-period average with a 1.000 reported tail and a 1.010 paid\n", + "tail. Following Friedland, the age-to-age factors are rounded to three decimals\n", + "before being cumulated to CDFs. The reported CDFs for the oldest accident years\n", + "fall just below 1.0, so they are capped at 1.0 (this avoids negative implied\n", + "unreported percentages; Friedland notes the cap is not strictly required)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "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" + } + }, + "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", + "
CDF ReportedCDF Paid% Unreported% Unpaid
19981.0001.0100.0000.010
19991.0001.0140.0000.014
20001.0001.0310.0000.030
20011.0001.0540.0000.051
20021.0031.1160.0030.104
20031.0131.2680.0130.211
20041.0641.5250.0600.344
20051.0852.0070.0780.502
20061.1963.1600.1640.684
20071.5126.5690.3390.848
20082.55121.9990.6080.955
\n", + "
" + ], + "text/plain": [ + " CDF Reported CDF Paid % Unreported % Unpaid\n", + "1998 1.000 1.010 0.000 0.010\n", + "1999 1.000 1.014 0.000 0.014\n", + "2000 1.000 1.031 0.000 0.030\n", + "2001 1.000 1.054 0.000 0.051\n", + "2002 1.003 1.116 0.003 0.104\n", + "2003 1.013 1.268 0.013 0.211\n", + "2004 1.064 1.525 0.060 0.344\n", + "2005 1.085 2.007 0.078 0.502\n", + "2006 1.196 3.160 0.164 0.684\n", + "2007 1.512 6.569 0.339 0.848\n", + "2008 2.551 21.999 0.608 0.955" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "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", + "\n", + "# Friedland cumulates CDFs from age-to-age factors rounded to three decimals.\n", + "reported_dev.ldf_ = reported_dev.ldf_.round(3)\n", + "paid_dev.ldf_ = paid_dev.ldf_.round(3)\n", + "\n", + "# CDF to ultimate per accident year (oldest origin -> highest maturity).\n", + "reported_cdf = np.maximum(reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten()[::-1], 1.0)\n", + "paid_cdf = paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten()[::-1]\n", + "pct_unreported = 1 - 1 / reported_cdf\n", + "pct_unpaid = 1 - 1 / paid_cdf\n", + "\n", + "patterns = pd.DataFrame(index=years)\n", + "patterns[\"CDF Reported\"] = reported_cdf.round(3)\n", + "patterns[\"CDF Paid\"] = paid_cdf.round(3)\n", + "patterns[\"% Unreported\"] = pct_unreported.round(3)\n", + "patterns[\"% Unpaid\"] = pct_unpaid.round(3)\n", + "display(patterns)" + ] + }, + { + "cell_type": "markdown", + "id": "f7e34483", + "metadata": {}, + "source": [ + "## Expected claims (a priori)\n", + "\n", + "The a priori expected claims come from the expected claims technique (Chapter 8):\n", + "earned premium multiplied by a selected claim ratio. The earned premium is now\n", + "carried in the `friedland_xyz_auto_bi` sample and read directly from its latest\n", + "diagonal. The expected claims feed the BF method as the `sample_weight`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "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" + } + }, + "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", + "
Earned PremiumClaim RatioExpected Claims
199820000.00.78415670
199931500.00.78324680
200045000.00.78335256
200150000.00.78339174
200261183.00.78347935
200369175.00.78354197
200499322.00.87186528
2005138151.00.783108241
2006107578.00.65870769
200762438.00.63839841
200847797.00.82539429
\n", + "
" + ], + "text/plain": [ + " Earned Premium Claim Ratio Expected Claims\n", + "1998 20000.0 0.784 15670\n", + "1999 31500.0 0.783 24680\n", + "2000 45000.0 0.783 35256\n", + "2001 50000.0 0.783 39174\n", + "2002 61183.0 0.783 47935\n", + "2003 69175.0 0.783 54197\n", + "2004 99322.0 0.871 86528\n", + "2005 138151.0 0.783 108241\n", + "2006 107578.0 0.658 70769\n", + "2007 62438.0 0.638 39841\n", + "2008 47797.0 0.825 39429" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Earned premium is carried in the friedland_xyz_auto_bi sample ($000).\n", + "earned_premium = col(tri[\"Earned Premium\"].latest_diagonal)\n", + "\n", + "# A priori expected claims from the expected claims technique (Chapter 8, $000).\n", + "expected_claims = [15670, 24680, 35256, 39174, 47935, 54197, 86528, 108241, 70769, 39841, 39429]\n", + "\n", + "\n", + "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", + " return d\n", + "\n", + "\n", + "apriori = as_diagonal(reported, expected_claims)\n", + "\n", + "priori = pd.DataFrame(index=years)\n", + "priori[\"Earned Premium\"] = earned_premium\n", + "priori[\"Claim Ratio\"] = (np.array(expected_claims) / np.array(earned_premium)).round(3)\n", + "priori[\"Expected Claims\"] = expected_claims\n", + "display(priori)" + ] + }, + { + "cell_type": "markdown", + "id": "c683f4d9", + "metadata": {}, + "source": [ + "## Projection of ultimate claims\n", + "\n", + "Applying `BornhuetterFerguson` on both the reported and paid bases produces the\n", + "projected ultimate claims. The reported IBNR is floored at zero for the capped\n", + "accident years. This recreates the *Ultimate Claims Projection* exhibit." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "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" + } + }, + "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", + "
ReportedPaidCDF ReportedCDF Paid% Unreported% UnpaidEarned PremiumExpected ClaimsBF Ultimate (Reported)BF Ultimate (Paid)
199815822.015822.01.0001.0100.0000.01020000.01567015822.015977.0
199925107.024817.01.0001.0140.0000.01431500.02468025107.025159.0
200037246.036782.01.0001.0310.0000.03045000.03525637246.037851.0
200138798.038519.01.0001.0540.0000.05150000.03917438798.040525.0
200248169.044437.01.0031.1160.0030.10461183.04793548309.049425.0
200344373.039320.01.0131.2680.0130.21169175.05419745066.050773.0
200470288.052811.01.0641.5250.0600.34499322.08652875462.082612.0
200570655.040026.01.0852.0070.0780.502138151.010824179123.094345.0
200648804.022819.01.1963.1600.1640.684107578.07076960378.071190.0
200731732.011865.01.5126.5690.3390.84862438.03984145229.045641.0
200818632.03409.02.55121.9990.6080.95547797.03942942607.041046.0
\n", + "
" + ], + "text/plain": [ + " Reported Paid CDF Reported CDF Paid % Unreported % Unpaid Earned Premium Expected Claims BF Ultimate (Reported) BF Ultimate (Paid)\n", + "1998 15822.0 15822.0 1.000 1.010 0.000 0.010 20000.0 15670 15822.0 15977.0\n", + "1999 25107.0 24817.0 1.000 1.014 0.000 0.014 31500.0 24680 25107.0 25159.0\n", + "2000 37246.0 36782.0 1.000 1.031 0.000 0.030 45000.0 35256 37246.0 37851.0\n", + "2001 38798.0 38519.0 1.000 1.054 0.000 0.051 50000.0 39174 38798.0 40525.0\n", + "2002 48169.0 44437.0 1.003 1.116 0.003 0.104 61183.0 47935 48309.0 49425.0\n", + "2003 44373.0 39320.0 1.013 1.268 0.013 0.211 69175.0 54197 45066.0 50773.0\n", + "2004 70288.0 52811.0 1.064 1.525 0.060 0.344 99322.0 86528 75462.0 82612.0\n", + "2005 70655.0 40026.0 1.085 2.007 0.078 0.502 138151.0 108241 79123.0 94345.0\n", + "2006 48804.0 22819.0 1.196 3.160 0.164 0.684 107578.0 70769 60378.0 71190.0\n", + "2007 31732.0 11865.0 1.512 6.569 0.339 0.848 62438.0 39841 45229.0 45641.0\n", + "2008 18632.0 3409.0 2.551 21.999 0.608 0.955 47797.0 39429 42607.0 41046.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(reported_dev, sample_weight=apriori)\n", + "bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(paid_dev, sample_weight=apriori)\n", + "\n", + "reported_ibnr = np.nan_to_num(np.maximum(col(bf_reported.ibnr_), 0.0))\n", + "reported_ult = reported_latest + reported_ibnr\n", + "paid_ult = col(bf_paid.ultimate_)\n", + "\n", + "projection = pd.DataFrame(index=years)\n", + "projection[\"Reported\"] = reported_latest\n", + "projection[\"Paid\"] = paid_latest\n", + "projection[\"CDF Reported\"] = reported_cdf.round(3)\n", + "projection[\"CDF Paid\"] = paid_cdf.round(3)\n", + "projection[\"% Unreported\"] = pct_unreported.round(3)\n", + "projection[\"% Unpaid\"] = pct_unpaid.round(3)\n", + "projection[\"Earned Premium\"] = earned_premium\n", + "projection[\"Expected Claims\"] = expected_claims\n", + "projection[\"BF Ultimate (Reported)\"] = reported_ult.round(0)\n", + "projection[\"BF Ultimate (Paid)\"] = paid_ult.round(0)\n", + "display(projection)" + ] + }, + { + "cell_type": "markdown", + "id": "db8655a7", + "metadata": {}, + "source": [ + "## Development of unpaid claim estimate\n", + "\n", + "From the projected ultimates, the IBNR and total unpaid estimates follow by\n", + "simple differences: IBNR is ultimate minus reported claims, and total unpaid is\n", + "ultimate minus paid claims. This recreates the *Unpaid Claims* exhibit." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "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" + } + }, + "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", + "
BF Ultimate (Reported)BF Ultimate (Paid)Case OutstandingIBNR (Reported)IBNR (Paid)Total Unpaid (Reported)Total Unpaid (Paid)
199815822.015977.00.00.0155.00.0155.0
199925107.025159.0290.00.052.0290.0342.0
200037246.037851.0464.00.0605.0464.01069.0
200138798.040525.0279.00.01727.0279.02006.0
200248309.049425.03732.0140.01256.03872.04988.0
200345066.050773.05053.0693.06400.05746.011453.0
200475462.082612.017477.05174.012324.022651.029801.0
200579123.094345.030629.08468.023690.039097.054319.0
200660378.071190.025985.011574.022386.037559.048371.0
200745229.045641.019867.013497.013909.033364.033776.0
200842607.041046.015223.023975.022414.039198.037637.0
\n", + "
" + ], + "text/plain": [ + " BF Ultimate (Reported) BF Ultimate (Paid) Case Outstanding IBNR (Reported) IBNR (Paid) Total Unpaid (Reported) Total Unpaid (Paid)\n", + "1998 15822.0 15977.0 0.0 0.0 155.0 0.0 155.0\n", + "1999 25107.0 25159.0 290.0 0.0 52.0 290.0 342.0\n", + "2000 37246.0 37851.0 464.0 0.0 605.0 464.0 1069.0\n", + "2001 38798.0 40525.0 279.0 0.0 1727.0 279.0 2006.0\n", + "2002 48309.0 49425.0 3732.0 140.0 1256.0 3872.0 4988.0\n", + "2003 45066.0 50773.0 5053.0 693.0 6400.0 5746.0 11453.0\n", + "2004 75462.0 82612.0 17477.0 5174.0 12324.0 22651.0 29801.0\n", + "2005 79123.0 94345.0 30629.0 8468.0 23690.0 39097.0 54319.0\n", + "2006 60378.0 71190.0 25985.0 11574.0 22386.0 37559.0 48371.0\n", + "2007 45229.0 45641.0 19867.0 13497.0 13909.0 33364.0 33776.0\n", + "2008 42607.0 41046.0 15223.0 23975.0 22414.0 39198.0 37637.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "unpaid = pd.DataFrame(index=years)\n", + "unpaid[\"BF Ultimate (Reported)\"] = reported_ult.round(0)\n", + "unpaid[\"BF Ultimate (Paid)\"] = paid_ult.round(0)\n", + "unpaid[\"Case Outstanding\"] = (reported_latest - paid_latest).round(0)\n", + "unpaid[\"IBNR (Reported)\"] = (reported_ult - reported_latest).round(0)\n", + "unpaid[\"IBNR (Paid)\"] = (paid_ult - reported_latest).round(0)\n", + "unpaid[\"Total Unpaid (Reported)\"] = (reported_ult - paid_latest).round(0)\n", + "unpaid[\"Total Unpaid (Paid)\"] = (paid_ult - paid_latest).round(0)\n", + "display(unpaid)" + ] + }, + { + "cell_type": "markdown", + "id": "b135b0ef", + "metadata": {}, + "source": [ + "## Reconciliation to Friedland\n", + "\n", + "The selected CDFs, the projected ultimate claims, and the IBNR estimates are\n", + "reconciled to the printed Chapter 9 exhibit below (values in $000)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "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" + } + }, + "outputs": [], + "source": [ + "# Selected CDFs to ultimate\n", + "assert np.allclose(reported_cdf.round(3),\n", + " [1.000, 1.000, 1.000, 1.000, 1.003, 1.013, 1.064, 1.085, 1.196, 1.512, 2.551], atol=1e-3)\n", + "assert np.allclose(paid_cdf.round(3),\n", + " [1.010, 1.014, 1.031, 1.054, 1.116, 1.268, 1.525, 2.007, 3.160, 6.569, 21.999], atol=1e-3)\n", + "# Projected ultimate claims\n", + "assert np.allclose(reported_ult,\n", + " [15822, 25107, 37246, 38798, 48309, 45066, 75462, 79123, 60378, 45229, 42607], atol=1)\n", + "assert np.allclose(paid_ult,\n", + " [15977, 25159, 37851, 40525, 49425, 50773, 82612, 94345, 71190, 45641, 41046], atol=1)\n", + "# IBNR (ultimate minus reported)\n", + "assert np.allclose(unpaid[\"IBNR (Reported)\"].values,\n", + " [0, 0, 0, 0, 140, 693, 5174, 8468, 11574, 13497, 23975], atol=1)\n", + "assert np.allclose(unpaid[\"IBNR (Paid)\"].values,\n", + " [155, 52, 605, 1727, 1256, 6400, 12324, 23690, 22386, 13909, 22414], atol=1)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "chainladder", + "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.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}