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"],
)