Working with Triangles

Getting Started

Welcome! We drafted these tutorials to help you get familiar with some of the common functionalities that most actuaries can use in their day-to-day responsibilities. The package also comes with pre-installed datasets that you can play with, which are also used in the tutorials here.

The tutorials assume that you have the basic understanding of commonly used actuarial terms, and can independently perform an actuarial analysis in another tool, such as Microsoft Excel or another actuarial software. Furthermore, it is assumed that you already have some familiarity with Python, and that you have the basic knowledge and experience in using some common packages that are popular in the Python community, such as pandas and numpy.

This tutorial is linted using black via nb_black. This step is optional.

%load_ext lab_black

All tutorials and exercises rely on chainladder v0.8.5 and later. It is highly recomended that you keep your packages up-to-date.

import pandas as pd
import numpy as np
import chainladder as cl

print("pandas: " + pd.__version__)
print("numpy: " + np.__version__)
print("chainladder: " + cl.__version__)
pandas: 1.4.2
numpy: 1.21.5
chainladder: 0.8.13

Since we will be plotting for quite a bit, here’s a magic function in IPython, which sets the backend of matplotlib to the ‘inline’ backend. With this backend, the output of plotting commands is displayed inline within frontends like the Jupyter notebook, directly below the code cell that produced it. The resulting plots will then also be stored in the notebook document.

%matplotlib inline

Disclaimer

Note that a lot of the examples shown might not be applicable in a real world scenario, and is only meant to demonstrate some of the functionalities included in the package. The user should always follow all applicable laws, the Code of Professional Conduct, applicable Actuarial Standards of Practice, and exercise their best actuarial judgement.

Working with a Triangle

Let’s begin by looking at an unprocessed triangle data and load it into a pandas.DataFrame. We’ll use the data raa, which is available from the repository.

raa_df = pd.read_csv(
    "https://raw.githubusercontent.com/casact/chainladder-python/master/chainladder/utils/data/raa.csv"
)
raa_df.head(20)
development origin values
0 1981 1981 5012.0
1 1982 1982 106.0
2 1983 1983 3410.0
3 1984 1984 5655.0
4 1985 1985 1092.0
5 1986 1986 1513.0
6 1987 1987 557.0
7 1988 1988 1351.0
8 1989 1989 3133.0
9 1990 1990 2063.0
10 1982 1981 8269.0
11 1983 1982 4285.0
12 1984 1983 8992.0
13 1985 1984 11555.0
14 1986 1985 9565.0
15 1987 1986 6445.0
16 1988 1987 4020.0
17 1989 1988 6947.0
18 1990 1989 5395.0
19 1983 1981 10907.0

The data has three columns:

  • development: or valuation time, in this case, valuation year

  • origin: or accident date, in this case, accident year

  • values: the values recorded for the specific accident date at the specific valuation time (such as incurred losses, paid losses, or claim counts)

A table of loss experience showing total losses for a certain period (origin) at various, regular valuation dates (development), reflect the change in amounts as claims mature and emerge. Older periods in the table will have one more entry than the next youngest period, leading to the triangle shape of the data in the table or any other measure that matures over time from an origin date. Loss triangles can be used to determine loss development for a given risk.

Let’s put our data into the triangle format.

raa = cl.Triangle(
    raa_df,
    origin="origin",
    development="development",
    columns="values",
    cumulative=True,
)
raa
12 24 36 48 60 72 84 96 108 120
1981 5,012 8,269 10,907 11,805 13,539 16,181 18,009 18,608 18,662 18,834
1982 106 4,285 5,396 10,666 13,782 15,599 15,496 16,169 16,704
1983 3,410 8,992 13,873 16,141 18,735 22,214 22,863 23,466
1984 5,655 11,555 15,766 21,266 23,425 26,083 27,067
1985 1,092 9,565 15,836 22,169 25,955 26,180
1986 1,513 6,445 11,702 12,935 15,852
1987 557 4,020 10,946 12,314
1988 1,351 6,947 13,112
1989 3,133 5,395
1990 2,063

You can also load the example data directly, using load_sample:

raa = cl.load_sample("raa")
raa
12 24 36 48 60 72 84 96 108 120
1981 5,012 8,269 10,907 11,805 13,539 16,181 18,009 18,608 18,662 18,834
1982 106 4,285 5,396 10,666 13,782 15,599 15,496 16,169 16,704
1983 3,410 8,992 13,873 16,141 18,735 22,214 22,863 23,466
1984 5,655 11,555 15,766 21,266 23,425 26,083 27,067
1985 1,092 9,565 15,836 22,169 25,955 26,180
1986 1,513 6,445 11,702 12,935 15,852
1987 557 4,020 10,946 12,314
1988 1,351 6,947 13,112
1989 3,133 5,395
1990 2,063

A triangle has more properties than just what is displayed. For example we can see the underlying link_ratios, which represent the multiplicative change in amounts from one development period to the next.

raa.link_ratio
12-24 24-36 36-48 48-60 60-72 72-84 84-96 96-108 108-120
1981 1.6498 1.3190 1.0823 1.1469 1.1951 1.1130 1.0333 1.0029 1.0092
1982 40.4245 1.2593 1.9766 1.2921 1.1318 0.9934 1.0434 1.0331
1983 2.6370 1.5428 1.1635 1.1607 1.1857 1.0292 1.0264
1984 2.0433 1.3644 1.3489 1.1015 1.1135 1.0377
1985 8.7592 1.6556 1.3999 1.1708 1.0087
1986 4.2597 1.8157 1.1054 1.2255
1987 7.2172 2.7229 1.1250
1988 5.1421 1.8874
1989 1.7220

We can also view (and manipulate) the latest_diagonal of the triangle.

raa.latest_diagonal
1990
1981 18,834
1982 16,704
1983 23,466
1984 27,067
1985 26,180
1986 15,852
1987 12,314
1988 13,112
1989 5,395
1990 2,063
raa.latest_diagonal / 1000
1990
1981 18.83
1982 16.70
1983 23.47
1984 27.07
1985 26.18
1986 15.85
1987 12.31
1988 13.11
1989 5.39
1990 2.06

The latest diagonal also corresponds to a valuation_date. Note that ‘valuation_date’ is a datetime that is at the terminal timestamp of the period (i.e. the last split second of the year).

raa.valuation_date
Timestamp('1990-12-31 23:59:59.999999999')

We can also tell whether our triangle:

  • is_cumulative: returns True if the data across the development periods is cumulative, or False if it is incremental.

  • is_ultimate: returns True if the ultimate values are contained in the triangle.

  • is_val_tri: returns True if the development period is stated as a valuation data as opposed to an age, i.e. Schedule P style triangle (True) or the more commonly used triangle by development age (False).

  • is_full: returns True if the triangle has been “squared”.

print("Is triangle cumulative?", raa.is_cumulative)
print("Does triangle contain ultimate projections?", raa.is_ultimate)
print("Is this a valuation triangle?", raa.is_val_tri)
print('Has the triangle been "squared"?', raa.is_full)
Is triangle cumulative? True
Does triangle contain ultimate projections? False
Is this a valuation triangle? False
Has the triangle been "squared"? False

We can also inspect the triangle to understand its data granularity with origin_grain and development_grain.

print("Origin grain:", raa.origin_grain)
print("Development grain:", raa.development_grain)
Origin grain: Y
Development grain: Y

The package supports monthly (“M”), quarterly (“Q”), semester (or semi-annually), (“S”) and yearly (“Y”) grains for both origin_grain and development_grain.

The Triangle Structure

The triangle described so far is a two-dimensional (accident date by valuation date) structure that spans multiple cells of data. This is a useful structure for exploring individual triangles, but becomes more problematic when working with sets of triangles. Pandas does not have a triangle dtype, but if it did, working with sets of triangles would be much more convenient. To facilitate working with more than one triangle at a time the chainladder.Triangle acts like a pandas dataframe (with an index and columns) where each cell (row x col) is an individual triangle. This structure manifests itself as a four-dimensional space. Let’s take a look at another dataset clrd.

clrd_df = pd.read_csv(
    "https://raw.githubusercontent.com/casact/chainladder-python/master/chainladder/utils/data/clrd.csv"
)
clrd_df
GRCODE GRNAME AccidentYear DevelopmentYear DevelopmentLag IncurLoss CumPaidLoss BulkLoss EarnedPremDIR EarnedPremCeded EarnedPremNet Single PostedReserve97 LOB
0 86 Allstate Ins Co Grp 1988 1988 1 367404 70571 127737 400699 5957 394742 0 281872 wkcomp
1 86 Allstate Ins Co Grp 1988 1989 2 362988 155905 60173 400699 5957 394742 0 281872 wkcomp
2 86 Allstate Ins Co Grp 1988 1990 3 347288 220744 27763 400699 5957 394742 0 281872 wkcomp
3 86 Allstate Ins Co Grp 1988 1991 4 330648 251595 15280 400699 5957 394742 0 281872 wkcomp
4 86 Allstate Ins Co Grp 1988 1992 5 354690 274156 27689 400699 5957 394742 0 281872 wkcomp
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
42840 44598 College Liability Ins Co Ltd RRG 1995 1996 2 343 249 82 397 0 397 1 630 othliab
42841 44598 College Liability Ins Co Ltd RRG 1995 1997 3 839 575 190 397 0 397 1 630 othliab
42842 44598 College Liability Ins Co Ltd RRG 1996 1996 1 125 6 98 257 0 257 1 630 othliab
42843 44598 College Liability Ins Co Ltd RRG 1996 1997 2 95 17 28 257 0 257 1 630 othliab
42844 44598 College Liability Ins Co Ltd RRG 1997 1997 1 83 4 77 256 0 256 1 630 othliab

42845 rows × 14 columns

Let’s load the data into the sets of triangles.

clrd = cl.Triangle(
    clrd_df,
    origin="AccidentYear",
    development="DevelopmentYear",
    columns=[
        "IncurLoss",
        "CumPaidLoss",
        "BulkLoss",
        "EarnedPremDIR",
        "EarnedPremCeded",
        "EarnedPremNet",
    ],
    index=["GRNAME", "LOB"],
    cumulative=True,
)
clrd
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (775, 6, 10, 10)
Index: [GRNAME, LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet]

Since 4D strucures do not fit nicely on 2D screens, we see a summary view instead that describes the structure rather than the underlying data itself.

We see 5 rows of information:

  • Valuation: the valuation date.

  • Grain: the granularity of the data, O stands for origin, and D stands for development, OYDY represents triangles with accident year by development year.

  • Shape: contains 4 numbers, represents the 4-D structure. This sample triangle represents a collection of 775x6 or 4,650 triangles that are themselves 10 accident years by 10 development periods.

    • 775: the number of segments, which is the combination of index, that represents the data segments. In this case, it is each of the GRNAME and LOB unique combination.

    • 6: the number of triangles for each segment, which is also the columns [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet]. They could be paid amounts, incurred amounts, reported counts, loss ratios, closure rates, excess losses, premium, etc.

    • 10: the number of accident periods.

    • 10: the number of valuation periods.

  • Index: the segmentation level of the triangles.

  • Columns: the value types recorded in the triangles.

To summarize the set of triangles:

  • We have a total of 775 segments, which are at the GRNAME and LOB level.

  • Each segment contains 6 triangles, which are IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet.

  • Each triangle is 10 accident years x 10 development periods.

Using index.head() allows us to see the first 5 segments in the set of triangles. Note that as the data is loaded, the triangle are sorted by index, in this case, GRNAME first, then by LOB.

clrd.index.head()
GRNAME LOB
0 Adriatic Ins Co othliab
1 Adriatic Ins Co ppauto
2 Aegis Grp comauto
3 Aegis Grp othliab
4 Aegis Grp ppauto

Under the hood, the data structure is a numpy.ndarray with the equivalent shape. Like pandas, you can directly access the underlying numpy structure with the values property. By exposing the underlying ndarray you are free to manipulate the underlying data directly with numpy should that be an easier route to solving a problem.

print("Data structure of clrd:", type(clrd.values))
print("Sum of all data values:", np.nansum(clrd.values))
Data structure of clrd: <class 'numpy.ndarray'>
Sum of all data values: 3661713596.0

Keep in mind though, the chainladder.Triangle has several methods and properties beyond the raw numpy representation and these are kept in sync by using the chainladder.Triangle directly.

Pandas-Style Slicing

As mentioned, the 4D structure is intended to behave like a pandas DataFrame. Like pandas, we can subset a dataframe by referencing individual columns by name.

clrd[["CumPaidLoss", "IncurLoss", "BulkLoss"]]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (775, 3, 10, 10)
Index: [GRNAME, LOB]
Columns: [CumPaidLoss, IncurLoss, BulkLoss]

We can also boolean-index the rows of the Triangle.

clrd[clrd["LOB"] == "wkcomp"]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (132, 6, 10, 10)
Index: [GRNAME, LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet]

We can even use the typical loc, iloc functionality similar to pandas to access subsets of data. These features can be chained together as much as you want.

clrd.loc["Allstate Ins Co Grp"].iloc[-1]["CumPaidLoss"]
12 24 36 48 60 72 84 96 108 120
1988 70,571 155,905 220,744 251,595 274,156 287,676 298,499 304,873 321,808 325,322
1989 66,547 136,447 179,142 211,343 231,430 244,750 254,557 270,059 273,873
1990 52,233 133,370 178,444 204,442 222,193 232,940 253,337 256,788
1991 59,315 128,051 169,793 196,685 213,165 234,676 239,195
1992 39,991 89,873 114,117 133,003 154,362 159,496
1993 19,744 47,229 61,909 85,099 87,215
1994 20,379 46,773 88,636 91,077
1995 18,756 84,712 87,311
1996 42,609 44,916
1997 691

Pandas-Style Arithmetic

With complete flexibility in the ability to slice subsets of triangles, we can use basic arithmetic to derive new triangles, which is commonly used as diagnostics to explore trends.

clrd["CaseIncurLoss"] = clrd["IncurLoss"] - clrd["BulkLoss"]
clrd["PaidToInc"] = clrd["CumPaidLoss"] / clrd["CaseIncurLoss"]
clrd[["CaseIncurLoss", "PaidToInc"]]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (775, 2, 10, 10)
Index: [GRNAME, LOB]
Columns: [CaseIncurLoss, PaidToInc]

We can also aggregating the values across all triangles into one triangle.

clrd["CumPaidLoss"].sum()
12 24 36 48 60 72 84 96 108 120
1988 3,577,780 7,059,966 8,826,151 9,862,687 10,474,698 10,814,576 10,994,014 11,091,363 11,171,590 11,203,949
1989 4,090,680 7,964,702 9,937,520 11,098,588 11,766,488 12,118,790 12,311,629 12,434,826 12,492,899
1990 4,578,442 8,808,486 10,985,347 12,229,001 12,878,545 13,238,667 13,452,993 13,559,557
1991 4,648,756 8,961,755 11,154,244 12,409,592 13,092,037 13,447,481 13,642,414
1992 5,139,142 9,757,699 12,027,983 13,289,485 13,992,821 14,347,271
1993 5,653,379 10,599,423 12,953,812 14,292,516 15,005,138
1994 6,246,447 11,394,960 13,845,764 15,249,326
1995 6,473,843 11,612,151 14,010,098
1996 6,591,599 11,473,912
1997 6,451,896

We can also construct a paid loss ratio triangle against EarnedPremNet.

clrd["CumPaidLoss"].sum() / clrd["EarnedPremNet"].sum()
12 24 36 48 60 72 84 96 108 120
1988 0.2581 0.5093 0.6368 0.7115 0.7557 0.7802 0.7932 0.8002 0.8060 0.8083
1989 0.2686 0.5230 0.6526 0.7288 0.7727 0.7958 0.8085 0.8166 0.8204
1990 0.2728 0.5249 0.6546 0.7287 0.7674 0.7888 0.8016 0.8080
1991 0.2540 0.4896 0.6093 0.6779 0.7152 0.7346 0.7453
1992 0.2595 0.4927 0.6074 0.6711 0.7066 0.7245
1993 0.2645 0.4959 0.6061 0.6687 0.7021
1994 0.2709 0.4942 0.6005 0.6614
1995 0.2651 0.4755 0.5737
1996 0.2635 0.4586
1997 0.2552

Aggregating all segments together is interesting, but it is often more useful to aggregate across segments using groupby. For example, we may want to group the triangles by line of business and get a sum across all companies for each industry.

clrd.groupby("LOB").sum()
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (6, 8, 10, 10)
Index: [LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet, CaseIncurLoss, PaidToInc]

The shape is (6, 8, 10, 10) because now we have 6 LOBs with 8 triangles for each LOB. We can also note that index is now on [LOB] only.

np.unique(clrd["LOB"])
array(['comauto', 'medmal', 'othliab', 'ppauto', 'prodliab', 'wkcomp'],
      dtype=object)

The aggregation functions, e.g. sum, mean, std, min, max, etc. don’t have to just apply to the index axis. You can apply them to any of the four axes in the triangle object, which are segments (axis 0, or the index axis), columns (axis 1, the various financial fields), origin (axis 2, across triangle rows), and development period (axis 3, across triangle columns). You can also use either the axis name or number. Let’s try to sum all of the segments ([GRNAME, LOB]) and columns ([IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet]) using axis name and number, respectively.

clrd.sum(axis="index").sum(axis="segments")
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (1, 8, 10, 10)
Index: [GRNAME, LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet, CaseIncurLoss, PaidToInc]
clrd.sum(axis=0).sum(axis=1)
12 24 36 48 60 72 84 96 108 120
1988 56,387,734 59,928,520 61,653,458 62,644,631 63,182,710 63,438,080 63,440,904 63,452,111 63,522,981 63,518,183
1989 62,840,448 66,705,570 68,666,928 69,702,283 70,188,013 70,355,679 70,389,857 70,453,158 70,483,269
1990 70,064,967 74,084,388 75,879,276 76,812,990 77,179,572 77,240,321 77,283,937 77,345,588
1991 74,611,611 78,513,121 80,235,908 80,967,061 81,178,876 81,185,484 81,278,636
1992 81,213,791 85,089,370 86,443,882 86,783,107 86,908,610 87,086,637
1993 87,896,227 91,685,456 93,018,485 93,167,810 93,473,079
1994 94,593,702 98,130,718 99,071,781 99,809,122
1995 97,722,814 101,192,332 102,056,680
1996 98,497,932 100,917,690
1997 96,832,221

Accessor Methods

Pandas has special “accessor” methods for str and dt. These allow for the manipulation of data within each cell of data:

# splits lastname from first name by a comma-delimiter
df['Last_First'].str.split(',')

# pulls the year out of each date in a dataframe column
df['Accident Date'].dt.year 

chainladder also has special “accessor” methods to help us manipulate the origin, development and valuation vectors of a triangle.

We may want to extract only the latest accident period for every triangle.

clrd[clrd.origin == clrd.origin.max()]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (775, 8, 1, 10)
Index: [GRNAME, LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet, CaseIncurLoss, PaidToInc]

Note that this triangle has only 1 row; however, all of the columns would exist, but only the youngest age would have values.

We may want to extract particular diagonals from our triangles using its valuation vector.

clrd[(clrd.valuation >= "1994") & (clrd.valuation < "1995")]["CumPaidLoss"].sum()
12 24 36 48 60 72 84
1988 10,994,014
1989 12,118,790
1990 12,878,545
1991 12,409,592
1992 12,027,983
1993 10,599,423
1994 6,246,447

We may even want to slice particular development periods to explore aspects of our data by development age. For example, we can look at the development factors between ages 24 and 36.

clrd[(clrd.development > 12) & (clrd.development <= 36)]["CumPaidLoss"].sum().link_ratio
24-36
1988 1.2502
1989 1.2477
1990 1.2471
1991 1.2446
1992 1.2327
1993 1.2221
1994 1.2151
1995 1.2065
1996
1997

Moving Back to Pandas

When the shape of a Triangle object can be expressed as a 2D structure (i.e. two of its four axes have a length of 1) or less, you can use the to_frame method to convert your data into a pandas.DataFrame. Let’s pick only one financial field, CumPaidLoss and only the latest diagonal, with latest_diagonal. We are now left with LOBs and origin period as our 2 axes.

clrd.groupby("LOB").sum().latest_diagonal["CumPaidLoss"]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (6, 1, 10, 1)
Index: [LOB]
Columns: [CumPaidLoss]
clrd.groupby("LOB").sum().latest_diagonal["CumPaidLoss"].to_frame().astype(int)
/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.
  warnings.warn(warning)
origin 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997
LOB
comauto 626097 674441 718396 711762 731033 762039 768095 675166 510191 272342
medmal 217239 222707 235717 275923 267007 276235 252449 209222 107474 20361
othliab 317889 350684 361103 426085 389250 434995 402244 294332 191258 54130
ppauto 8690036 9823747 10728411 10713621 11555121 12249826 12600432 11807279 9900842 5754249
prodliab 110973 112614 121255 100276 76059 94462 111264 62018 28107 10682
wkcomp 1241715 1308706 1394675 1414747 1328801 1187581 1114842 962081 736040 340132

We can also aggregate process away 3 dimensions, then use to_frame.

clrd[clrd.origin == "1990"].groupby("LOB").sum().latest_diagonal[
    "CumPaidLoss"
].to_frame(origin_as_datetime=False).astype(int)
LOB
comauto       718396
medmal        235717
othliab       361103
ppauto      10728411
prodliab      121255
wkcomp       1394675
dtype: int64

Exercises

Use the clrd dataset for all of the exercises.

  1. How do we create a new column named “NetPaidLossRatio” in the triangle using the existing columns?

clrd["NetPaidLossRatio"] = clrd["CumPaidLoss"] / clrd["EarnedPremNet"]
  1. What is the highest paid loss ratio for across all segments for origin 1997 at age 12?

clrd[clrd.origin == "1997"][clrd.development == 12]["NetPaidLossRatio"].max()
4.769123134328358
  1. How do we subset the overall triangle to just include “Alaska Nat Ins Co”?

clrd[clrd["GRNAME"] == "Alaska Nat Ins Co"]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (4, 9, 10, 10)
Index: [GRNAME, LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet, CaseIncurLoss, PaidToInc, NetPaidLossRatio]
  1. How do we create a triangle subset that includes all triangles for companies with names starting with the letter “B”?

clrd[clrd["GRNAME"].str[0] == "B"]
Triangle Summary
Valuation: 1997-12
Grain: OYDY
Shape: (31, 9, 10, 10)
Index: [GRNAME, LOB]
Columns: [IncurLoss, CumPaidLoss, BulkLoss, EarnedPremDIR, EarnedPremCeded, EarnedPremNet, CaseIncurLoss, PaidToInc, NetPaidLossRatio]
  1. Which are the top 5 companies by net premium share for in 1990?

clrd.latest_diagonal.groupby("GRNAME").sum()[clrd.origin == "1990"][
    "EarnedPremNet"
].to_frame().sort_values(ascending=False).iloc[0:5]
/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.
  warnings.warn(warning)
GRNAME
State Farm Mut Grp                    10532675.0
United Services Automobile Asn Grp     1378791.0
Federal Ins Co Grp                      477150.0
New Jersey Manufacturers Grp            383870.0
FL Farm Bureau Grp                      342036.0
dtype: float64

Initializing a Triangle With Your Own Data

The chainladder.Triangle class is designed to ingest pandas.DataFrame objects. However, you do not need to worry about shaping the dataframe into triangle format yourself. This happens at the time you ingest the data.

Let’s look at the initialization signature and its docstring.

cl.Triangle?

Let’s use a new dataset prism to construct our triangles.

prism_df = pd.read_csv(
    "https://raw.githubusercontent.com/casact/chainladder-python/master/chainladder/utils/data/prism.csv"
)
prism_df.head()
ClaimNo AccidentDate ReportDate Line Type ClaimLiability Limit Deductible TotalPayment PaymentDate CloseDate Status reportedCount closedPaidCount closedUnPaidCount openCount Paid Outstanding Incurred
0 1 2008-01-22 4/19/2010 Home Dwelling False 300000.0 20000 0.00000 2010-10-08 10/8/2010 CLOSED 1 0 1 0 0.00000 0 0.00000
1 2 2008-01-02 4/20/2010 Home Dwelling False 200000.0 20000 0.00000 2010-11-30 11/30/2010 CLOSED 1 0 1 0 0.00000 0 0.00000
2 3 2008-01-01 9/23/2009 Home Dwelling True 200000.0 20000 115744.77370 2010-02-17 2/17/2010 CLOSED 1 1 0 0 115744.77370 0 115744.77370
3 4 2008-01-02 7/25/2009 Home Dwelling True 200000.0 20000 63678.87713 2009-11-18 11/18/2009 CLOSED 1 1 0 0 63678.87713 0 63678.87713
4 5 2008-01-16 12/7/2009 Home Dwelling True 200000.0 20000 112175.55590 2010-04-30 4/30/2010 CLOSED 1 1 0 0 112175.55590 0 112175.55590
prism_df.dtypes
ClaimNo                int64
AccidentDate          object
ReportDate            object
Line                  object
Type                  object
ClaimLiability          bool
Limit                float64
Deductible             int64
TotalPayment         float64
PaymentDate           object
CloseDate             object
Status                object
reportedCount          int64
closedPaidCount        int64
closedUnPaidCount      int64
openCount              int64
Paid                 float64
Outstanding            int64
Incurred             float64
dtype: object

We must specify the origin, development, columns, and cumulative to create a triangle object. By limiting our columns to one measure and not specifying an index, we can create a single triangle. For example, if we are only interested in the paid triangle. Because the data we have is transactional data, the cumulative variable should be set to False.

prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    development="PaymentDate",
    columns="Paid",
    cumulative=False,
)
prism
1 2 3 4 5 6 7 8 9 10 ... 111 112 113 114 115 116 117 118 119 120
2008-01 46,915 19,899 57,216 89,783 18,302 95,054 10,332 91,772 ...
2008-02 28,749 22,109 79,033 63,455 59,993 64,683 68,502 33,695 71,670 ...
2008-03 48,806 27,949 90,413 54,557 83,507 12,591 72,035 70,353 35,223 ...
2008-04 30,758 17,763 70,872 30,233 39,494 66,729 122,100 20,998 38,218 ...
2008-05 38,672 86,974 20,483 58,400 112,015 31,354 22,457 53,778 93,303 ...
2008-06 56,789 73,351 97,840 64,816 61,083 41,685 87,172 61,418 63,097 ...
2008-07 27,867 45,804 61,495 73,279 71,591 111,807 80,249 40,632 32,434 ...
2008-08 4,832 23,831 52,511 62,649 45,955 133,133 81,733 39,709 112,115 21,367 ...
2008-09 43,464 86,157 52,664 89,928 88,738 55,617 26,599 69,522 46,788 ...
2008-10 12,488 21,939 53,388 52,415 92,086 45,781 72,242 83,359 47,594 ...
2008-11 45,473 9,416 78,967 84,826 39,549 41,011 74,000 33,257 35,802 ...
2008-12 3,640 9,905 54,792 87,057 70,686 54,775 31,412 44,057 59,448 31,643 ...
2009-01 14,021 45,350 51,208 35,520 81,655 54,275 41,538 74,129 47,962 ...
2009-02 10,305 35,026 37,289 58,773 67,579 57,589 115,083 57,142 65,315 55,019 ...
2009-03 48,360 40,502 74,342 64,538 49,778 85,591 82,582 56,449 88,307 ...
2009-04 40,943 47,517 64,024 27,573 62,634 54,051 63,305 61,339 70,406 ...
2009-05 16,944 74,478 63,856 59,266 79,707 63,857 59,326 88,817 50,656 ...
2009-06 9,655 43,720 12,805 79,404 46,219 46,868 36,234 35,654 24,676 26,641 ...
2009-07 14,828 48,605 90,527 54,508 57,881 64,905 16,717 46,880 89,721 ...
2009-08 42,061 32,247 64,174 63,443 65,428 95,432 91,809 131,904 67,284 ...
2009-09 21,000 27,607 34,679 37,523 56,305 42,851 72,404 54,366 67,543 71,032 ...
2009-10 14,000 62,040 45,134 97,016 54,356 20,848 63,521 78,494 67,441 22,107 ...
2009-11 22,027 64,189 20,836 33,303 50,594 79,852 54,870 111,592 70,473 ...
2009-12 38,594 17,902 100,470 43,866 102,051 56,901 33,876 40,416 54,734 ...
2010-01 15,192 53,338 57,394 56,985 62,175 84,624 84,959 102,734 26,495 ...
2010-02 7,000 31,464 32,942 38,108 28,707 100,091 97,795 98,314 53,259 96,477 ...
2010-03 36,937 64,551 61,774 39,580 81,253 35,751 87,788 41,076 117,835 ...
2010-04 21,026 32,422 46,661 103,442 89,860 96,976 51,116 89,352 50,595 13,981 ...
2010-05 8,668 8,920 116,376 61,586 148,647 14,288 46,840 51,205 82,714 4,240 ...
2010-06 12,578 92,156 44,495 112,868 79,732 73,743 61,814 73,713 23,724 ...
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2015-07 5,872 61,431 113,522 96,030 133,267 130,305 64,905 58,480 94,985 72,184 ...
2015-08 12,276 73,666 69,772 156,546 96,829 122,893 77,525 120,597 42,213 81,603 ...
2015-09 29,402 45,522 76,968 146,840 171,674 109,429 86,091 98,514 83,465 61,289 ...
2015-10 14,000 18,824 64,809 86,112 33,952 94,798 41,473 114,279 95,470 95,322 ...
2015-11 24,866 74,507 52,944 109,360 98,136 160,381 46,511 206,774 128,625 ...
2015-12 14,000 34,498 85,505 113,846 118,239 121,277 179,033 131,330 130,357 89,312 ...
2016-01 3,214 16,521 48,605 63,442 64,330 156,536 70,536 90,186 88,647 87,278 ...
2016-02 13,892 20,722 112,166 154,319 93,667 117,578 114,468 77,207 79,020 85,925 ...
2016-03 18,354 48,495 70,675 115,886 83,612 45,276 123,736 114,339 79,022 96,179 ...
2016-04 14,781 35,741 51,818 124,195 76,220 168,821 117,505 82,978 99,148 112,664 ...
2016-05 32,921 80,574 128,677 98,491 138,236 110,125 110,887 150,281 74,849 ...
2016-06 17,959 9,031 35,721 163,644 172,139 157,653 154,168 120,193 100,214 62,572 ...
2016-07 36,454 24,783 105,234 116,616 64,114 118,426 107,283 101,315 136,164 95,888 ...
2016-08 45,280 14,224 93,541 147,758 88,370 195,297 117,494 57,656 132,907 100,460 ...
2016-09 13,544 56,468 84,514 106,896 127,088 89,314 132,691 153,691 62,591 133,208 ...
2016-10 1,766 61,196 80,437 123,346 82,947 158,137 168,589 110,019 86,504 162,549 ...
2016-11 55,837 111,442 110,741 111,772 44,847 164,147 180,126 90,889 71,973 ...
2016-12 52,806 55,386 116,175 107,611 109,759 74,635 145,938 100,703 114,897 ...
2017-01 8,338 26,000 121,221 104,138 113,300 86,086 139,369 127,013 66,155 111,713 ...
2017-02 50,237 101,651 103,479 94,798 94,285 160,239 141,802 131,585 94,257 ...
2017-03 4,476 20,775 74,392 72,250 143,323 56,307 210,407 117,054 101,546 93,077 ...
2017-04 30,751 126,808 154,839 157,090 72,106 102,587 91,454 142,689 ...
2017-05 3,022 36,811 54,334 80,437 81,315 160,838 180,652 88,441 ...
2017-06 31,858 37,911 81,229 121,619 85,185 110,103 ...
2017-07 28,827 30,987 126,230 72,826 80,075 119,390 ...
2017-08 14,000 105,952 81,991 174,870 ...
2017-09 18,589 33,317 86,057 152,144 ...
2017-10 35,037 104,444 ...
2017-11 4,088 10,599 ...
2017-12 10,748 ...

Note that the lowest (most-detailed) grain supported is the monthly grain, so the triangle above is aggregated to the OMDM level.

prism.origin_grain
'M'
prism.development_grain
'M'

If we want to include more columns or indices we can certainly do so. Note that as we do this, we move into the 4D arena changing the display of the overall object.

prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    development="PaymentDate",
    columns=["Paid", "Incurred"],
    cumulative=False,
)
prism
Triangle Summary
Valuation: 2017-12
Grain: OMDM
Shape: (1, 2, 120, 120)
Index: [Total]
Columns: [Paid, Incurred]

Pandas has wonderful datetime inference functionality that the Triangle heavily uses to infer origin and development granularity. Even still, there are occassions where date format inferences can fail. It is often better to explicitly tell the triangle the date format, and is usually good pratice to explicitly state the date format instead.

prism_df.head()
ClaimNo AccidentDate ReportDate Line Type ClaimLiability Limit Deductible TotalPayment PaymentDate CloseDate Status reportedCount closedPaidCount closedUnPaidCount openCount Paid Outstanding Incurred
0 1 2008-01-22 4/19/2010 Home Dwelling False 300000.0 20000 0.00000 2010-10-08 10/8/2010 CLOSED 1 0 1 0 0.00000 0 0.00000
1 2 2008-01-02 4/20/2010 Home Dwelling False 200000.0 20000 0.00000 2010-11-30 11/30/2010 CLOSED 1 0 1 0 0.00000 0 0.00000
2 3 2008-01-01 9/23/2009 Home Dwelling True 200000.0 20000 115744.77370 2010-02-17 2/17/2010 CLOSED 1 1 0 0 115744.77370 0 115744.77370
3 4 2008-01-02 7/25/2009 Home Dwelling True 200000.0 20000 63678.87713 2009-11-18 11/18/2009 CLOSED 1 1 0 0 63678.87713 0 63678.87713
4 5 2008-01-16 12/7/2009 Home Dwelling True 200000.0 20000 112175.55590 2010-04-30 4/30/2010 CLOSED 1 1 0 0 112175.55590 0 112175.55590
prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    origin_format="%Y-%m-%d",
    development="PaymentDate",
    development_format="%Y-%m-%d",
    columns=["Paid", "Incurred"],
    cumulative=False,
)
prism
Triangle Summary
Valuation: 2017-12
Grain: OMDM
Shape: (1, 2, 120, 120)
Index: [Total]
Columns: [Paid, Incurred]

Up until now, we’ve been playing with symmetric triangles (i.e. orgin and development periods have the same grain). However, nothing precludes us from having a different grain. Often times in practice the development axis is more granular than the origin axis. All the functionality available to symmetric triangles works equally well for asymmetric triangles.

prism_df["AccYr"] = prism_df["AccidentDate"].str[:4]

prism = cl.Triangle(
    data=prism_df,
    origin="AccYr",
    origin_format="%Y",
    development="PaymentDate",
    development_format="%Y-%m-%d",
    columns=["Paid", "Incurred"],
    cumulative=False,
)
prism["Paid"]
1 2 3 4 5 6 7 8 9 10 ... 111 112 113 114 115 116 117 118 119 120
2008 75,664 90,814 194,956 300,085 347,487 400,011 356,795 598,532 ... 7,000
2009 24,327 80,376 136,857 175,737 297,690 358,624 325,462 529,832 540,217 ...
2010 22,192 84,802 148,299 200,733 208,237 456,692 519,691 604,946 484,188 ...
2011 24,934 60,404 107,198 233,346 268,904 401,595 530,490 501,836 513,133 ...
2012 18,794 98,320 165,689 296,865 335,643 320,954 529,733 592,985 686,689 ...
2013 70,959 99,628 238,991 180,138 302,213 477,224 585,963 648,734 637,203 ...
2014 18,194 32,772 124,801 276,336 344,635 425,875 449,242 541,203 587,789 819,083 ...
2015 31,221 86,418 242,766 276,598 418,870 575,634 620,098 858,771 787,203 ...
2016 3,214 30,412 87,680 238,884 325,065 468,785 521,981 560,611 853,548 976,906 ...
2017 8,338 26,000 175,935 226,565 324,943 416,752 646,836 649,985 811,832 957,490 ...

While exposure triangles make sense for auditable lines such as workers’ compensation lines, most other lines of business’ exposures can be expressed as a 1D array (along origin period) as exposures do not develop over time. chainladder arithmetic requires that operations happen between a triangle and either an int, float, or another Triangle. To create a 1D exposure array, simply omit the development argument at initialization.

The prism data does not consist of exposure data, but we can contrive one. Let’s assume that the premium is thrice the incurred amount.

prism_df["Premium"] = 3 * prism_df["Incurred"]

prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    origin_format="%Y-%m-%d",
    columns="Premium",
    cumulative=False,
)

prism
2017-12
2008-01 41,872,294
2008-02 43,542,684
2008-03 35,920,802
2008-04 30,521,592
2008-05 46,807,611
2008-06 35,926,236
2008-07 33,305,612
2008-08 35,667,952
2008-09 36,773,174
2008-10 36,858,772
2008-11 41,939,041
2008-12 34,716,883
2009-01 40,435,087
2009-02 51,803,670
2009-03 38,273,957
2009-04 44,045,041
2009-05 38,573,516
2009-06 34,674,998
2009-07 39,372,633
2009-08 41,936,689
2009-09 39,120,076
2009-10 33,485,416
2009-11 35,884,763
2009-12 35,640,359
2010-01 39,187,508
2010-02 36,408,113
2010-03 46,100,329
2010-04 39,311,907
2010-05 38,617,436
2010-06 38,204,654
... ...
2015-07 18,075,734
2015-08 12,204,463
2015-09 11,209,737
2015-10 7,647,748
2015-11 6,591,418
2015-12 7,948,603
2016-01 5,502,819
2016-02 5,677,216
2016-03 4,541,502
2016-04 4,938,965
2016-05 4,522,476
2016-06 4,664,779
2016-07 5,134,766
2016-08 4,554,252
2016-09 4,406,520
2016-10 4,278,755
2016-11 3,955,199
2016-12 3,586,924
2017-01 3,446,326
2017-02 3,095,700
2017-03 2,680,826
2017-04 2,634,972
2017-05 2,057,550
2017-06 1,403,713
2017-07 1,375,008
2017-08 1,130,442
2017-09 870,320
2017-10 418,445
2017-11 44,062
2017-12 32,244

Let’s seperate our data by segments using index:

prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    origin_format="%Y-%m-%d",
    development="PaymentDate",
    development_format="%Y-%m-%d",
    columns=["Paid", "Incurred"],
    index="Line",
    cumulative=False,
)
prism
Triangle Summary
Valuation: 2017-12
Grain: OMDM
Shape: (2, 2, 120, 120)
Index: [Line]
Columns: [Paid, Incurred]

We can futher index by coverages, or sublines:

prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    origin_format="%Y-%m-%d",
    development="PaymentDate",
    development_format="%Y-%m-%d",
    columns=["Paid", "Incurred"],
    index=["Line", "Type"],
    cumulative=False,
)
prism
Triangle Summary
Valuation: 2017-12
Grain: OMDM
Shape: (2, 2, 120, 120)
Index: [Line, Type]
Columns: [Paid, Incurred]

Triangle Methods not Available in Pandas

Up until now, we’ve kept pretty close to the pandas API for triangle manipulation. However, there are data transformations commonly applied to triangles that don’t have a nice pandas analogy.

For example, we often want to convert a triangle from an incremental view into a cumulative view and vice versa. This can be accomplished with the incr_to_cum and cum_to_incr methods.

prism["Paid"].sum()
1 2 3 4 5 6 7 8 9 10 ... 111 112 113 114 115 116 117 118 119 120
2008-01 46,915 19,899 57,216 89,783 18,302 95,054 10,332 91,772 ...
2008-02 28,749 22,109 79,033 63,455 59,993 64,683 68,502 33,695 71,670 ...
2008-03 48,806 27,949 90,413 54,557 83,507 12,591 72,035 70,353 35,223 ...
2008-04 30,758 17,763 70,872 30,233 39,494 66,729 122,100 20,998 38,218 ...
2008-05 38,672 86,974 20,483 58,400 112,015 31,354 22,457 53,778 93,303 ...
2008-06 56,789 73,351 97,840 64,816 61,083 41,685 87,172 61,418 63,097 ...
2008-07 27,867 45,804 61,495 73,279 71,591 111,807 80,249 40,632 32,434 ...
2008-08 4,832 23,831 52,511 62,649 45,955 133,133 81,733 39,709 112,115 21,367 ...
2008-09 43,464 86,157 52,664 89,928 88,738 55,617 26,599 69,522 46,788 ...
2008-10 12,488 21,939 53,388 52,415 92,086 45,781 72,242 83,359 47,594 ...
2008-11 45,473 9,416 78,967 84,826 39,549 41,011 74,000 33,257 35,802 ...
2008-12 3,640 9,905 54,792 87,057 70,686 54,775 31,412 44,057 59,448 31,643 ...
2009-01 14,021 45,350 51,208 35,520 81,655 54,275 41,538 74,129 47,962 ...
2009-02 10,305 35,026 37,289 58,773 67,579 57,589 115,083 57,142 65,315 55,019 ...
2009-03 48,360 40,502 74,342 64,538 49,778 85,591 82,582 56,449 88,307 ...
2009-04 40,943 47,517 64,024 27,573 62,634 54,051 63,305 61,339 70,406 ...
2009-05 16,944 74,478 63,856 59,266 79,707 63,857 59,326 88,817 50,656 ...
2009-06 9,655 43,720 12,805 79,404 46,219 46,868 36,234 35,654 24,676 26,641 ...
2009-07 14,828 48,605 90,527 54,508 57,881 64,905 16,717 46,880 89,721 ...
2009-08 42,061 32,247 64,174 63,443 65,428 95,432 91,809 131,904 67,284 ...
2009-09 21,000 27,607 34,679 37,523 56,305 42,851 72,404 54,366 67,543 71,032 ...
2009-10 14,000 62,040 45,134 97,016 54,356 20,848 63,521 78,494 67,441 22,107 ...
2009-11 22,027 64,189 20,836 33,303 50,594 79,852 54,870 111,592 70,473 ...
2009-12 38,594 17,902 100,470 43,866 102,051 56,901 33,876 40,416 54,734 ...
2010-01 15,192 53,338 57,394 56,985 62,175 84,624 84,959 102,734 26,495 ...
2010-02 7,000 31,464 32,942 38,108 28,707 100,091 97,795 98,314 53,259 96,477 ...
2010-03 36,937 64,551 61,774 39,580 81,253 35,751 87,788 41,076 117,835 ...
2010-04 21,026 32,422 46,661 103,442 89,860 96,976 51,116 89,352 50,595 13,981 ...
2010-05 8,668 8,920 116,376 61,586 148,647 14,288 46,840 51,205 82,714 4,240 ...
2010-06 12,578 92,156 44,495 112,868 79,732 73,743 61,814 73,713 23,724 ...
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2015-07 5,872 61,431 113,522 96,030 133,267 130,305 64,905 58,480 94,985 72,184 ...
2015-08 12,276 73,666 69,772 156,546 96,829 122,893 77,525 120,597 42,213 81,603 ...
2015-09 29,402 45,522 76,968 146,840 171,674 109,429 86,091 98,514 83,465 61,289 ...
2015-10 14,000 18,824 64,809 86,112 33,952 94,798 41,473 114,279 95,470 95,322 ...
2015-11 24,866 74,507 52,944 109,360 98,136 160,381 46,511 206,774 128,625 ...
2015-12 14,000 34,498 85,505 113,846 118,239 121,277 179,033 131,330 130,357 89,312 ...
2016-01 3,214 16,521 48,605 63,442 64,330 156,536 70,536 90,186 88,647 87,278 ...
2016-02 13,892 20,722 112,166 154,319 93,667 117,578 114,468 77,207 79,020 85,925 ...
2016-03 18,354 48,495 70,675 115,886 83,612 45,276 123,736 114,339 79,022 96,179 ...
2016-04 14,781 35,741 51,818 124,195 76,220 168,821 117,505 82,978 99,148 112,664 ...
2016-05 32,921 80,574 128,677 98,491 138,236 110,125 110,887 150,281 74,849 ...
2016-06 17,959 9,031 35,721 163,644 172,139 157,653 154,168 120,193 100,214 62,572 ...
2016-07 36,454 24,783 105,234 116,616 64,114 118,426 107,283 101,315 136,164 95,888 ...
2016-08 45,280 14,224 93,541 147,758 88,370 195,297 117,494 57,656 132,907 100,460 ...
2016-09 13,544 56,468 84,514 106,896 127,088 89,314 132,691 153,691 62,591 133,208 ...
2016-10 1,766 61,196 80,437 123,346 82,947 158,137 168,589 110,019 86,504 162,549 ...
2016-11 55,837 111,442 110,741 111,772 44,847 164,147 180,126 90,889 71,973 ...
2016-12 52,806 55,386 116,175 107,611 109,759 74,635 145,938 100,703 114,897 ...
2017-01 8,338 26,000 121,221 104,138 113,300 86,086 139,369 127,013 66,155 111,713 ...
2017-02 50,237 101,651 103,479 94,798 94,285 160,239 141,802 131,585 94,257 ...
2017-03 4,476 20,775 74,392 72,250 143,323 56,307 210,407 117,054 101,546 93,077 ...
2017-04 30,751 126,808 154,839 157,090 72,106 102,587 91,454 142,689 ...
2017-05 3,022 36,811 54,334 80,437 81,315 160,838 180,652 88,441 ...
2017-06 31,858 37,911 81,229 121,619 85,185 110,103 ...
2017-07 28,827 30,987 126,230 72,826 80,075 119,390 ...
2017-08 14,000 105,952 81,991 174,870 ...
2017-09 18,589 33,317 86,057 152,144 ...
2017-10 35,037 104,444 ...
2017-11 4,088 10,599 ...
2017-12 10,748 ...
prism_cum = prism.incr_to_cum()
prism_cum["Paid"].sum()
1 2 3 4 5 6 7 8 9 10 ... 111 112 113 114 115 116 117 118 119 120
2008-01 46,915 66,814 124,030 213,813 232,115 327,169 337,502 429,274 ... 13,957,431 13,957,431 13,957,431 13,957,431 13,957,431 13,957,431 13,957,431 13,957,431 13,957,431 13,957,431
2008-02 28,749 50,859 129,891 193,346 253,339 318,022 386,523 420,218 491,888 ... 14,514,228 14,514,228 14,514,228 14,514,228 14,514,228 14,514,228 14,514,228 14,514,228 14,514,228
2008-03 48,806 76,755 167,168 221,724 305,232 317,823 389,858 460,211 495,434 ... 11,973,601 11,973,601 11,973,601 11,973,601 11,973,601 11,973,601 11,973,601 11,973,601
2008-04 30,758 48,521 119,393 149,626 189,120 255,849 377,949 398,947 437,165 ... 10,173,864 10,173,864 10,173,864 10,173,864 10,173,864 10,173,864 10,173,864
2008-05 38,672 125,646 146,129 204,529 316,543 347,897 370,354 424,131 517,435 ... 15,602,537 15,602,537 15,602,537 15,602,537 15,602,537 15,602,537
2008-06 56,789 130,140 227,980 292,796 353,879 395,565 482,736 544,154 607,251 ... 11,975,412 11,975,412 11,975,412 11,975,412 11,975,412
2008-07 27,867 73,671 135,167 208,446 280,037 391,844 472,093 512,725 545,159 ... 11,101,871 11,101,871 11,101,871 11,101,871
2008-08 4,832 28,663 81,174 143,824 189,779 322,912 404,645 444,354 556,469 577,837 ... 11,889,317 11,889,317 11,889,317
2008-09 43,464 129,621 182,285 272,213 360,952 416,569 443,168 512,690 559,478 ... 12,257,725 12,257,725
2008-10 12,488 34,427 87,815 140,229 232,315 278,096 350,338 433,697 481,291 ... 12,286,257
2008-11 45,473 54,888 133,855 218,682 258,231 299,241 373,241 406,498 442,300 ...
2008-12 3,640 13,544 68,336 155,393 226,080 280,855 312,267 356,324 415,772 447,415 ...
2009-01 14,021 59,371 110,579 146,099 227,754 282,028 323,566 397,696 445,657 ...
2009-02 10,305 45,331 82,621 141,394 208,972 266,561 381,645 438,786 504,101 559,120 ...
2009-03 48,360 88,861 163,203 227,741 277,519 363,110 445,692 502,141 590,448 ...
2009-04 40,943 88,459 152,483 180,056 242,690 296,742 360,047 421,386 491,791 ...
2009-05 16,944 91,422 155,278 214,543 294,250 358,107 417,433 506,250 556,906 ...
2009-06 9,655 53,374 66,180 145,584 191,803 238,671 274,905 310,559 335,235 361,875 ...
2009-07 14,828 63,434 153,961 208,469 266,350 331,255 347,971 394,851 484,572 ...
2009-08 42,061 74,308 138,483 201,926 267,354 362,786 454,595 586,499 653,783 ...
2009-09 21,000 48,607 83,286 120,808 177,113 219,965 292,369 346,735 414,277 485,309 ...
2009-10 14,000 76,040 121,174 218,190 272,546 293,395 356,915 435,409 502,850 524,957 ...
2009-11 22,027 86,215 107,052 140,355 190,949 270,801 325,670 437,262 507,735 ...
2009-12 38,594 56,496 156,966 200,832 302,883 359,784 393,660 434,076 488,811 ...
2010-01 15,192 68,531 125,925 182,910 245,085 329,709 414,668 517,402 543,897 ...
2010-02 7,000 38,464 71,406 109,514 138,221 238,312 336,107 434,422 487,681 584,158 ...
2010-03 36,937 101,487 163,262 202,842 284,095 319,846 407,635 448,711 566,546 ...
2010-04 21,026 53,448 100,108 203,550 293,410 390,386 441,502 530,854 581,449 595,430 ...
2010-05 8,668 17,587 133,963 195,549 344,196 358,484 405,324 456,529 539,244 543,483 ...
2010-06 12,578 104,735 149,230 262,098 341,830 415,573 477,388 551,101 574,825 ...
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2015-07 5,872 67,304 180,826 276,856 410,123 540,428 605,334 663,814 758,798 830,982 ...
2015-08 12,276 85,942 155,713 312,260 409,088 531,981 609,506 730,103 772,317 853,920 ...
2015-09 29,402 74,924 151,892 298,732 470,405 579,835 665,925 764,440 847,904 909,194 ...
2015-10 14,000 32,824 97,632 183,745 217,697 312,495 353,968 468,248 563,718 659,040 ...
2015-11 24,866 99,373 152,317 261,677 359,813 520,193 566,705 773,479 902,104 ...
2015-12 14,000 48,498 134,004 247,850 366,089 487,366 666,399 797,729 928,086 1,017,398 ...
2016-01 3,214 19,734 68,339 131,782 196,112 352,647 423,183 513,369 602,016 689,294 ...
2016-02 13,892 34,613 146,780 301,098 394,766 512,344 626,812 704,019 783,039 868,964 ...
2016-03 18,354 66,848 137,523 253,408 337,021 382,297 506,033 620,372 699,394 795,573 ...
2016-04 14,781 50,522 102,340 226,534 302,754 471,575 589,080 672,058 771,207 883,870 ...
2016-05 32,921 113,494 242,172 340,663 478,898 589,024 699,911 850,192 925,041 ...
2016-06 17,959 26,990 62,711 226,354 398,493 556,146 710,314 830,507 930,721 993,293 ...
2016-07 36,454 61,237 166,471 283,087 347,202 465,628 572,910 674,225 810,389 906,277 ...
2016-08 45,280 59,505 153,045 300,803 389,173 584,470 701,964 759,620 892,527 992,987 ...
2016-09 13,544 70,012 154,527 261,422 388,510 477,824 610,516 764,207 826,799 960,007 ...
2016-10 1,766 62,962 143,398 266,745 349,691 507,828 676,418 786,436 872,940 1,035,489 ...
2016-11 55,837 167,279 278,020 389,792 434,640 598,787 778,913 869,802 941,776 ...
2016-12 52,806 108,192 224,367 331,978 441,737 516,372 662,311 763,014 877,911 ...
2017-01 8,338 34,338 155,559 259,698 372,997 459,083 598,452 725,466 791,620 903,333 ...
2017-02 50,237 151,889 255,367 350,165 444,450 604,689 746,491 878,076 972,333 ...
2017-03 4,476 25,251 99,644 171,894 315,217 371,524 581,932 698,986 800,532 893,609 ...
2017-04 30,751 157,559 312,397 469,488 541,593 644,181 735,635 878,324 ...
2017-05 3,022 39,834 94,168 174,605 255,919 416,757 597,409 685,850 ...
2017-06 31,858 69,769 150,997 272,616 357,801 467,904 ...
2017-07 28,827 59,815 186,044 258,870 338,946 458,336 ...
2017-08 14,000 119,952 201,944 376,814 ...
2017-09 18,589 51,906 137,963 290,107 ...
2017-10 35,037 139,482 ...
2017-11 4,088 14,687 ...
2017-12 10,748 ...
prism_incr = prism_cum.cum_to_incr()
prism_incr["Paid"].sum()
1 2 3 4 5 6 7 8 9 10 ... 111 112 113 114 115 116 117 118 119 120
2008-01 46,915 19,899 57,216 89,783 18,302 95,054 10,332 91,772 ...
2008-02 28,749 22,109 79,033 63,455 59,993 64,683 68,502 33,695 71,670 ...
2008-03 48,806 27,949 90,413 54,557 83,507 12,591 72,035 70,353 35,223 ...
2008-04 30,758 17,763 70,872 30,233 39,494 66,729 122,100 20,998 38,218 ...
2008-05 38,672 86,974 20,483 58,400 112,015 31,354 22,457 53,778 93,303 ...
2008-06 56,789 73,351 97,840 64,816 61,083 41,685 87,172 61,418 63,097 ...
2008-07 27,867 45,804 61,495 73,279 71,591 111,807 80,249 40,632 32,434 ...
2008-08 4,832 23,831 52,511 62,649 45,955 133,133 81,733 39,709 112,115 21,367 ...
2008-09 43,464 86,157 52,664 89,928 88,738 55,617 26,599 69,522 46,788 ...
2008-10 12,488 21,939 53,388 52,415 92,086 45,781 72,242 83,359 47,594 ...
2008-11 45,473 9,416 78,967 84,826 39,549 41,011 74,000 33,257 35,802 ...
2008-12 3,640 9,905 54,792 87,057 70,686 54,775 31,412 44,057 59,448 31,643 ...
2009-01 14,021 45,350 51,208 35,520 81,655 54,275 41,538 74,129 47,962 ...
2009-02 10,305 35,026 37,289 58,773 67,579 57,589 115,083 57,142 65,315 55,019 ...
2009-03 48,360 40,502 74,342 64,538 49,778 85,591 82,582 56,449 88,307 ...
2009-04 40,943 47,517 64,024 27,573 62,634 54,051 63,305 61,339 70,406 ...
2009-05 16,944 74,478 63,856 59,266 79,707 63,857 59,326 88,817 50,656 ...
2009-06 9,655 43,720 12,805 79,404 46,219 46,868 36,234 35,654 24,676 26,641 ...
2009-07 14,828 48,605 90,527 54,508 57,881 64,905 16,717 46,880 89,721 ...
2009-08 42,061 32,247 64,174 63,443 65,428 95,432 91,809 131,904 67,284 ...
2009-09 21,000 27,607 34,679 37,523 56,305 42,851 72,404 54,366 67,543 71,032 ...
2009-10 14,000 62,040 45,134 97,016 54,356 20,848 63,521 78,494 67,441 22,107 ...
2009-11 22,027 64,189 20,836 33,303 50,594 79,852 54,870 111,592 70,473 ...
2009-12 38,594 17,902 100,470 43,866 102,051 56,901 33,876 40,416 54,734 ...
2010-01 15,192 53,338 57,394 56,985 62,175 84,624 84,959 102,734 26,495 ...
2010-02 7,000 31,464 32,942 38,108 28,707 100,091 97,795 98,314 53,259 96,477 ...
2010-03 36,937 64,551 61,774 39,580 81,253 35,751 87,788 41,076 117,835 ...
2010-04 21,026 32,422 46,661 103,442 89,860 96,976 51,116 89,352 50,595 13,981 ...
2010-05 8,668 8,920 116,376 61,586 148,647 14,288 46,840 51,205 82,714 4,240 ...
2010-06 12,578 92,156 44,495 112,868 79,732 73,743 61,814 73,713 23,724 ...
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2015-07 5,872 61,431 113,522 96,030 133,267 130,305 64,905 58,480 94,985 72,184 ...
2015-08 12,276 73,666 69,772 156,546 96,829 122,893 77,525 120,597 42,213 81,603 ...
2015-09 29,402 45,522 76,968 146,840 171,674 109,429 86,091 98,514 83,465 61,289 ...
2015-10 14,000 18,824 64,809 86,112 33,952 94,798 41,473 114,279 95,470 95,322 ...
2015-11 24,866 74,507 52,944 109,360 98,136 160,381 46,511 206,774 128,625 ...
2015-12 14,000 34,498 85,505 113,846 118,239 121,277 179,033 131,330 130,357 89,312 ...
2016-01 3,214 16,521 48,605 63,442 64,330 156,536 70,536 90,186 88,647 87,278 ...
2016-02 13,892 20,722 112,166 154,319 93,667 117,578 114,468 77,207 79,020 85,925 ...
2016-03 18,354 48,495 70,675 115,886 83,612 45,276 123,736 114,339 79,022 96,179 ...
2016-04 14,781 35,741 51,818 124,195 76,220 168,821 117,505 82,978 99,148 112,664 ...
2016-05 32,921 80,574 128,677 98,491 138,236 110,125 110,887 150,281 74,849 ...
2016-06 17,959 9,031 35,721 163,644 172,139 157,653 154,168 120,193 100,214 62,572 ...
2016-07 36,454 24,783 105,234 116,616 64,114 118,426 107,283 101,315 136,164 95,888 ...
2016-08 45,280 14,224 93,541 147,758 88,370 195,297 117,494 57,656 132,907 100,460 ...
2016-09 13,544 56,468 84,514 106,896 127,088 89,314 132,691 153,691 62,591 133,208 ...
2016-10 1,766 61,196 80,437 123,346 82,947 158,137 168,589 110,019 86,504 162,549 ...
2016-11 55,837 111,442 110,741 111,772 44,847 164,147 180,126 90,889 71,973 ...
2016-12 52,806 55,386 116,175 107,611 109,759 74,635 145,938 100,703 114,897 ...
2017-01 8,338 26,000 121,221 104,138 113,300 86,086 139,369 127,013 66,155 111,713 ...
2017-02 50,237 101,651 103,479 94,798 94,285 160,239 141,802 131,585 94,257 ...
2017-03 4,476 20,775 74,392 72,250 143,323 56,307 210,407 117,054 101,546 93,077 ...
2017-04 30,751 126,808 154,839 157,090 72,106 102,587 91,454 142,689 ...
2017-05 3,022 36,811 54,334 80,437 81,315 160,838 180,652 88,441 ...
2017-06 31,858 37,911 81,229 121,619 85,185 110,103 ...
2017-07 28,827 30,987 126,230 72,826 80,075 119,390 ...
2017-08 14,000 105,952 81,991 174,870 ...
2017-09 18,589 33,317 86,057 152,144 ...
2017-10 35,037 104,444 ...
2017-11 4,088 10,599 ...
2017-12 10,748 ...

By default (and in concert with the pandas philosophy), the methods associated with the Triangle class strive for immutability. This means that the incr_to_cum and cum_to_incr methods will return new a new triangle object that must be assigned, or it is thrown away. Many of the chainladder.Triangle methods have an inplace argument, or alternatively you can just use variable reassignment to store the transformed triangle object.

# This works
prism.incr_to_cum(inplace=True)
# So does this
prism = prism.incr_to_cum()

When dealing with triangles that have an origin axis, development axis, or both at a monthly or quarterly grain, the triangle can be summarized to a higher grain using the grain method.

The grain to which you want your triangle converted, specified as “OxDy” where “x” and “y” can take on values of “M”, “Q”, or “Y”. For example:

  • grain(OYDY) for Origin Year x Development Year.

  • grain(OQDM) for Origin Quarter x Development Month.

prism_OYDY = prism.grain("OYDY")
prism_OYDY["Paid"].sum()
12 24 36 48 60 72 84 96 108 120
2008 3,404,254 11,191,085 74,613,012 150,342,751 150,982,873 151,152,726 151,228,872 151,264,806 151,277,217 151,284,217
2009 3,609,385 11,002,927 80,726,352 156,970,789 157,599,460 157,697,094 157,736,386 157,743,386 157,748,735
2010 4,067,321 12,396,777 74,210,043 161,049,586 161,641,453 161,787,135 161,859,565 161,870,156
2011 4,125,232 13,183,144 81,239,771 161,412,913 162,187,629 162,417,460 162,490,681
2012 4,584,036 14,001,178 77,794,522 152,118,384 152,588,090 152,819,473
2013 4,889,623 14,607,742 84,418,503 161,110,312 161,673,036
2014 5,546,158 16,408,126 77,256,792 154,969,931
2015 5,909,029 17,427,611 80,914,580
2016 6,080,962 18,588,057
2017 6,396,536

Depending on the type of analysis being done, it may be more convenient to look at a triangle with its development axis expressed as a valuation rather than an age. This is also what the Schedule Ps look like. To do this, Triangle has two methods for toggling between a development triangle and a valuation triangle. The methods are dev_to_val and its inverse val_to_dev.

prism_OYDY_val = prism_OYDY.dev_to_val()
prism_OYDY_val["Paid"].sum()
2008 2009 2010 2011 2012 2013 2014 2015 2016 2017
2008 3,404,254 11,191,085 74,613,012 150,342,751 150,982,873 151,152,726 151,228,872 151,264,806 151,277,217 151,284,217
2009 3,609,385 11,002,927 80,726,352 156,970,789 157,599,460 157,697,094 157,736,386 157,743,386 157,748,735
2010 4,067,321 12,396,777 74,210,043 161,049,586 161,641,453 161,787,135 161,859,565 161,870,156
2011 4,125,232 13,183,144 81,239,771 161,412,913 162,187,629 162,417,460 162,490,681
2012 4,584,036 14,001,178 77,794,522 152,118,384 152,588,090 152,819,473
2013 4,889,623 14,607,742 84,418,503 161,110,312 161,673,036
2014 5,546,158 16,408,126 77,256,792 154,969,931
2015 5,909,029 17,427,611 80,914,580
2016 6,080,962 18,588,057
2017 6,396,536

When working with real-world data, the triangles can have holes, such as missing evluation(s), or no losses in certain origin(s). In these cases, it doesn’t make sense to include empty accident periods or development ages. For example, in the prism dataset, the “Home” line has its latest accidents through 2016, and have no payments in development age ‘12’. Sometimes, dropping the non-applicable fields is usefule with the dropna() method.

prism_OYDY.loc["Home"]["Paid"]
12 24 36 48 60 72 84 96 108 120
2008 1,129,305 61,658,874 136,520,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554
2009 187,292 67,134,599 142,267,946 142,547,946 142,547,946 142,547,946 142,547,946 142,547,946
2010 620,603 59,082,456 144,757,200 144,757,200 144,757,200 144,757,200 144,757,200
2011 503,296 65,048,051 143,971,618 144,251,618 144,251,618 144,251,618
2012 599,277 60,536,642 133,412,416 133,412,416 133,412,416
2013 536,303 66,443,157 141,645,782 141,645,782
2014 965,973 57,394,263 133,542,848
2015 371,015 59,047,107
2016 640,179
2017

Let’s see what happens if we have no data for 2011.

prism_df_2011 = prism_df.copy()
prism_df_2011.loc[
    (prism_df_2011["AccidentDate"] >= "2011-01-01")
    & (prism_df_2011["AccidentDate"] < "2012-01-01"),
    "Paid",
] = None  # Let's assume we hav no payments for losses occurred in 2011
prism_2011 = cl.Triangle(
    data=prism_df_2011,
    origin="AccidentDate",
    origin_format="%Y-%m-%d",
    development="PaymentDate",
    development_format="%Y-%m-%d",
    columns=["Paid", "Incurred"],
    index=["Line", "Type"],
    cumulative=False,
)
prism_2011.incr_to_cum(inplace=True)
prism_2011_OYDY = prism_2011.grain("OYDY")
prism_2011_OYDY.loc["Home"]["Paid"]
12 24 36 48 60 72 84 96 108 120
2008 1,129,305 61,658,874 136,520,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554
2009 187,292 67,134,599 142,267,946 142,547,946 142,547,946 142,547,946 142,547,946 142,547,946
2010 620,603 59,082,456 144,757,200 144,757,200 144,757,200 144,757,200 144,757,200
2011
2012 599,277 60,536,642 133,412,416 133,412,416 133,412,416
2013 536,303 66,443,157 141,645,782 141,645,782
2014 965,973 57,394,263 133,542,848
2015 371,015 59,047,107
2016 640,179
2017
prism_df_2011 = prism_df.copy()
prism_df_2011.loc[
    (prism_df_2011["AccidentDate"] >= "2011-01-01")
    & (prism_df_2011["AccidentDate"] < "2012-01-01"),
    "Paid",
] = None  # Let's assume we hav no payments for losses occurred in 2011
prism_2011 = cl.Triangle(
    data=prism_df_2011,
    origin="AccidentDate",
    origin_format="%Y-%m-%d",
    development="PaymentDate",
    development_format="%Y-%m-%d",
    columns=["Paid", "Incurred"],
    index=["Line", "Type"],
    cumulative=False,
)
prism_2011.incr_to_cum(inplace=True)
prism_2011_OYDY = prism_2011.grain("OYDY")
prism_2011_OYDY.loc["Home"]["Paid"]
12 24 36 48 60 72 84 96 108 120
2008 1,129,305 61,658,874 136,520,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554
2009 187,292 67,134,599 142,267,946 142,547,946 142,547,946 142,547,946 142,547,946 142,547,946
2010 620,603 59,082,456 144,757,200 144,757,200 144,757,200 144,757,200 144,757,200
2011
2012 599,277 60,536,642 133,412,416 133,412,416 133,412,416
2013 536,303 66,443,157 141,645,782 141,645,782
2014 965,973 57,394,263 133,542,848
2015 371,015 59,047,107
2016 640,179
2017

Note that the dropna() method will retain empty periods if they are surrounded by non-empty periods with valid data.

prism_2011_OYDY_droppedna = prism_2011_OYDY.loc["Home"].dropna()
prism_2011_OYDY_droppedna.loc["Home"]["Paid"]
24 36 48 60 72 84 96 108 120
2008 1,129,305 61,658,874 136,520,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554
2009 187,292 67,134,599 142,267,946 142,547,946 142,547,946 142,547,946 142,547,946 142,547,946
2010 620,603 59,082,456 144,757,200 144,757,200 144,757,200 144,757,200 144,757,200
2011
2012 599,277 60,536,642 133,412,416 133,412,416 133,412,416
2013 536,303 66,443,157 141,645,782 141,645,782
2014 965,973 57,394,263 133,542,848
2015 371,015 59,047,107
2016 640,179

Commutative Properties of Triangle Methods

Where it makes sense, which is in most cases, the methods described above are commutative and can be applied in any order.

print("Commutative?", prism.sum().latest_diagonal == prism.latest_diagonal.sum())
print("Commutative?", prism.loc["Home"].link_ratio == prism.link_ratio.loc["Home"])
Commutative? True
Commutative? True

Triangle Imports and Exports

To the extent the Triangle can be expressed as a pandas.DataFrame, you can use any of the pandas IO to send the data in and out. Note that converting to pandas is a one-way ticket with no inverse functions.

Another useful function is to copy the triangle and put it in the clipboard so we can paste it elsewhere (such as Excel):

prism_OYDY.loc["Home", "Paid"]
12 24 36 48 60 72 84 96 108 120
2008 1,129,305 61,658,874 136,520,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554 136,800,554
2009 187,292 67,134,599 142,267,946 142,547,946 142,547,946 142,547,946 142,547,946 142,547,946
2010 620,603 59,082,456 144,757,200 144,757,200 144,757,200 144,757,200 144,757,200
2011 503,296 65,048,051 143,971,618 144,251,618 144,251,618 144,251,618
2012 599,277 60,536,642 133,412,416 133,412,416 133,412,416
2013 536,303 66,443,157 141,645,782 141,645,782
2014 965,973 57,394,263 133,542,848
2015 371,015 59,047,107
2016 640,179
2017
prism_OYDY.loc["Home", "Paid"].to_clipboard()

Try to paste it elsewhere.

Alternatively, if you want to store the triangle elsewhere but be able to reconstitute a triangle out of it later, then you can use:

  • Triangle.to_json and its inverse cl.read_json for json format

  • Triangle.to_pickle and its inverse cl.read_pickle for pickle format

These have the added benefit of working on multi-dimensional triangles that don’t fit into a DataFrame.

Exercises

prism = cl.Triangle(
    data=prism_df,
    origin="AccidentDate",
    development="PaymentDate",
    columns=["Paid", "Incurred"],
    index=["Line", "Type"],  # multiple indices
    cumulative=False,
).incr_to_cum()
prism
Triangle Summary
Valuation: 2017-12
Grain: OMDM
Shape: (2, 2, 120, 120)
Index: [Line, Type]
Columns: [Paid, Incurred]
  1. What is the case incurred activity for calendar periods in 2015Q2 (March, April, and May in 2015) by “Line”?

incr_by_line = prism.groupby("Line").sum().cum_to_incr()["Incurred"].dev_to_val()
incr_by_line[
    (incr_by_line.valuation >= "2015-04-01") & (incr_by_line.valuation < "2015-07-01")
].sum("origin").to_frame().astype(int)
/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.
  warnings.warn(warning)
development 2015-04 2015-05 2015-06
Line
Auto 1883976 1704338 1692254
Home 10367939 10813821 14537617
  1. For accident year 2015, what proportion of our Paid amounts come from each “Type” of claims?

prism_OYDY = prism.grain("OYDY")
prism_OYDY
Triangle Summary
Valuation: 2017-12
Grain: OYDY
Shape: (2, 2, 10, 10)
Index: [Line, Type]
Columns: [Paid, Incurred]
by_type = (
    prism_OYDY.latest_diagonal[prism_OYDY.origin == "2015"]["Paid"]
    .groupby("Type")
    .sum()
    .to_frame()
)
by_type / by_type.sum()
/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.
  warnings.warn(warning)
Type
Dwelling    0.729746
PD          0.270254
dtype: float64