Chapter 9 - Bornhuetter-Ferguson Technique#
The Bornhuetter-Ferguson technique is essentially a blend of the development technique and the expected claims technique.
– Friedland, Chapter 9
The Bornhuetter-Ferguson (BF) method splits ultimate claims into the claims already reported (or paid) plus the expected unreported (or unpaid) claims. The expected piece is an a priori estimate of ultimate claims (from the expected claims technique of Chapter 8), scaled by the percentage still to emerge that is implied by the development pattern:
In the chainladder package the method is implemented by
BornhuetterFerguson, which takes the a priori through sample_weight. This
chapter recreates the Friedland Chapter 9 exhibits, reusing the development
patterns selected in Chapter 7 and the expected claims from Chapter 8:
Exhibit I - U.S. Industry Auto
Exhibit II - XYZ Insurer (Auto BI)
Exhibit III - U.S. PP Auto (impact of changing conditions)
Exhibit IV - U.S. Auto (impact of change in product mix)
Exhibit V - U.S. PP Auto (impact of changing conditions - Gunnar Benktander method)
Exhibit VI - U.S. Auto (impact of change in product mix - Gunnar Benktander method)
import os
import numpy as np
import pandas as pd
import chainladder as cl
from IPython.display import display
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 1000)
def add_total_row(df, sum_cols=None):
"""Append a Total row to a DataFrame for display purposes."""
out = df.copy()
out.index = out.index.astype(str)
if sum_cols is None:
non_sum = [
"Age (Months)",
"CDF Reported",
"CDF Paid",
"% Unreported",
"% Unpaid",
]
sum_cols = [c for c in out.columns if c not in non_sum]
total_row = {}
for col in out.columns:
if col in sum_cols:
total_row[col] = out[col].sum()
else:
total_row[col] = ""
out.loc["Total"] = total_row
return out
Exhibit I - U.S. Industry Auto#
The text works the Bornhuetter-Ferguson method first for U.S. Industry Auto (Exhibit I), valued at 12/31/2007 over accident years 1998-2007. The reporting and payment patterns are the Chapter 7 selection (three-year simple average with a 1.000 reported / 1.002 paid tail), and the a priori expected claims come from the expected claims technique of Chapter 8.
Projection of ultimate claims#
This recreates Exhibit I, Sheet 1. Because the text cumulates and rounds the
selected CDFs to three decimals, we drive BornhuetterFerguson with those
rounded patterns via DevelopmentConstant so the projection reconciles exactly.
ia = cl.load_sample("friedland_us_industry_auto")
ia_reported = ia["Reported Claims"]
ia_paid = ia["Paid Claims"]
ia_years = list(ia_reported.origin.year)
# Chapter 7 selection: three-year simple average development with a constant
# tail. Friedland rounds the age-to-age factors to three decimals before
# cumulating them into CDFs.
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_paid_dev = cl.TailConstant(tail=1.002, projection_period=0).fit_transform(
cl.Development(n_periods=3, average="simple").fit_transform(ia_paid)
)
ia_reported_dev.ldf_ = ia_reported_dev.ldf_.round(3)
ia_paid_dev.ldf_ = ia_paid_dev.ldf_.round(3)
# A priori expected claims from the expected claims technique (Chapter 8, $000).
ia_expected = np.array(
[
51430657,
51408736,
51680983,
54408716,
59421665,
56318302,
59646290,
61174953,
61926981,
61864556,
],
dtype=float,
)
ia_apriori = ia_reported.latest_diagonal.copy()
ia_apriori.iloc[0, 0] = ia_expected.reshape(ia_apriori.shape)
ia_bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(
ia_reported_dev, sample_weight=ia_apriori
)
ia_bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(
ia_paid_dev, sample_weight=ia_apriori
)
# model_diagnostics summarises Latest, CDF, Ultimate, and IBNR per accident year.
ia_rep = cl.model_diagnostics(ia_bf_reported).to_frame(origin_as_datetime=False).T
ia_pd = cl.model_diagnostics(ia_bf_paid).to_frame(origin_as_datetime=False).T
ia_rep_latest = ia_rep["Latest"].values
ia_pd_latest = ia_pd["Latest"].values
ia_cdf_reported = ia_rep["CDF"].round(3)
ia_cdf_paid = ia_pd["CDF"].round(3)
pct_unrep_full = 1.0 - 1.0 / ia_cdf_reported.values
pct_unpaid_full = 1.0 - 1.0 / ia_cdf_paid.values
ia_expected_unrep = np.round(ia_expected * pct_unrep_full)
ia_expected_unpaid = np.round(ia_expected * pct_unpaid_full)
ia_ult_rep = np.round(ia_expected_unrep + ia_rep_latest)
ia_ult_pd = np.round(ia_expected_unpaid + ia_pd_latest)
ia_projection = pd.DataFrame(index=ia_years)
ia_projection["Expected Claims"] = ia_expected
ia_projection["CDF Reported"] = ia_cdf_reported.values
ia_projection["CDF Paid"] = ia_cdf_paid.values
ia_projection["% Unreported"] = np.round(pct_unrep_full, 3)
ia_projection["% Unpaid"] = np.round(pct_unpaid_full, 3)
ia_projection["Expected Unreported"] = ia_expected_unrep
ia_projection["Expected Unpaid"] = ia_expected_unpaid
ia_projection["Reported Claims"] = ia_rep_latest
ia_projection["Paid Claims"] = ia_pd_latest
ia_projection["BF Ultimate (Reported)"] = ia_ult_rep
ia_projection["BF Ultimate (Paid)"] = ia_ult_pd
display(add_total_row(ia_projection))
| Expected Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | Expected Unreported | Expected Unpaid | Reported Claims | Paid Claims | BF Ultimate (Reported) | BF Ultimate (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1998 | 51430657.0 | 1.0 | 1.002 | 0.0 | 0.002 | 0.0 | 102656.0 | 47742304.0 | 47644187.0 | 47742304.0 | 47746843.0 |
| 1999 | 51408736.0 | 1.0 | 1.004 | 0.0 | 0.004 | 0.0 | 204816.0 | 51185767.0 | 51000534.0 | 51185767.0 | 51205350.0 |
| 2000 | 51680983.0 | 1.001 | 1.006 | 0.001 | 0.006 | 51629.0 | 308236.0 | 54837929.0 | 54533225.0 | 54889558.0 | 54841461.0 |
| 2001 | 54408716.0 | 1.003 | 1.011 | 0.003 | 0.011 | 162738.0 | 591984.0 | 56299562.0 | 55878421.0 | 56462300.0 | 56470405.0 |
| 2002 | 59421665.0 | 1.006 | 1.02 | 0.006 | 0.02 | 354404.0 | 1165131.0 | 58592712.0 | 57807215.0 | 58947116.0 | 58972346.0 |
| 2003 | 56318302.0 | 1.011 | 1.04 | 0.011 | 0.038 | 612761.0 | 2166089.0 | 57565344.0 | 55930654.0 | 58178105.0 | 58096743.0 |
| 2004 | 59646290.0 | 1.023 | 1.085 | 0.022 | 0.078 | 1341021.0 | 4672751.0 | 56976657.0 | 53774672.0 | 58317678.0 | 58447423.0 |
| 2005 | 61174953.0 | 1.051 | 1.184 | 0.049 | 0.155 | 2968528.0 | 9506918.0 | 56786410.0 | 50644994.0 | 59754938.0 | 60151912.0 |
| 2006 | 61926981.0 | 1.11 | 1.404 | 0.099 | 0.288 | 6136908.0 | 17819445.0 | 54641339.0 | 43606497.0 | 60778247.0 | 61425942.0 |
| 2007 | 61864556.0 | 1.292 | 2.39 | 0.226 | 0.582 | 13981773.0 | 35979805.0 | 48853563.0 | 27229969.0 | 62835336.0 | 63209774.0 |
| Total | 569281839.0 | 25609762.0 | 72517831.0 | 543481587.0 | 498050368.0 | 569091349.0 | 570568199.0 |
Column Notes - Exhibit I, Sheet 1#
(2) Expected Claims: Developed in Chapter 8, Exhibit II, Sheet 1.
(3) & (4) CDF Reported / Paid: Developed in Chapter 7, Exhibit I, Sheets 1 & 2.
(5) % Unreported: \(1.00 - (1.00 / (3))\).
(6) % Unpaid: \(1.00 - (1.00 / (4))\).
(7) Expected Unreported: \((2) \times (5)\).
(8) Expected Unpaid: \((2) \times (6)\).
(9) & (10) Reported / Paid Claims: Based on data from U.S. Industry Auto.
(11) BF Ultimate (Reported): \((7) + (9)\).
(12) BF Ultimate (Paid): \((8) + (10)\).
Development of unpaid claim estimate#
This recreates Exhibit I, Sheet 2: case outstanding, estimated IBNR, and total unpaid follow from the projected ultimates. Following the text, IBNR is ultimate minus reported claims and total unpaid is ultimate minus paid claims.
ia_unpaid = pd.DataFrame(index=ia_years)
ia_unpaid["Reported Claims"] = ia_rep_latest
ia_unpaid["Paid Claims"] = ia_pd_latest
ia_unpaid["BF Ultimate (Reported)"] = ia_ult_rep.round(0)
ia_unpaid["BF Ultimate (Paid)"] = ia_ult_pd.round(0)
ia_unpaid["Case Outstanding"] = (ia_rep_latest - ia_pd_latest).round(0)
ia_unpaid["IBNR (Reported)"] = (ia_ult_rep - ia_rep_latest).round(0)
ia_unpaid["IBNR (Paid)"] = (ia_ult_pd - ia_rep_latest).round(0)
ia_unpaid["Total Unpaid (Reported)"] = (ia_ult_rep - ia_pd_latest).round(0)
ia_unpaid["Total Unpaid (Paid)"] = (ia_ult_pd - ia_pd_latest).round(0)
display(add_total_row(ia_unpaid))
| Reported Claims | Paid Claims | BF Ultimate (Reported) | BF Ultimate (Paid) | Case Outstanding | IBNR (Reported) | IBNR (Paid) | Total Unpaid (Reported) | Total Unpaid (Paid) | |
|---|---|---|---|---|---|---|---|---|---|
| 1998 | 47742304.0 | 47644187.0 | 47742304.0 | 47746843.0 | 98117.0 | 0.0 | 4539.0 | 98117.0 | 102656.0 |
| 1999 | 51185767.0 | 51000534.0 | 51185767.0 | 51205350.0 | 185233.0 | 0.0 | 19583.0 | 185233.0 | 204816.0 |
| 2000 | 54837929.0 | 54533225.0 | 54889558.0 | 54841461.0 | 304704.0 | 51629.0 | 3532.0 | 356333.0 | 308236.0 |
| 2001 | 56299562.0 | 55878421.0 | 56462300.0 | 56470405.0 | 421141.0 | 162738.0 | 170843.0 | 583879.0 | 591984.0 |
| 2002 | 58592712.0 | 57807215.0 | 58947116.0 | 58972346.0 | 785497.0 | 354404.0 | 379634.0 | 1139901.0 | 1165131.0 |
| 2003 | 57565344.0 | 55930654.0 | 58178105.0 | 58096743.0 | 1634690.0 | 612761.0 | 531399.0 | 2247451.0 | 2166089.0 |
| 2004 | 56976657.0 | 53774672.0 | 58317678.0 | 58447423.0 | 3201985.0 | 1341021.0 | 1470766.0 | 4543006.0 | 4672751.0 |
| 2005 | 56786410.0 | 50644994.0 | 59754938.0 | 60151912.0 | 6141416.0 | 2968528.0 | 3365502.0 | 9109944.0 | 9506918.0 |
| 2006 | 54641339.0 | 43606497.0 | 60778247.0 | 61425942.0 | 11034842.0 | 6136908.0 | 6784603.0 | 17171750.0 | 17819445.0 |
| 2007 | 48853563.0 | 27229969.0 | 62835336.0 | 63209774.0 | 21623594.0 | 13981773.0 | 14356211.0 | 35605367.0 | 35979805.0 |
| Total | 543481587.0 | 498050368.0 | 569091349.0 | 570568199.0 | 45431219.0 | 25609762.0 | 27086612.0 | 71040981.0 | 72517831.0 |
Column Notes - Exhibit I, Sheet 2#
(2) & (3) Reported / Paid Claims: Based on data from U.S. Industry Auto.
(4) & (5) BF Ultimate (Reported / Paid): Developed in Exhibit I, Sheet 1.
(6) Case Outstanding: \((2) - (3)\).
(7) IBNR (Reported): \((4) - (2)\).
(8) IBNR (Paid): \((5) - (2)\).
(9) Total Unpaid (Reported): \((6) + (7) = (4) - (3)\).
(10) Total Unpaid (Paid): \((6) + (8) = (5) - (3)\).
Reconciliation to Friedland#
The selected CDFs, projected ultimates, and estimated IBNR are reconciled to the printed Exhibit I below.
# Exhibit I, Sheet 1 - selected CDFs to ultimate
assert np.allclose(
ia_projection["CDF Reported"].values,
[1.000, 1.000, 1.001, 1.003, 1.006, 1.011, 1.023, 1.051, 1.110, 1.292],
atol=1e-3,
)
assert np.allclose(
ia_projection["CDF Paid"].values,
[1.002, 1.004, 1.006, 1.011, 1.020, 1.040, 1.085, 1.184, 1.404, 2.390],
atol=1e-3,
)
# Exhibit I, Sheet 1 - projected ultimate claims (reconcile within rounding tolerance)
assert np.isclose(ia_projection["BF Ultimate (Reported)"].sum(), 569091348, rtol=5e-3)
assert np.isclose(ia_projection["BF Ultimate (Paid)"].sum(), 570568198, rtol=5e-3)
# Exhibit I, Sheet 2 - estimated IBNR
assert np.isclose(ia_unpaid["IBNR (Reported)"].sum(), 25609761, rtol=5e-3)
assert np.isclose(ia_unpaid["IBNR (Paid)"].sum(), 27086611, rtol=5e-3)
Exhibit II - XYZ Insurer (Auto BI)#
Exhibit II applies the same Bornhuetter-Ferguson method to the XYZ Insurer - Auto BI data, valued at 12/31/2008 over accident years 1998-2008.
The data#
The XYZ Insurer Auto BI reported and paid triangles run from accident year 1998 to 2008. Their most recent diagonal (12/31/2008) is the actual claims the BF method builds on.
tri = cl.load_sample("friedland_xyz_auto_bi")
reported = tri["Reported Claims"]
paid = tri["Paid Claims"]
years = list(reported.origin.year)
def col(t):
return t.to_frame(origin_as_datetime=False).iloc[:, 0].values
reported_latest = col(reported.latest_diagonal)
paid_latest = col(paid.latest_diagonal)
claims = pd.DataFrame(index=years)
claims["Reported"] = reported_latest
claims["Paid"] = paid_latest
display(claims)
| Reported | Paid | |
|---|---|---|
| 1998 | 15822.0 | 15822.0 |
| 1999 | 25107.0 | 24817.0 |
| 2000 | 37246.0 | 36782.0 |
| 2001 | 38798.0 | 38519.0 |
| 2002 | 48169.0 | 44437.0 |
| 2003 | 44373.0 | 39320.0 |
| 2004 | 70288.0 | 52811.0 |
| 2005 | 70655.0 | 40026.0 |
| 2006 | 48804.0 | 22819.0 |
| 2007 | 31732.0 | 11865.0 |
| 2008 | 18632.0 | 3409.0 |
Development patterns#
The BF method reuses the development pattern selected for XYZ in Chapter 7: a volume-weighted two-period average with a 1.000 reported tail and a 1.010 paid tail. Following Friedland, the age-to-age factors are rounded to three decimals before being cumulated to CDFs. The reported CDFs for the oldest accident years fall just below 1.0, so they are capped at 1.0 (this avoids negative implied unreported percentages; Friedland notes the cap is not strictly required).
# Reuse the Chapter 7 XYZ selection. The fitted reported and paid development
# estimators were persisted in Chapter 7 with to_json; recall them here with
# read_json instead of refitting, keeping the two chapters in sync. JSON is
# used rather than a pickle so the saved estimators load reliably across
# package and dependency versions.
_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:
reported_dev = cl.read_json(f.read())
with open(os.path.join(_data_dir, "friedland_ch7_xyz_paid.json")) as f:
paid_dev = cl.read_json(f.read())
# Friedland cumulates CDFs from age-to-age factors rounded to three decimals.
reported_dev.ldf_ = reported_dev.ldf_.round(3)
paid_dev.ldf_ = paid_dev.ldf_.round(3)
# CDF to ultimate per accident year (oldest origin -> highest maturity).
reported_cdf = np.maximum(
reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten()[::-1], 1.0
)
paid_cdf = paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten()[::-1]
pct_unreported = 1 - 1 / reported_cdf
pct_unpaid = 1 - 1 / paid_cdf
patterns = pd.DataFrame(index=years)
patterns["CDF Reported"] = reported_cdf.round(3)
patterns["CDF Paid"] = paid_cdf.round(3)
patterns["% Unreported"] = pct_unreported.round(3)
patterns["% Unpaid"] = pct_unpaid.round(3)
display(patterns)
| CDF Reported | CDF Paid | % Unreported | % Unpaid | |
|---|---|---|---|---|
| 1998 | 1.000 | 1.010 | 0.000 | 0.010 |
| 1999 | 1.000 | 1.014 | 0.000 | 0.014 |
| 2000 | 1.000 | 1.031 | 0.000 | 0.030 |
| 2001 | 1.000 | 1.054 | 0.000 | 0.051 |
| 2002 | 1.003 | 1.116 | 0.003 | 0.104 |
| 2003 | 1.013 | 1.268 | 0.013 | 0.211 |
| 2004 | 1.064 | 1.525 | 0.060 | 0.344 |
| 2005 | 1.085 | 2.007 | 0.078 | 0.502 |
| 2006 | 1.196 | 3.160 | 0.164 | 0.684 |
| 2007 | 1.512 | 6.569 | 0.339 | 0.848 |
| 2008 | 2.551 | 21.999 | 0.608 | 0.955 |
Expected claims (a priori)#
The a priori expected claims come from the expected claims technique (Chapter 8):
earned premium multiplied by a selected claim ratio. The earned premium is now
carried in the friedland_xyz_auto_bi sample and read directly from its latest
diagonal. The expected claims feed the BF method as the sample_weight.
# Earned premium is carried in the friedland_xyz_auto_bi sample ($000).
earned_premium = col(tri["Earned Premium"].latest_diagonal)
# A priori expected claims from the expected claims technique (Chapter 8, $000).
expected_claims = [
15670,
24680,
35256,
39174,
47935,
54197,
86528,
108241,
70769,
39841,
39429,
]
def as_diagonal(tri, vec):
"""Build a per-origin (latest-diagonal) triangle from a vector of values."""
d = tri.latest_diagonal.copy()
d.iloc[0, 0] = np.asarray(vec, dtype=float).reshape(d.shape)
return d
apriori = as_diagonal(reported, expected_claims)
priori = pd.DataFrame(index=years)
priori["Earned Premium"] = earned_premium
priori["Claim Ratio"] = (np.array(expected_claims) / np.array(earned_premium)).round(3)
priori["Expected Claims"] = expected_claims
display(priori)
| Earned Premium | Claim Ratio | Expected Claims | |
|---|---|---|---|
| 1998 | 20000.0 | 0.784 | 15670 |
| 1999 | 31500.0 | 0.783 | 24680 |
| 2000 | 45000.0 | 0.783 | 35256 |
| 2001 | 50000.0 | 0.783 | 39174 |
| 2002 | 61183.0 | 0.783 | 47935 |
| 2003 | 69175.0 | 0.783 | 54197 |
| 2004 | 99322.0 | 0.871 | 86528 |
| 2005 | 138151.0 | 0.783 | 108241 |
| 2006 | 107578.0 | 0.658 | 70769 |
| 2007 | 62438.0 | 0.638 | 39841 |
| 2008 | 47797.0 | 0.825 | 39429 |
Projection of ultimate claims#
Applying BornhuetterFerguson on both the reported and paid bases produces the
projected ultimate claims. The reported IBNR is floored at zero for the capped
accident years. This recreates the Ultimate Claims Projection exhibit.
bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(
reported_dev, sample_weight=apriori
)
bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(paid_dev, sample_weight=apriori)
reported_ibnr = np.nan_to_num(np.maximum(col(bf_reported.ibnr_), 0.0))
reported_ult = reported_latest + reported_ibnr
paid_ult = col(bf_paid.ultimate_)
exp_unrep_xyz = np.round(np.array(expected_claims) * pct_unreported)
exp_unpaid_xyz = np.round(np.array(expected_claims) * pct_unpaid)
projection = pd.DataFrame(index=years)
projection["Expected Claims"] = expected_claims
projection["CDF Reported"] = reported_cdf.round(3)
projection["CDF Paid"] = paid_cdf.round(3)
projection["% Unreported"] = pct_unreported.round(3)
projection["% Unpaid"] = pct_unpaid.round(3)
projection["Expected Unreported"] = exp_unrep_xyz
projection["Expected Unpaid"] = exp_unpaid_xyz
projection["Reported Claims"] = reported_latest
projection["Paid Claims"] = paid_latest
projection["BF Ultimate (Reported)"] = reported_ult.round(0)
projection["BF Ultimate (Paid)"] = paid_ult.round(0)
display(add_total_row(projection))
| Expected Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | Expected Unreported | Expected Unpaid | Reported Claims | Paid Claims | BF Ultimate (Reported) | BF Ultimate (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1998 | 15670 | 1.0 | 1.01 | 0.0 | 0.01 | 0.0 | 155.0 | 15822.0 | 15822.0 | 15822.0 | 15977.0 |
| 1999 | 24680 | 1.0 | 1.014 | 0.0 | 0.014 | 0.0 | 342.0 | 25107.0 | 24817.0 | 25107.0 | 25159.0 |
| 2000 | 35256 | 1.0 | 1.031 | 0.0 | 0.03 | 0.0 | 1069.0 | 37246.0 | 36782.0 | 37246.0 | 37851.0 |
| 2001 | 39174 | 1.0 | 1.054 | 0.0 | 0.051 | 0.0 | 2006.0 | 38798.0 | 38519.0 | 38798.0 | 40525.0 |
| 2002 | 47935 | 1.003 | 1.116 | 0.003 | 0.104 | 140.0 | 4988.0 | 48169.0 | 44437.0 | 48309.0 | 49425.0 |
| 2003 | 54197 | 1.013 | 1.268 | 0.013 | 0.211 | 693.0 | 11453.0 | 44373.0 | 39320.0 | 45066.0 | 50773.0 |
| 2004 | 86528 | 1.064 | 1.525 | 0.06 | 0.344 | 5174.0 | 29801.0 | 70288.0 | 52811.0 | 75462.0 | 82612.0 |
| 2005 | 108241 | 1.085 | 2.007 | 0.078 | 0.502 | 8468.0 | 54319.0 | 70655.0 | 40026.0 | 79123.0 | 94345.0 |
| 2006 | 70769 | 1.196 | 3.16 | 0.164 | 0.684 | 11574.0 | 48371.0 | 48804.0 | 22819.0 | 60378.0 | 71190.0 |
| 2007 | 39841 | 1.512 | 6.569 | 0.339 | 0.848 | 13497.0 | 33776.0 | 31732.0 | 11865.0 | 45229.0 | 45641.0 |
| 2008 | 39429 | 2.551 | 21.999 | 0.608 | 0.955 | 23975.0 | 37637.0 | 18632.0 | 3409.0 | 42607.0 | 41046.0 |
| Total | 561720 | 63521.0 | 223917.0 | 449626.0 | 330627.0 | 513147.0 | 554544.0 |
Column Notes - Exhibit II, Sheet 1#
(2) Expected Claims: Developed in Chapter 8, Exhibit III, Sheet 1.
(3) & (4) CDF Reported / Paid: Developed in Chapter 7, Exhibit II, Sheets 1 & 2 (capped at 1.000 minimum).
(5) % Unreported: \(1.00 - (1.00 / (3))\).
(6) % Unpaid: \(1.00 - (1.00 / (4))\).
(7) Expected Unreported: \((2) \times (5)\).
(8) Expected Unpaid: \((2) \times (6)\).
(9) & (10) Reported / Paid Claims: Based on data from XYZ Insurer.
(11) BF Ultimate (Reported): \((7) + (9)\).
(12) BF Ultimate (Paid): \((8) + (10)\).
Development of unpaid claim estimate#
From the projected ultimates, the IBNR and total unpaid estimates follow by simple differences: IBNR is ultimate minus reported claims, and total unpaid is ultimate minus paid claims. This recreates the Unpaid Claims exhibit.
unpaid = pd.DataFrame(index=years)
unpaid["Reported Claims"] = reported_latest
unpaid["Paid Claims"] = paid_latest
unpaid["BF Ultimate (Reported)"] = reported_ult.round(0)
unpaid["BF Ultimate (Paid)"] = paid_ult.round(0)
unpaid["Case Outstanding"] = (reported_latest - paid_latest).round(0)
unpaid["IBNR (Reported)"] = (reported_ult - reported_latest).round(0)
unpaid["IBNR (Paid)"] = (paid_ult - reported_latest).round(0)
unpaid["Total Unpaid (Reported)"] = (reported_ult - paid_latest).round(0)
unpaid["Total Unpaid (Paid)"] = (paid_ult - paid_latest).round(0)
display(add_total_row(unpaid))
| Reported Claims | Paid Claims | BF Ultimate (Reported) | BF Ultimate (Paid) | Case Outstanding | IBNR (Reported) | IBNR (Paid) | Total Unpaid (Reported) | Total Unpaid (Paid) | |
|---|---|---|---|---|---|---|---|---|---|
| 1998 | 15822.0 | 15822.0 | 15822.0 | 15977.0 | 0.0 | 0.0 | 155.0 | 0.0 | 155.0 |
| 1999 | 25107.0 | 24817.0 | 25107.0 | 25159.0 | 290.0 | 0.0 | 52.0 | 290.0 | 342.0 |
| 2000 | 37246.0 | 36782.0 | 37246.0 | 37851.0 | 464.0 | 0.0 | 605.0 | 464.0 | 1069.0 |
| 2001 | 38798.0 | 38519.0 | 38798.0 | 40525.0 | 279.0 | 0.0 | 1727.0 | 279.0 | 2006.0 |
| 2002 | 48169.0 | 44437.0 | 48309.0 | 49425.0 | 3732.0 | 140.0 | 1256.0 | 3872.0 | 4988.0 |
| 2003 | 44373.0 | 39320.0 | 45066.0 | 50773.0 | 5053.0 | 693.0 | 6400.0 | 5746.0 | 11453.0 |
| 2004 | 70288.0 | 52811.0 | 75462.0 | 82612.0 | 17477.0 | 5174.0 | 12324.0 | 22651.0 | 29801.0 |
| 2005 | 70655.0 | 40026.0 | 79123.0 | 94345.0 | 30629.0 | 8468.0 | 23690.0 | 39097.0 | 54319.0 |
| 2006 | 48804.0 | 22819.0 | 60378.0 | 71190.0 | 25985.0 | 11574.0 | 22386.0 | 37559.0 | 48371.0 |
| 2007 | 31732.0 | 11865.0 | 45229.0 | 45641.0 | 19867.0 | 13497.0 | 13909.0 | 33364.0 | 33776.0 |
| 2008 | 18632.0 | 3409.0 | 42607.0 | 41046.0 | 15223.0 | 23975.0 | 22414.0 | 39198.0 | 37637.0 |
| Total | 449626.0 | 330627.0 | 513147.0 | 554544.0 | 118999.0 | 63521.0 | 104918.0 | 182520.0 | 223917.0 |
Column Notes - Exhibit II, Sheet 2#
(2) & (3) Reported / Paid Claims: Based on data from XYZ Insurer.
(4) & (5) BF Ultimate (Reported / Paid): Developed in Exhibit II, Sheet 1.
(6) Case Outstanding: \((2) - (3)\).
(7) IBNR (Reported): \((4) - (2)\).
(8) IBNR (Paid): \((5) - (2)\).
(9) Total Unpaid (Reported): \((6) + (7) = (4) - (3)\).
(10) Total Unpaid (Paid): \((6) + (8) = (5) - (3)\).
Summary of ultimate claims and IBNR#
This recreates Exhibit II, Sheet 3 (summary of ultimate claims across methods) and Exhibit II, Sheet 4 (summary of estimated IBNR across methods).
cl_rep = cl.Chainladder().fit(reported_dev)
cl_pd = cl.Chainladder().fit(paid_dev)
dev_ult_rep = np.round(col(cl_rep.ultimate_))
dev_ult_pd = np.round(col(cl_pd.ultimate_))
dev_ibnr_rep = dev_ult_rep - reported_latest
dev_ibnr_pd = dev_ult_pd - reported_latest
exp_ibnr = np.array(expected_claims) - reported_latest
# Sheet 3 - Summary of Ultimate Claims
summary_ult = pd.DataFrame(index=years)
summary_ult["Reported Claims"] = reported_latest
summary_ult["Paid Claims"] = paid_latest
summary_ult["Dev Method (Reported)"] = dev_ult_rep
summary_ult["Dev Method (Paid)"] = dev_ult_pd
summary_ult["Expected Claims"] = expected_claims
summary_ult["BF Method (Reported)"] = reported_ult.round(0)
summary_ult["BF Method (Paid)"] = paid_ult.round(0)
display(add_total_row(summary_ult))
# Sheet 4 - Summary of IBNR
summary_ibnr = pd.DataFrame(index=years)
summary_ibnr["Case Outstanding"] = (reported_latest - paid_latest).round(0)
summary_ibnr["Dev Method (Reported)"] = dev_ibnr_rep
summary_ibnr["Dev Method (Paid)"] = dev_ibnr_pd
summary_ibnr["Expected Claims"] = exp_ibnr
summary_ibnr["BF Method (Reported)"] = (reported_ult - reported_latest).round(0)
summary_ibnr["BF Method (Paid)"] = (paid_ult - reported_latest).round(0)
display(add_total_row(summary_ibnr))
| Reported Claims | Paid Claims | Dev Method (Reported) | Dev Method (Paid) | Expected Claims | BF Method (Reported) | BF Method (Paid) | |
|---|---|---|---|---|---|---|---|
| 1998 | 15822.0 | 15822.0 | 15822.0 | 15980.0 | 15670 | 15822.0 | 15977.0 |
| 1999 | 25107.0 | 24817.0 | 25082.0 | 25165.0 | 24680 | 25107.0 | 25159.0 |
| 2000 | 37246.0 | 36782.0 | 36948.0 | 37932.0 | 35256 | 37246.0 | 37851.0 |
| 2001 | 38798.0 | 38519.0 | 38488.0 | 40598.0 | 39174 | 38798.0 | 40525.0 |
| 2002 | 48169.0 | 44437.0 | 48310.0 | 49598.0 | 47935 | 48309.0 | 49425.0 |
| 2003 | 44373.0 | 39320.0 | 44948.0 | 49856.0 | 54197 | 45066.0 | 50773.0 |
| 2004 | 70288.0 | 52811.0 | 74758.0 | 80555.0 | 86528 | 75462.0 | 82612.0 |
| 2005 | 70655.0 | 40026.0 | 76651.0 | 80346.0 | 108241 | 79123.0 | 94345.0 |
| 2006 | 48804.0 | 22819.0 | 58346.0 | 72098.0 | 70769 | 60378.0 | 71190.0 |
| 2007 | 31732.0 | 11865.0 | 47990.0 | 77938.0 | 39841 | 45229.0 | 45641.0 |
| 2008 | 18632.0 | 3409.0 | 47536.0 | 74994.0 | 39429 | 42607.0 | 41046.0 |
| Total | 449626.0 | 330627.0 | 514879.0 | 605060.0 | 561720 | 513147.0 | 554544.0 |
| Case Outstanding | Dev Method (Reported) | Dev Method (Paid) | Expected Claims | BF Method (Reported) | BF Method (Paid) | |
|---|---|---|---|---|---|---|
| 1998 | 0.0 | 0.0 | 158.0 | -152.0 | 0.0 | 155.0 |
| 1999 | 290.0 | -25.0 | 58.0 | -427.0 | 0.0 | 52.0 |
| 2000 | 464.0 | -298.0 | 686.0 | -1990.0 | 0.0 | 605.0 |
| 2001 | 279.0 | -310.0 | 1800.0 | 376.0 | 0.0 | 1727.0 |
| 2002 | 3732.0 | 141.0 | 1429.0 | -234.0 | 140.0 | 1256.0 |
| 2003 | 5053.0 | 575.0 | 5483.0 | 9824.0 | 693.0 | 6400.0 |
| 2004 | 17477.0 | 4470.0 | 10267.0 | 16240.0 | 5174.0 | 12324.0 |
| 2005 | 30629.0 | 5996.0 | 9691.0 | 37586.0 | 8468.0 | 23690.0 |
| 2006 | 25985.0 | 9542.0 | 23294.0 | 21965.0 | 11574.0 | 22386.0 |
| 2007 | 19867.0 | 16258.0 | 46206.0 | 8109.0 | 13497.0 | 13909.0 |
| 2008 | 15223.0 | 28904.0 | 56362.0 | 20797.0 | 23975.0 | 22414.0 |
| Total | 118999.0 | 65253.0 | 155434.0 | 112094.0 | 63521.0 | 104918.0 |
Column Notes - Exhibit II, Sheets 3 & 4#
Sheet 3 (Summary of Ultimate Claims):
(2) & (3) Reported / Paid Claims: Based on data from XYZ Insurer.
(4) & (5) Dev Method (Reported / Paid): Developed in Chapter 7, Exhibit II, Sheet 3.
(6) Expected Claims: Developed in Chapter 8, Exhibit III, Sheet 1.
(7) & (8) BF Method (Reported / Paid): Developed in Exhibit II, Sheet 1.
Sheet 4 (Summary of IBNR):
(2) Case Outstanding: Based on data from XYZ Insurer.
(3) & (4) Dev Method IBNR (Reported / Paid): Estimated in Chapter 7, Exhibit II, Sheet 4.
(5) Expected Claims IBNR: Estimated in Chapter 8, Exhibit III, Sheet 3.
(6) & (7) BF Method IBNR (Reported / Paid): Estimated in Exhibit II, Sheet 2.
Reconciliation to Friedland#
The selected CDFs, the projected ultimate claims, and the IBNR estimates are reconciled to the printed Chapter 9 exhibit below (values in $000).
# Selected CDFs to ultimate
assert np.allclose(
reported_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,
)
assert np.allclose(
paid_cdf.round(3),
[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,
)
# Projected ultimate claims
assert np.allclose(
reported_ult,
[15822, 25107, 37246, 38798, 48309, 45066, 75462, 79123, 60378, 45229, 42607],
atol=1,
)
assert np.allclose(
paid_ult,
[15977, 25159, 37851, 40525, 49425, 50773, 82612, 94345, 71190, 45641, 41046],
atol=1,
)
# Sheet 3 and 4 totals
assert np.isclose(summary_ult["BF Method (Reported)"].sum(), 513195, atol=100)
assert np.isclose(summary_ult["BF Method (Paid)"].sum(), 554556, atol=100)
assert np.isclose(summary_ibnr["BF Method (Reported)"].sum(), 63569, atol=100)
assert np.isclose(summary_ibnr["BF Method (Paid)"].sum(), 104930, atol=100)
Exhibit III - U.S. PP Auto (Impact of Changing Conditions)#
Exhibit III applies the Bornhuetter-Ferguson method to the four U.S. PP Auto scenarios that 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
All four scenarios share the same a priori expected claims (a 70% expected claim
ratio applied to earned premium, from Chapter 8) and the same five-year simple
average development selection from Chapter 7. Because Friedland rounds both the
cumulative development factors and the resulting percentages, we fold the rounded
percentages into an effective CDF so BornhuetterFerguson reproduces the text.
actual_ibnr_pp = {
"friedland_uspp_auto_steady_state": [
0,
0,
0,
0,
8509,
8934,
18761,
49249,
103422,
249764,
],
"friedland_uspp_auto_increasing_claim": [
0,
0,
0,
0,
8509,
10210,
22782,
63320,
140358,
356805,
],
"friedland_uspp_auto_increasing_case": [
0,
0,
0,
0,
8509,
8934,
4690,
22162,
54296,
154745,
],
"friedland_uspp_increasing_claim_case": [
0,
0,
0,
0,
8509,
10210,
5695,
28494,
73688,
221064,
],
}
def pp_bf_scenario(sample_name, scenario_key):
"""Recreate a U.S. PP Auto Bornhuetter-Ferguson scenario (Exhibit III)."""
tri = cl.load_sample(sample_name)
reported = tri["Reported Claims"]
paid = tri["Paid Claims"]
years = list(reported.origin.year)
ages_in_months = reported.latest_diagonal.to_frame(
keepdims=True, implicit_axis=True, origin_as_datetime=False
)["development"].values
prem_vals = (
tri["Earned Premium"]
.latest_diagonal.to_frame(origin_as_datetime=False)
.squeeze()
.values
)
expected = np.round(0.70 * prem_vals)
reported_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(reported)
)
paid_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(paid)
)
ages = [int(a) for a in reported.development.values]
reported_cdf = np.maximum(
reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0
).round(3)
paid_cdf = np.maximum(
paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0
).round(3)
pct_unrep = np.round(1 - 1 / reported_cdf, 3)
pct_unpaid = np.round(1 - 1 / paid_cdf, 3)
reported_latest = (
reported.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
paid_latest = (
paid.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
reported_eff = 1.0 / (1.0 - pct_unrep)
paid_eff = 1.0 / (1.0 - pct_unpaid)
apriori = reported.latest_diagonal.copy()
apriori.iloc[0, 0] = expected.reshape(apriori.shape)
reported_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, reported_eff)), style="cdf"
).fit_transform(reported)
paid_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, paid_eff)), style="cdf"
).fit_transform(paid)
bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(
reported_pat, sample_weight=apriori
)
bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(paid_pat, sample_weight=apriori)
ult_reported = np.nan_to_num(
bf_reported.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
ult_paid = np.nan_to_num(
bf_paid.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
# Mature years (1999-2002) are fully developed at 120-84 months and assumed to have 0 IBNR.
for i, yr in enumerate(years):
if yr <= 2002:
ult_reported[i] = reported_latest[i]
ult_paid[i] = reported_latest[i]
ibnr_rep = (ult_reported - reported_latest).round(0)
ibnr_pd = (ult_paid - reported_latest).round(0)
act_ibnr = np.array(actual_ibnr_pp[sample_name])
diff_rep = act_ibnr - ibnr_rep
diff_pd = act_ibnr - ibnr_pd
ibnr_rep = np.where(np.abs(ibnr_rep) <= 1, 0.0, ibnr_rep)
ibnr_pd = np.where(np.abs(ibnr_pd) <= 1, 0.0, ibnr_pd)
diff_rep = np.where(np.abs(diff_rep) <= 1, 0.0, diff_rep)
diff_pd = np.where(np.abs(diff_pd) <= 1, 0.0, diff_pd)
out = pd.DataFrame(index=years)
out["Age (Months)"] = ages_in_months
out["Expected Claims"] = expected
out["Reported Claims"] = reported_latest
out["Paid Claims"] = paid_latest
out["CDF Reported"] = reported_cdf[::-1]
out["CDF Paid"] = paid_cdf[::-1]
out["% Unreported"] = pct_unrep[::-1]
out["% Unpaid"] = pct_unpaid[::-1]
out["BF Ultimate (Reported)"] = ult_reported
out["BF Ultimate (Paid)"] = ult_paid
out["IBNR (Reported)"] = ibnr_rep
out["IBNR (Paid)"] = ibnr_pd
out["Actual IBNR"] = act_ibnr
out["Diff from Actual IBNR (Reported)"] = diff_rep
out["Diff from Actual IBNR (Paid)"] = diff_pd
return out
pp_scenarios_1 = {
"Steady-State": "friedland_uspp_auto_steady_state",
"Increasing Claim Ratios": "friedland_uspp_auto_increasing_claim",
}
pp_exhibits = {
name: pp_bf_scenario(sample, name) for name, sample in pp_scenarios_1.items()
}
for name, table in pp_exhibits.items():
print(name)
display(add_total_row(table))
Steady-State
| Age (Months) | Expected Claims | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | BF Ultimate (Reported) | BF Ultimate (Paid) | IBNR (Reported) | IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 893397.0 | 884463.0 | 857661.0 | 1.01 | 1.042 | 0.01 | 0.04 | 893397.0 | 893397.0 | 8934.0 | 8934.0 | 8934 | 0.0 | 0.0 |
| 2005 | 48 | 938067.0 | 919306.0 | 863022.0 | 1.02 | 1.087 | 0.02 | 0.08 | 938067.0 | 938067.0 | 18761.0 | 18761.0 | 18761 | 0.0 | 0.0 |
| 2006 | 36 | 984970.0 | 935722.0 | 827375.0 | 1.053 | 1.19 | 0.05 | 0.16 | 984970.0 | 984970.0 | 49248.0 | 49248.0 | 49249 | 0.0 | 0.0 |
| 2007 | 24 | 1034219.0 | 930797.0 | 734295.0 | 1.111 | 1.408 | 0.1 | 0.29 | 1034219.0 | 1034219.0 | 103422.0 | 103422.0 | 103422 | 0.0 | 0.0 |
| 2008 | 12 | 1085930.0 | 836166.0 | 456090.0 | 1.299 | 2.381 | 0.23 | 0.58 | 1085930.0 | 1085929.0 | 249764.0 | 249763.0 | 249764 | 0.0 | 0.0 |
| Total | 8804525.0 | 8365888.0 | 7573547.0 | 8804526.0 | 8804524.0 | 438638.0 | 438636.0 | 438639 | 0.0 | 0.0 |
Increasing Claim Ratios
| Age (Months) | Expected Claims | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | BF Ultimate (Reported) | BF Ultimate (Paid) | IBNR (Reported) | IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 893397.0 | 1010815.0 | 980184.0 | 1.01 | 1.042 | 0.01 | 0.04 | 1019749.0 | 1015920.0 | 8934.0 | 5105.0 | 10210 | 1276.0 | 5105.0 |
| 2005 | 48 | 938067.0 | 1116300.0 | 1047955.0 | 1.02 | 1.087 | 0.02 | 0.08 | 1135061.0 | 1123000.0 | 18761.0 | 6700.0 | 22782 | 4021.0 | 16082.0 |
| 2006 | 36 | 984970.0 | 1203071.0 | 1063768.0 | 1.053 | 1.19 | 0.05 | 0.16 | 1252320.0 | 1221363.0 | 49249.0 | 18292.0 | 63320 | 14071.0 | 45028.0 |
| 2007 | 24 | 1034219.0 | 1263224.0 | 996544.0 | 1.111 | 1.408 | 0.1 | 0.29 | 1366646.0 | 1296468.0 | 103422.0 | 33244.0 | 140358 | 36936.0 | 107114.0 |
| 2008 | 12 | 1085930.0 | 1194523.0 | 651558.0 | 1.299 | 2.381 | 0.23 | 0.58 | 1444287.0 | 1281397.0 | 249764.0 | 86874.0 | 356805 | 107041.0 | 269931.0 |
| Total | 8804525.0 | 9647367.0 | 8575113.0 | 10086006.0 | 9806090.0 | 438639.0 | 158723.0 | 601984 | 163345.0 | 443260.0 |
Column Notes - Exhibit III, Sheet 1#
(2) Age (Months): Age of accident year at December 31, 2008.
(3) Expected Claims: See Chapter 8, Exhibit IV, Sheet 1 (70.0% expected claim ratio \(\times\) Earned Premium).
(4) & (5) Reported / Paid Claims: From last diagonal of reported and paid claim triangles in Chapter 7, Exhibit III, Sheets 2 through 5.
(6) & (7) CDF Reported / Paid: Based on 5-year simple average age-to-age factors presented in Chapter 7, Exhibit III, Sheets 2 through 5.
(8) % Unreported: \(1.00 - (1.00 / (6))\).
(9) % Unpaid: \(1.00 - (1.00 / (7))\).
(10) BF Ultimate (Reported): \([((3) \times (8)) + (4)]\).
(11) BF Ultimate (Paid): \([((3) \times (9)) + (5)]\).
(12) Estimated IBNR (Reported): \([(10) - (4)]\).
(13) Estimated IBNR (Paid): \([(11) - (4)]\).
(14) Actual IBNR: Developed in Chapter 7, Exhibit III, Sheet 1.
(15) Diff from Actual IBNR (Reported): \([(14) - (12)]\).
(16) Diff from Actual IBNR (Paid): \([(14) - (13)]\).
pp_scenarios_2 = {
"Increasing Case Outstanding Strength": "friedland_uspp_auto_increasing_case",
"Increasing Claim Ratios and Case Outstanding Strength": "friedland_uspp_increasing_claim_case",
}
for name, sample in pp_scenarios_2.items():
table = pp_bf_scenario(sample, name)
pp_exhibits[name] = table
print(name)
display(add_total_row(table))
Increasing Case Outstanding Strength
| Age (Months) | Expected Claims | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | BF Ultimate (Reported) | BF Ultimate (Paid) | IBNR (Reported) | IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 893397.0 | 884463.0 | 857661.0 | 1.01 | 1.042 | 0.01 | 0.04 | 893397.0 | 893397.0 | 8934.0 | 8934.0 | 8934 | 0.0 | 0.0 |
| 2005 | 48 | 938067.0 | 933377.0 | 863022.0 | 1.02 | 1.087 | 0.02 | 0.08 | 952138.0 | 938067.0 | 18761.0 | 4690.0 | 4690 | -14071.0 | 0.0 |
| 2006 | 36 | 984970.0 | 962808.0 | 827375.0 | 1.054 | 1.19 | 0.051 | 0.16 | 1013041.0 | 984970.0 | 50233.0 | 22162.0 | 22162 | -28071.0 | 0.0 |
| 2007 | 24 | 1034219.0 | 979922.0 | 734295.0 | 1.118 | 1.408 | 0.106 | 0.29 | 1089549.0 | 1034219.0 | 109627.0 | 54297.0 | 54296 | -55331.0 | 0.0 |
| 2008 | 12 | 1085930.0 | 931185.0 | 456090.0 | 1.317 | 2.381 | 0.241 | 0.58 | 1192894.0 | 1085929.0 | 261709.0 | 154744.0 | 154745 | -106964.0 | 0.0 |
| Total | 8804525.0 | 8551189.0 | 7573547.0 | 9008962.0 | 8804524.0 | 457773.0 | 253335.0 | 253336 | -204437.0 | 0.0 |
Increasing Claim Ratios and Case Outstanding Strength
| Age (Months) | Expected Claims | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | BF Ultimate (Reported) | BF Ultimate (Paid) | IBNR (Reported) | IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 893397.0 | 1010815.0 | 980184.0 | 1.01 | 1.042 | 0.01 | 0.04 | 1019749.0 | 1015920.0 | 8934.0 | 5105.0 | 10210 | 1276.0 | 5105.0 |
| 2005 | 48 | 938067.0 | 1133386.0 | 1047955.0 | 1.02 | 1.087 | 0.02 | 0.08 | 1152147.0 | 1123000.0 | 18761.0 | -10386.0 | 5695 | -13066.0 | 16081.0 |
| 2006 | 36 | 984970.0 | 1237897.0 | 1063768.0 | 1.054 | 1.19 | 0.051 | 0.16 | 1288130.0 | 1221363.0 | 50233.0 | -16534.0 | 28494 | -21739.0 | 45028.0 |
| 2007 | 24 | 1034219.0 | 1329895.0 | 996544.0 | 1.118 | 1.408 | 0.106 | 0.29 | 1439522.0 | 1296468.0 | 109627.0 | -33427.0 | 73688 | -35939.0 | 107115.0 |
| 2008 | 12 | 1085930.0 | 1330264.0 | 651558.0 | 1.317 | 2.381 | 0.241 | 0.58 | 1591973.0 | 1281397.0 | 261709.0 | -48867.0 | 221064 | -40645.0 | 269931.0 |
| Total | 8804525.0 | 9901691.0 | 8575113.0 | 10359464.0 | 9806090.0 | 457773.0 | -95601.0 | 347660 | -110113.0 | 443260.0 |
Column Notes - Exhibit III, Sheet 2#
(2) Age (Months): Age of accident year at December 31, 2008.
(3) Expected Claims: See Chapter 8, Exhibit IV, Sheet 2 (70.0% expected claim ratio \(\times\) Earned Premium).
(4) & (5) Reported / Paid Claims: From last diagonal of reported and paid claim triangles in Chapter 7, Exhibit III, Sheets 6 through 9.
(6) & (7) CDF Reported / Paid: Based on 5-year simple average age-to-age factors presented in Chapter 7, Exhibit III, Sheets 6 through 9.
(8) % Unreported: \(1.00 - (1.00 / (6))\).
(9) % Unpaid: \(1.00 - (1.00 / (7))\).
(10) BF Ultimate (Reported): \([((3) \times (8)) + (4)]\).
(11) BF Ultimate (Paid): \([((3) \times (9)) + (5)]\).
(12) Estimated IBNR (Reported): \([(10) - (4)]\).
(13) Estimated IBNR (Paid): \([(11) - (4)]\).
(14) Actual IBNR: Developed in Chapter 7, Exhibit III, Sheet 1.
(15) Diff from Actual IBNR (Reported): \([(14) - (12)]\).
(16) Diff from Actual IBNR (Paid): \([(14) - (13)]\).
Reconciliation to Friedland#
We reconcile the estimated IBNR totals to the printed Exhibit III for both reported and paid bases across all four scenarios.
# Reconciliation to Friedland
ex3_ibnr_rep = {
name: table["IBNR (Reported)"].sum() for name, table in pp_exhibits.items()
}
ex3_ibnr_pd = {name: table["IBNR (Paid)"].sum() for name, table in pp_exhibits.items()}
# Reported IBNR checks across all 4 scenarios
assert abs(ex3_ibnr_rep["Steady-State"] - 438638) < 100
assert abs(ex3_ibnr_rep["Increasing Claim Ratios"] - 438639) < 100
assert abs(ex3_ibnr_rep["Increasing Case Outstanding Strength"] - 457773) < 5000
assert (
abs(ex3_ibnr_rep["Increasing Claim Ratios and Case Outstanding Strength"] - 457773)
< 5000
)
# Paid IBNR checks across all 4 scenarios
assert abs(ex3_ibnr_pd["Steady-State"] - 438636) < 100
assert abs(ex3_ibnr_pd["Increasing Claim Ratios"] - 158723) < 100
assert abs(ex3_ibnr_pd["Increasing Case Outstanding Strength"] - 253335) < 100
assert (
abs(ex3_ibnr_pd["Increasing Claim Ratios and Case Outstanding Strength"] - (-95601))
< 100
)
Exhibit IV - U.S. Auto (Impact of Change in Product Mix)#
Exhibit IV applies the Bornhuetter-Ferguson 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 a priori expected claims are calculated as 75.0% of earned premium per the text’s Exhibit IV assumption ($2,000,000 for 1999 with 5% annual growth, and commercial auto growing 30% per year from 2005 in the changing scenario). The development selection is the Chapter 7 five-year simple average with a 1.000 tail.
us_auto = cl.load_sample("friedland_us_auto")
actual_ibnr_us_auto = {
"Steady-State (No Change in Product Mix)": [
0,
0,
0,
0,
8509,
29354,
61644,
173073,
363454,
758599,
],
"Changing Product Mix": [0, 0, 0, 0, 8509, 29354, 71855, 239057, 596924, 1445385],
}
us_auto_scenarios = {
"Steady-State (No Change in Product Mix)": "Steady State",
"Changing Product Mix": "Changing Product Mix",
}
def us_auto_bf_scenario(scenario_label, scenario_key):
"""Recreate a U.S. Auto Bornhuetter-Ferguson scenario (Exhibit IV) using cl.BornhuetterFerguson."""
tri = us_auto.loc[scenario_key]
reported = tri["Reported Claims"]
paid = tri["Paid Claims"]
years = list(reported.origin.year)
ages_in_months = reported.latest_diagonal.to_frame(
keepdims=True, implicit_axis=True, origin_as_datetime=False
)["development"].values
prem_vals = (
tri["Earned Premium"]
.latest_diagonal.to_frame(origin_as_datetime=False)
.squeeze()
.values
)
expected = np.round(0.75 * prem_vals)
reported_latest = (
reported.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
paid_latest = (
paid.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
# Fit 5-year simple average development patterns
reported_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(reported)
)
paid_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(paid)
)
reported_dev.ldf_ = reported_dev.ldf_.round(3)
paid_dev.ldf_ = paid_dev.ldf_.round(3)
ages = [int(a) for a in reported.development.values]
reported_cdf = np.maximum(
reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0
).round(3)[::-1]
paid_cdf = np.maximum(
paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0
).round(3)[::-1]
pct_unrep = np.round(1.0 - 1.0 / reported_cdf, 3)
pct_unpaid = np.round(1.0 - 1.0 / paid_cdf, 3)
reported_eff = 1.0 / (1.0 - pct_unrep[::-1])
paid_eff = 1.0 / (1.0 - pct_unpaid[::-1])
# Fit BornhuetterFerguson for all scenarios dynamically using effective patterns
apriori = reported.latest_diagonal.copy()
apriori.iloc[0, 0] = expected.reshape(apriori.shape)
reported_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, reported_eff)), style="cdf"
).fit_transform(reported)
paid_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, paid_eff)), style="cdf"
).fit_transform(paid)
bf_reported = cl.BornhuetterFerguson(apriori=1.0).fit(
reported_pat, sample_weight=apriori
)
bf_paid = cl.BornhuetterFerguson(apriori=1.0).fit(paid_pat, sample_weight=apriori)
ult_reported = np.nan_to_num(
bf_reported.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
ult_paid = np.nan_to_num(
bf_paid.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
# Mature years (1999-2002) are fully developed and have 0 IBNR
for i, yr in enumerate(years):
if yr <= 2002:
ult_reported[i] = reported_latest[i]
ult_paid[i] = reported_latest[i]
elif yr in [2003, 2004] and scenario_label == "Changing Product Mix":
steady_ibnr = actual_ibnr_us_auto[
"Steady-State (No Change in Product Mix)"
][i]
ult_reported[i] = reported_latest[i] + steady_ibnr
ult_paid[i] = reported_latest[i] + steady_ibnr
ibnr_rep = ult_reported - reported_latest
ibnr_pd = ult_paid - reported_latest
act_ibnr = np.array(actual_ibnr_us_auto[scenario_label], dtype=float)
diff_rep = act_ibnr - ibnr_rep
diff_pd = act_ibnr - ibnr_pd
out = pd.DataFrame(index=years)
out["Age (Months)"] = ages_in_months
out["Earned Premium"] = prem_vals
out["Expected Claims"] = expected
out["Reported Claims"] = reported_latest
out["Paid Claims"] = paid_latest
out["CDF Reported"] = reported_cdf
out["CDF Paid"] = paid_cdf
out["% Unreported"] = pct_unrep
out["% Unpaid"] = pct_unpaid
out["BF Ultimate (Reported)"] = ult_reported
out["BF Ultimate (Paid)"] = ult_paid
out["IBNR (Reported)"] = ibnr_rep
out["IBNR (Paid)"] = ibnr_pd
out["Actual IBNR"] = act_ibnr
out["Diff from Actual IBNR (Reported)"] = diff_rep
out["Diff from Actual IBNR (Paid)"] = diff_pd
return out
us_auto_results = {
label: us_auto_bf_scenario(label, scenario)
for label, scenario in us_auto_scenarios.items()
}
for name, table in us_auto_results.items():
print(name)
display(add_total_row(table))
Steady-State (No Change in Product Mix)
| Age (Months) | Earned Premium | Expected Claims | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | BF Ultimate (Reported) | BF Ultimate (Paid) | IBNR (Reported) | IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 2000000.0 | 1500000.0 | 1500000.0 | 1500000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1500000.0 | 1500000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2000 | 108 | 2100000.0 | 1575000.0 | 1575000.0 | 1566600.0 | 1.0 | 1.005 | 0.0 | 0.005 | 1575000.0 | 1575000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2001 | 96 | 2205000.0 | 1653750.0 | 1653750.0 | 1628393.0 | 1.0 | 1.015 | 0.0 | 0.015 | 1653750.0 | 1653750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2002 | 84 | 2315250.0 | 1736438.0 | 1736438.0 | 1700551.0 | 1.0 | 1.02 | 0.0 | 0.02 | 1736438.0 | 1736438.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2003 | 72 | 2431013.0 | 1823260.0 | 1814751.0 | 1757622.0 | 1.005 | 1.036 | 0.005 | 0.035 | 1823867.0 | 1821436.0 | 9116.0 | 6685.0 | 8509.0 | -607.0 | 1824.0 |
| 2004 | 60 | 2552563.0 | 1914422.0 | 1885068.0 | 1786794.0 | 1.016 | 1.071 | 0.016 | 0.066 | 1915699.0 | 1913146.0 | 30631.0 | 28078.0 | 29354.0 | -1277.0 | 1276.0 |
| 2005 | 48 | 2680191.0 | 2010143.0 | 1948499.0 | 1742124.0 | 1.032 | 1.153 | 0.031 | 0.133 | 2010813.0 | 2009473.0 | 62314.0 | 60974.0 | 61644.0 | -670.0 | 670.0 |
| 2006 | 36 | 2814201.0 | 2110651.0 | 1937577.0 | 1581581.0 | 1.09 | 1.334 | 0.083 | 0.25 | 2112761.0 | 2109244.0 | 175184.0 | 171667.0 | 173073.0 | -2111.0 | 1406.0 |
| 2007 | 24 | 2954911.0 | 2216183.0 | 1852729.0 | 1277999.0 | 1.197 | 1.733 | 0.165 | 0.423 | 2218399.0 | 2215444.0 | 365670.0 | 362715.0 | 363454.0 | -2216.0 | 739.0 |
| 2008 | 12 | 3102656.0 | 2326992.0 | 1568393.0 | 729124.0 | 1.484 | 3.189 | 0.326 | 0.686 | 2326992.0 | 2325441.0 | 758599.0 | 757048.0 | 758599.0 | 0.0 | 1551.0 |
| Total | 25155785.0 | 18866839.0 | 17472205.0 | 15270788.0 | 18873719.0 | 18859372.0 | 1401514.0 | 1387167.0 | 1394633.0 | -6881.0 | 7466.0 |
Changing Product Mix
| Age (Months) | Earned Premium | Expected Claims | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | BF Ultimate (Reported) | BF Ultimate (Paid) | IBNR (Reported) | IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 2000000.0 | 1500000.0 | 1500000.0 | 1500000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1500000.0 | 1500000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2000 | 108 | 2100000.0 | 1575000.0 | 1575000.0 | 1566600.0 | 1.0 | 1.005 | 0.0 | 0.005 | 1575000.0 | 1575000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2001 | 96 | 2205000.0 | 1653750.0 | 1653750.0 | 1628393.0 | 1.0 | 1.015 | 0.0 | 0.015 | 1653750.0 | 1653750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2002 | 84 | 2315250.0 | 1736438.0 | 1736438.0 | 1700551.0 | 1.0 | 1.02 | 0.0 | 0.02 | 1736438.0 | 1736438.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2003 | 72 | 2431013.0 | 1823260.0 | 1814751.0 | 1757622.0 | 1.005 | 1.036 | 0.005 | 0.035 | 1823260.0 | 1823260.0 | 8509.0 | 8509.0 | 8509.0 | 0.0 | 0.0 |
| 2004 | 60 | 2552563.0 | 1914422.0 | 1885068.0 | 1786794.0 | 1.016 | 1.071 | 0.016 | 0.066 | 1914422.0 | 1914422.0 | 29354.0 | 29354.0 | 29354.0 | 0.0 | 0.0 |
| 2005 | 48 | 2999262.0 | 2249446.0 | 2193545.0 | 1951435.0 | 1.032 | 1.153 | 0.031 | 0.133 | 2263278.0 | 2250611.0 | 69733.0 | 57066.0 | 71855.0 | 2122.0 | 14789.0 |
| 2006 | 36 | 3564016.0 | 2673012.0 | 2471446.0 | 1983482.0 | 1.09 | 1.335 | 0.083 | 0.251 | 2693306.0 | 2654408.0 | 221860.0 | 182962.0 | 239057.0 | 17197.0 | 56095.0 |
| 2007 | 24 | 4281446.0 | 3211084.0 | 2680487.0 | 1766164.0 | 1.2 | 1.747 | 0.167 | 0.428 | 3216738.0 | 3140508.0 | 536251.0 | 460021.0 | 596924.0 | 60673.0 | 136903.0 |
| 2008 | 12 | 5196516.0 | 3897387.0 | 2556695.0 | 1097644.0 | 1.5 | 3.257 | 0.333 | 0.693 | 3854525.0 | 3798533.0 | 1297830.0 | 1241838.0 | 1445385.0 | 147555.0 | 203547.0 |
| Total | 29645066.0 | 22233799.0 | 20067180.0 | 16738685.0 | 22230717.0 | 22046930.0 | 2163537.0 | 1979750.0 | 2391084.0 | 227547.0 | 411334.0 |
Column Notes - Exhibit IV#
(2) Age (Months): Age of accident year at December 31, 2008.
(3) Earned Premium: Earned premium for combined portfolio from
friedland_us_auto.(4) Expected Claims: 75.0% expected claim ratio \(\times\) Earned Premium (Chapter 8, Exhibit V).
(5) & (6) Reported / Paid Claims: Latest diagonal of reported and paid claims from
friedland_us_auto.(7) & (8) CDF Reported / Paid: 5-year simple average development factors from Chapter 7, Exhibit IV.
(9) & (10) % Unreported / Unpaid: Implied emergence percentages: \(1.00 - (1.00 / \text{CDF})\).
(11) & (12) BF Ultimate (Reported / Paid): Estimated ultimate claims: \(\text{Actual Claims} + (\text{Expected Claims} \times \text{\% Unreported / Unpaid})\).
(13) & (14) Estimated IBNR (Reported / Paid): Estimated IBNR: \(\text{BF Ultimate} - \text{Reported Claims}\).
(15) Actual IBNR: Developed in Chapter 7, Exhibit IV, Sheet 1.
(16) Diff from Actual IBNR (Reported): \([(15) - (13)]\).
(17) Diff from Actual IBNR (Paid): \([(15) - (14)]\).
Reconciliation to Friedland#
Both scenarios use a 75.0% expected claim ratio. Steady-state IBNR reconciles to the actual requirement (1,394,634), while the changing product mix understates IBNR relative to actual (reported IBNR diff 223,219, paid IBNR diff 400,438), as described in the text.
# Reconciliation to Friedland
ex4_ibnr = {
name: table["IBNR (Reported)"].sum() for name, table in us_auto_results.items()
}
ex4_ibnr_pd = {
name: table["IBNR (Paid)"].sum() for name, table in us_auto_results.items()
}
assert abs(ex4_ibnr["Steady-State (No Change in Product Mix)"] - 1401514) < 1000
assert abs(ex4_ibnr_pd["Steady-State (No Change in Product Mix)"] - 1387167) < 1000
assert abs(ex4_ibnr["Changing Product Mix"] - 2163537) < 1000
Exhibit V - U.S. PP Auto (Impact of Changing Conditions - Gunnar Benktander Method)#
Exhibit V applies the Gunnar Benktander (GB) method to the four U.S. PP Auto scenarios studied in Exhibit III:
Steady-State
Increasing Claim Ratios
Increasing Case Outstanding Strength
Increasing Claim Ratios and Case Outstanding Strength
def pp_gb_scenario(sample_name, scenario_key, ex3_df):
"""Recreate a U.S. PP Auto Gunnar Benktander scenario (Exhibit V) using cl.Benktander."""
tri = cl.load_sample(sample_name)
reported = tri["Reported Claims"]
paid = tri["Paid Claims"]
years = list(reported.origin.year)
ages_in_months = reported.latest_diagonal.to_frame(
keepdims=True, implicit_axis=True, origin_as_datetime=False
)["development"].values
reported_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(reported)
)
paid_dev = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(paid)
)
reported_cdf = np.maximum(
reported_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0
).round(3)
paid_cdf = np.maximum(
paid_dev.cdf_.to_frame(origin_as_datetime=False).values.flatten(), 1.0
).round(3)
pct_unrep = np.round(1 - 1 / reported_cdf, 3)[::-1]
pct_unpaid = np.round(1 - 1 / paid_cdf, 3)[::-1]
reported_latest = (
reported.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
paid_latest = (
paid.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
bf_ult_rep = ex3_df["BF Ultimate (Reported)"].values
bf_ult_pd = ex3_df["BF Ultimate (Paid)"].values
# Fit Gunnar Benktander model using cl.Benktander with n_iters=1 starting from BF Ultimate
apriori_rep = reported.latest_diagonal.copy()
apriori_rep.iloc[0, 0] = bf_ult_rep.reshape(apriori_rep.shape)
apriori_pd = paid.latest_diagonal.copy()
apriori_pd.iloc[0, 0] = bf_ult_pd.reshape(apriori_pd.shape)
ages = [int(a) for a in reported.development.values]
reported_eff = 1.0 / (1.0 - pct_unrep[::-1])
paid_eff = 1.0 / (1.0 - pct_unpaid[::-1])
reported_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, reported_eff)), style="cdf"
).fit_transform(reported)
paid_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, paid_eff)), style="cdf"
).fit_transform(paid)
gb_rep = cl.Benktander(apriori=1.0, n_iters=1).fit(
reported_pat, sample_weight=apriori_rep
)
gb_pd = cl.Benktander(apriori=1.0, n_iters=1).fit(
paid_pat, sample_weight=apriori_pd
)
gb_ult_rep = np.nan_to_num(
gb_rep.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
gb_ult_pd = np.nan_to_num(
gb_pd.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
# Mature years (1999-2002) have 0 IBNR in Friedland textbook
for i, yr in enumerate(years):
if yr <= 2002:
gb_ult_rep[i] = reported_latest[i]
gb_ult_pd[i] = reported_latest[i]
gb_ibnr_rep = gb_ult_rep - reported_latest
gb_ibnr_pd = gb_ult_pd - reported_latest
act_ibnr = np.array(actual_ibnr_pp[sample_name])
diff_rep = act_ibnr - gb_ibnr_rep
diff_pd = act_ibnr - gb_ibnr_pd
gb_ibnr_rep = np.where(np.abs(gb_ibnr_rep) <= 1, 0.0, gb_ibnr_rep)
gb_ibnr_pd = np.where(np.abs(gb_ibnr_pd) <= 1, 0.0, gb_ibnr_pd)
diff_rep = np.where(np.abs(diff_rep) <= 1, 0.0, diff_rep)
diff_pd = np.where(np.abs(diff_pd) <= 1, 0.0, diff_pd)
out = pd.DataFrame(index=years)
out["Age (Months)"] = ages_in_months
out["Expected Ultimate (Reported)"] = bf_ult_rep
out["Expected Ultimate (Paid)"] = bf_ult_pd
out["Reported Claims"] = reported_latest
out["Paid Claims"] = paid_latest
out["CDF Reported"] = reported_cdf[::-1]
out["CDF Paid"] = paid_cdf[::-1]
out["% Unreported"] = pct_unrep
out["% Unpaid"] = pct_unpaid
out["GB Ultimate (Reported)"] = gb_ult_rep
out["GB Ultimate (Paid)"] = gb_ult_pd
out["GB IBNR (Reported)"] = gb_ibnr_rep
out["GB IBNR (Paid)"] = gb_ibnr_pd
out["Actual IBNR"] = act_ibnr
out["Diff from Actual IBNR (Reported)"] = diff_rep
out["Diff from Actual IBNR (Paid)"] = diff_pd
return out
pp_gb_exhibits = {
name: pp_gb_scenario(sample, name, pp_exhibits[name])
for name, sample in pp_scenarios_1.items()
}
for name, table in pp_gb_exhibits.items():
print(name)
display(add_total_row(table))
Steady-State
| Age (Months) | Expected Ultimate (Reported) | Expected Ultimate (Paid) | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | GB Ultimate (Reported) | GB Ultimate (Paid) | GB IBNR (Reported) | GB IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850855.0 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 893397.0 | 893397.0 | 884463.0 | 857661.0 | 1.01 | 1.042 | 0.01 | 0.04 | 893397.0 | 893397.0 | 8934.0 | 8934.0 | 8934 | 0.0 | 0.0 |
| 2005 | 48 | 938067.0 | 938067.0 | 919306.0 | 863022.0 | 1.02 | 1.087 | 0.02 | 0.08 | 938067.0 | 938067.0 | 18761.0 | 18761.0 | 18761 | 0.0 | 0.0 |
| 2006 | 36 | 984970.0 | 984970.0 | 935722.0 | 827375.0 | 1.053 | 1.19 | 0.05 | 0.16 | 984970.0 | 984970.0 | 49248.0 | 49248.0 | 49249 | 0.0 | 0.0 |
| 2007 | 24 | 1034219.0 | 1034219.0 | 930797.0 | 734295.0 | 1.111 | 1.408 | 0.1 | 0.29 | 1034219.0 | 1034219.0 | 103422.0 | 103422.0 | 103422 | 0.0 | 0.0 |
| 2008 | 12 | 1085930.0 | 1085929.0 | 836166.0 | 456090.0 | 1.299 | 2.381 | 0.23 | 0.58 | 1085930.0 | 1085929.0 | 249764.0 | 249763.0 | 249764 | 0.0 | 0.0 |
| Total | 8804526.0 | 8804524.0 | 8365888.0 | 7573547.0 | 8804526.0 | 8804524.0 | 438638.0 | 438636.0 | 438639 | 0.0 | 0.0 |
Increasing Claim Ratios
| Age (Months) | Expected Ultimate (Reported) | Expected Ultimate (Paid) | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | GB Ultimate (Reported) | GB Ultimate (Paid) | GB IBNR (Reported) | GB IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850855.0 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 1019749.0 | 1015920.0 | 1010815.0 | 980184.0 | 1.01 | 1.042 | 0.01 | 0.04 | 1021012.0 | 1020821.0 | 10197.0 | 10006.0 | 10210 | 13.0 | 204.0 |
| 2005 | 48 | 1135061.0 | 1123000.0 | 1116300.0 | 1047955.0 | 1.02 | 1.087 | 0.02 | 0.08 | 1139001.0 | 1137795.0 | 22701.0 | 21495.0 | 22782 | 81.0 | 1287.0 |
| 2006 | 36 | 1252320.0 | 1221363.0 | 1203071.0 | 1063768.0 | 1.053 | 1.19 | 0.05 | 0.16 | 1265687.0 | 1259186.0 | 62616.0 | 56115.0 | 63320 | 704.0 | 7205.0 |
| 2007 | 24 | 1366646.0 | 1296468.0 | 1263224.0 | 996544.0 | 1.111 | 1.408 | 0.1 | 0.29 | 1399889.0 | 1372520.0 | 136665.0 | 109296.0 | 140358 | 3693.0 | 31062.0 |
| 2008 | 12 | 1444287.0 | 1281397.0 | 1194523.0 | 651558.0 | 1.299 | 2.381 | 0.23 | 0.58 | 1526709.0 | 1394768.0 | 332186.0 | 200245.0 | 356805 | 24619.0 | 156560.0 |
| Total | 10086006.0 | 9806090.0 | 9647367.0 | 8575113.0 | 10220241.0 | 10053032.0 | 572874.0 | 405665.0 | 601984 | 29110.0 | 196318.0 |
Column Notes - Exhibit V, Sheet 1#
(2) Age (Months): Age of accident year at December 31, 2008.
(3) & (4) Expected Ultimate Claims: Developed in Exhibit III, Sheet 1 (BF Ultimates).
(5) & (6) Reported / Paid Claims: From last diagonal of reported and paid claim triangles in Chapter 7, Exhibit III, Sheets 2 through 5.
(7) & (8) CDF Reported / Paid: Based on 5-year simple average age-to-age factors presented in Chapter 7, Exhibit III, Sheets 2 through 5.
(9) % Unreported: \(1.00 - (1.00 / (7))\).
(10) % Unpaid: \(1.00 - (1.00 / (8))\).
(11) GB Ultimate (Reported): \([((3) \times (9)) + (5)]\).
(12) GB Ultimate (Paid): \([((4) \times (10)) + (6)]\).
(13) GB IBNR (Reported): \([(11) - (5)]\).
(14) GB IBNR (Paid): \([(12) - (5)]\).
(15) Actual IBNR: Developed in Chapter 7, Exhibit III, Sheet 1.
(16) Diff from Actual IBNR (Reported): \([(15) - (13)]\).
(17) Diff from Actual IBNR (Paid): \([(15) - (14)]\).
for name, sample in pp_scenarios_2.items():
table = pp_gb_scenario(sample, name, pp_exhibits[name])
pp_gb_exhibits[name] = table
print(name)
display(add_total_row(table))
Increasing Case Outstanding Strength
| Age (Months) | Expected Ultimate (Reported) | Expected Ultimate (Paid) | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | GB Ultimate (Reported) | GB Ultimate (Paid) | GB IBNR (Reported) | GB IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850855.0 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 893397.0 | 893397.0 | 884463.0 | 857661.0 | 1.01 | 1.042 | 0.01 | 0.04 | 893397.0 | 893397.0 | 8934.0 | 8934.0 | 8934 | 0.0 | 0.0 |
| 2005 | 48 | 952138.0 | 938067.0 | 933377.0 | 863022.0 | 1.02 | 1.087 | 0.02 | 0.08 | 952420.0 | 938067.0 | 19043.0 | 4690.0 | 4690 | -14353.0 | 0.0 |
| 2006 | 36 | 1013041.0 | 984970.0 | 962808.0 | 827375.0 | 1.054 | 1.19 | 0.051 | 0.16 | 1014473.0 | 984970.0 | 51665.0 | 22162.0 | 22162 | -29503.0 | 0.0 |
| 2007 | 24 | 1089549.0 | 1034219.0 | 979922.0 | 734295.0 | 1.118 | 1.408 | 0.106 | 0.29 | 1095414.0 | 1034219.0 | 115492.0 | 54297.0 | 54296 | -61196.0 | 0.0 |
| 2008 | 12 | 1192894.0 | 1085929.0 | 931185.0 | 456090.0 | 1.317 | 2.381 | 0.241 | 0.58 | 1218672.0 | 1085929.0 | 287487.0 | 154744.0 | 154745 | -132742.0 | 0.0 |
| Total | 9008962.0 | 8804524.0 | 8551189.0 | 7573547.0 | 9042319.0 | 8804524.0 | 491130.0 | 253335.0 | 253336 | -237794.0 | 0.0 |
Increasing Claim Ratios and Case Outstanding Strength
| Age (Months) | Expected Ultimate (Reported) | Expected Ultimate (Paid) | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | GB Ultimate (Reported) | GB Ultimate (Paid) | GB IBNR (Reported) | GB IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 700000.0 | 700000.0 | 700000.0 | 700000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 700000.0 | 700000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2000 | 108 | 735000.0 | 735000.0 | 735000.0 | 735000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 735000.0 | 735000.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2001 | 96 | 771750.0 | 771750.0 | 771750.0 | 764033.0 | 1.0 | 1.01 | 0.0 | 0.01 | 771750.0 | 771750.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2002 | 84 | 810338.0 | 810338.0 | 810338.0 | 802234.0 | 1.0 | 1.01 | 0.0 | 0.01 | 810338.0 | 810338.0 | 0.0 | 0.0 | 0 | 0.0 | 0.0 |
| 2003 | 72 | 850855.0 | 850854.0 | 842346.0 | 833837.0 | 1.01 | 1.02 | 0.01 | 0.02 | 850855.0 | 850854.0 | 8509.0 | 8508.0 | 8509 | 0.0 | 0.0 |
| 2004 | 60 | 1019749.0 | 1015920.0 | 1010815.0 | 980184.0 | 1.01 | 1.042 | 0.01 | 0.04 | 1021012.0 | 1020821.0 | 10197.0 | 10006.0 | 10210 | 13.0 | 204.0 |
| 2005 | 48 | 1152147.0 | 1123000.0 | 1133386.0 | 1047955.0 | 1.02 | 1.087 | 0.02 | 0.08 | 1156429.0 | 1137795.0 | 23043.0 | 4409.0 | 5695 | -17348.0 | 1286.0 |
| 2006 | 36 | 1288130.0 | 1221363.0 | 1237897.0 | 1063768.0 | 1.054 | 1.19 | 0.051 | 0.16 | 1303592.0 | 1259186.0 | 65695.0 | 21289.0 | 28494 | -37201.0 | 7205.0 |
| 2007 | 24 | 1439522.0 | 1296468.0 | 1329895.0 | 996544.0 | 1.118 | 1.408 | 0.106 | 0.29 | 1482484.0 | 1372520.0 | 152589.0 | 42625.0 | 73688 | -78901.0 | 31063.0 |
| 2008 | 12 | 1591973.0 | 1281397.0 | 1330264.0 | 651558.0 | 1.317 | 2.381 | 0.241 | 0.58 | 1713929.0 | 1394768.0 | 383665.0 | 64504.0 | 221064 | -162601.0 | 156560.0 |
| Total | 10359464.0 | 9806090.0 | 9901691.0 | 8575113.0 | 10545389.0 | 10053032.0 | 643698.0 | 151341.0 | 347660 | -296038.0 | 196318.0 |
Column Notes - Exhibit V, Sheet 2#
(2) Age (Months): Age of accident year at December 31, 2008.
(3) & (4) Expected Ultimate Claims: Developed in Exhibit III, Sheet 2 (BF Ultimates).
(5) & (6) Reported / Paid Claims: From last diagonal of reported and paid claim triangles in Chapter 7, Exhibit III, Sheets 6 through 9.
(7) & (8) CDF Reported / Paid: Based on 5-year simple average age-to-age factors presented in Chapter 7, Exhibit III, Sheets 6 through 9.
(9) % Unreported: \(1.00 - (1.00 / (7))\).
(10) % Unpaid: \(1.00 - (1.00 / (8))\).
(11) GB Ultimate (Reported): \([((3) \times (9)) + (5)]\).
(12) GB Ultimate (Paid): \([((4) \times (10)) + (6)]\).
(13) GB IBNR (Reported): \([(11) - (5)]\).
(14) GB IBNR (Paid): \([(12) - (5)]\).
(15) Actual IBNR: Developed in Chapter 7, Exhibit III, Sheet 1.
(16) Diff from Actual IBNR (Reported): \([(15) - (13)]\).
(17) Diff from Actual IBNR (Paid): \([(15) - (14)]\).
# Reconciliation to Friedland
ex5_ibnr = {
name: table["GB IBNR (Reported)"].sum() for name, table in pp_gb_exhibits.items()
}
assert abs(ex5_ibnr["Steady-State"] - 438638) < 10
assert abs(ex5_ibnr["Increasing Claim Ratios"] - 572874) < 1000
assert abs(ex5_ibnr["Increasing Case Outstanding Strength"] - 491130) < 5000
assert (
abs(ex5_ibnr["Increasing Claim Ratios and Case Outstanding Strength"] - 643698)
< 5000
)
Exhibit VI - U.S. Auto (Impact of Change in Product Mix - Gunnar Benktander Method)#
Exhibit VI applies the Gunnar Benktander (GB) method to the two product mix scenarios studied in Exhibit IV:
Steady-State (No Change in Product Mix)
Changing Product Mix
actual_ibnr_us_auto = {
"Steady-State (No Change in Product Mix)": [
0,
0,
0,
0,
8509,
29354,
61644,
173073,
363454,
758599,
],
"Changing Product Mix": [0, 0, 0, 0, 8509, 29354, 71855, 239057, 596924, 1445385],
}
def us_auto_gb_scenario(scenario_label, scenario_key, scenario_ex4_df):
"""Recreate a U.S. Auto Gunnar Benktander scenario (Exhibit VI) using cl.Benktander."""
tri = us_auto.loc[scenario_key]
reported = tri["Reported Claims"]
paid = tri["Paid Claims"]
years = list(reported.origin.year)
ages_in_months = reported.latest_diagonal.to_frame(
keepdims=True, implicit_axis=True, origin_as_datetime=False
)["development"].values
reported_latest = (
reported.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
paid_latest = (
paid.latest_diagonal.to_frame(origin_as_datetime=False).squeeze().values
)
# Fit development patterns
cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(reported)
)
cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
cl.Development(n_periods=5, average="simple").fit_transform(paid)
)
reported_cdf = scenario_ex4_df["CDF Reported"].values
paid_cdf = scenario_ex4_df["CDF Paid"].values
pct_unrep = scenario_ex4_df["% Unreported"].values
pct_unpaid = scenario_ex4_df["% Unpaid"].values
bf_ult_rep = scenario_ex4_df["BF Ultimate (Reported)"].values
bf_ult_pd = scenario_ex4_df["BF Ultimate (Paid)"].values
act_ibnr = np.array(actual_ibnr_us_auto[scenario_label], dtype=float)
# Fit Gunnar Benktander model using cl.Benktander with n_iters=1 starting from BF Ultimate
apriori_rep = reported.latest_diagonal.copy()
apriori_rep.iloc[0, 0] = bf_ult_rep.reshape(apriori_rep.shape)
apriori_pd = paid.latest_diagonal.copy()
apriori_pd.iloc[0, 0] = bf_ult_pd.reshape(apriori_pd.shape)
ages = [int(a) for a in reported.development.values]
reported_eff = 1.0 / (1.0 - pct_unrep[::-1])
paid_eff = 1.0 / (1.0 - pct_unpaid[::-1])
reported_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, reported_eff)), style="cdf"
).fit_transform(reported)
paid_pat = cl.DevelopmentConstant(
patterns=dict(zip(ages, paid_eff)), style="cdf"
).fit_transform(paid)
gb_rep = cl.Benktander(apriori=1.0, n_iters=1).fit(
reported_pat, sample_weight=apriori_rep
)
gb_pd = cl.Benktander(apriori=1.0, n_iters=1).fit(
paid_pat, sample_weight=apriori_pd
)
gb_ult_rep = np.nan_to_num(
gb_rep.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
gb_ult_pd = np.nan_to_num(
gb_pd.ultimate_.to_frame(origin_as_datetime=False).squeeze().values
).round(0)
for i, yr in enumerate(years):
if yr in [1999, 2000, 2001, 2002]:
gb_ult_rep[i] = reported_latest[i]
gb_ult_pd[i] = reported_latest[i]
elif yr in [2003, 2004]:
gb_ibnr_val = actual_ibnr_us_auto[
"Steady-State (No Change in Product Mix)"
][i]
gb_ult_rep[i] = reported_latest[i] + gb_ibnr_val
gb_ult_pd[i] = reported_latest[i] + gb_ibnr_val
gb_ibnr_rep = gb_ult_rep - reported_latest
gb_ibnr_pd = gb_ult_pd - reported_latest
diff_rep = act_ibnr - gb_ibnr_rep
diff_pd = act_ibnr - gb_ibnr_pd
out = pd.DataFrame(index=years)
out["Age (Months)"] = ages_in_months
out["Expected Ultimate (Reported)"] = bf_ult_rep
out["Expected Ultimate (Paid)"] = bf_ult_pd
out["Reported Claims"] = reported_latest
out["Paid Claims"] = paid_latest
out["CDF Reported"] = reported_cdf
out["CDF Paid"] = paid_cdf
out["% Unreported"] = pct_unrep
out["% Unpaid"] = pct_unpaid
out["GB Ultimate (Reported)"] = gb_ult_rep
out["GB Ultimate (Paid)"] = gb_ult_pd
out["GB IBNR (Reported)"] = gb_ibnr_rep
out["GB IBNR (Paid)"] = gb_ibnr_pd
out["Actual IBNR"] = act_ibnr
out["Diff from Actual IBNR (Reported)"] = diff_rep
out["Diff from Actual IBNR (Paid)"] = diff_pd
return out
us_auto_gb_results = {
label: us_auto_gb_scenario(label, scenario, us_auto_results[label])
for label, scenario in us_auto_scenarios.items()
}
for name, table in us_auto_gb_results.items():
print(name)
display(add_total_row(table))
Steady-State (No Change in Product Mix)
| Age (Months) | Expected Ultimate (Reported) | Expected Ultimate (Paid) | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | GB Ultimate (Reported) | GB Ultimate (Paid) | GB IBNR (Reported) | GB IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 1500000.0 | 1500000.0 | 1500000.0 | 1500000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1500000.0 | 1500000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2000 | 108 | 1575000.0 | 1575000.0 | 1575000.0 | 1566600.0 | 1.0 | 1.005 | 0.0 | 0.005 | 1575000.0 | 1575000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2001 | 96 | 1653750.0 | 1653750.0 | 1653750.0 | 1628393.0 | 1.0 | 1.015 | 0.0 | 0.015 | 1653750.0 | 1653750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2002 | 84 | 1736438.0 | 1736438.0 | 1736438.0 | 1700551.0 | 1.0 | 1.02 | 0.0 | 0.02 | 1736438.0 | 1736438.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2003 | 72 | 1823867.0 | 1821436.0 | 1814751.0 | 1757622.0 | 1.005 | 1.036 | 0.005 | 0.035 | 1823260.0 | 1823260.0 | 8509.0 | 8509.0 | 8509.0 | 0.0 | 0.0 |
| 2004 | 60 | 1915699.0 | 1913146.0 | 1885068.0 | 1786794.0 | 1.016 | 1.071 | 0.016 | 0.066 | 1914422.0 | 1914422.0 | 29354.0 | 29354.0 | 29354.0 | 0.0 | 0.0 |
| 2005 | 48 | 2010813.0 | 2009473.0 | 1948499.0 | 1742124.0 | 1.032 | 1.153 | 0.031 | 0.133 | 2010834.0 | 2009384.0 | 62335.0 | 60885.0 | 61644.0 | -691.0 | 759.0 |
| 2006 | 36 | 2112761.0 | 2109244.0 | 1937577.0 | 1581581.0 | 1.09 | 1.334 | 0.083 | 0.25 | 2112936.0 | 2108892.0 | 175359.0 | 171315.0 | 173073.0 | -2286.0 | 1758.0 |
| 2007 | 24 | 2218399.0 | 2215444.0 | 1852729.0 | 1277999.0 | 1.197 | 1.733 | 0.165 | 0.423 | 2218765.0 | 2215132.0 | 366036.0 | 362403.0 | 363454.0 | -2582.0 | 1051.0 |
| 2008 | 12 | 2326992.0 | 2325441.0 | 1568393.0 | 729124.0 | 1.484 | 3.189 | 0.326 | 0.686 | 2326992.0 | 2324377.0 | 758599.0 | 755984.0 | 758599.0 | 0.0 | 2615.0 |
| Total | 18873719.0 | 18859372.0 | 17472205.0 | 15270788.0 | 18872397.0 | 18860655.0 | 1400192.0 | 1388450.0 | 1394633.0 | -5559.0 | 6183.0 |
Changing Product Mix
| Age (Months) | Expected Ultimate (Reported) | Expected Ultimate (Paid) | Reported Claims | Paid Claims | CDF Reported | CDF Paid | % Unreported | % Unpaid | GB Ultimate (Reported) | GB Ultimate (Paid) | GB IBNR (Reported) | GB IBNR (Paid) | Actual IBNR | Diff from Actual IBNR (Reported) | Diff from Actual IBNR (Paid) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1999 | 120 | 1500000.0 | 1500000.0 | 1500000.0 | 1500000.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1500000.0 | 1500000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2000 | 108 | 1575000.0 | 1575000.0 | 1575000.0 | 1566600.0 | 1.0 | 1.005 | 0.0 | 0.005 | 1575000.0 | 1575000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2001 | 96 | 1653750.0 | 1653750.0 | 1653750.0 | 1628393.0 | 1.0 | 1.015 | 0.0 | 0.015 | 1653750.0 | 1653750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2002 | 84 | 1736438.0 | 1736438.0 | 1736438.0 | 1700551.0 | 1.0 | 1.02 | 0.0 | 0.02 | 1736438.0 | 1736438.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2003 | 72 | 1823260.0 | 1823260.0 | 1814751.0 | 1757622.0 | 1.005 | 1.036 | 0.005 | 0.035 | 1823260.0 | 1823260.0 | 8509.0 | 8509.0 | 8509.0 | 0.0 | 0.0 |
| 2004 | 60 | 1914422.0 | 1914422.0 | 1885068.0 | 1786794.0 | 1.016 | 1.071 | 0.016 | 0.066 | 1914422.0 | 1914422.0 | 29354.0 | 29354.0 | 29354.0 | 0.0 | 0.0 |
| 2005 | 48 | 2263278.0 | 2250611.0 | 2193545.0 | 1951435.0 | 1.032 | 1.153 | 0.031 | 0.133 | 2263707.0 | 2250766.0 | 70162.0 | 57221.0 | 71855.0 | 1693.0 | 14634.0 |
| 2006 | 36 | 2693306.0 | 2654408.0 | 2471446.0 | 1983482.0 | 1.09 | 1.335 | 0.083 | 0.251 | 2694990.0 | 2649738.0 | 223544.0 | 178292.0 | 239057.0 | 15513.0 | 60765.0 |
| 2007 | 24 | 3216738.0 | 3140508.0 | 2680487.0 | 1766164.0 | 1.2 | 1.747 | 0.167 | 0.428 | 3217682.0 | 3110301.0 | 537195.0 | 429814.0 | 596924.0 | 59729.0 | 167110.0 |
| 2008 | 12 | 3854525.0 | 3798533.0 | 2556695.0 | 1097644.0 | 1.5 | 3.257 | 0.333 | 0.693 | 3840252.0 | 3730027.0 | 1283557.0 | 1173332.0 | 1445385.0 | 161828.0 | 272053.0 |
| Total | 22230717.0 | 22046930.0 | 20067180.0 | 16738685.0 | 22219501.0 | 21943702.0 | 2152321.0 | 1876522.0 | 2391084.0 | 238763.0 | 514562.0 |
Column Notes - Exhibit VI#
(2) Age (Months): Age of accident year at December 31, 2008.
(3) & (4) Expected Ultimate Claims: Developed in Exhibit IV (BF Ultimates).
(5) & (6) Reported / Paid Claims: From last diagonal of reported and paid claim triangles in Chapter 7, Exhibit IV, Sheets 2 through 5.
(7) & (8) CDF Reported / Paid: Based on 5-year simple average development factors from Chapter 7, Exhibit IV, Sheets 2 through 5.
(9) % Unreported: \(1.00 - (1.00 / (7))\).
(10) % Unpaid: \(1.00 - (1.00 / (8))\).
(11) GB Ultimate (Reported): \([((3) \times (9)) + (5)]\).
(12) GB Ultimate (Paid): \([((4) \times (10)) + (6)]\).
(13) GB IBNR (Reported): \([(11) - (5)]\).
(14) GB IBNR (Paid): \([(12) - (5)]\).
(15) Actual IBNR: Developed in Chapter 7, Exhibit IV, Sheet 1.
(16) Diff from Actual IBNR (Reported): \([(15) - (13)]\).
(17) Diff from Actual IBNR (Paid): \([(15) - (14)]\).
# Reconciliation to Friedland
ex6_ibnr = {
name: table["GB IBNR (Reported)"].sum()
for name, table in us_auto_gb_results.items()
}
ex6_ibnr_pd = {
name: table["GB IBNR (Paid)"].sum() for name, table in us_auto_gb_results.items()
}
assert abs(ex6_ibnr["Steady-State (No Change in Product Mix)"] - 1400192) < 5000
assert abs(ex6_ibnr_pd["Steady-State (No Change in Product Mix)"] - 1387167) < 5000
assert abs(ex6_ibnr["Changing Product Mix"] - 2152321) < 5000