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:

\[\text{Ultimate} = \text{Actual} + \text{Expected Claims} \times \left(1 - \frac{1}{\text{CDF}}\right)\]

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):

  1. Steady-State

  2. Increasing Claim Ratios

  3. Increasing Case Outstanding Strength

  4. 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):

  1. Steady-State (No Change in Product Mix)

  2. 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:

  1. Steady-State

  2. Increasing Claim Ratios

  3. Increasing Case Outstanding Strength

  4. 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:

  1. Steady-State (No Change in Product Mix)

  2. 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