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


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

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.

'2261-12-31 23:59:59.999999999'
cl.options.set_option('ULT_VAL', '2050-12-31 23:59:59.999999999')
'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']

This throws an error since the model itself is not sliceable:

ult = model['paid'].ultimate_


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

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_
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

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']
2014 2015 2016
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


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.



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.


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.


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.


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


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_
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



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.


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 method

  • n=1 yields the traditional :class:BornhuetterFerguson method

  • n>>1 converges to the traditional :class:Chainladder method.

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(
12 24 36 48 60 72 84 96 9999
2007 3,511 6,726 8,992 10,704 11,763 12,350 12,690 18,750 18,750
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 18,248 18,750 18,750 18,750
2010 4,295 7,750 9,773 11,093 17,363 18,248 18,750 18,750 18,750
2011 4,150 7,897 10,217 15,832 17,363 18,248 18,750 18,750 18,750
2012 5,102 9,650 13,801 15,832 17,363 18,248 18,750 18,750 18,750
2013 6,283 10,762 13,801 15,832 17,363 18,248 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.



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']
m1 = cl.CapeCod().fit(loss, sample_weight=sample_weight)


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(

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)
    m1.detrended_apriori_.to_frame().iloc[:, 0].rename('Detrended Apriori'),
    m1.apriori_.to_frame().iloc[:, 0].rename('Apriori')), axis=1
/home/docs/checkouts/readthedocs.org/user_builds/chainladder-python/conda/latest/lib/python3.10/site-packages/chainladder/core/pandas.py:62: UserWarning: In an upcoming version of the package, `origin_as_datetime` will be defaulted to `True` in to_frame(...), use `origin_as_datetime=False` to preserve current setting.
/home/docs/checkouts/readthedocs.org/user_builds/chainladder-python/conda/latest/lib/python3.10/site-packages/chainladder/core/pandas.py:62: UserWarning: In an upcoming version of the package, `origin_as_datetime` will be defaulted to `True` in to_frame(...), use `origin_as_datetime=False` to preserve current setting.
Detrended Apriori Apriori
1988 0.483539 0.750128
1989 0.507716 0.750128
1990 0.533102 0.750128
1991 0.559757 0.750128
1992 0.587745 0.750128
1993 0.617132 0.750128
1994 0.647989 0.750128
1995 0.680388 0.750128
1996 0.714407 0.750128
1997 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.


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.