diff --git a/docs/_toc.yml b/docs/_toc.yml
index 14c63ae7..6dc7bba2 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -38,6 +38,7 @@ parts:
- file: friedland/chapter_8.ipynb
- file: friedland/chapter_9.ipynb
- file: friedland/chapter_10.ipynb
+ - file: friedland/chapter_15.ipynb
- chapters:
- file: gallery/index.md
- chapters:
diff --git a/docs/friedland/chapter_15.ipynb b/docs/friedland/chapter_15.ipynb
new file mode 100644
index 00000000..23181a43
--- /dev/null
+++ b/docs/friedland/chapter_15.ipynb
@@ -0,0 +1,653 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "ch15-intro",
+ "metadata": {},
+ "source": [
+ "# Chapter 15 - Evaluation of Techniques\n",
+ "\n",
+ "> We use numerous methodologies for the same examples, not simply for the\n",
+ "> purpose of demonstration, but because actuaries should use more than one\n",
+ "> method when analyzing unpaid claims. No single method can produce the best\n",
+ "> estimate in all situations.\n",
+ ">\n",
+ "> -- Friedland, Chapter 15\n",
+ "\n",
+ "Chapter 15 does not introduce a new estimator. It brings the methods from\n",
+ "Chapters 7 through 14 together and asks whether they agree. This notebook\n",
+ "recreates the two comparison tables that can be built from work already in\n",
+ "the package:\n",
+ "\n",
+ "- **U.S. Industry Auto** — IBNR and total unpaid from Development, Expected\n",
+ " Claims, Bornhuetter-Ferguson, and Cape Cod, valued at 12/31/2007.\n",
+ "- **Changing conditions** — estimated IBNR across the U.S. PP Auto claim-ratio\n",
+ " and case-outstanding scenarios and the U.S. Auto product-mix scenario,\n",
+ " including Benktander.\n",
+ "\n",
+ "The XYZ Insurer Exhibit I comparison, the Berquist-Sherman summaries, and the\n",
+ "DC Insurer monitoring exhibits depend on Chapters 11–13 and are left for a\n",
+ "later slice. Case Outstanding Development is omitted for the same reason.\n",
+ "\n",
+ "Selections follow the earlier Friedland notebooks: Chapter 7 development\n",
+ "patterns (age-to-age factors rounded to three decimals), Chapter 8 expected\n",
+ "claim ratios, and the Chapter 9 device of folding a rounded percent unreported\n",
+ "back into an effective CDF so `BornhuetterFerguson` and `Benktander` match the\n",
+ "text."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "ch15-imports",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-16T08:19:23.216504Z",
+ "iopub.status.busy": "2026-08-16T08:19:23.216235Z",
+ "iopub.status.idle": "2026-08-16T08:19:28.116403Z",
+ "shell.execute_reply": "2026-08-16T08:19:28.114676Z"
+ }
+ },
+ "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 col(triangle):\n",
+ " \"\"\"Pull a 1-column triangle (latest diagonal, ultimate, IBNR) as a vector.\"\"\"\n",
+ " return triangle.to_frame(origin_as_datetime=False).iloc[:, 0].values\n",
+ "\n",
+ "\n",
+ "def as_apriori(triangle, values):\n",
+ " \"\"\"Broadcast a per-origin expected-claims vector onto a sample_weight triangle.\"\"\"\n",
+ " apriori = triangle.latest_diagonal.copy()\n",
+ " apriori.iloc[0, 0] = np.asarray(values, dtype=float).reshape(apriori.shape)\n",
+ " return apriori\n",
+ "\n",
+ "\n",
+ "def rounded_ldf_dev(triangle, n_periods, tail=1.0):\n",
+ " \"\"\"Chapter 7/10 selection: n-period simple average, constant tail, LDF rounded to 3dp.\"\"\"\n",
+ " dev = cl.TailConstant(tail=tail, projection_period=0).fit_transform(\n",
+ " cl.Development(n_periods=n_periods, average=\"simple\").fit_transform(triangle)\n",
+ " )\n",
+ " dev.ldf_ = dev.ldf_.round(3)\n",
+ " return dev\n",
+ "\n",
+ "\n",
+ "def _simple_dev(triangle, n_periods, tail=1.0):\n",
+ " return cl.TailConstant(tail=tail, projection_period=0).fit_transform(\n",
+ " cl.Development(n_periods=n_periods, average=\"simple\").fit_transform(triangle)\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def rounded_cdf_dev(triangle, n_periods, tail=1.0):\n",
+ " \"\"\"Apply a CDF rounded to three decimals via DevelopmentConstant.\"\"\"\n",
+ " dev = _simple_dev(triangle, n_periods, tail=tail)\n",
+ " ages = [int(age) for age in triangle.development.values]\n",
+ " cdf = np.maximum(\n",
+ " dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0\n",
+ " ).round(3)\n",
+ " return cl.DevelopmentConstant(\n",
+ " patterns=dict(zip(ages, cdf)), style=\"cdf\"\n",
+ " ).fit_transform(triangle)\n",
+ "\n",
+ "\n",
+ "def bf_style_dev(triangle, n_periods, tail=1.0):\n",
+ " \"\"\"Chapter 9: fold a rounded percent unreported / unpaid back into an effective CDF.\"\"\"\n",
+ " dev = _simple_dev(triangle, n_periods, tail=tail)\n",
+ " ages = [int(age) for age in triangle.development.values]\n",
+ " cdf = np.maximum(\n",
+ " dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0\n",
+ " ).round(3)\n",
+ " pct = np.round(1 - 1 / cdf, 3)\n",
+ " effective = 1.0 / (1.0 - pct)\n",
+ " return cl.DevelopmentConstant(\n",
+ " patterns=dict(zip(ages, effective)), style=\"cdf\"\n",
+ " ).fit_transform(triangle)\n",
+ "\n",
+ "\n",
+ "def ibnr_from_ultimate(ultimate, reported):\n",
+ " \"\"\"IBNR is ultimate minus reported, including for paid-basis methods.\"\"\"\n",
+ " return float(np.nansum(\n",
+ " np.nan_to_num(col(ultimate)) - col(reported.latest_diagonal)\n",
+ " ))\n",
+ "\n",
+ "\n",
+ "def unpaid_from_ultimate(ultimate, paid):\n",
+ " return float(np.nansum(\n",
+ " np.nan_to_num(col(ultimate)) - col(paid.latest_diagonal)\n",
+ " ))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ch15-ia-md",
+ "metadata": {},
+ "source": [
+ "## U.S. Industry Auto\n",
+ "\n",
+ "For the consolidated U.S. private passenger automobile portfolio the methods\n",
+ "agree, as the text expects given the volume of business. The table below is\n",
+ "the Chapter 15 summary of estimated unpaid claims as of 12/31/2007, in\n",
+ "billions of dollars.\n",
+ "\n",
+ "Development, Expected Claims, and Bornhuetter-Ferguson reuse the Chapter 8\n",
+ "selected CDFs and the Chapter 8 selected claim ratios (75% for 1998–2002,\n",
+ "65% for 2003–2007). Cape Cod reuses the Chapter 7 three-year simple-average\n",
+ "reported pattern with a 1.000 tail, rounding the age-to-age factors to three\n",
+ "decimals before `CapeCod` derives the all-years claim ratio."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "ch15-ia-fit",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-16T08:19:28.120091Z",
+ "iopub.status.busy": "2026-08-16T08:19:28.119707Z",
+ "iopub.status.idle": "2026-08-16T08:19:28.330310Z",
+ "shell.execute_reply": "2026-08-16T08:19:28.329566Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " IBNR | \n",
+ " Total | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | Development – Reported | \n",
+ " 26 | \n",
+ " 71 | \n",
+ "
\n",
+ " \n",
+ " | Development – Paid | \n",
+ " 29 | \n",
+ " 74 | \n",
+ "
\n",
+ " \n",
+ " | Expected Claims | \n",
+ " 26 | \n",
+ " 71 | \n",
+ "
\n",
+ " \n",
+ " | Bornhuetter-Ferguson – Reported | \n",
+ " 26 | \n",
+ " 71 | \n",
+ "
\n",
+ " \n",
+ " | Bornhuetter-Ferguson – Paid | \n",
+ " 27 | \n",
+ " 73 | \n",
+ "
\n",
+ " \n",
+ " | Cape Cod | \n",
+ " 27 | \n",
+ " 73 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " IBNR Total\n",
+ "Development – Reported 26 71\n",
+ "Development – Paid 29 74\n",
+ "Expected Claims 26 71\n",
+ "Bornhuetter-Ferguson – Reported 26 71\n",
+ "Bornhuetter-Ferguson – Paid 27 73\n",
+ "Cape Cod 27 73"
+ ]
+ },
+ "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_premium = ia[\"Earned Premium\"].latest_diagonal\n",
+ "\n",
+ "# Chapter 8 selected CDFs (three-year simple average, rounded, with tails).\n",
+ "ia_reported_pattern = {\n",
+ " 12: 1.292, 24: 1.110, 36: 1.051, 48: 1.023, 60: 1.011,\n",
+ " 72: 1.006, 84: 1.003, 96: 1.001, 108: 1.000, 120: 1.000,\n",
+ "}\n",
+ "ia_paid_pattern = {\n",
+ " 12: 2.390, 24: 1.404, 36: 1.184, 48: 1.085, 60: 1.040,\n",
+ " 72: 1.020, 84: 1.011, 96: 1.006, 108: 1.004, 120: 1.002,\n",
+ "}\n",
+ "ia_rep_dev = cl.DevelopmentConstant(\n",
+ " patterns=ia_reported_pattern, style=\"cdf\"\n",
+ ").fit_transform(ia_reported)\n",
+ "ia_paid_dev = cl.DevelopmentConstant(\n",
+ " patterns=ia_paid_pattern, style=\"cdf\"\n",
+ ").fit_transform(ia_paid)\n",
+ "\n",
+ "ia_cl_reported = cl.Chainladder().fit(ia_rep_dev)\n",
+ "ia_cl_paid = cl.Chainladder().fit(ia_paid_dev)\n",
+ "\n",
+ "# Chapter 8 selected claim ratios applied to earned premium.\n",
+ "ia_claim_ratio = np.array(\n",
+ " [0.75, 0.75, 0.75, 0.75, 0.75, 0.65, 0.65, 0.65, 0.65, 0.65]\n",
+ ")\n",
+ "ia_el = cl.ExpectedLoss(apriori=1).fit(\n",
+ " ia_reported,\n",
+ " sample_weight=ia_premium * ia_claim_ratio.reshape(1, 1, -1, 1),\n",
+ ")\n",
+ "\n",
+ "# Chapter 8 expected claims carried into the Chapter 9 BF projection.\n",
+ "ia_expected = np.array(\n",
+ " [51430657, 51408736, 51680983, 54408716, 59421665,\n",
+ " 56318302, 59646290, 61174953, 61926981, 61864556],\n",
+ " dtype=float,\n",
+ ")\n",
+ "ia_apriori = as_apriori(ia_reported, ia_expected)\n",
+ "ia_bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(\n",
+ " ia_rep_dev, sample_weight=ia_apriori\n",
+ ")\n",
+ "ia_bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(\n",
+ " ia_paid_dev, sample_weight=ia_apriori\n",
+ ")\n",
+ "\n",
+ "ia_cc_dev = rounded_ldf_dev(ia_reported, n_periods=3, tail=1.000)\n",
+ "ia_cc = cl.CapeCod().fit(ia_cc_dev, sample_weight=ia_premium)\n",
+ "\n",
+ "ia_methods = {\n",
+ " \"Development – Reported\": ia_cl_reported.ultimate_,\n",
+ " \"Development – Paid\": ia_cl_paid.ultimate_,\n",
+ " \"Expected Claims\": ia_el.ultimate_,\n",
+ " \"Bornhuetter-Ferguson – Reported\": ia_bf_reported.ultimate_,\n",
+ " \"Bornhuetter-Ferguson – Paid\": ia_bf_paid.ultimate_,\n",
+ " \"Cape Cod\": ia_cc.ultimate_,\n",
+ "}\n",
+ "\n",
+ "ia_raw = pd.DataFrame({\n",
+ " method: {\n",
+ " \"IBNR\": ibnr_from_ultimate(ultimate, ia_reported),\n",
+ " \"Total Unpaid\": unpaid_from_ultimate(ultimate, ia_paid),\n",
+ " }\n",
+ " for method, ultimate in ia_methods.items()\n",
+ "}).T\n",
+ "\n",
+ "# Friedland prints this table in $ billions (the sample is in $000).\n",
+ "ia_billions = (ia_raw / 1e6).round(0).astype(int)\n",
+ "ia_billions.columns = [\"IBNR\", \"Total\"]\n",
+ "display(ia_billions)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ch15-ia-recon-md",
+ "metadata": {},
+ "source": [
+ "### Reconciliation to Friedland\n",
+ "\n",
+ "The printed Chapter 15 Industry Auto table is in billions of dollars. After\n",
+ "converting the $000 sample totals, every method rounds to the published IBNR\n",
+ "and total unpaid. Case Outstanding Development (printed 24 / 70) is omitted\n",
+ "until Chapter 12 is available."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "ch15-ia-assert",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-16T08:19:28.333807Z",
+ "iopub.status.busy": "2026-08-16T08:19:28.333308Z",
+ "iopub.status.idle": "2026-08-16T08:19:28.338574Z",
+ "shell.execute_reply": "2026-08-16T08:19:28.337699Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "ia_printed = pd.DataFrame(\n",
+ " {\n",
+ " \"IBNR\": [26, 29, 26, 26, 27, 27],\n",
+ " \"Total\": [71, 74, 71, 71, 73, 73],\n",
+ " },\n",
+ " index=ia_billions.index,\n",
+ ")\n",
+ "assert (ia_billions - ia_printed).abs().max().max() <= 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ch15-cc-md",
+ "metadata": {},
+ "source": [
+ "## Changing Conditions\n",
+ "\n",
+ "Chapters 7 through 10 run the same methods through four U.S. PP Auto\n",
+ "environments and a combined private-passenger / commercial auto portfolio.\n",
+ "When the portfolio is in a steady state every method recovers the true IBNR.\n",
+ "When claim ratios, case outstanding strength, or product mix change, the\n",
+ "methods diverge. Chapter 15 summarises that divergence.\n",
+ "\n",
+ "The first line of the table is the true IBNR required in each scenario (from\n",
+ "the Chapter 8 exhibits). The remaining rows are the IBNR implied by each\n",
+ "technique: ultimate minus reported, including for paid-basis methods.\n",
+ "\n",
+ "U.S. PP Auto uses a 70% expected claim ratio and a five-year simple-average\n",
+ "development selection. The product-mix scenario uses a 75% expected claim\n",
+ "ratio on the same five-year selection. Benktander is the two-iteration form\n",
+ "(`n_iters=2`), which sits between Bornhuetter-Ferguson and chain ladder."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "ch15-cc-fit",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-16T08:19:28.341911Z",
+ "iopub.status.busy": "2026-08-16T08:19:28.341654Z",
+ "iopub.status.idle": "2026-08-16T08:19:29.690253Z",
+ "shell.execute_reply": "2026-08-16T08:19:29.688807Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Increasing Claim Ratios | \n",
+ " Increasing Case Outstanding Strength | \n",
+ " Increasing Claim Ratios and Case Outstanding Strength | \n",
+ " Changing Product Mix | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | True IBNR | \n",
+ " 602 | \n",
+ " 253 | \n",
+ " 348 | \n",
+ " 2391 | \n",
+ "
\n",
+ " \n",
+ " | Development – Reported | \n",
+ " 602 | \n",
+ " 499 | \n",
+ " 687 | \n",
+ " 2146 | \n",
+ "
\n",
+ " \n",
+ " | Development – Paid | \n",
+ " 601 | \n",
+ " 252 | \n",
+ " 347 | \n",
+ " 1702 | \n",
+ "
\n",
+ " \n",
+ " | Expected Claims | \n",
+ " -843 | \n",
+ " 253 | \n",
+ " -1097 | \n",
+ " 2167 | \n",
+ "
\n",
+ " \n",
+ " | Bornhuetter-Ferguson – Reported | \n",
+ " 439 | \n",
+ " 458 | \n",
+ " 458 | \n",
+ " 2165 | \n",
+ "
\n",
+ " \n",
+ " | Bornhuetter-Ferguson – Paid | \n",
+ " 159 | \n",
+ " 253 | \n",
+ " -96 | \n",
+ " 1980 | \n",
+ "
\n",
+ " \n",
+ " | Benktander – Reported | \n",
+ " 573 | \n",
+ " 491 | \n",
+ " 644 | \n",
+ " 2154 | \n",
+ "
\n",
+ " \n",
+ " | Benktander – Paid | \n",
+ " 406 | \n",
+ " 253 | \n",
+ " 151 | \n",
+ " 1876 | \n",
+ "
\n",
+ " \n",
+ " | Cape Cod | \n",
+ " 507 | \n",
+ " 465 | \n",
+ " 538 | \n",
+ " 2166 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Increasing Claim Ratios Increasing Case Outstanding Strength Increasing Claim Ratios and Case Outstanding Strength Changing Product Mix\n",
+ "True IBNR 602 253 348 2391\n",
+ "Development – Reported 602 499 687 2146\n",
+ "Development – Paid 601 252 347 1702\n",
+ "Expected Claims -843 253 -1097 2167\n",
+ "Bornhuetter-Ferguson – Reported 439 458 458 2165\n",
+ "Bornhuetter-Ferguson – Paid 159 253 -96 1980\n",
+ "Benktander – Reported 573 491 644 2154\n",
+ "Benktander – Paid 406 253 151 1876\n",
+ "Cape Cod 507 465 538 2166"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "def scenario_ibnr(triangle, claim_ratio, n_periods=5):\n",
+ " \"\"\"IBNR for each Chapter 15 technique on one changing-conditions portfolio.\"\"\"\n",
+ " reported = triangle[\"Reported Claims\"]\n",
+ " paid = triangle[\"Paid Claims\"]\n",
+ " premium = triangle[\"Earned Premium\"].latest_diagonal\n",
+ " expected = np.round(claim_ratio * col(premium))\n",
+ " apriori = as_apriori(reported, expected)\n",
+ "\n",
+ " cl_reported = cl.Chainladder().fit(rounded_cdf_dev(reported, n_periods))\n",
+ " cl_paid = cl.Chainladder().fit(rounded_cdf_dev(paid, n_periods))\n",
+ " el = cl.ExpectedLoss(apriori=claim_ratio).fit(\n",
+ " reported, sample_weight=np.round(premium, 0)\n",
+ " )\n",
+ " bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(\n",
+ " bf_style_dev(reported, n_periods), sample_weight=apriori\n",
+ " )\n",
+ " bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(\n",
+ " bf_style_dev(paid, n_periods), sample_weight=apriori\n",
+ " )\n",
+ " bk_reported = cl.Benktander(apriori=1.0, n_iters=2).fit(\n",
+ " bf_style_dev(reported, n_periods), sample_weight=apriori\n",
+ " )\n",
+ " bk_paid = cl.Benktander(apriori=1.0, n_iters=2).fit(\n",
+ " bf_style_dev(paid, n_periods), sample_weight=apriori\n",
+ " )\n",
+ " cc = cl.CapeCod().fit(\n",
+ " rounded_ldf_dev(reported, n_periods), sample_weight=premium\n",
+ " )\n",
+ " return {\n",
+ " \"Development – Reported\": ibnr_from_ultimate(cl_reported.ultimate_, reported),\n",
+ " \"Development – Paid\": ibnr_from_ultimate(cl_paid.ultimate_, reported),\n",
+ " \"Expected Claims\": ibnr_from_ultimate(el.ultimate_, reported),\n",
+ " \"Bornhuetter-Ferguson – Reported\": ibnr_from_ultimate(\n",
+ " bf_reported.ultimate_, reported\n",
+ " ),\n",
+ " \"Bornhuetter-Ferguson – Paid\": ibnr_from_ultimate(bf_paid.ultimate_, reported),\n",
+ " \"Benktander – Reported\": ibnr_from_ultimate(bk_reported.ultimate_, reported),\n",
+ " \"Benktander – Paid\": ibnr_from_ultimate(bk_paid.ultimate_, reported),\n",
+ " \"Cape Cod\": ibnr_from_ultimate(cc.ultimate_, reported),\n",
+ " }\n",
+ "\n",
+ "\n",
+ "pp_scenarios = {\n",
+ " \"Increasing Claim Ratios\": (\n",
+ " cl.load_sample(\"friedland_uspp_auto_increasing_claim\"), 0.70, 601984,\n",
+ " ),\n",
+ " \"Increasing Case Outstanding Strength\": (\n",
+ " cl.load_sample(\"friedland_uspp_auto_increasing_case\"), 0.70, 253336,\n",
+ " ),\n",
+ " \"Increasing Claim Ratios and Case Outstanding Strength\": (\n",
+ " cl.load_sample(\"friedland_uspp_increasing_claim_case\"), 0.70, 347660,\n",
+ " ),\n",
+ "}\n",
+ "us_auto = cl.load_sample(\"friedland_us_auto\")\n",
+ "\n",
+ "cc_raw = {}\n",
+ "for name, (triangle, claim_ratio, true_ibnr) in pp_scenarios.items():\n",
+ " row = scenario_ibnr(triangle, claim_ratio)\n",
+ " row[\"True IBNR\"] = true_ibnr\n",
+ " cc_raw[name] = row\n",
+ "\n",
+ "mix_row = scenario_ibnr(us_auto.loc[\"Changing Product Mix\"], 0.75)\n",
+ "mix_row[\"True IBNR\"] = 2391084\n",
+ "cc_raw[\"Changing Product Mix\"] = mix_row\n",
+ "\n",
+ "method_order = [\n",
+ " \"True IBNR\",\n",
+ " \"Development – Reported\",\n",
+ " \"Development – Paid\",\n",
+ " \"Expected Claims\",\n",
+ " \"Bornhuetter-Ferguson – Reported\",\n",
+ " \"Bornhuetter-Ferguson – Paid\",\n",
+ " \"Benktander – Reported\",\n",
+ " \"Benktander – Paid\",\n",
+ " \"Cape Cod\",\n",
+ "]\n",
+ "# Friedland prints this table in thousands of the $000 sample (nearest unit).\n",
+ "cc_table = (\n",
+ " pd.DataFrame(cc_raw).T[method_order].T / 1000\n",
+ ").round(0).astype(int)\n",
+ "display(cc_table)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ch15-cc-recon-md",
+ "metadata": {},
+ "source": [
+ "### Reconciliation to Friedland\n",
+ "\n",
+ "The Increasing Claim Ratios column uses the same clean sample as Chapters 8–10\n",
+ "and reconciles to the printed table. The two case-outstanding columns and the\n",
+ "Changing Product Mix column are checked with a relative tolerance. Chapters 9\n",
+ "and 10 already note that `friedland_uspp_auto_increasing_case`,\n",
+ "`friedland_uspp_increasing_claim_case`, and the combined-auto sample differ\n",
+ "slightly from the text; the same gap appears here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "ch15-cc-assert",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-16T08:19:29.693404Z",
+ "iopub.status.busy": "2026-08-16T08:19:29.692872Z",
+ "iopub.status.idle": "2026-08-16T08:19:29.701995Z",
+ "shell.execute_reply": "2026-08-16T08:19:29.701175Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "cc_printed = pd.DataFrame(\n",
+ " {\n",
+ " \"Increasing Claim Ratios\": [602, 602, 602, -843, 439, 159, 573, 406, 506],\n",
+ " \"Increasing Case Outstanding Strength\": [253, 501, 253, 253, 458, 253, 492, 253, 470],\n",
+ " \"Increasing Claim Ratios and Case Outstanding Strength\": [\n",
+ " 348, 694, 348, -1097, 460, -96, 648, 151, 546,\n",
+ " ],\n",
+ " \"Changing Product Mix\": [2391, 2153, 1723, 2167, 2168, 1991, 2159, 1893, 2168],\n",
+ " },\n",
+ " index=method_order,\n",
+ ")\n",
+ "\n",
+ "# Clean column: every method rounds to the printed IBNR.\n",
+ "assert (\n",
+ " cc_table[\"Increasing Claim Ratios\"] - cc_printed[\"Increasing Claim Ratios\"]\n",
+ ").abs().max() <= 2\n",
+ "\n",
+ "# Remaining columns: Chapters 9 and 10 already note that the case-outstanding\n",
+ "# and product-mix samples differ slightly from the text.\n",
+ "other = [c for c in cc_printed.columns if c != \"Increasing Claim Ratios\"]\n",
+ "assert np.allclose(\n",
+ " cc_table[other].astype(float), cc_printed[other].astype(float), rtol=0.03, atol=10\n",
+ ")"
+ ]
+ }
+ ],
+ "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
+}