reserving.Triangle

A loss triangle with four axes: index (segment), column (measure), origin

Usage

reserving.Triangle()

and development age, in chainladder-python’s order.

Segments are named by key columns such as "lob" and "state": keys gives their names and index one label per segment. A triangle without keys has one segment, "Total".

Build one from a long table with from_long or from_frame. Ages are whole months from the start of the origin period, so age 12 on a 2021 accident year is valued at December 2021. Cells that were not observed are nan in values; an observed zero stays zero.

Examples

>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     origin=[2020, 2020, 2021],
...     development=[12, 24, 12],
...     values={"paid": [100.0, 150.0, 110.0]},
... )
>>> tri.shape

(1, 1, 2, 2)

>>> tri.origins, tri.development, tri.valuation

([‘2020’, ‘2021’], [12, 24], datetime.date(2021, 12, 31))

>>> tri.values[0][0]

[[100.0, 150.0], [110.0, nan]]

Attributes

Name Description
columns Measure column names.
development Development ages in months, youngest first.
development_grain Development grain: "Y", "S", "Q" or "M".
index Segment labels: a str each with one key, a tuple of key values with
is_cumulative Whether the values are cumulative (otherwise incremental).
keys Names of the key columns, in key order; empty without keys.
origin_grain Origin grain: "Y", "S", "Q" or "M".
origins Origin periods, oldest first: "2021", "2021H1",
shape Axis lengths: (index, column, origin, development).
valuation Valuation date of the latest diagonal: the last day of its month,
values Values as nested lists indexed [index][column][origin][development],

columns

Measure column names.

columns: list[str]


development

Development ages in months, youngest first.

development: list[int]


development_grain

Development grain: "Y", "S", "Q" or "M".

development_grain: str


index

Segment labels: a str each with one key, a tuple of key values with

index: list[Any]

several, and ["Total"] without keys.


is_cumulative

Whether the values are cumulative (otherwise incremental).

is_cumulative: bool


keys

Names of the key columns, in key order; empty without keys.

keys: list[str]


origin_grain

Origin grain: "Y", "S", "Q" or "M".

origin_grain: str


origins

Origin periods, oldest first: "2021", "2021H1",

origins: list[str]

"2021Q3" or "2021-07" by grain.


shape

Axis lengths: (index, column, origin, development).

shape: tuple[int, int, int, int]


valuation

Valuation date of the latest diagonal: the last day of its month,

valuation: date

as a datetime.date.


values

Values as nested lists indexed [index][column][origin][development],

values: list[list[list[list[float]]]]

nan where unobserved. numpy.asarray gives the 4-D array.

Methods

Name Description
from_frame() Builds a triangle from a data frame in long format.
from_long() Builds a triangle from the columns of a long table, one row per
grain() The triangle at a coarser origin and/or development grain.
group_by() Sums the segments that share the values of keys, dropping the
latest_diagonal() The latest observed value of each origin, as nested lists indexed
link_ratios() Age-to-age link ratios of the cumulative values. Development
select() The segments whose key values match, and the measure columns named.
summary() One row per segment and measure: the key values, column,
to_cumulative() Cumulative values: running sums of the observed increments.
to_frame() The long table of to_long as a pandas DataFrame. Needs pandas.
to_incremental() Incremental values: each observed value minus the previous observed
to_long() The triangle as a long table: a dict of equal-length lists with one
to_string() The printout as text: the origin × development grid for a triangle
view() One segment and measure as an origin × development table.

from_frame()

Builds a triangle from a data frame in long format.

Usage

from_frame(
    data,
    origin,
    development,
    columns,
    keys=None,
    origin_grain="Y",
    development_grain="Y",
    cumulative=True,
    development_is_valuation=False
)

Columns are looked up with data[name], so a pandas or Polars DataFrame works, as does a dict of columns.

Parameters
data: DataFrame or dict
origin: str

Name of the origin column (dates or integer years).

development: str

Name of the development column (ages in months, or valuation dates when development_is_valuation is true).

columns: str or list of str

Names of the measure columns.

keys: str or list of str = None

Names of the key columns, such as ["lob", "state"]. By default every row is in one segment, "Total".

origin_grain: (Y, S, Q, M) = "Y"
development_grain: (Y, S, Q, M) = "Y"
cumulative: bool = True
development_is_valuation: bool = False
Returns
Triangle
Raises
ValueError
As for from_long.
Examples
>>> from prospicio.reserving import Triangle
>>> df = {
...     "lob": ["Auto", "Auto", "Auto", "Home"],
...     "year": [2020, 2020, 2021, 2020],
...     "age": [12, 24, 12, 12],
...     "paid": [100.0, 150.0, 110.0, 50.0],
... }
>>> tri = Triangle.from_frame(df, "year", "age", "paid", keys="lob")
>>> tri.keys, tri.index, tri.shape

([‘lob’], [‘Auto’, ‘Home’], (2, 1, 2, 2))


from_long()

Builds a triangle from the columns of a long table, one row per

Usage

from_long(
    origin,
    development,
    values,
    keys=None,
    origin_grain="Y",
    development_grain="Y",
    cumulative=True,
    development_is_valuation=False
)

(keys, origin, development).

Origins span every period from the earliest to the latest row and ages every development period from the youngest to the oldest. Rows with the same (keys, origin, age) are summed; nan values are missing. Incremental input treats a missing row as a period without movement, as chainladder-python does.

Parameters
origin: array - like

Any date in each row’s origin period (numpy datetime64, datetime.date, pandas Timestamp), or integer years.

development: array - like

Development age of each row in months (12, 24, …), or its valuation date when development_is_valuation is true.

values: dict of str to array-like, or array-like

Measure columns by name. A single array-like is one column named "values".

keys: dict of str to array-like = None

Key columns by name, such as {"lob": [...], "state": [...]}, in key order. Values are stored as strings (str() of each); None and nan are not allowed. Each distinct combination is a segment. By default every row is in one segment, "Total".

origin_grain: (Y, S, Q, M) = "Y"

Length of an origin period.

development_grain: (Y, S, Q, M) = "Y"

Spacing of development ages; must divide the origin grain.

cumulative: bool = True

Whether the values are cumulative (otherwise incremental).

development_is_valuation: bool = False
Whether development holds valuation dates instead of ages.
Returns
Triangle
Raises
ValueError
If columns differ in length, an age is not on the development grid, a value is infinite, the grains are incompatible, a key name is repeated or also a value column, or a key has missing values.
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     origin=[2020, 2020, 2021, 2020],
...     development=[12, 24, 12, 12],
...     values={"paid": [100.0, 150.0, 110.0, 50.0]},
...     keys={"lob": ["Auto", "Auto", "Auto", "Home"], "state": ["CA", "CA", "CA", "NY"]},
... )
>>> tri.keys, tri.index

([‘lob’, ‘state’], [(‘Auto’, ‘CA’), (‘Home’, ‘NY’)])

Valuation dates instead of ages:

>>> import datetime
>>> from prospicio.reserving import Triangle
>>> d = datetime.date
>>> tri = Triangle.from_long(
...     origin=[d(2021, 2, 1), d(2021, 2, 1), d(2021, 5, 1)],
...     development=[d(2021, 3, 31), d(2021, 6, 30), d(2021, 6, 30)],
...     values=[10.0, 25.0, 7.0],
...     origin_grain="Q",
...     development_grain="Q",
...     development_is_valuation=True,
... )
>>> tri.origins, tri.development

([‘2021Q1’, ‘2021Q2’], [3, 6])


grain()

The triangle at a coarser origin and/or development grain.

Usage

grain(origin_grain, development_grain=None)
Parameters
origin_grain: (Y, S, Q, M) = "Y"
development_grain: (Y, S, Q, M) = "Y"
By default origin_grain.
Returns
Triangle
Raises
ValueError
If a grain is finer than the current one, or the development grain does not divide the origin grain.
Examples
>>> from prospicio.reserving import Triangle
>>> q = Triangle.from_long(
...     [2020, 2020], [3, 6], [1.0, 2.0], origin_grain="Q", development_grain="Q"
... )
>>> y = q.grain("Y")
>>> y.origins, y.development_grain

([‘2020’], ‘Y’)


group_by()

Sums the segments that share the values of keys, dropping the

Usage

group_by(keys)

other keys.

Cumulative values are summed cell by cell, and a cell is observed if any segment in the group observes it. An incremental triangle is summed as cumulative values and returned incremental.

Parameters
keys: str or list of str
The keys to keep, in the order the result has them. [] sums every segment into one, labelled "Total".
Returns
Triangle
Raises
ValueError
If a key is unknown or named twice.
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     [2020, 2020, 2020],
...     [12, 12, 12],
...     {"paid": [1.0, 2.0, 3.0]},
...     keys={"lob": ["Auto", "Auto", "Home"], "state": ["CA", "NY", "NY"]},
... )
>>> by_lob = tri.group_by("lob")
>>> by_lob.index, by_lob.to_long()["paid"]

([‘Auto’, ‘Home’], [3.0, 3.0])

>>> tri.group_by([]).to_long()["paid"]

[6.0]


latest_diagonal()

The latest observed value of each origin, as nested lists indexed

Usage

latest_diagonal()

[index][column][origin], nan for an origin with no value.

Returns
list of list of list of float


select()

The segments whose key values match, and the measure columns named.

Usage

select(columns=None, **keys)

Each keyword names a key and gives one value or a list of values to keep; segments must match every keyword, and keep their order. Values are compared as strings, as keys are stored (str() of a value). A key named columns cannot be selected this way.

Parameters
columns: str or list of str = None

Measure columns to keep, in this order. By default every column.

**keys: value or list of values
For example lob="Auto" or state=["CA", "NY"]; any iterable that is not a string (a tuple, set, NumPy array or pandas Series) is a list of values.
Returns
Triangle
Raises
ValueError
If a key, value or column is unknown or given twice, a list of values is empty, or no segment matches.
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     [2020, 2020, 2020],
...     [12, 12, 12],
...     {"paid": [1.0, 2.0, 3.0], "incurred": [2.0, 3.0, 4.0]},
...     keys={"lob": ["Auto", "Auto", "Home"], "state": ["CA", "NY", "NY"]},
... )
>>> tri.select(state="NY").index

[(‘Auto’, ‘NY’), (‘Home’, ‘NY’)]

>>> tri.select(lob="Auto", state=["CA", "NY"], columns="paid").shape

(2, 1, 1, 1)


summary()

One row per segment and measure: the key values, column,

Usage

summary()

n_origins (origins with an observed value), first_origin and last_origin of those, valuation (the last day of the latest valuation with an observed value), latest (the sum over origins of the latest cumulative value, so for an incremental triangle the sum of every increment) and cumulative. An origin or valuation is missing (None, which pandas may show as NaN) when the segment has no observed value of the measure.

Returns
pandas.DataFrame or dict
A DataFrame with pandas installed, a dict of lists otherwise.
Raises
ValueError
If a key has the name of one of the summary’s columns.
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     [2020, 2020, 2021, 2020],
...     [12, 24, 12, 12],
...     {"paid": [100.0, 150.0, 110.0, 50.0]},
...     keys={"lob": ["Auto", "Auto", "Auto", "Home"]},
... )
>>> s = tri.summary()
>>> s["lob"].tolist(), s["n_origins"].tolist(), s["latest"].tolist()

([‘Auto’, ‘Home’], [2, 1], [260.0, 50.0])


to_cumulative()

Cumulative values: running sums of the observed increments.

Usage

to_cumulative()
Returns
Triangle

to_frame()

The long table of to_long as a pandas DataFrame. Needs pandas.

Usage

to_frame()
Returns
pandas.DataFrame

to_incremental()

Incremental values: each observed value minus the previous observed

Usage

to_incremental()

value in its row.

Returns
Triangle

to_long()

The triangle as a long table: a dict of equal-length lists with one

Usage

to_long()

entry per key column (by name), "origin" (start of the origin period, a datetime.date), "development" (age in months) and one per measure column, with a row per (segment, origin, age) that has an observed measure. It feeds back into from_frame (with keys=tri.keys) or pandas.DataFrame.

Returns
dict of str to list
Raises
ValueError
If a key or measure column is named origin or development.
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     [2020, 2020], [12, 12], {"paid": [1.0, 2.0]}, keys={"lob": ["Auto", "Home"]}
... )
>>> long = tri.to_long()
>>> list(long), long["lob"]

([‘lob’, ‘origin’, ‘development’, ‘paid’], [‘Auto’, ‘Home’])


to_string()

The printout as text: the origin × development grid for a triangle

Usage

to_string(max_rows=..., max_cols=...)

with one segment and one measure (as view), otherwise the summary table. Numbers are rounded for reading; view and summary give exact values.

Parameters
max_rows: int = 20

Rows shown before the middle ones are left out; 0 for no limit.

max_cols: int = 12
Development ages shown before the middle ones are left out; 0 for no limit.
Returns
str
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long([2020, 2020, 2021], [12, 24, 12], {"paid": [1000.0, 1500.0, 1100.0]})
>>> print(tri.to_string())

Triangle: paid (cumulative, valuation 2021-12) 12 24 2020 1,000 1,500 2021 1,100


view()

One segment and measure as an origin × development table.

Usage

view(column=None, **keys)
Parameters
column: str = None

The measure; may be left out when the triangle has one column.

**keys: value
One value per key, such as lob="Auto", compared as str() of it. Keys not named may take any value, but the choice must leave one segment; a triangle with one segment needs none. A key named column cannot be chosen this way (select it first).
Returns
pandas.DataFrame or dict
With pandas installed, a DataFrame with the origin labels as its index (named origin), the ages in months as its columns (named development) and nan where a cell is not observed. Without pandas, a dict of lists as the other tables of this module: "origin", then one list per age keyed by the age.
Raises
ValueError
If a key, value or column is unknown, the keys match several segments, or the column is left out and there are several.
Examples
>>> from prospicio.reserving import Triangle
>>> tri = Triangle.from_long(
...     [2020, 2020, 2021, 2020],
...     [12, 24, 12, 12],
...     {"paid": [100.0, 150.0, 110.0, 50.0]},
...     keys={"lob": ["Auto", "Auto", "Auto", "Home"]},
... )
>>> v = tri.view(lob="Auto")
>>> list(v.index), list(v.columns), float(v.loc["2021", 12])

([‘2020’, ‘2021’], [12, 24], 110.0)