Online Sandbox Tutorial#

Welcome! If you’ve come here to explore the capabilities of the chainladder-python package, you’ve landed in the perfect spot. This online sandbox tutorial is designed to provide you with a glimpse of the package’s functionalities.

We recommend setting aside about one hour to complete it.

Got Stuck? Click here for the filled in workbook. Have questions? Join the discussion on GitHub.

Setting Up#

We will first need to install the package, as Google Colab’s default environment doesn’t have the chainladder package pre-installed.

Simply execute pip install chainladder, Colab is smart enough to know that this is not a piece of python code, but to execute it in shell. FYI, pip stands for “Package Installer for Python”. You will need to run this step using your terminal instead of using a python notebook when you are ready to install the package on your machine.

pip install __fill_in_code__
ERROR: Invalid requirement: '__fill_in_code__'

Note: you may need to restart the kernel to use updated packages.

%load_ext lab_black is a linter, it makes code prettier, you may ignore this line.

%load_ext lab_black

Other commonly used packages, such as numpy, pandas, and matplotlib are already pre-installed, we just need to load them into our environment.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import chainladder as cl

print("chainladder", cl.__version__)
chainladder 0.8.18

Your Journey Begins#

Let’s begin by looking at a sample dataset, called xyz, which is hosted on https://raw.githubusercontent.com/casact/chainladder-python/master/chainladder/utils/data/xyz.csv.

Let’s load the dataset into the memory with pandas, then inspect its “head”.

xyz_df = pd.read_csv(
    __fill_in_code__
)
xyz_df.head()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 2
      1 xyz_df = pd.read_csv(
----> 2     __fill_in_code__
      3 )
      4 xyz_df.head()

NameError: name '__fill_in_code__' is not defined

Can you list all of the unique accident years?

xyz_df[__fill_in_code__].unique()

How many are there?

xyz_df[__fill_in_code__].nunique()

Triangle Basics#

Let’s load the data into the chainladder triangle format. And let’s call it xyz_tri.

xyz_tri = cl.Triangle(
    data=__fill_in_code__,
    origin="AccidentYear",
    development="DevelopmentYear",
    columns=["Incurred", "Paid", "Reported", "Closed", "Premium"],
    cumulative=True,
)
xyz_tri

What does the incurred triangle look like?

xyz_tri[__fill_in_code__]

How about paid?

xyz_tri[__fill_in_code__]

Pandas-like Operations#

Let’s see how .iloc[...] and .loc[...] similarly to pandas. They take 4 parameters: [index, column, origin, valuation].

What if we want the row from AY 1998 Incurred data?

xyz_tri.iloc[__fill_in_code__, __fill_in_code__, __fill_in_code__, __fill_in_code__]

What if you only want the valuation at age 60 of AY 1998?

xyz_tri.iloc[__fill_in_code__, __fill_in_code__, __fill_in_code__, __fill_in_code__]

Let’s use .loc[...] to get the incurred triangle.

xyz_tri.loc[__fill_in_code__, __fill_in_code__, __fill_in_code__, __fill_in_code__]

How do we get the latest Incurred diagonal only?

xyz_tri["Incurred"].__fill_in_code__

Very often, we want incremental triangles instead. Let’s convert the Incurred triangle to the incremental form.

xyz_tri["Incurred"].__fill_in_code__

We can also convert the triangle to the valuation format, what we often see on Schedule Ps.

xyz_tri["Incurred"].__fill_in_code__

Another function that is often useful is the .heatmap() method. Let’s inspect the incurred amount and see if there are trends.

xyz_tri["Incurred"].__fill_in_code__

Development#

How can we get the incurred link ratios?

xyz_tri["Incurred"].__fill_in_code__

We can also apply a .heatmap() to make it too, to help us visulize the highs and lows.

xyz_tri["Incurred"].__fill_in_code__.__fill_in_code__

Let’s get a volume-weighted average LDFs for our Incurred triangle.

cl.Development(average="volume").fit(__fill_in_code__).ldf_

How about the CDFs?

cl.Development(average="volume").fit(__fill_in_code__).__fill_in_code__

We can also use only the latest 3 periods in the calculation of CDFs.

cl.Development(average="volume", n_periods=__fill_in_code__).fit(xyz_tri["Incurred"]).cdf_

Deterministic Models#

Before we can build any models, we need to use fit_transform(), so that the object is actually modified with our selected development pattern(s).

Set the development of the triangle to use only 3 periods.

cl.Development(average=__fill_in_code__, n_periods=__fill_in_code__).fit_transform(__fill_in_code__)

Let’s fit a chainladder model to our Incurred triangle.

cl_mod = cl.Chainladder().fit(__fill_in_code__)
cl_mod

How can we get the model’s ultimate estimate?

cl_mod.__fill_in_code__

How about just the IBNR?

cl_mod.__fill_in_code__

Let’s fit an Expected Loss model, with an aprior of 90% on Premium, and get its ultimates.

cl.ExpectedLoss(apriori=__fill_in_code__).fit(
    xyz_tri["Incurred"], sample_weight=xyz_tri["Premium"].latest_diagonal
).ultimate_

Try it on the Paid triangle, do you get the same ultimate?

cl.ExpectedLoss(apriori=0.90).fit(
    xyz_tri["Paid"], sample_weight=__fill_in_code__
).ultimate_

How about a Bornhuetter-Ferguson model?

cl.__fill_in_code__(apriori=0.90).fit(
    xyz_tri["Incurred"], sample_weight=xyz_tri["Premium"].latest_diagonal
).ultimate_

How about Benktander, with 1 iteration, which is the same as BF?

cl.__fill_in_code__(apriori=0.90, n_iters=__fill_in_code__).fit(
    xyz_tri["Incurred"], sample_weight=xyz_tri["Premium"].latest_diagonal
).ultimate_

How about Cape Cod?

cl.__fill_in_code__().fit(
    xyz_tri["Incurred"], sample_weight=xyz_tri["Premium"].latest_diagonal
).ultimate_

Let’s store the Cape Cod model as cc_result. We can also use .to_frame() to leave chainladder and go to a DataFrame. Let’s make a bar chart over origin years to see what they look like.

cc_result = (
    cl.CapeCod()
    .fit(xyz_tri["Incurred"], sample_weight=xyz_tri["Premium"].latest_diagonal)
    .ultimate_
).to_frame()

plt.plot(
    cc_result.index.year,
    cc_result[__fill_in_code__]
)

Stochastic Models#

The Mack’s Chainladder model is available. Let’s use it on the Incurred triangle.

mcl_mod = cl.__fill_in_code__().fit(__fill_in_code__)
mcl_mod

There are many attributes that are available, such as full_std_err_, total_process_risk_, total_parameter_risk_, mack_std_err_ and total_mack_std_err_.

__fill_in_code__.full_std_err_

MackChainladder also has a summary_ attribute.

__fill_in_code__.summary_

Let’s make a graph, that shows the Reported and IBNR as stacked bars, and error bars showing Mack Standard Errors.

plt.bar(
    mcl_mod.summary_.to_frame(origin_as_datetime=True).index.year,
    mcl_mod.summary_.to_frame(origin_as_datetime=True)[__fill_in_code__],
    label="Reported",
)
plt.bar(
    mcl_mod.summary_.to_frame(origin_as_datetime=True).index.year,
    mcl_mod.summary_.to_frame(origin_as_datetime=True)[__fill_in_code__],
    bottom=mcl_mod.summary_.to_frame(origin_as_datetime=True)[__fill_in_code__],
    yerr=mcl_mod.summary_.to_frame(origin_as_datetime=True)[__fill_in_code__],
    label="IBNR",
)
plt.legend(loc="upper left")

ODP Bootstrap is also available. Let’s build sample 10,000 Incurred triangles.

xyz_tri_sampled = (
    cl.BootstrapODPSample(n_sims=__fill_in_code__).fit(__fill_in_code__).resampled_triangles_
)
xyz_tri_sampled

We can fit a basic chainladder to all sampled triangles. We now have 10,000 simulated chainladder models, all (most) with unique LDFs.

cl_mod_bootstrapped = cl.Chainladder().fit(xyz_tri_sampled)
cl_mod_bootstrapped

Let’s make another graph.

plt.bar(
    cl_mod_bootstrapped.ultimate_.__fill_in_code__.to_frame(origin_as_datetime=True).index.year,
    cl_mod_bootstrapped.ultimate_.__fill_in_code__.to_frame(origin_as_datetime=True)["2261"],
    yerr=cl_mod_bootstrapped.ultimate_.__fill_in_code__.to_frame(origin_as_datetime=True)["2261"],
)