Chapter 10 - Cape Cod Technique#
The key innovation of the Stanard-Buhlmann (Cape Cod) method is that the ultimate expected loss ratio for all years combined is estimated from the overall reported claims experience, instead of being selected judgmentally, as in the Bornhuetter-Ferguson method.
– Friedland, Chapter 10
The Cape Cod (Stanard-Buhlmann) method is a close cousin of the Bornhuetter-Ferguson technique. It splits ultimate claims into the claims already reported (or paid) plus the expected unreported (or unpaid) claims:
The only difference from Bornhuetter-Ferguson is the source of the expected claims. Rather than selecting an a priori claim ratio judgmentally, Cape Cod derives it from the reported experience. The all-years-combined expected claim ratio is the reported claims divided by the used-up premium - the earned premium scaled by the percentage of claims expected to be reported to date:
In the chainladder package the method is implemented by CapeCod, which takes
the exposure (earned premium) through sample_weight and returns the derived
claim ratio in apriori_. This chapter recreates the Friedland Chapter 10
exhibits, reusing the development patterns selected in Chapter 7:
Exhibit I - U.S. Industry Auto
Exhibit II - XYZ Insurer (Auto BI), with on-level premium and adjusted claims
Exhibit III - U.S. PP Auto (impact of changing conditions)
import numpy as np
import pandas as pd
import chainladder as cl
import os
from IPython.display import display
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 1000)
# Pull the single-column latest diagonal (or any 1x1xNx1 triangle) as a vector.
col = lambda t: t.to_frame(origin_as_datetime=False).iloc[:, 0].values
Exhibit I - U.S. Industry Auto#
The text works the Cape Cod method first for U.S. Industry Auto (Exhibit I), valued at 12/31/2007 over accident years 1998-2007. The reporting pattern is the Chapter 7 selection (three-year simple average with a 1.000 reported tail).
Development of expected claim ratio#
This recreates Exhibit I, Sheet 1. The used-up premium is the earned
premium multiplied by the percentage reported (the inverse of the cumulative
development factor). Dividing reported claims by used-up premium gives each
year’s estimated claim ratio; the all-years-combined ratio is the CapeCod
apriori that drives the projection.
ia = cl.load_sample("friedland_us_industry_auto")
ia_reported = ia["Reported Claims"]
ia_paid = ia["Paid Claims"]
ia_premium = ia["Earned Premium"].latest_diagonal
ia_years = list(ia_reported.origin.year)
# Chapter 7 selection: three-year simple average reported development, 1.000 tail.
# Friedland cumulates the age-to-age factors rounded to three decimals.
ia_reported_dev = cl.TailConstant(tail=1.000, projection_period=0).fit_transform(
cl.Development(n_periods=3, average="simple").fit_transform(ia_reported))
ia_reported_dev.ldf_ = ia_reported_dev.ldf_.round(3)
# CapeCod derives the expected claim ratio (apriori) from the reported claims and
# the earned-premium exposure. With the default decay=1 and trend=0, every origin
# receives the same used-up-premium-weighted claim ratio.
ia_cc = cl.CapeCod().fit(ia_reported_dev, sample_weight=ia_premium)
ia_apriori = float(ia_cc.apriori_.to_frame().iloc[0, 0])
# model_diagnostics packages Latest, CDF, Ultimate, and IBNR by accident year.
ia_diag = cl.model_diagnostics(ia_cc)
ia_cdf = col(ia_diag["CDF"])
ia_pct_reported = 1 / ia_cdf
ia_reported_latest = col(ia_diag["Latest"])
ia_used_up = col(ia_premium) * ia_pct_reported
sheet1 = pd.DataFrame(index=ia_years)
sheet1["Earned Premium"] = col(ia_premium)
sheet1["Age"] = [(2007 - y) * 12 + 12 for y in ia_years]
sheet1["Reported Claims"] = ia_reported_latest
sheet1["CDF to Ultimate"] = ia_cdf.round(3)
sheet1["% Reported"] = ia_pct_reported.round(3)
sheet1["Used-Up Premium"] = ia_used_up.round(0)
sheet1["Estimated Claim Ratio"] = (ia_reported_latest / ia_used_up).round(3)
display(sheet1)
print(f"All-years estimated claim ratio (apriori): {ia_apriori:.3f}")
| Earned Premium | Age | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | |
|---|---|---|---|---|---|---|---|
| 1998 | 68574209.0 | 120 | 47742304.0 | 1.000 | 1.000 | 68574209.0 | 0.696 |
| 1999 | 68544981.0 | 108 | 51185767.0 | 1.000 | 1.000 | 68544981.0 | 0.747 |
| 2000 | 68907977.0 | 96 | 54837929.0 | 1.001 | 0.999 | 68839138.0 | 0.797 |
| 2001 | 72544955.0 | 84 | 56299562.0 | 1.003 | 0.997 | 72327827.0 | 0.778 |
| 2002 | 79228887.0 | 72 | 58592712.0 | 1.006 | 0.994 | 78755487.0 | 0.744 |
| 2003 | 86643542.0 | 60 | 57565344.0 | 1.011 | 0.989 | 85697352.0 | 0.672 |
| 2004 | 91763523.0 | 48 | 56976657.0 | 1.023 | 0.977 | 89685198.0 | 0.635 |
| 2005 | 94115312.0 | 36 | 56786410.0 | 1.051 | 0.952 | 89565455.0 | 0.634 |
| 2006 | 95272279.0 | 24 | 54641339.0 | 1.110 | 0.901 | 85858419.0 | 0.636 |
| 2007 | 95176240.0 | 12 | 48853563.0 | 1.292 | 0.774 | 73687173.0 | 0.663 |
All-years estimated claim ratio (apriori): 0.695
Projection of ultimate claims#
This recreates Exhibit I, Sheet 2. Expected claims are the earned premium times
the derived claim ratio. The expected unreported claims (CapeCod.ibnr_) are the
expected claims scaled by the percentage still to emerge, and the projected
ultimate is reported claims plus expected unreported.
ia_expected_claims = col(ia_premium) * ia_apriori
ia_pct_unreported = 1 - ia_pct_reported
# CapeCod returns NaN IBNR for fully developed years (CDF = 1.000); the text
# shows these as zero, so coerce NaN to 0.
ia_ibnr = np.nan_to_num(col(ia_diag["IBNR"]))
ia_expected_unreported = ia_ibnr
ia_ult = col(ia_diag["Ultimate"])
sheet2 = pd.DataFrame(index=ia_years)
sheet2["Earned Premium"] = col(ia_premium)
sheet2["Expected Claim Ratio"] = round(ia_apriori, 3)
sheet2["Expected Claims"] = ia_expected_claims.round(0)
sheet2["CDF to Ultimate"] = ia_cdf.round(3)
sheet2["% Unreported"] = ia_pct_unreported.round(3)
sheet2["Expected Unreported"] = ia_expected_unreported.round(0)
sheet2["Reported Claims"] = ia_reported_latest
sheet2["Projected Ultimate"] = ia_ult.round(0)
display(sheet2)
| Earned Premium | Expected Claim Ratio | Expected Claims | CDF to Ultimate | % Unreported | Expected Unreported | Reported Claims | Projected Ultimate | |
|---|---|---|---|---|---|---|---|---|
| 1998 | 68574209.0 | 0.695 | 47686679.0 | 1.000 | 0.000 | 0.0 | 47742304.0 | 47742304.0 |
| 1999 | 68544981.0 | 0.695 | 47666354.0 | 1.000 | 0.000 | 0.0 | 51185767.0 | 51185767.0 |
| 2000 | 68907977.0 | 0.695 | 47918782.0 | 1.001 | 0.001 | 47871.0 | 54837929.0 | 54885800.0 |
| 2001 | 72544955.0 | 0.695 | 50447946.0 | 1.003 | 0.003 | 150991.0 | 56299562.0 | 56450553.0 |
| 2002 | 79228887.0 | 0.695 | 55095969.0 | 1.006 | 0.006 | 329203.0 | 58592712.0 | 58921915.0 |
| 2003 | 86643542.0 | 0.695 | 60252139.0 | 1.011 | 0.011 | 657983.0 | 57565344.0 | 58223327.0 |
| 2004 | 91763523.0 | 0.695 | 63812587.0 | 1.023 | 0.023 | 1445272.0 | 56976657.0 | 58421929.0 |
| 2005 | 94115312.0 | 0.695 | 65448027.0 | 1.051 | 0.048 | 3163982.0 | 56786410.0 | 59950392.0 |
| 2006 | 95272279.0 | 0.695 | 66252584.0 | 1.110 | 0.099 | 6546422.0 | 54641339.0 | 61187761.0 |
| 2007 | 95176240.0 | 0.695 | 66185799.0 | 1.292 | 0.226 | 14943552.0 | 48853563.0 | 63797115.0 |
Development of unpaid claim estimate#
This recreates Exhibit I, Sheet 3. Case outstanding is reported minus paid, estimated IBNR is ultimate minus reported, and the total unpaid estimate is ultimate minus paid.
ia_paid_latest = col(ia_paid.latest_diagonal)
sheet3 = pd.DataFrame(index=ia_years)
sheet3["Reported Claims"] = ia_reported_latest
sheet3["Paid Claims"] = ia_paid_latest
sheet3["Projected Ultimate"] = ia_ult.round(0)
sheet3["Case Outstanding"] = (ia_reported_latest - ia_paid_latest).round(0)
sheet3["IBNR"] = ia_ibnr.round(0)
sheet3["Total Unpaid"] = (ia_ult - ia_paid_latest).round(0)
display(sheet3)
| Reported Claims | Paid Claims | Projected Ultimate | Case Outstanding | IBNR | Total Unpaid | |
|---|---|---|---|---|---|---|
| 1998 | 47742304.0 | 47644187.0 | 47742304.0 | 98117.0 | 0.0 | 98117.0 |
| 1999 | 51185767.0 | 51000534.0 | 51185767.0 | 185233.0 | 0.0 | 185233.0 |
| 2000 | 54837929.0 | 54533225.0 | 54885800.0 | 304704.0 | 47871.0 | 352575.0 |
| 2001 | 56299562.0 | 55878421.0 | 56450553.0 | 421141.0 | 150991.0 | 572132.0 |
| 2002 | 58592712.0 | 57807215.0 | 58921915.0 | 785497.0 | 329203.0 | 1114700.0 |
| 2003 | 57565344.0 | 55930654.0 | 58223327.0 | 1634690.0 | 657983.0 | 2292673.0 |
| 2004 | 56976657.0 | 53774672.0 | 58421929.0 | 3201985.0 | 1445272.0 | 4647257.0 |
| 2005 | 56786410.0 | 50644994.0 | 59950392.0 | 6141416.0 | 3163982.0 | 9305398.0 |
| 2006 | 54641339.0 | 43606497.0 | 61187761.0 | 11034842.0 | 6546422.0 | 17581264.0 |
| 2007 | 48853563.0 | 27229969.0 | 63797115.0 | 21623594.0 | 14943552.0 | 36567146.0 |
Reconciliation to Friedland#
The derived claim ratio, the projected ultimate claims, and the estimated IBNR are reconciled to the printed Exhibit I below. Because the text derives used-up premium from CDFs rounded to three decimals, the totals reconcile within a small relative tolerance.
# Exhibit I, Sheet 1 - all-years estimated claim ratio
assert np.isclose(ia_apriori, 0.695, atol=1e-3)
# Exhibit I, Sheet 1 - selected CDFs to ultimate
assert np.allclose(ia_cdf.round(3),
[1.000, 1.000, 1.001, 1.003, 1.006, 1.011, 1.023, 1.051, 1.110, 1.292], atol=1e-3)
# Exhibit I, Sheet 2 - projected ultimate claims
assert np.isclose(ia_ult.sum(), 570800677, rtol=5e-3)
# Exhibit I, Sheet 3 - estimated IBNR
assert np.isclose(ia_ibnr.sum(), 27319090, rtol=5e-3)
Exhibit II - XYZ Insurer (Auto BI)#
Exhibit II applies the Cape Cod method to the XYZ Insurer - Auto BI data, valued at 12/31/2008 over accident years 1998-2008. Unlike U.S. Industry Auto, the text has detailed rate-change and legal-reform information, so it adjusts both premium and claims before deriving the claim ratio:
On-level premium: earned premium restated to the 2008 rate level.
Adjusted claims: reported claims trended for pure-premium inflation and adjusted for tort reform.
The all-years adjusted claim ratio is then detrended back to each accident year’s rate, inflation, and tort environment to give the expected claim ratios used in the projection.
Development of expected claim ratio#
This recreates Exhibit II, Sheet 1. The on-level and tort-reform factors are
produced by ParallelogramOLF from the text’s rate-change and reform histories.
The all-years apriori is then derived through a Pipeline that applies the
tort-reform on-level factors, develops the reported claims on the floored
Chapter 7 pattern (DevelopmentConstant), and fits CapeCod with the
pure-premium trend, using on-level premium as the exposure. The resulting 70.8%
adjusted claim ratio is detrended back per year in Column (15).
xyz = cl.load_sample("friedland_xyz_auto_bi")
xyz_reported = xyz["Reported Claims"]
xyz_paid = xyz["Paid Claims"]
xyz_premium = xyz["Earned Premium"].latest_diagonal
xyz_years = list(xyz_reported.origin.year)
# Reuse the Chapter 7 XYZ reported development, persisted there with to_json.
_data_dir = os.path.join(os.path.dirname(cl.__file__), "utils", "data")
with open(os.path.join(_data_dir, "friedland_ch7_xyz_reported.json")) as f:
xyz_reported_dev = cl.read_json(f.read())
xyz_reported_dev.ldf_ = xyz_reported_dev.ldf_.round(3)
# Adjustment methods (Exhibit II, Sheet 1). The text restates premium to the 2008
# rate level and adjusts reported claims for pure-premium trend and tort reform.
# The rate-change and tort-reform histories are from the text; the parallelogram
# method (vertical_line=True) reproduces its on-level and tort factors, and the
# 3.42% pure-premium trend is applied through CapeCod's trend parameter.
rate_history = pd.DataFrame({
"date": ["1/1/1999", "1/1/2000", "1/1/2001", "1/1/2002", "1/1/2003",
"1/1/2004", "1/1/2005", "1/1/2006", "1/1/2007", "1/1/2008"],
"rate_change": [0.02, 0.02, 0.02, 0.02, 0.05, 0.075, 0.15, 0.10, -0.20, -0.20],
})
tort_history = pd.DataFrame({
"date": ["1/1/2006", "1/1/2007"],
"rate_change": [-0.1067, -0.25],
})
pp_trend_rate = 0.0342
onlevel = cl.ParallelogramOLF(
rate_history, change_col="rate_change", date_col="date", vertical_line=True)
tort = cl.ParallelogramOLF(
tort_history, change_col="rate_change", date_col="date", vertical_line=True)
onlevel_adj = col(onlevel.fit(xyz_premium).olf_)
tort_reform = col(tort.fit(xyz_reported.latest_diagonal).olf_)
pp_trend = (1 + pp_trend_rate) ** (max(xyz_years) - np.array(xyz_years))
# Reported cumulative development factors are floored at 1.0 (caps %reported at 100%).
xyz_cdf = np.maximum(
xyz_reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten()[::-1], 1.0).round(3)
xyz_pct_reported = 1 / xyz_cdf
xyz_reported_latest = col(xyz_reported.latest_diagonal)
xyz_prem = col(xyz_premium)
on_level_premium = xyz_prem * onlevel_adj
adjusted_claims = xyz_reported_latest * pp_trend * tort_reform
used_up_on_level = on_level_premium * xyz_pct_reported
# Chain the adjustments into a pipeline to derive the all-years apriori: the
# tort-reform OLF and the floored Chapter 7 pattern develop the reported claims,
# CapeCod applies the pure-premium trend, and on-level premium is the exposure.
floored_patterns = dict(zip(
[int(age) for age in xyz_reported_dev.cdf_.ddims],
np.maximum(xyz_reported_dev.cdf_.values.flatten(), 1.0)))
onlevel_premium = xyz_premium * onlevel.olf_
xyz_cc = cl.Pipeline([
("tort", tort),
("dev", cl.DevelopmentConstant(patterns=floored_patterns, style="cdf")),
("capecod", cl.CapeCod(trend=pp_trend_rate)),
]).fit(xyz_reported, sample_weight=onlevel_premium).named_steps["capecod"]
xyz_apriori = float(xyz_cc.apriori_.to_frame().iloc[0, 0])
sheet1 = pd.DataFrame(index=xyz_years)
sheet1["Earned Premium"] = xyz_prem
sheet1["On-Level Factor"] = onlevel_adj
sheet1["On-Level Premium"] = on_level_premium.round(0)
sheet1["Reported Claims"] = xyz_reported_latest
sheet1["Trend"] = pp_trend
sheet1["Tort Reform"] = tort_reform
sheet1["Adjusted Claims"] = adjusted_claims.round(0)
sheet1["CDF to Ultimate"] = xyz_cdf.round(3)
sheet1["% Reported"] = xyz_pct_reported.round(3)
sheet1["Used-Up On-Level Premium"] = used_up_on_level.round(0)
sheet1["Estimated Adjusted Claim Ratio"] = (adjusted_claims / used_up_on_level).round(3)
display(sheet1)
print(f"All-years adjusted claim ratio (apriori): {xyz_apriori:.3f}")
| Earned Premium | On-Level Factor | On-Level Premium | Reported Claims | Trend | Tort Reform | Adjusted Claims | CDF to Ultimate | % Reported | Used-Up On-Level Premium | Estimated Adjusted Claim Ratio | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1998 | 20000.0 | 0.989165 | 19783.0 | 15822.0 | 1.399733 | 0.669975 | 14838.0 | 1.000 | 1.000 | 19783.0 | 0.750 |
| 1999 | 31500.0 | 0.969770 | 30548.0 | 25107.0 | 1.353446 | 0.669975 | 22766.0 | 1.000 | 1.000 | 30548.0 | 0.745 |
| 2000 | 45000.0 | 0.950755 | 42784.0 | 37246.0 | 1.308688 | 0.669975 | 32657.0 | 1.000 | 1.000 | 42784.0 | 0.763 |
| 2001 | 50000.0 | 0.932113 | 46606.0 | 38798.0 | 1.265411 | 0.669975 | 32893.0 | 1.000 | 1.000 | 46606.0 | 0.706 |
| 2002 | 61183.0 | 0.913836 | 55911.0 | 48169.0 | 1.223565 | 0.669975 | 39487.0 | 1.003 | 0.997 | 55744.0 | 0.708 |
| 2003 | 69175.0 | 0.870320 | 60204.0 | 44373.0 | 1.183103 | 0.669975 | 35172.0 | 1.013 | 0.987 | 59432.0 | 0.592 |
| 2004 | 99322.0 | 0.809600 | 80411.0 | 70288.0 | 1.143979 | 0.669975 | 53871.0 | 1.064 | 0.940 | 75574.0 | 0.713 |
| 2005 | 138151.0 | 0.704000 | 97258.0 | 70655.0 | 1.106149 | 0.669975 | 52362.0 | 1.085 | 0.922 | 89639.0 | 0.584 |
| 2006 | 107578.0 | 0.640000 | 68850.0 | 48804.0 | 1.069570 | 0.750000 | 39149.0 | 1.196 | 0.836 | 57567.0 | 0.680 |
| 2007 | 62438.0 | 0.800000 | 49950.0 | 31732.0 | 1.034200 | 1.000000 | 32817.0 | 1.512 | 0.661 | 33036.0 | 0.993 |
| 2008 | 47797.0 | 1.000000 | 47797.0 | 18632.0 | 1.000000 | 1.000000 | 18632.0 | 2.551 | 0.392 | 18737.0 | 0.994 |
All-years adjusted claim ratio (apriori): 0.708
Projection of ultimate claims#
This recreates Exhibit II, Sheet 2. The all-years adjusted claim ratio is detrended back to each accident year’s environment (Column 15 of Sheet 1) to give the per-year expected claim ratios. Expected claims are earned premium times that ratio; the projected ultimate is reported claims plus expected unreported.
# Detrend the all-years apriori back to each year's rate, inflation, and tort
# environment (Exhibit II, Sheet 1, Column 15).
xyz_unadjusted_ratio = xyz_apriori * onlevel_adj / pp_trend / tort_reform
xyz_expected_claims = xyz_prem * xyz_unadjusted_ratio
xyz_pct_unreported = 1 - xyz_pct_reported
xyz_expected_unreported = xyz_expected_claims * xyz_pct_unreported
xyz_ult = xyz_reported_latest + xyz_expected_unreported
sheet2 = pd.DataFrame(index=xyz_years)
sheet2["Earned Premium"] = xyz_prem
sheet2["Expected Claim Ratio"] = xyz_unadjusted_ratio.round(3)
sheet2["Expected Claims"] = xyz_expected_claims.round(0)
sheet2["CDF to Ultimate"] = xyz_cdf.round(3)
sheet2["% Unreported"] = xyz_pct_unreported.round(3)
sheet2["Expected Unreported"] = xyz_expected_unreported.round(0)
sheet2["Reported Claims"] = xyz_reported_latest
sheet2["Projected Ultimate"] = xyz_ult.round(0)
display(sheet2)
| Earned Premium | Expected Claim Ratio | Expected Claims | CDF to Ultimate | % Unreported | Expected Unreported | Reported Claims | Projected Ultimate | |
|---|---|---|---|---|---|---|---|---|
| 1998 | 20000.0 | 0.746 | 14926.0 | 1.000 | 0.000 | 0.0 | 15822.0 | 15822.0 |
| 1999 | 31500.0 | 0.757 | 23836.0 | 1.000 | 0.000 | 0.0 | 25107.0 | 25107.0 |
| 2000 | 45000.0 | 0.767 | 34525.0 | 1.000 | 0.000 | 0.0 | 37246.0 | 37246.0 |
| 2001 | 50000.0 | 0.778 | 38895.0 | 1.000 | 0.000 | 0.0 | 38798.0 | 38798.0 |
| 2002 | 61183.0 | 0.789 | 48257.0 | 1.003 | 0.003 | 144.0 | 48169.0 | 48313.0 |
| 2003 | 69175.0 | 0.777 | 53739.0 | 1.013 | 0.013 | 690.0 | 44373.0 | 45063.0 |
| 2004 | 99322.0 | 0.747 | 74231.0 | 1.064 | 0.060 | 4465.0 | 70288.0 | 74753.0 |
| 2005 | 138151.0 | 0.672 | 92854.0 | 1.085 | 0.078 | 7274.0 | 70655.0 | 77929.0 |
| 2006 | 107578.0 | 0.564 | 60727.0 | 1.196 | 0.164 | 9952.0 | 48804.0 | 58756.0 |
| 2007 | 62438.0 | 0.547 | 34173.0 | 1.512 | 0.339 | 11572.0 | 31732.0 | 43304.0 |
| 2008 | 47797.0 | 0.708 | 33818.0 | 2.551 | 0.608 | 20561.0 | 18632.0 | 39193.0 |
Development of unpaid claim estimate#
This recreates Exhibit II, Sheet 3: case outstanding, estimated IBNR, and total unpaid follow from the projected ultimates.
xyz_paid_latest = col(xyz_paid.latest_diagonal)
sheet3 = pd.DataFrame(index=xyz_years)
sheet3["Reported Claims"] = xyz_reported_latest
sheet3["Paid Claims"] = xyz_paid_latest
sheet3["Projected Ultimate"] = xyz_ult.round(0)
sheet3["Case Outstanding"] = (xyz_reported_latest - xyz_paid_latest).round(0)
sheet3["IBNR"] = (xyz_ult - xyz_reported_latest).round(0)
sheet3["Total Unpaid"] = (xyz_ult - xyz_paid_latest).round(0)
display(sheet3)
| Reported Claims | Paid Claims | Projected Ultimate | Case Outstanding | IBNR | Total Unpaid | |
|---|---|---|---|---|---|---|
| 1998 | 15822.0 | 15822.0 | 15822.0 | 0.0 | 0.0 | 0.0 |
| 1999 | 25107.0 | 24817.0 | 25107.0 | 290.0 | 0.0 | 290.0 |
| 2000 | 37246.0 | 36782.0 | 37246.0 | 464.0 | 0.0 | 464.0 |
| 2001 | 38798.0 | 38519.0 | 38798.0 | 279.0 | 0.0 | 279.0 |
| 2002 | 48169.0 | 44437.0 | 48313.0 | 3732.0 | 144.0 | 3876.0 |
| 2003 | 44373.0 | 39320.0 | 45063.0 | 5053.0 | 690.0 | 5743.0 |
| 2004 | 70288.0 | 52811.0 | 74753.0 | 17477.0 | 4465.0 | 21942.0 |
| 2005 | 70655.0 | 40026.0 | 77929.0 | 30629.0 | 7274.0 | 37903.0 |
| 2006 | 48804.0 | 22819.0 | 58756.0 | 25985.0 | 9952.0 | 35937.0 |
| 2007 | 31732.0 | 11865.0 | 43304.0 | 19867.0 | 11572.0 | 31439.0 |
| 2008 | 18632.0 | 3409.0 | 39193.0 | 15223.0 | 20561.0 | 35784.0 |
Reconciliation to Friedland#
The derived adjusted claim ratio, the per-year detrended claim ratios, and the projected ultimate and IBNR totals are reconciled to the printed Exhibit II below (values in $000).
# Exhibit II, Sheet 1 - all-years adjusted claim ratio
assert np.isclose(xyz_apriori, 0.708, atol=2e-3)
# Exhibit II, Sheet 1 - selected CDFs to ultimate (limited to a minimum of 1.00)
assert np.allclose(xyz_cdf.round(3),
[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)
# Exhibit II, Sheet 1 - detrended (unadjusted) claim ratios, Column 15
assert np.allclose(xyz_unadjusted_ratio,
[0.746, 0.757, 0.767, 0.778, 0.789, 0.777, 0.747, 0.672, 0.565, 0.547, 0.708], atol=2e-3)
# Exhibit II, Sheet 2 - projected ultimate claims
assert np.isclose(xyz_ult.sum(), 504300, rtol=5e-3)
# Exhibit II, Sheet 3 - estimated IBNR
assert np.isclose((xyz_ult - xyz_reported_latest).sum(), 54674, rtol=5e-3)
Comparison of methods#
This recreates Exhibit II, Sheets 4 and 5, which compare the Cape Cod projection against the development, expected claims, and Bornhuetter-Ferguson methods from Chapters 7-9. The non-Cape-Cod columns are the published results of those chapters (cited in the text’s column notes); the Cape Cod column is the one computed in this chapter. Values in $000.
# Ultimate claims by method (Sheet 4). Every column other than Cape Cod is the
# published result of a prior chapter - development (Chapter 7), expected claims
# (Chapter 8), and Bornhuetter-Ferguson (Chapter 9) - reproduced from the text's
# column notes for comparison. The Cape Cod column is computed above.
summary_ult = pd.DataFrame(index=xyz_years)
summary_ult["Reported"] = xyz_reported_latest
summary_ult["Paid"] = xyz_paid_latest
summary_ult["Development (Reported)"] = [15822, 25082, 36948, 38487, 48313, 44950, 74787, 76661, 58370, 47979, 47530]
summary_ult["Development (Paid)"] = [15980, 25164, 37922, 40600, 49592, 49858, 80537, 80333, 72108, 77941, 74995]
summary_ult["Expected Claims"] = [15660, 24665, 35235, 39150, 47906, 54164, 86509, 108172, 70786, 39835, 39433]
summary_ult["BF (Reported)"] = [15822, 25107, 37246, 38798, 48312, 45068, 75492, 79129, 60404, 45221, 42607]
summary_ult["BF (Paid)"] = [15977, 25158, 37841, 40525, 49417, 50768, 82593, 94301, 71205, 45636, 41049]
summary_ult["Cape Cod"] = xyz_ult.round(0)
display(summary_ult)
# Estimated IBNR by method (Sheet 5): projected ultimate minus reported claims.
summary_ibnr = pd.DataFrame(index=xyz_years)
summary_ibnr["Case Outstanding"] = (xyz_reported_latest - xyz_paid_latest).round(0)
summary_ibnr["Development (Reported)"] = [0, -25, -298, -310, 145, 577, 4498, 6006, 9566, 16247, 28898]
summary_ibnr["Development (Paid)"] = [158, 58, 676, 1802, 1423, 5485, 10249, 9678, 23304, 46209, 56363]
summary_ibnr["Expected Claims"] = [-162, -442, -2011, 352, -262, 9791, 16221, 37517, 21982, 8103, 20801]
summary_ibnr["BF (Reported)"] = [0, 0, 0, 0, 143, 695, 5204, 8474, 11600, 13489, 23975]
summary_ibnr["BF (Paid)"] = [155, 51, 595, 1728, 1248, 6396, 12305, 23646, 22401, 13904, 22417]
summary_ibnr["Cape Cod"] = (xyz_ult - xyz_reported_latest).round(0)
display(summary_ibnr)
# Reconcile the Cape Cod column of Sheets 4 and 5 to the printed exhibit.
assert np.isclose(summary_ult["Cape Cod"].sum(), 504300, rtol=5e-3)
assert np.isclose(summary_ibnr["Cape Cod"].sum(), 54674, rtol=5e-3)
| Reported | Paid | Development (Reported) | Development (Paid) | Expected Claims | BF (Reported) | BF (Paid) | Cape Cod | |
|---|---|---|---|---|---|---|---|---|
| 1998 | 15822.0 | 15822.0 | 15822 | 15980 | 15660 | 15822 | 15977 | 15822.0 |
| 1999 | 25107.0 | 24817.0 | 25082 | 25164 | 24665 | 25107 | 25158 | 25107.0 |
| 2000 | 37246.0 | 36782.0 | 36948 | 37922 | 35235 | 37246 | 37841 | 37246.0 |
| 2001 | 38798.0 | 38519.0 | 38487 | 40600 | 39150 | 38798 | 40525 | 38798.0 |
| 2002 | 48169.0 | 44437.0 | 48313 | 49592 | 47906 | 48312 | 49417 | 48313.0 |
| 2003 | 44373.0 | 39320.0 | 44950 | 49858 | 54164 | 45068 | 50768 | 45063.0 |
| 2004 | 70288.0 | 52811.0 | 74787 | 80537 | 86509 | 75492 | 82593 | 74753.0 |
| 2005 | 70655.0 | 40026.0 | 76661 | 80333 | 108172 | 79129 | 94301 | 77929.0 |
| 2006 | 48804.0 | 22819.0 | 58370 | 72108 | 70786 | 60404 | 71205 | 58756.0 |
| 2007 | 31732.0 | 11865.0 | 47979 | 77941 | 39835 | 45221 | 45636 | 43304.0 |
| 2008 | 18632.0 | 3409.0 | 47530 | 74995 | 39433 | 42607 | 41049 | 39193.0 |
| Case Outstanding | Development (Reported) | Development (Paid) | Expected Claims | BF (Reported) | BF (Paid) | Cape Cod | |
|---|---|---|---|---|---|---|---|
| 1998 | 0.0 | 0 | 158 | -162 | 0 | 155 | 0.0 |
| 1999 | 290.0 | -25 | 58 | -442 | 0 | 51 | 0.0 |
| 2000 | 464.0 | -298 | 676 | -2011 | 0 | 595 | 0.0 |
| 2001 | 279.0 | -310 | 1802 | 352 | 0 | 1728 | 0.0 |
| 2002 | 3732.0 | 145 | 1423 | -262 | 143 | 1248 | 144.0 |
| 2003 | 5053.0 | 577 | 5485 | 9791 | 695 | 6396 | 690.0 |
| 2004 | 17477.0 | 4498 | 10249 | 16221 | 5204 | 12305 | 4465.0 |
| 2005 | 30629.0 | 6006 | 9678 | 37517 | 8474 | 23646 | 7274.0 |
| 2006 | 25985.0 | 9566 | 23304 | 21982 | 11600 | 22401 | 9952.0 |
| 2007 | 19867.0 | 16247 | 46209 | 8103 | 13489 | 13904 | 11572.0 |
| 2008 | 15223.0 | 28898 | 56363 | 20801 | 23975 | 22417 | 20561.0 |
Exhibit III - U.S. PP Auto (Impact of Changing Conditions)#
Exhibit III applies the Cape Cod method to the four U.S. PP Auto scenarios Friedland uses to study a changing environment (valued at 12/31/2008, accident years 1999-2008):
Steady-State
Increasing Claim Ratios
Increasing Case Outstanding Strength
Increasing Claim Ratios and Case Outstanding Strength
Unlike Bornhuetter-Ferguson (whose a priori is fixed at a 70% claim ratio for all four scenarios), Cape Cod derives the expected claim ratio from each scenario’s reported experience. This is what makes it more responsive when claim ratios rise (Scenario 2) but prone to overstatement when case-outstanding strength increases (Scenarios 3 and 4).
The development selection is the Chapter 7 five-year simple average with a 1.000 tail, and reported CDFs are limited to a minimum of 1.000.
Note on sample data. The reported figures for the two case outstanding strength scenarios do not yet reconcile exactly. The reported development in the
friedland_uspp_auto_increasing_caseandfriedland_uspp_increasing_claim_casesamples differs slightly from the text (a known data correction to these CSVs is still outstanding), so the reconciliation below asserts the IBNR total only for the two scenarios with clean data. The derived claim ratio reconciles for all four.
def pp_cc_scenario(sample):
"""Recreate a U.S. PP Auto Cape Cod scenario (Exhibit III)."""
tri = cl.load_sample(sample)
reported = tri["Reported Claims"]
paid = tri["Paid Claims"]
premium = tri["Earned Premium"].latest_diagonal
years = list(reported.origin.year)
# Chapter 7 selection: five-year simple average reported development, 1.000
# tail; CDFs cumulated from age-to-age factors rounded to three decimals.
reported_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(reported))
reported_dev.ldf_ = reported_dev.ldf_.round(3)
cc = cl.CapeCod().fit(reported_dev, sample_weight=premium)
apriori = float(cc.apriori_.to_frame().iloc[0, 0])
# model_diagnostics packages Latest, CDF, Ultimate, and IBNR by accident year.
diag = cl.model_diagnostics(cc)
cdf = np.maximum(col(diag["CDF"]), 1.0).round(3)
prem = col(premium)
reported_latest = col(diag["Latest"])
used_up = prem / cdf
# CapeCod returns NaN IBNR for fully developed years (CDF = 1.000); coerce to 0.
ibnr = np.nan_to_num(col(diag["IBNR"]))
out = pd.DataFrame(index=years)
out["Earned Premium"] = prem
out["Reported Claims"] = reported_latest
out["CDF to Ultimate"] = cdf.round(3)
out["% Reported"] = (1 / cdf).round(3)
out["Used-Up Premium"] = used_up.round(0)
out["Estimated Claim Ratio"] = (reported_latest / used_up).round(3)
out["Expected Claims"] = (prem * apriori).round(0)
out["Projected Ultimate"] = col(diag["Ultimate"]).round(0)
out["IBNR"] = ibnr.round(0)
return apriori, out
pp_scenarios = {
"Steady-State": "friedland_uspp_auto_steady_state",
"Increasing Claim Ratios": "friedland_uspp_auto_increasing_claim",
"Increasing Case Outstanding Strength": "friedland_uspp_auto_increasing_case",
"Increasing Claim Ratios and Case Outstanding Strength": "friedland_uspp_increasing_claim_case",
}
pp_results = {name: pp_cc_scenario(sample) for name, sample in pp_scenarios.items()}
for name, (apriori, table) in pp_results.items():
print(f"{name} - all-years estimated claim ratio {apriori:.3f}")
display(table)
Steady-State - all-years estimated claim ratio 0.700
| Earned Premium | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | Expected Claims | Projected Ultimate | IBNR | |
|---|---|---|---|---|---|---|---|---|---|
| 1999 | 1000000.000 | 700000.0 | 1.000 | 1.000 | 1000000.0 | 0.700 | 700059.0 | 700000.0 | 0.0 |
| 2000 | 1050000.000 | 735000.0 | 1.000 | 1.000 | 1050000.0 | 0.700 | 735062.0 | 735000.0 | 0.0 |
| 2001 | 1102500.000 | 771750.0 | 1.000 | 1.000 | 1102500.0 | 0.700 | 771815.0 | 771750.0 | 0.0 |
| 2002 | 1157625.000 | 810338.0 | 1.000 | 1.000 | 1157625.0 | 0.700 | 810405.0 | 810338.0 | 0.0 |
| 2003 | 1215506.250 | 842346.0 | 1.010 | 0.990 | 1203472.0 | 0.700 | 850926.0 | 850771.0 | 8425.0 |
| 2004 | 1276281.563 | 884463.0 | 1.010 | 0.990 | 1263645.0 | 0.700 | 893472.0 | 893309.0 | 8846.0 |
| 2005 | 1340095.641 | 919306.0 | 1.020 | 0.980 | 1313819.0 | 0.700 | 938146.0 | 937791.0 | 18485.0 |
| 2006 | 1407100.423 | 935722.0 | 1.053 | 0.950 | 1336278.0 | 0.700 | 985053.0 | 985074.0 | 49352.0 |
| 2007 | 1477455.444 | 930797.0 | 1.112 | 0.899 | 1328647.0 | 0.701 | 1034306.0 | 1034718.0 | 103921.0 |
| 2008 | 1551328.216 | 836166.0 | 1.300 | 0.769 | 1193329.0 | 0.701 | 1086021.0 | 1086512.0 | 250346.0 |
Increasing Claim Ratios - all-years estimated claim ratio 0.807
| Earned Premium | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | Expected Claims | Projected Ultimate | IBNR | |
|---|---|---|---|---|---|---|---|---|---|
| 1999 | 1000000.000 | 700000.0 | 1.000 | 1.000 | 1000000.0 | 0.700 | 807293.0 | 700000.0 | 0.0 |
| 2000 | 1050000.000 | 735000.0 | 1.000 | 1.000 | 1050000.0 | 0.700 | 847658.0 | 735000.0 | 0.0 |
| 2001 | 1102500.000 | 771750.0 | 1.000 | 1.000 | 1102500.0 | 0.700 | 890041.0 | 771750.0 | 0.0 |
| 2002 | 1157625.000 | 810338.0 | 1.000 | 1.000 | 1157625.0 | 0.700 | 934543.0 | 810338.0 | 0.0 |
| 2003 | 1215506.250 | 842346.0 | 1.010 | 0.990 | 1203472.0 | 0.700 | 981270.0 | 852062.0 | 9716.0 |
| 2004 | 1276281.563 | 1010815.0 | 1.010 | 0.990 | 1263645.0 | 0.800 | 1030333.0 | 1021016.0 | 10201.0 |
| 2005 | 1340095.641 | 1116300.0 | 1.020 | 0.980 | 1313819.0 | 0.850 | 1081850.0 | 1137617.0 | 21317.0 |
| 2006 | 1407100.423 | 1203071.0 | 1.053 | 0.950 | 1336278.0 | 0.900 | 1135942.0 | 1259983.0 | 56912.0 |
| 2007 | 1477455.444 | 1263224.0 | 1.112 | 0.899 | 1328647.0 | 0.951 | 1192740.0 | 1383064.0 | 119840.0 |
| 2008 | 1551328.216 | 1194523.0 | 1.300 | 0.769 | 1193329.0 | 1.001 | 1252377.0 | 1483217.0 | 288694.0 |
Increasing Case Outstanding Strength - all-years estimated claim ratio 0.717
| Earned Premium | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | Expected Claims | Projected Ultimate | IBNR | |
|---|---|---|---|---|---|---|---|---|---|
| 1999 | 1000000.000 | 700000.0 | 1.000 | 1.000 | 1000000.0 | 0.700 | 716820.0 | 700000.0 | 0.0 |
| 2000 | 1050000.000 | 735000.0 | 1.000 | 1.000 | 1050000.0 | 0.700 | 752661.0 | 735000.0 | 0.0 |
| 2001 | 1102500.000 | 771750.0 | 1.000 | 1.000 | 1102500.0 | 0.700 | 790294.0 | 771750.0 | 0.0 |
| 2002 | 1157625.000 | 810338.0 | 1.000 | 1.000 | 1157625.0 | 0.700 | 829809.0 | 810338.0 | 0.0 |
| 2003 | 1215506.250 | 842346.0 | 1.010 | 0.990 | 1203472.0 | 0.700 | 871300.0 | 850973.0 | 8627.0 |
| 2004 | 1276281.563 | 884463.0 | 1.010 | 0.990 | 1263645.0 | 0.700 | 914865.0 | 893521.0 | 9058.0 |
| 2005 | 1340095.641 | 933377.0 | 1.019 | 0.981 | 1315109.0 | 0.710 | 960608.0 | 951371.0 | 17994.0 |
| 2006 | 1407100.423 | 962808.0 | 1.054 | 0.949 | 1335010.0 | 0.721 | 1008638.0 | 1014247.0 | 51439.0 |
| 2007 | 1477455.444 | 979922.0 | 1.117 | 0.895 | 1322700.0 | 0.741 | 1059070.0 | 1090823.0 | 110901.0 |
| 2008 | 1551328.216 | 931185.0 | 1.316 | 0.760 | 1178821.0 | 0.790 | 1112024.0 | 1198066.0 | 266881.0 |
Increasing Claim Ratios and Case Outstanding Strength - all-years estimated claim ratio 0.830
| Earned Premium | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | Expected Claims | Projected Ultimate | IBNR | |
|---|---|---|---|---|---|---|---|---|---|
| 1999 | 1000000.000 | 700000.0 | 1.000 | 1.000 | 1000000.0 | 0.700 | 830029.0 | 700000.0 | 0.0 |
| 2000 | 1050000.000 | 735000.0 | 1.000 | 1.000 | 1050000.0 | 0.700 | 871530.0 | 735000.0 | 0.0 |
| 2001 | 1102500.000 | 771750.0 | 1.000 | 1.000 | 1102500.0 | 0.700 | 915107.0 | 771750.0 | 0.0 |
| 2002 | 1157625.000 | 810338.0 | 1.000 | 1.000 | 1157625.0 | 0.700 | 960862.0 | 810338.0 | 0.0 |
| 2003 | 1215506.250 | 842346.0 | 1.010 | 0.990 | 1203472.0 | 0.700 | 1008905.0 | 852335.0 | 9989.0 |
| 2004 | 1276281.563 | 1010815.0 | 1.010 | 0.990 | 1263645.0 | 0.800 | 1059351.0 | 1021304.0 | 10489.0 |
| 2005 | 1340095.641 | 1133386.0 | 1.019 | 0.981 | 1315109.0 | 0.862 | 1112318.0 | 1154222.0 | 20836.0 |
| 2006 | 1407100.423 | 1237897.0 | 1.054 | 0.949 | 1335010.0 | 0.927 | 1167934.0 | 1297460.0 | 59563.0 |
| 2007 | 1477455.444 | 1329895.0 | 1.117 | 0.895 | 1322700.0 | 1.005 | 1226331.0 | 1458311.0 | 128416.0 |
| 2008 | 1551328.216 | 1330264.0 | 1.316 | 0.760 | 1178821.0 | 1.128 | 1287647.0 | 1639294.0 | 309030.0 |
Reconciliation to Friedland#
We reconcile the derived all-years claim ratio for all four scenarios, and the estimated IBNR total for the two scenarios with clean sample data.
pp_apriori = {name: apriori for name, (apriori, table) in pp_results.items()}
pp_ibnr = {name: table["IBNR"].sum() for name, (apriori, table) in pp_results.items()}
# All four scenarios reproduce the text's all-years estimated claim ratio.
assert np.isclose(pp_apriori["Steady-State"], 0.700, atol=2e-3)
assert np.isclose(pp_apriori["Increasing Claim Ratios"], 0.807, atol=2e-3)
assert np.isclose(pp_apriori["Increasing Case Outstanding Strength"], 0.717, atol=2e-3)
assert np.isclose(pp_apriori["Increasing Claim Ratios and Case Outstanding Strength"], 0.831, atol=2e-3)
# Estimated IBNR reconciles for the two scenarios with clean sample data.
assert np.isclose(pp_ibnr["Steady-State"], 438638, rtol=5e-3)
assert np.isclose(pp_ibnr["Increasing Claim Ratios"], 505828, rtol=5e-3)
Exhibit IV - U.S. Auto (Impact of Change in Product Mix)#
Exhibit IV applies the Cape Cod method to a combined private-passenger and commercial automobile portfolio under two scenarios (valued at 12/31/2008, accident years 1999-2008):
Steady-State (No Change in Product Mix)
Changing Product Mix (commercial auto growing faster than private passenger)
The earned premium follows the text’s Exhibit IV, Sheet 1 assumption: $2,000,000 for the first year (1999) with 5% annual growth, and in the change scenario commercial auto grows 30% per year from 2005. The development selection is the Chapter 7 five-year simple average with a 1.000 tail.
Cape Cod generates the correct IBNR when the product mix is stable, but understates it when the mix shifts, because the reporting pattern changes and the method does not respond appropriately.
# The friedland_us_auto sample carries all scenarios under a Scenario index, with
# earned premium per the text's Exhibit IV, Sheet 1 assumptions. Select the two
# product-mix scenarios by label.
us_auto = cl.load_sample("friedland_us_auto")
us_auto_scenarios = {
"Steady-State (No Change in Product Mix)": "Steady State",
"Changing Product Mix": "Changing Product Mix",
}
def us_auto_cc_scenario(scenario):
"""Recreate a U.S. Auto product-mix Cape Cod scenario (Exhibit IV)."""
tri = us_auto.loc[scenario]
reported = tri["Reported Claims"]
premium = tri["Earned Premium"].latest_diagonal
years = list(reported.origin.year)
# Chapter 7 selection: five-year simple average reported development, 1.000 tail.
reported_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(reported))
reported_dev.ldf_ = reported_dev.ldf_.round(3)
cc = cl.CapeCod().fit(reported_dev, sample_weight=premium)
apriori = float(cc.apriori_.to_frame().iloc[0, 0])
# model_diagnostics packages Latest, CDF, Ultimate, and IBNR by accident year.
diag = cl.model_diagnostics(cc)
cdf = np.maximum(col(diag["CDF"]), 1.0).round(3)
prem = col(premium)
reported_latest = col(diag["Latest"])
used_up = prem / cdf
# CapeCod returns NaN IBNR for fully developed years (CDF = 1.000); coerce to 0.
ibnr = np.nan_to_num(col(diag["IBNR"]))
out = pd.DataFrame(index=years)
out["Earned Premium"] = prem
out["Reported Claims"] = reported_latest
out["CDF to Ultimate"] = cdf.round(3)
out["% Reported"] = (1 / cdf).round(3)
out["Used-Up Premium"] = used_up.round(0)
out["Estimated Claim Ratio"] = (reported_latest / used_up).round(3)
out["Expected Claims"] = (prem * apriori).round(0)
out["Projected Ultimate"] = col(diag["Ultimate"]).round(0)
out["IBNR"] = ibnr.round(0)
return apriori, out
us_auto_results = {
label: us_auto_cc_scenario(scenario) for label, scenario in us_auto_scenarios.items()
}
for name, (apriori, table) in us_auto_results.items():
print(f"{name} - all-years estimated claim ratio {apriori:.3f}")
display(table)
Steady-State (No Change in Product Mix) - all-years estimated claim ratio 0.750
| Earned Premium | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | Expected Claims | Projected Ultimate | IBNR | |
|---|---|---|---|---|---|---|---|---|---|
| 1999 | 2000000.0 | 1500000.0 | 1.000 | 1.000 | 2000000.0 | 0.750 | 1500509.0 | 1500000.0 | 0.0 |
| 2000 | 2100000.0 | 1575000.0 | 1.000 | 1.000 | 2100000.0 | 0.750 | 1575535.0 | 1575000.0 | 0.0 |
| 2001 | 2205000.0 | 1653750.0 | 1.000 | 1.000 | 2205000.0 | 0.750 | 1654312.0 | 1653750.0 | 0.0 |
| 2002 | 2315250.0 | 1736438.0 | 1.000 | 1.000 | 2315250.0 | 0.750 | 1737027.0 | 1736438.0 | 0.0 |
| 2003 | 2431013.0 | 1814751.0 | 1.005 | 0.995 | 2418918.0 | 0.750 | 1823879.0 | 1823825.0 | 9074.0 |
| 2004 | 2552563.0 | 1885068.0 | 1.016 | 0.984 | 2512365.0 | 0.750 | 1915072.0 | 1915329.0 | 30261.0 |
| 2005 | 2680191.0 | 1948499.0 | 1.032 | 0.969 | 2597084.0 | 0.750 | 2010826.0 | 2011439.0 | 62940.0 |
| 2006 | 2814201.0 | 1937577.0 | 1.090 | 0.917 | 2581836.0 | 0.750 | 2111367.0 | 2112126.0 | 174549.0 |
| 2007 | 2954911.0 | 1852729.0 | 1.197 | 0.835 | 2468597.0 | 0.751 | 2216936.0 | 2217516.0 | 364787.0 |
| 2008 | 3102656.0 | 1568393.0 | 1.484 | 0.674 | 2090739.0 | 0.750 | 2327782.0 | 2327823.0 | 759430.0 |
Changing Product Mix - all-years estimated claim ratio 0.750
| Earned Premium | Reported Claims | CDF to Ultimate | % Reported | Used-Up Premium | Estimated Claim Ratio | Expected Claims | Projected Ultimate | IBNR | |
|---|---|---|---|---|---|---|---|---|---|
| 1999 | 2000000.0 | 1500000.0 | 1.000 | 1.000 | 2000000.0 | 0.750 | 1499954.0 | 1500000.0 | 0.0 |
| 2000 | 2100000.0 | 1575000.0 | 1.000 | 1.000 | 2100000.0 | 0.750 | 1574952.0 | 1575000.0 | 0.0 |
| 2001 | 2205000.0 | 1653750.0 | 1.000 | 1.000 | 2205000.0 | 0.750 | 1653699.0 | 1653750.0 | 0.0 |
| 2002 | 2315250.0 | 1736438.0 | 1.000 | 1.000 | 2315250.0 | 0.750 | 1736384.0 | 1736438.0 | 0.0 |
| 2003 | 2431013.0 | 1814751.0 | 1.005 | 0.995 | 2418918.0 | 0.750 | 1823204.0 | 1823822.0 | 9071.0 |
| 2004 | 2552563.0 | 1885068.0 | 1.016 | 0.984 | 2512365.0 | 0.750 | 1914363.0 | 1915317.0 | 30249.0 |
| 2005 | 2999262.0 | 2193545.0 | 1.032 | 0.969 | 2906262.0 | 0.755 | 2249377.0 | 2263952.0 | 70407.0 |
| 2006 | 3564016.0 | 2471446.0 | 1.090 | 0.917 | 3269739.0 | 0.756 | 2672930.0 | 2692420.0 | 220974.0 |
| 2007 | 4281446.0 | 2680487.0 | 1.200 | 0.833 | 3567872.0 | 0.751 | 3210986.0 | 3216150.0 | 535663.0 |
| 2008 | 5196516.0 | 2556695.0 | 1.500 | 0.667 | 3464344.0 | 0.738 | 3897267.0 | 3856268.0 | 1299573.0 |
Reconciliation to Friedland#
Both scenarios derive a 75.0% all-years claim ratio. The steady-state IBNR reconciles to the actual requirement, while the changing-product-mix IBNR is understated relative to the actual, as the text describes.
us_apriori = {name: apriori for name, (apriori, table) in us_auto_results.items()}
us_ibnr = {name: table["IBNR"].sum() for name, (apriori, table) in us_auto_results.items()}
# Both scenarios derive the text's 75.0% all-years estimated claim ratio.
assert np.isclose(us_apriori["Steady-State (No Change in Product Mix)"], 0.750, atol=2e-3)
assert np.isclose(us_apriori["Changing Product Mix"], 0.750, atol=2e-3)
# Steady-state reconciles to the actual IBNR requirement; the changing product mix
# understates it (the text's actual is 2,391,084).
assert np.isclose(us_ibnr["Steady-State (No Change in Product Mix)"], 1394634, rtol=6e-3)
assert np.isclose(us_ibnr["Changing Product Mix"], 2168000, rtol=6e-3)