IBNR Models#
The IBNR Estimators are the final stage in analyzing reserve estimates in the
chainladder
package. These Estimators have a predict
method as opposed
to a transform
method.
Basics and Commonalities#
Ultimates#
All reserving methods determine some ultimate cost of insurance claims. These
ultimates are captured in the ultimate_
property of the estimator.
import chainladder as cl
import pandas as pd
cl.Chainladder().fit(cl.load_sample('raa')).ultimate_
2261 | |
---|---|
1981 | 18,834 |
1982 | 16,858 |
1983 | 24,083 |
1984 | 28,703 |
1985 | 28,927 |
1986 | 19,501 |
1987 | 17,749 |
1988 | 24,019 |
1989 | 16,045 |
1990 | 18,402 |
Ultimates are measured at a valuation date way into the future. The library is
extraordinarily conservative in picking this date, and sets it to December 31, 2261.
This is set globally and can be viewed by referencing the ULT_VAL
constant.
cl.options.get_option('ULT_VAL')
'2261-12-31 23:59:59.999999999'
cl.options.set_option('ULT_VAL', '2050-12-31 23:59:59.999999999')
cl.options.get_option('ULT_VAL')
'2050-12-31 23:59:59.999999999'
The ultimate_
along with most of the other properties of IBNR models are triangles
and can be manipulated. However, it is important to note that the model itself
is not a Triangle, it is an scikit-learn style Estimator. This distinction is
important when wanting to manipulate model attributes.
triangle = cl.load_sample('quarterly')
model = cl.Chainladder().fit(triangle)
# This works since we're slicing the ultimate Triangle
ult = model.ultimate_['paid']
/home/docs/checkouts/readthedocs.org/user_builds/chainladder-python/conda/latest/lib/python3.11/site-packages/chainladder/core/base.py:250: UserWarning: The argument 'infer_datetime_format' is deprecated and will be removed in a future version. A strict version of it is now the default, see https://pandas.pydata.org/pdeps/0004-consistent-to-datetime-parsing.html. You can safely remove this argument.
arr = dict(zip(datetime_arg, pd.to_datetime(**item)))
/home/docs/checkouts/readthedocs.org/user_builds/chainladder-python/conda/latest/lib/python3.11/site-packages/chainladder/core/base.py:250: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
arr = dict(zip(datetime_arg, pd.to_datetime(**item)))
This throws an error since the model itself is not sliceable:
ult = model['paid'].ultimate_
IBNR#
Any difference between an ultimate_
and the latest_diagonal
of a Triangle
is contained in the ibnr_
property of an estimator. While technically, as in
the example of a paid triangle, there can be case reserves included in the ibnr_
estimate, the distinction is not made by the chainladder
package and must be
managed by you.
triangle = cl.load_sample('quarterly')
model = cl.Chainladder().fit(triangle)
# Determine outstanding case reserves
case_reserves = (triangle['incurred']-triangle['paid']).latest_diagonal
# Net case reserves off of paid IBNR
true_ibnr = model.ibnr_['paid'] - case_reserves
true_ibnr.sum()
/home/docs/checkouts/readthedocs.org/user_builds/chainladder-python/conda/latest/lib/python3.11/site-packages/chainladder/core/base.py:250: UserWarning: The argument 'infer_datetime_format' is deprecated and will be removed in a future version. A strict version of it is now the default, see https://pandas.pydata.org/pdeps/0004-consistent-to-datetime-parsing.html. You can safely remove this argument.
arr = dict(zip(datetime_arg, pd.to_datetime(**item)))
/home/docs/checkouts/readthedocs.org/user_builds/chainladder-python/conda/latest/lib/python3.11/site-packages/chainladder/core/base.py:250: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
arr = dict(zip(datetime_arg, pd.to_datetime(**item)))
2431.2695585474003
Complete Triangles#
The full_triangle_
and full_expectation_
attributes give a view of the
completed Triangle
. While the full_expectation_
is entirely based on
ultimate_
values and development patterns, the full_triangle_
is a
blend of the existing triangle. These are useful for conducting an analysis
of actual results vs model expectations.
model = cl.Chainladder().fit(cl.load_sample('ukmotor'))
residuals = model.full_expectation_ - model.full_triangle_
residuals[residuals.valuation<=model.X_.valuation_date]
12 | 24 | 36 | 48 | 60 | 72 | 84 | |
---|---|---|---|---|---|---|---|
2007 | 344.49 | 557.93 | 348.77 | 10.85 | -11.41 | ||
2008 | -21.88 | -185.51 | -340.72 | -102.58 | 11.41 | ||
2009 | -92.22 | -233.62 | 94.51 | 91.74 | |||
2010 | -303.44 | -209.00 | -102.57 | ||||
2011 | 67.16 | 70.21 | |||||
2012 | 5.89 | ||||||
2013 |
Another typical analysis is to forecast the IBNR run-off for future periods.
expected_3y_run_off = model.full_triangle_.dev_to_val().cum_to_incr().loc[..., '2014':'2016']
expected_3y_run_off
2014 | 2015 | 2016 | |
---|---|---|---|
2007 | |||
2008 | 351 | ||
2009 | 662 | 376 | |
2010 | 1,073 | 620 | 352 |
2011 | 1,503 | 1,134 | 655 |
2012 | 2,725 | 1,820 | 1,374 |
2013 | 5,587 | 3,352 | 2,239 |
Chainladder#
The distinguishing characteristic of the :class:Chainladder
method is that ultimate claims for each
accident year are produced from recorded values assuming that future claims’ development is
similar to prior years’ development. In this method, the actuary uses the development triangles to
track the development history of a specific group of claims. The underlying assumption in the
development technique is that claims recorded to date will continue to develop in a similar manner
in the future – that the past is indicative of the future. That is, the development technique assumes
that the relative change in a given year’s claims from one evaluation point to the next is similar to
the relative change in prior years’ claims at similar evaluation points.
An implicit assumption in the development technique is that, for an immature accident year, the claims observed thus far tell you something about the claims yet to be observed. This is in contrast to the assumptions underlying the expected claims technique.
Other important assumptions of the development method include: consistent claim processing, a stable mix of types of claims, stable policy limits, and stable reinsurance (or excess insurance) retention limits throughout the experience period.
Though the algorithm underling the basic chainladder is trivial, the properties
of the Chainladder
estimator allow for a concise access to relevant information.
As an example, we can use the estimator to determine actual vs expected run-off of a subsequent valuation period.
MackChainladder#
The :class:MackChainladder
model can be regarded as a special form of a
weighted linear regression through the origin for each development period. By using
a regression framework, statistics about the variability of the data and the parameter
estimates allows for the estimation of prediction errors. The Mack Chainladder
method is the most basic of stochastic methods.
Compatibility#
Because of the regression framework underlying the MackChainladder
, it is not
compatible with all development and tail estimators of the library. In fact,
it really should only be used with the Development
estimator and TailCurve
tail estimator.
Warning
While the MackChainladder might not error with other options for development and
tail, the stochastic properties should be ignored, in which case the basic
Chainladder
should be used.
Examples#
[Mack, 1993] [Mack, 1999]
BornhuetterFerguson#
The :class:BornhuetterFerguson
technique is essentially a blend of the
development and expected claims techniques. In the development technique, we multiply actual
claims by a cumulative claim development factor. This technique can lead to erratic, unreliable
projections when the cumulative development factor is large because a relatively small swing in
reported claims or the reporting of an unusually large claim could result in a very large swing in
projected ultimate claims. In the expected claims technique, the unpaid claim estimate is equal to
the difference between a predetermined estimate of expected claims and the actual payments.
This has the advantage of stability, but it completely ignores actual results as reported. The
Bornhuetter-Ferguson technique combines the two techniques by splitting ultimate claims into
two components: actual reported (or paid) claims and expected unreported (or unpaid) claims. As
experience matures, more weight is given to the actual claims and the expected claims become
gradually less important.
Exposure base#
The :class:BornhuetterFerguson
technique is the first we explore of the Expected
Loss techniques. In this family of techniques, we need some measure of exposure.
This is handled by passing a Triangle
representing the exposure to the sample_weight
argument of the fit
method of the Estimator.
All scikit-learn style estimators optionally support a sample_weight
argument
and this is used by the chainladder
package to capture the exposure base
of these Expected Loss techniques.
raa = cl.load_sample('raa')
sample_weight = raa.latest_diagonal*0+40_000
cl.BornhuetterFerguson(apriori=0.7).fit(
X=raa,
sample_weight=sample_weight
).ibnr_.sum()
75203.23550854485
Apriori#
We’ve fit a :class:BornhuetterFerguson
model with the assumption that our
prior belief, or apriori
is a 70% Loss Ratio. The method supports any constant
for the apriori
hyperparameter. The apriori
then gets
multiplied into our sample weight to determine our prior belief on expected losses
prior to considering that actual emerged to date.
Because of the multiplicative nature of apriori
and sample_weight
we don’t
have to limit ourselves to a single constant for the apriori
. Instead, we
can exploit the model structure to make our sample_weight
represent our
prior belief on ultimates while setting the apriori
to 1.0.
For example, we can use the :class:Chainladder
ultimates as our prior belief
in the :class:BornhuetterFerguson
method.
cl_ult = cl.Chainladder().fit(raa).ultimate_ # Chainladder Ultimate
apriori = cl_ult*0+(cl_ult.sum()/10) # Mean Chainladder Ultimate
cl.BornhuetterFerguson(apriori=1).fit(raa, sample_weight=apriori).ultimate_
2050 | |
---|---|
1981 | 18,834 |
1982 | 16,899 |
1983 | 24,012 |
1984 | 28,282 |
1985 | 28,204 |
1986 | 19,840 |
1987 | 18,840 |
1988 | 22,790 |
1989 | 19,541 |
1990 | 20,986 |
Benktander#
The :class:Benktander
method is a credibility-weighted
average of the :class:BornhuetterFerguson
technique and the development technique.
The advantage cited by the authors is that this method will prove more
responsive than the Bornhuetter-Ferguson technique and more stable
than the development technique.
Iterations#
The Benktander
method is also known as the iterated :class:BornhuetterFerguson
method. This is because it is a generalization of the :class:BornhuetterFerguson
technique.
The generalized formula based on n_iters
, n is:
$Ultimate = Apriori\times (1-\frac{1}{CDF})^{n} + Latest\times \sum_{k=0}^{n-1}(1-\frac{1}{CDF})^{k}$
n=0
yields the expected loss methodn=1
yields the traditional :class:BornhuetterFerguson
methodn>>1
converges to the traditional :class:Chainladder
method.
Examples#
Expected Loss Method#
Setting n_iters
to 0 will emulate that Expected Loss method. That is to say,
the actual emerged loss experience of the Triangle will be completely ignored in
determining the ultimate. While it is a trivial calculation, it allows for
run-off patterns to be developed, which is useful for new programs new lines
of businesses.
triangle = cl.load_sample('ukmotor')
exposure = triangle.latest_diagonal*0 + 25_000
cl.Benktander(apriori=0.75, n_iters=0).fit(
X=triangle,
sample_weight=exposure
).full_triangle_.round(0)
12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 9999 | |
---|---|---|---|---|---|---|---|---|---|
2007 | 3,511 | 6,726 | 8,992 | 10,704 | 11,763 | 12,350 | 12,690 | 12,690 | 12,690 |
2008 | 4,001 | 7,703 | 9,981 | 11,161 | 12,117 | 12,746 | 18,750 | 18,750 | 18,750 |
2009 | 4,355 | 8,287 | 10,233 | 11,755 | 12,993 | 16,664 | 18,750 | 18,750 | 18,750 |
2010 | 4,295 | 7,750 | 9,773 | 11,093 | 15,112 | 17,432 | 18,750 | 18,750 | 18,750 |
2011 | 4,150 | 7,897 | 10,217 | 13,718 | 16,359 | 17,884 | 18,750 | 18,750 | 18,750 |
2012 | 5,102 | 9,650 | 13,112 | 15,425 | 17,170 | 18,178 | 18,750 | 18,750 | 18,750 |
2013 | 6,283 | 11,121 | 14,024 | 15,963 | 17,426 | 18,270 | 18,750 | 18,750 | 18,750 |
Mack noted the Benktander
method is found to have almost always a smaller mean
squared error than the other two methods and to be almost as precise as an exact
Bayesian procedure.
CapeCod#
The :class:CapeCod
method, also known as the Stanard-Buhlmann method, is similar to the
Bornhuetter-Ferguson technique. The primary difference between the two methods is the
derivation of the expected claim ratio. In the Cape Cod technique, the expected claim ratio
or apriori is obtained from the triangle itself instead of an independent and often judgmental
selection as in the Bornhuetter-Ferguson technique.
clrd = cl.load_sample('clrd')[['CumPaidLoss', 'EarnedPremDIR']].groupby('LOB').sum().loc['wkcomp']
loss = clrd['CumPaidLoss']
sample_weight=clrd['EarnedPremDIR'].latest_diagonal
m1 = cl.CapeCod().fit(loss, sample_weight=sample_weight)
m1.ibnr_.sum()
3030598.384680113
Apriori#
The default hyperparameters for the :class:CapeCod
method can be emulated by
the :class:BornhuetterFerguson
method. We can manually derive the apriori
implicit in the CapeCod estimate.
cl_ult = cl.Chainladder().fit(loss).ultimate_
apriori = loss.latest_diagonal.sum() / (sample_weight/(cl_ult/loss.latest_diagonal)).sum()
m2 = cl.BornhuetterFerguson(apriori).fit(
X=clrd['CumPaidLoss'],
sample_weight=clrd['EarnedPremDIR'].latest_diagonal)
m2.ibnr_.sum()
3030598.384680113
A parameter apriori_sigma
can also be specified to give sampling variance to the
estimated apriori. This along with random_state
can be used in conjuction with
the BootstrapODPSample
estimator to build a stochastic CapeCod
estimate.
Trend and On-level#
When using data implicit in the Triangle to derive the apriori, it is desirable
to bring the different origin periods to a common basis. The CapeCod
estimator
provides a trend
hyperparameter to allow for trending everything to the latest
origin period. However, the apriori used in the actual estimation of the IBNR is
the detrended_apriori_
detrended back to each of the specific origin periods.
m1 = cl.CapeCod(trend=0.05).fit(loss, sample_weight=sample_weight)
pd.concat((
m1.detrended_apriori_.to_frame().iloc[:, 0].rename('Detrended Apriori'),
m1.apriori_.to_frame().iloc[:, 0].rename('Apriori')), axis=1
)
Detrended Apriori | Apriori | |
---|---|---|
1988-01-01 | 0.483539 | 0.750128 |
1989-01-01 | 0.507716 | 0.750128 |
1990-01-01 | 0.533102 | 0.750128 |
1991-01-01 | 0.559757 | 0.750128 |
1992-01-01 | 0.587745 | 0.750128 |
1993-01-01 | 0.617132 | 0.750128 |
1994-01-01 | 0.647989 | 0.750128 |
1995-01-01 | 0.680388 | 0.750128 |
1996-01-01 | 0.714407 | 0.750128 |
1997-01-01 | 0.750128 | 0.750128 |
Simple one-part trends are supported directly in the hyperparameter selection.
If a more complex trend assumption is required or on-leveling, then passing
Triangles transformed by the :class:Trend
and :class:ParallelogramOLF
estimators will capture these finer details as in this example from the
example gallery.
Examples#
Decay#
The default behavior of the CapeCod
is to include all origin periods in the
estimation of the apriori_
. A more localized approach, giving lesser weight
to origin periods that are farther from a target origin period, can be achieved
by flexing the decay
hyperparameter.
cl.CapeCod(decay=0.8).fit(loss, sample_weight=sample_weight).apriori_.T
1988 | 1989 | 1990 | 1991 | 1992 | 1993 | 1994 | 1995 | 1996 | 1997 | |
---|---|---|---|---|---|---|---|---|---|---|
2050 | 0.617945 | 0.613275 | 0.604879 | 0.591887 | 0.57637 | 0.559855 | 0.548615 | 0.542234 | 0.540979 | 0.541723 |
With a decay
less than 1.0, we see apriori_
estimates that vary by origin.