Chapter 7 - Development Technique#
The development technique, also known as the chain ladder technique, is one of the most frequently used methodologies for estimating unpaid claims.
—Friedland, p84
This chapter covers the foundational development/chainladder method. In the chainladder package, this is implemented in the Development estimator.
>>> import numpy as np
>>> import pandas as pd
>>> import chainladder as cl
>>> pd.set_option('display.max_columns', None)
>>> pd.set_option('display.width', 1000)
Exhibit I Sheet 1 p106#
Diving straight into Exhibit 1.
PART 1 - Data Triangle#
We have already imported the necessary packages loading the Triangle at the top of p106. Let’s take a look at the Triangle we just loaded.
>>> tri = cl.load_sample('friedland_us_industry_auto')
| 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 1998 | 37,017,487 | 43,169,009 | 45,568,919 | 46,784,558 | 47,337,318 | 47,533,264 | 47,634,419 | 47,689,655 | 47,724,678 | 47,742,304 |
| 1999 | 38,954,484 | 46,045,718 | 48,882,924 | 50,219,672 | 50,729,292 | 50,926,779 | 51,069,285 | 51,163,540 | 51,185,767 | |
| 2000 | 41,155,776 | 49,371,478 | 52,358,476 | 53,780,322 | 54,303,086 | 54,582,950 | 54,742,188 | 54,837,929 | ||
| 2001 | 42,394,069 | 50,584,112 | 53,704,296 | 55,150,118 | 55,895,583 | 56,156,727 | 56,299,562 | |||
| 2002 | 44,755,243 | 52,971,643 | 56,102,312 | 57,703,851 | 58,363,564 | 58,592,712 | ||||
| 2003 | 45,163,102 | 52,497,731 | 55,468,551 | 57,015,411 | 57,565,344 | |||||
| 2004 | 45,417,309 | 52,640,322 | 55,553,673 | 56,976,657 | ||||||
| 2005 | 46,360,869 | 53,790,061 | 56,786,410 | |||||||
| 2006 | 46,582,684 | 54,641,339 | ||||||||
| 2007 | 48,853,563 |
PART 2 - Age-to-Age Factors#
To calculate age-to-age factors, use the age-to-age attribute of the Triangle.
>>> tri['Reported Claims'].age_to_age.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
1998 1.166 1.056 1.027 1.012 1.004 1.002 1.001 1.001 1.0
1999 1.182 1.062 1.027 1.010 1.004 1.003 1.002 1.000 NaN
2000 1.200 1.061 1.027 1.010 1.005 1.003 1.002 NaN NaN
2001 1.193 1.062 1.027 1.014 1.005 1.003 NaN NaN NaN
2002 1.184 1.059 1.029 1.011 1.004 NaN NaN NaN NaN
2003 1.162 1.057 1.028 1.010 NaN NaN NaN NaN NaN
2004 1.159 1.055 1.026 NaN NaN NaN NaN NaN NaN
2005 1.160 1.056 NaN NaN NaN NaN NaN NaN NaN
2006 1.173 NaN NaN NaN NaN NaN NaN NaN NaN
PART 3 - Average Age-to-Age Factors#
To calculate the average age-to-age factors, we will use the Development estimator to fit_transform the original Triangle. This calculates the averages but also preserves the ability to apply other estimators later. The specific choices of average paramters (n_period, etc.) are provided to Development. The attribute for the calculated average age-to-age factors is the ldf_.
# Simple Average
# Latest 5
>>> reported_simple_5 = cl.Development(n_periods=5, average='simple').fit_transform(tri['Reported Claims'])
>>> reported_simple_5.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.168 1.058 1.027 1.011 1.004 1.003 1.002 1.001 1.0
# Latest 3
>>> reported_simple_3 = cl.Development(n_periods=3, average='simple').fit_transform(tri['Reported Claims'])
>>> reported_simple_3.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.164 1.056 1.027 1.012 1.005 1.003 1.002 1.001 1.0
# Medial Average
# Latest 5x1
>>> reported_medial_5x1 = cl.Development(n_periods=5, average='simple',drop_high = 1, drop_low = 1).fit_transform(tri['Reported Claims'])
>>> reported_medial_5x1.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.165 1.057 1.027 1.01 1.004 1.003 1.002 1.001 1.0
# Volume-weighted Average
# Latest 5
>>> reported_volume_5 = cl.Development(n_periods=5, average='volume').fit_transform(tri['Reported Claims'])
>>> reported_volume_5.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.168 1.058 1.027 1.011 1.004 1.003 1.002 1.001 1.0
# Latest 3
>>> reported_volume_3 = cl.Development(n_periods=3, average='volume').fit_transform(tri['Reported Claims'])
>>> reported_volume_3.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.164 1.056 1.027 1.012 1.005 1.003 1.002 1.001 1.0
# Geometric Average
# Latest 4
>>> reported_geometric_4 = cl.Development(n_periods=4, average='geometric').fit_transform(tri['Reported Claims'])
>>> reported_geometric_4.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.164 1.057 1.027 1.011 1.004 1.003 1.002 1.001 1.0
PART 4 - Selected Age-to-Age Factors#
For the prior selected, we need to create a hard-coded pattern, using the DevelopmentConstant estimator. In a production workflow, you can save the development pattern from the prior analysis and load for reference in a subsequent analysis.
We will also be using the TailConstant estimator to add a tail factor to selected development patterns. We add the tail factor to a transformed Triangle (i.e. applying fit_transforme of the Development estimator to a Triangle) by using fit_transform once again.
# Prior Selected
>>> reported_prior_method = cl.DevelopmentConstant(
... patterns = {
... 12:1.16,
... 24:1.057,
... 36:1.028,
... 48:1.012,
... 60:1.005,
... 72:1.003,
... 84:1.001,
... 96:1.001,
... 108:1.000
... },
... style='ldf'
... )
>>> reported_prior_ft = reported_prior_method.fit_transform(tri['Reported Claims'])
>>> reported_tail_method = cl.TailConstant(
... tail = 1,
... projection_period = 0
... )
>>> reported_prior_selected = reported_tail_method.fit_transform(reported_prior_ft)
>>> reported_prior_selected.ldf_
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
(All) 1.16 1.057 1.028 1.012 1.005 1.003 1.001 1.001 1.0 1.0
Next we can select some factors. We can also reuse the TailConstant from the previous step. This is fairly common in practice, as tail factors are selected less frequently than the development pattern itself, so need to be carried from analysis to analysis.
# Selected
>>> reported_selected_pattern = reported_tail_method.fit_transform(reported_simple_3)
>>> reported_selected_pattern.ldf_.round(decimals=3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
(All) 1.164 1.056 1.027 1.012 1.005 1.003 1.002 1.001 1.0 1.0
The Development estimator has a cdf_ attribute that will automatically multiply age-to-age factors cumulatively into age-to-ultimate factors. The Friedland text uses the rounded LDF to calculate CDF. This can be achieved in this package by using the incr_to_cum() method of the rounded age-to-age factors.
# CDF to Ultimate
# First without rounding
>>> reported_selected_pattern.cdf_
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult
(All) 1.289977 1.108139 1.049493 1.021554 1.009908 1.0053 1.00254 1.000954 1.000369 1.0
# Then with rounding
>>> reported_selected_cdf = reported_selected_pattern.ldf_.round(decimals = 3).incr_to_cum().round(decimals = 3)
>>> reported_selected_cdf
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult
(All) 1.292 1.11 1.051 1.023 1.011 1.006 1.003 1.001 1.0 1.0
To calculate % reported, we will use Triangle manipulation from Chapter 5 directly on the development pattern (which is also a Triangle).
# Percent Reported
>>> (1 / reported_selected_cdf).round(decimals = 3)
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult
(All) 0.774 0.901 0.951 0.978 0.989 0.994 0.997 0.999 1.0 1.0
Exhibit I Sheet 2 p107#
Moving onto the next page, all the calculations are identical to the previous page. We will manually repeat the same code. In a production workflow, commonly repeated methods and selections can be streamlined, which we will demonstrate in Exhibit II.
PART 1 - Data Triangle#
>>> tri['Paid Claims']
12 24 36 48 60 72 84 96 108 120
1998 18539254.0 33231039.0 40062008.0 43892039.0 45896535.0 46765422.0 47221322.0 47446877.0 47555456.0 47644187.0
1999 20410193.0 36090684.0 43259402.0 47159241.0 49208532.0 50162043.0 50625757.0 50878808.0 51000534.0 NaN
2000 22120843.0 38976014.0 46389282.0 50562385.0 52735280.0 53740101.0 54284334.0 54533225.0 NaN NaN
2001 22992259.0 40096198.0 47767835.0 52093916.0 54363436.0 55378801.0 55878421.0 NaN NaN NaN
2002 24092782.0 41795313.0 49903803.0 54352884.0 56754376.0 57807215.0 NaN NaN NaN NaN
2003 24084451.0 41399612.0 49070332.0 53584201.0 55930654.0 NaN NaN NaN NaN NaN
2004 24369770.0 41489863.0 49236678.0 53774672.0 NaN NaN NaN NaN NaN NaN
2005 25100697.0 42702229.0 50644994.0 NaN NaN NaN NaN NaN NaN NaN
2006 25608776.0 43606497.0 NaN NaN NaN NaN NaN NaN NaN NaN
2007 27229969.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
PART 2 - Age-to-Age Factors#
>>> tri['Paid Claims'].age_to_age.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
1998 1.792 1.206 1.096 1.046 1.019 1.010 1.005 1.002 1.002
1999 1.768 1.199 1.090 1.043 1.019 1.009 1.005 1.002 NaN
2000 1.762 1.190 1.090 1.043 1.019 1.010 1.005 NaN NaN
2001 1.744 1.191 1.091 1.044 1.019 1.009 NaN NaN NaN
2002 1.735 1.194 1.089 1.044 1.019 NaN NaN NaN NaN
2003 1.719 1.185 1.092 1.044 NaN NaN NaN NaN NaN
2004 1.703 1.187 1.092 NaN NaN NaN NaN NaN NaN
2005 1.701 1.186 NaN NaN NaN NaN NaN NaN NaN
2006 1.703 NaN NaN NaN NaN NaN NaN NaN NaN
PART 3 - Average Age-to-Age Factors#
# Simple Average
# Latest 5
>>> paid_simple_5 = cl.Development(n_periods=5, average='simple').fit_transform(tri['Paid Claims'])
>>> paid_simple_5.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.712 1.189 1.091 1.044 1.019 1.01 1.005 1.002 1.002
# Latest 3
>>> paid_simple_3 = cl.Development(n_periods=3, average='simple').fit_transform(tri['Paid Claims'])
>>> paid_simple_3.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.702 1.186 1.091 1.044 1.019 1.009 1.005 1.002 1.002
# Medial Average
# Latest 5x1
>>> paid_medial_5x1 = cl.Development(n_periods=5, average='simple',drop_high = 1, drop_low = 1).fit_transform(tri['Paid Claims'])
>>> paid_medial_5x1.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.708 1.188 1.091 1.044 1.019 1.009 1.005 1.002 1.002
# Volume-weighted Average
# Latest 5
>>> paid_volume_5 = cl.Development(n_periods=5, average='volume').fit_transform(tri['Paid Claims'])
>>> paid_volume_5.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.712 1.189 1.091 1.044 1.019 1.01 1.005 1.002 1.002
# Latest 3
>>> paid_volume_3 = cl.Development(n_periods=3, average='volume').fit_transform(tri['Paid Claims'])
>>> paid_volume_3.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.702 1.186 1.091 1.044 1.019 1.009 1.005 1.002 1.002
# Geometric Average
# Latest 4
>>> paid_geometric_4 = cl.Development(n_periods=4, average='geometric').fit_transform(tri['Paid Claims'])
>>> paid_geometric_4.ldf_.round(decimals = 3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
(All) 1.706 1.188 1.091 1.044 1.019 1.01 1.005 1.002 1.002
PART 4 - Selected Age-to-Age Factors#
# Prior Selected
>>> paid_prior_method = cl.DevelopmentConstant(
... patterns = {
... 12:1.707,
... 24:1.189,
... 36:1.091,
... 48:1.044,
... 60:1.019,
... 72:1.01,
... 84:1.005,
... 96:1.003,
... 108:1.001
... },
... style='ldf'
... )
>>> paid_prior_ft = paid_prior_method.fit_transform(tri['Paid Claims'])
>>> paid_tail_method = cl.TailConstant(
... tail = 1.002,
... projection_period = 0
... )
>>> paid_prior_selected = paid_tail_method.fit_transform(paid_prior_ft)
>>> paid_prior_selected.ldf_
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
(All) 1.707 1.189 1.091 1.044 1.019 1.01 1.005 1.003 1.001 1.002
# Selected
>>> paid_selected_pattern = paid_tail_method.fit_transform(paid_simple_3)
>>> paid_selected_pattern.ldf_.round(decimals=3)
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
(All) 1.702 1.186 1.091 1.044 1.019 1.009 1.005 1.002 1.002 1.002
# CDF to Ultimate
>>> paid_selected_cdf = paid_selected_pattern.ldf_.round(decimals = 3).incr_to_cum().round(decimals = 3)
>>> paid_selected_cdf
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult
(All) 2.39 1.404 1.184 1.085 1.04 1.02 1.011 1.006 1.004 1.002
# Percent Reported
>>> (1 / paid_selected_cdf).round(decimals = 3)
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult
(All) 0.418 0.712 0.845 0.922 0.962 0.98 0.989 0.994 0.996 0.998
Exhibit I Sheet 3 p108#
This is a common report layout for reserving analyses. Some Pandas manipulation is needed to retrieve all the figures from the transformed Triangle objects and achieve the tabular look. We will create a function to reuse the manipulation throughout this chapter.
>>> def development_summary(reported: cl.Triangle(), paid: cl.Triangle()) -> pd.DataFrame():
... output = pd.DataFrame() # initializing a DataFrame
... output["Reported Claims"] = reported.latest_diagonal.to_frame(origin_as_datetime=False) # using a vector of losses to anchor the exhibit index
... age = reported.development.iloc[::-1] # flipping the age order
... age.index = output.index # forcing the index to match
... output['Age'] = age
... output = output[['Age','Reported Claims']] # reordering the columns
... output ['Paid Claims'] = paid.latest_diagonal.to_frame(origin_as_datetime=False) # adding in paid losses
... reported_cdf = reported.cdf_.T # transposing the CDF
... reported_cdf.index = output.index[::-1] # forcing the index to match
... output["Reported CDF"] = reported_cdf
... paid_cdf = paid.cdf_.T
... paid_cdf.index = output.index[::-1]
... output["Paid CDF"] = paid_cdf
... output["Reported Ultimate"] = cl.Chainladder().fit(reported).ultimate_.to_frame(origin_as_datetime=False) # using the Chainladder estimator to return the ultimate
... output["Paid Ultimate"] = cl.Chainladder().fit(paid).ultimate_.to_frame(origin_as_datetime=False)
... return output
>>> exhibit = development_summary(reported_selected_pattern,paid_selected_pattern)
>>> exhibit
Age Reported Claims Paid Claims Reported CDF Paid CDF Reported Ultimate Paid Ultimate
1998 120 47742304.0 47644187.0 1.000000 1.002000 4.774230e+07 4.773948e+07
1999 108 51185767.0 51000534.0 1.000369 1.003870 5.120467e+07 5.119788e+07
2000 96 54837929.0 54533225.0 1.000954 1.006219 5.489024e+07 5.487237e+07
2001 84 56299562.0 55878421.0 1.002540 1.011036 5.644257e+07 5.649507e+07
2002 72 58592712.0 57807215.0 1.005300 1.020604 5.890327e+07 5.899830e+07
2003 60 57565344.0 55930654.0 1.009908 1.039752 5.813573e+07 5.815399e+07
2004 48 56976657.0 53774672.0 1.021554 1.085341 5.820476e+07 5.836386e+07
2005 36 56786410.0 50644994.0 1.049493 1.184218 5.959697e+07 5.997474e+07
2006 24 54641339.0 43606497.0 1.108139 1.404485 6.055018e+07 6.124466e+07
2007 12 48853563.0 27229969.0 1.289977 2.390688 6.301997e+07 6.509837e+07
Unfortunately this does not match the table from the text, due to rounding. We will construct a separate, rounded exhibit to reconcile to the text.
>>> def rounded_development_summary(reported: cl.Triangle(), paid: cl.Triangle()) -> pd.DataFrame():
... output = pd.DataFrame() # initializing a DataFrame
... output["Reported Claims"] = reported.latest_diagonal.to_frame(origin_as_datetime=False) # using a vector of losses to anchor the exhibit index
... age = reported.development.iloc[::-1] # flipping the age order
... age.index = output.index # forcing the index to match
... output['Age'] = age
... output = output[['Age','Reported Claims']] # reordering the columns
... output ['Paid Claims'] = paid.latest_diagonal.to_frame(origin_as_datetime=False) # adding in paid losses
... reported_cdf = reported.ldf_.round(decimals = 3).incr_to_cum().round(decimals = 3).T
... reported_cdf.index = output.index[::-1]
... output["Reported CDF"] = reported_cdf
... paid_cdf = paid.ldf_.round(decimals = 3).incr_to_cum().round(decimals = 3).T
... paid_cdf.index = output.index[::-1]
... output["Paid CDF"] = paid_cdf
... output["Reported Ultimate"] = (output['Reported Claims'] * output["Reported CDF"]).round(decimals = 0) # taking a short cut to calculate the ultimate without using Chainladder
... output["Paid Ultimate"] = (output['Paid Claims'] * output["Paid CDF"]).round(decimals = 0)
... return output
>>> rounded_exhibit = rounded_development_summary(reported_selected_pattern,paid_selected_pattern)
>>> rounded_exhibit[['Reported CDF','Paid CDF','Reported Ultimate','Paid Ultimate']] # only displaying the rounded columns
Reported CDF Paid CDF Reported Ultimate Paid Ultimate
1998 1.000 1.002 47742304.0 47739475.0
1999 1.000 1.004 51185767.0 51204536.0
2000 1.001 1.006 54892767.0 54860424.0
2001 1.003 1.011 56468461.0 56493084.0
2002 1.006 1.020 58944268.0 58963359.0
2003 1.011 1.040 58198563.0 58167880.0
2004 1.023 1.085 58287120.0 58345519.0
2005 1.051 1.184 59682517.0 59963673.0
2006 1.110 1.404 60651886.0 61223522.0
2007 1.292 2.390 63118803.0 65079626.0
Exhibit I Sheet 4 p109#
This is another common report layout for reserving analyses. The manipulation here are more straight-forward that the previous exhibit.
>>> def unpaid_summary(dev_sum: pd.DataFrame()) -> pd.DataFrame():
... output = dev_sum.loc[:,['Reported Claims','Paid Claims','Reported Ultimate','Paid Ultimate']]
... output['Case Outstanding'] = output['Reported Claims'] - output['Paid Claims']
... output['Reported Method IBNR'] = output['Reported Ultimate'] - output['Reported Claims']
... output['Paid Method IBNR'] = output['Paid Ultimate'] - output['Reported Claims']
... output['Reported Method Unpaid'] = output['Reported Method IBNR'] + output['Case Outstanding']
... output['Paid Method Unpaid'] = output['Paid Method IBNR'] + output['Case Outstanding']
... return output
>>> unpaid_exhibit = unpaid_summary(rounded_exhibit)
>>> unpaid_exhibit[['Case Outstanding','Reported Method IBNR','Paid Method IBNR','Reported Method Unpaid','Paid Method Unpaid']]
Case Outstanding Reported Method IBNR Paid Method IBNR Reported Method Unpaid Paid Method Unpaid
1998 98117.0 0.0 -2829.0 98117.0 95288.0
1999 185233.0 0.0 18769.0 185233.0 204002.0
2000 304704.0 54838.0 22495.0 359542.0 327199.0
2001 421141.0 168899.0 193522.0 590040.0 614663.0
2002 785497.0 351556.0 370647.0 1137053.0 1156144.0
2003 1634690.0 633219.0 602536.0 2267909.0 2237226.0
2004 3201985.0 1310463.0 1368862.0 4512448.0 4570847.0
2005 6141416.0 2896107.0 3177263.0 9037523.0 9318679.0
2006 11034842.0 6010547.0 6582183.0 17045389.0 17617025.0
2007 21623594.0 14265240.0 16226063.0 35888834.0 37849657.0
Exhibit II Sheet 1 p110#
Now that we have walked through an analysis step by step, let’s introduce some scaling by streamlining the entire exhibit into single function.
>>> def dev_exhibit(tri: cl.Triangle, avg_params: dict[str,int], selected_avg: str, tail: float) -> dict[cl.Triangle()]:
... print('PART 1 - Data Triangle')
... print(tri)
... print('PART 2 - Age-to-Age Factors')
... print(tri.age_to_age)
... devs = {}
... print('PART 3 - Average Age-to-Age Factor')
... for k,v in avg_params.items():
... devs[k] = cl.Development(**v).fit_transform(tri)
... def print_ldfs(ldf_dict:dict[cl.Triangle()]):
... print(pd.concat([v.to_frame().rename(index={'(All)':k}) for k,v in ldf_dict.items()]))
... return None
... print_ldfs({k:v.ldf_.round(decimals=3) for k,v in devs.items()})
... devs["Selected"] = cl.TailConstant(tail = tail, projection_period = 0).fit_transform(devs[selected_avg])
... selected = {}
... selected['CDF to Ultimate'] = devs["Selected"].ldf_.round(decimals=3).incr_to_cum().round(decimals=3)
... selected['Percent Reported'] = (1/selected['CDF to Ultimate']).round(decimals=3)
... print('PART 4 - Selected Age-to-Age Factor')
... print_ldfs({'Selected':devs['Selected'].ldf_.round(decimals=3)})
... print_ldfs(selected)
... return devs
>>> import re
>>> tri = cl.load_sample('friedland_xyz_auto_bi')
>>> assumptions_list = ['simple_5','simple_3','simple_2','volume_4','volume_3','volume_2','geometric_3']
>>> assumptions = {x:{'n_periods':int(re.match(r'.+_(.+)', x).group(1)),'average':re.match(r'(.+)_', x).group(1)} for x in assumptions_list}
>>> assumptions['medial 5x1'] = {'n_periods':5, 'average':'simple','drop_high':1, 'drop_low':1}
>>> reported_devs = dev_exhibit(tri['Reported Claims'],assumptions,'volume_2',1)
PART 1 - Data Triangle
12 24 36 48 60 72 84 96 108 120 132
1998 NaN NaN 11171.0 12380.0 13216.0 14067.0 14688.0 16366.0 16163.0 15835.0 15822.0
1999 NaN 13255.0 16405.0 19639.0 22473.0 23764.0 25094.0 24795.0 25071.0 25107.0 NaN
2000 15676.0 18749.0 21900.0 27144.0 29488.0 34458.0 36949.0 37505.0 37246.0 NaN NaN
2001 11827.0 16004.0 21022.0 26578.0 34205.0 37136.0 38541.0 38798.0 NaN NaN NaN
2002 12811.0 20370.0 26656.0 37667.0 44414.0 48701.0 48169.0 NaN NaN NaN NaN
2003 9651.0 16995.0 30354.0 40594.0 44231.0 44373.0 NaN NaN NaN NaN NaN
2004 16995.0 40180.0 58866.0 71707.0 70288.0 NaN NaN NaN NaN NaN NaN
2005 28674.0 47432.0 70340.0 70655.0 NaN NaN NaN NaN NaN NaN NaN
2006 27066.0 46783.0 48804.0 NaN NaN NaN NaN NaN NaN NaN NaN
2007 19477.0 31732.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2008 18632.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
PART 2 - Age-to-Age Factors
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
1998 NaN NaN 1.108227 1.067528 1.064392 1.044146 1.114243 0.987596 0.979707 0.999179
1999 NaN 1.237646 1.197135 1.144305 1.057447 1.055967 0.988085 1.011131 1.001436 NaN
2000 1.196032 1.168062 1.239452 1.086354 1.168543 1.072291 1.015048 0.993094 NaN NaN
2001 1.353175 1.313547 1.264295 1.286967 1.085689 1.037834 1.006668 NaN NaN NaN
2002 1.590040 1.308591 1.413078 1.179122 1.096524 0.989076 NaN NaN NaN NaN
2003 1.760957 1.786055 1.337353 1.089595 1.003210 NaN NaN NaN NaN NaN
2004 2.364225 1.465057 1.218140 0.980211 NaN NaN NaN NaN NaN NaN
2005 1.654181 1.482965 1.004478 NaN NaN NaN NaN NaN NaN NaN
2006 1.728479 1.043199 NaN NaN NaN NaN NaN NaN NaN NaN
2007 1.629204 NaN NaN NaN NaN NaN NaN NaN NaN NaN
PART 3 - Average Age-to-Age Factor
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
simple_5 1.827 1.417 1.247 1.124 1.082 1.040 1.031 0.997 0.991 0.999
simple_3 1.671 1.330 1.187 1.083 1.062 1.033 1.003 0.997 0.991 0.999
simple_2 1.679 1.263 1.111 1.035 1.050 1.013 1.011 1.002 0.991 0.999
volume_4 1.802 1.376 1.185 1.094 1.081 1.033 1.019 0.998 0.993 0.999
volume_3 1.674 1.325 1.147 1.060 1.060 1.028 1.005 0.998 0.993 0.999
volume_2 1.687 1.265 1.102 1.020 1.050 1.010 1.011 1.000 0.993 0.999
geometric_3 1.670 1.314 1.178 1.080 1.061 1.033 1.003 0.997 0.991 0.999
medial 5x1 1.715 1.419 1.273 1.118 1.080 1.046 1.011 0.993 0.991 0.999
PART 4 - Selected Age-to-Age Factor
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132 132-144
Selected 1.687 1.265 1.102 1.02 1.05 1.01 1.011 1.0 0.993 0.999 1.0
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult 132-Ult
CDF to Ultimate 2.551 1.512 1.196 1.085 1.064 1.013 1.003 0.992 0.992 0.999 1.0
Percent Reported 0.392 0.661 0.836 0.922 0.940 0.987 0.997 1.008 1.008 1.001 1.0
Exhibit II Sheet 2 p111#
>>> paid_devs = dev_exhibit(tri['Paid Claims'],assumptions,'volume_2',1.01)
PART 1 - Data Triangle
12 24 36 48 60 72 84 96 108 120 132
1998 NaN NaN 6309.0 8521.0 10082.0 11620.0 13242.0 14419.0 15311.0 15764.0 15822.0
1999 NaN 4666.0 9861.0 13971.0 18127.0 22032.0 23511.0 24146.0 24592.0 24817.0 NaN
2000 1302.0 6513.0 12139.0 17828.0 24030.0 28853.0 33222.0 35902.0 36782.0 NaN NaN
2001 1539.0 5952.0 12319.0 18609.0 24387.0 31090.0 37070.0 38519.0 NaN NaN NaN
2002 2318.0 7932.0 13822.0 22095.0 31945.0 40629.0 44437.0 NaN NaN NaN NaN
2003 1743.0 6240.0 12683.0 22892.0 34505.0 39320.0 NaN NaN NaN NaN NaN
2004 2221.0 9898.0 25950.0 43439.0 52811.0 NaN NaN NaN NaN NaN NaN
2005 3043.0 12219.0 27073.0 40026.0 NaN NaN NaN NaN NaN NaN NaN
2006 3531.0 11778.0 22819.0 NaN NaN NaN NaN NaN NaN NaN NaN
2007 3529.0 11865.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2008 3409.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
PART 2 - Age-to-Age Factors
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
1998 NaN NaN 1.350610 1.183194 1.152549 1.139587 1.088884 1.061863 1.029587 1.003679
1999 NaN 2.113373 1.416793 1.297473 1.215425 1.067130 1.027009 1.018471 1.009149 NaN
2000 5.002304 1.863811 1.468655 1.347880 1.200707 1.151423 1.080669 1.024511 NaN NaN
2001 3.867446 2.069724 1.510593 1.310495 1.274860 1.192345 1.039088 NaN NaN NaN
2002 3.421915 1.742562 1.598539 1.445802 1.271842 1.093726 NaN NaN NaN NaN
2003 3.580034 2.032532 1.804936 1.507295 1.139545 NaN NaN NaN NaN NaN
2004 4.456551 2.621742 1.673950 1.215751 NaN NaN NaN NaN NaN NaN
2005 4.015445 2.215648 1.478447 NaN NaN NaN NaN NaN NaN NaN
2006 3.335599 1.937426 NaN NaN NaN NaN NaN NaN NaN NaN
2007 3.362142 NaN NaN NaN NaN NaN NaN NaN NaN NaN
PART 3 - Average Age-to-Age Factor
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132
simple_5 3.750 2.110 1.613 1.365 1.220 1.129 1.059 1.035 1.019 1.004
simple_3 3.571 2.258 1.652 1.390 1.229 1.146 1.049 1.035 1.019 1.004
simple_2 3.349 2.077 1.576 1.362 1.206 1.143 1.060 1.021 1.019 1.004
volume_4 3.713 2.206 1.615 1.342 1.218 1.128 1.056 1.030 1.017 1.004
volume_3 3.550 2.238 1.619 1.349 1.222 1.141 1.051 1.030 1.017 1.004
volume_2 3.349 2.079 1.574 1.316 1.203 1.136 1.059 1.022 1.017 1.004
geometric_3 3.558 2.241 1.647 1.384 1.227 1.145 1.049 1.035 1.019 1.004
medial 5x1 3.653 2.062 1.594 1.368 1.229 1.128 1.060 1.025 1.019 1.004
PART 4 - Selected Age-to-Age Factor
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120 120-132 132-144
Selected 3.349 2.079 1.574 1.316 1.203 1.136 1.059 1.022 1.017 1.004 1.01
12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult 132-Ult
CDF to Ultimate 21.999 6.569 3.160 2.007 1.525 1.268 1.116 1.054 1.031 1.014 1.01
Percent Reported 0.045 0.152 0.316 0.498 0.656 0.789 0.896 0.949 0.970 0.986 0.99
Exhibit II Sheet 3 p112#
>>> exhibit = rounded_development_summary(reported_devs["Selected"],paid_devs["Selected"])
>>> exhibit
Age Reported Claims Paid Claims Reported CDF Paid CDF Reported Ultimate Paid Ultimate
1998 132 15822.0 15822.0 1.000 1.010 15822.0 15980.0
1999 120 25107.0 24817.0 0.999 1.014 25082.0 25164.0
2000 108 37246.0 36782.0 0.992 1.031 36948.0 37922.0
2001 96 38798.0 38519.0 0.992 1.054 38488.0 40599.0
2002 84 48169.0 44437.0 1.003 1.116 48314.0 49592.0
2003 72 44373.0 39320.0 1.013 1.268 44950.0 49858.0
2004 60 70288.0 52811.0 1.064 1.525 74786.0 80537.0
2005 48 70655.0 40026.0 1.085 2.007 76661.0 80332.0
2006 36 48804.0 22819.0 1.196 3.160 58370.0 72108.0
2007 24 31732.0 11865.0 1.512 6.569 47979.0 77941.0
2008 12 18632.0 3409.0 2.551 21.999 47530.0 74995.0
Exhibit II Sheet 4 p113#
>>> unpaid_exhibit = unpaid_summary(exhibit)
>>> unpaid_exhibit[['Case Outstanding','Reported Method IBNR','Paid Method IBNR','Reported Method Unpaid','Paid Method Unpaid']]
Case Outstanding Reported Method IBNR Paid Method IBNR Reported Method Unpaid Paid Method Unpaid
1998 0.0 0.0 158.0 0.0 158.0
1999 290.0 -25.0 57.0 265.0 347.0
2000 464.0 -298.0 676.0 166.0 1140.0
2001 279.0 -310.0 1801.0 -31.0 2080.0
2002 3732.0 145.0 1423.0 3877.0 5155.0
2003 5053.0 577.0 5485.0 5630.0 10538.0
2004 17477.0 4498.0 10249.0 21975.0 27726.0
2005 30629.0 6006.0 9677.0 36635.0 40306.0
2006 25985.0 9566.0 23304.0 35551.0 49289.0
2007 19867.0 16247.0 46209.0 36114.0 66076.0
2008 15223.0 28898.0 56363.0 44121.0 71586.0
Exhibit III Sheet 1 p114#
WIP