Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
876be18
Improved docstring and added examples
kennethshsu May 22, 2026
2763b68
Added the link ratios
kennethshsu May 22, 2026
30c63e7
docs: add doctest examples for correlation classes
priyam0k May 23, 2026
78272e8
docs: add BootstrapODPSample doctest examples
priyam0k May 23, 2026
04e95f0
docs: reframe drop_high example as sensitivity check
priyam0k May 23, 2026
3d827d9
docs: move per-diagonal vs total mode note between testcode blocks
priyam0k May 25, 2026
cd0a78f
docs: add README documenting docs build sources and outputs (refs #845)
priyam0k May 25, 2026
bb0e64d
Added the transformed link ratio triangle using fit_transform to exam…
kennethshsu May 26, 2026
9e833f6
Improved docstrings for ValuationCorrelation and DevelopmentConstant
kennethshsu May 26, 2026
8219602
Removed the multiple testouputs
kennethshsu May 26, 2026
9669d2d
docs: address review feedback on docs README (refs #845)
priyam0k May 26, 2026
4633bda
[REFACTOR]: Remove repetitive code. Remove dead Python 3.8 code. Add …
genedan May 26, 2026
f11497b
[FIX]: Limit test to only those values meant to be changed. Use reali…
genedan May 26, 2026
ee4525e
[DOCS]: Finish updating Options docstring.
genedan May 26, 2026
ce9f9ac
[FIX]: Fix ending state of test.
genedan May 26, 2026
0b06614
Merge pull request #848 from priyam0k/docs/845-doc-build-readme
kennethshsu May 26, 2026
d7d22ac
[FIX]: Reset backend after sparse-only run.
genedan May 26, 2026
cffab2c
Merge pull request #834 from casact/#704_development
kennethshsu May 26, 2026
51a1ad1
[REFACTOR] Create template fixture for sample data sets.
genedan May 26, 2026
70fe2a5
[Fix]: Remove duplicate fixture.
genedan May 26, 2026
2334029
Merge pull request #857 from casact/#856-init-cleanup
genedan May 27, 2026
5dd0800
[REFACTOR]: Move datetime defaults out of Options. Add validation to …
genedan May 27, 2026
3daaacb
FIX: Apply Bugbot fix.
genedan May 27, 2026
17953c3
FIX: Apply Bugbot fix.
genedan May 27, 2026
3f33270
FIX: Apply Bugbot fix.
genedan May 27, 2026
d6b4315
DOCS: Add docstring, clean up test.
genedan May 27, 2026
8030ac6
DOCS: Update docstring.
genedan May 27, 2026
49b2a31
Friedland Chapter 6 and half of Chapter 7 (#837)
henrydingliu May 28, 2026
449b5c1
Merge pull request #867 from casact/#856-init-cleanup
genedan May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
## code changes will send PR to following users
* @jbogaardt @kennethshsu @genedan @henrydingliu
* @jbogaardt @kennethshsu @genedan @henrydingliu
162 changes: 117 additions & 45 deletions chainladder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,128 @@
"""
The chainladder-python package was built to be able to handle all of your actuarial reserving needs in python.
It consists of popular actuarial tools, such as triangle data manipulation, link ratios calculation, and
IBNR estimates using both deterministic and stochastic models. We build this package so you no longer have to rely
on outdated software and tools when performing actuarial pricing or reserving indications.

This package strives to be minimalistic in needing its own API. The syntax mimics popular packages such as pandas for
data manipulation and scikit-learn for model construction. An actuary that is already familiar with these tools will be
able to pick up this package with ease. You will be able to save your mental energy for actual actuarial work.

The __init__.py file governs package configuration, including datetime datatypes and precision, backend and ultimate
valuation defaults, as well as package metadata such as version number.
"""
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

import copy
import numpy as np
import pandas as pd
from sklearn.utils import deprecated
from importlib.metadata import version


_DT64_DTYPE = pd.to_datetime(["2000-01-01"]).dtype
_ULT_VAL: str = str(
pd.Timestamp("2262-01-01") - \
pd.Timedelta(1, unit=np.datetime_data(_DT64_DTYPE)[0])
)
# Get the default datetime64 data type and precision, extracted from Pandas installation.
# Used for cross-version compatibility between Pandas 2 and Pandas 3.
__dt64_dtype__: str = pd.to_datetime(["2000-01-01"]).dtype.name
__dt64_unit__: str = np.datetime_data(__dt64_dtype__)[0]


class Options:
ARRAY_BACKEND = "numpy"
AUTO_SPARSE = True
ARRAY_PRIORITY = ["dask", "sparse", "cupy", "numpy"]
ULT_VAL: str = _ULT_VAL
DT64_UNIT: str = np.datetime_data(_DT64_DTYPE)[0]
DT64_DTYPE: str = str(_DT64_DTYPE)

@classmethod
def get_option(cls, option=None):
return getattr(cls, option)

@classmethod
def set_option(cls, option, value):
setattr(cls, option, value)

def reset_option(self):
self.set_option('ARRAY_BACKEND', "numpy")
self.set_option('AUTO_SPARSE', True)
self.set_option('ARRAY_PRIORITY', ["dask", "sparse", "cupy", "numpy"])
self.set_option('ULT_VAL', _ULT_VAL)
self.set_option('DT64_UNIT', np.datetime_data(_DT64_DTYPE)[0])
self.set_option('DT64_DTYPE', str(_DT64_DTYPE))

def describe_option(self):
pass
"""
Used to set defaults for array backend and datetime units.

options = Options()
Attributes
----------

ARRAY_BACKEND: str
The default array backend for chainladder.
AUTO_SPARSE: bool
Controls whether chainladder automatically converts a triangle's backing array to a sparse representation
when it would be memory-efficient to do so.
ARRAY_PRIORITY: list
Determines which backend wins when two triangles with different backends interact, i.e.,
when comparing or concatenating them.
ULT_VAL: str
The default ultimate valuation datetime, precision set to default of Pandas installation.

"""
def __init__(self):
self.ARRAY_BACKEND = "numpy"
self.AUTO_SPARSE = True
self.ARRAY_PRIORITY = ["dask", "sparse", "cupy", "numpy"]
self.ULT_VAL = str(
pd.Timestamp("2262-01-01") - \
pd.Timedelta(1, unit=__dt64_unit__)
)
# Store initial values as defaults.
self._defaults = copy.deepcopy({k: v for k, v in vars(self).items() if not k.startswith('_')})

def get_option(self, option: str) -> str | bool | list:
"""
Get the option value for the specified option.

Parameters
----------
option: str
The option you wish to get the values for.

Returns
-------
The option value.

"""
self._validate_option(option)
return getattr(self, option)

@deprecated("In an upcoming version of the package, this function will be deprecated. Use `chainladder.options.set_option('ARRAY_BACKEND', value)` to avoid breakage.")
def array_backend(array_backend="numpy"):
options.set_option('ARRAY_BACKEND', array_backend)
def set_option(
self,
option: str,
value: str | bool | list
) -> None:
"""
Set the option value for the specified option.

@deprecated("In an upcoming version of the package, this function will be deprecated. Use `chainladder.options.set_option('AUTO_SPARSE', value)` to avoid breakage.")
def auto_sparse(auto_sparse=True):
options.set_option('AUTO_SPARSE', auto_sparse)
Parameters
----------
option: str
The option you wish to set the value for.
value: str | bool | list
The option value.

Returns
-------
None

"""
self._validate_option(option)
setattr(self, option, value)

def reset_option(self, option: str | None = None) -> None:
"""
Restores the default value for the specified option. Restores default values for
all options if option is None.

Returns
-------
None

"""

if option is not None:
self._validate_option(option)
setattr(self, option, copy.deepcopy(self._defaults[option]))
else:
self.__init__()

def _validate_option(self, option: str) -> None:

if option not in self._defaults:
raise ValueError(f"Invalid option(s): {option}. Must be one of {list(self._defaults)}.")

def describe_option(self, option: str) -> str:
pass

options = Options()


from chainladder.utils import * # noqa (API Import)
Expand All @@ -55,10 +133,4 @@ def auto_sparse(auto_sparse=True):
from chainladder.methods import * # noqa (API Import)
from chainladder.workflow import * # noqa (API Import)

try:
from importlib.metadata import version
__version__ = version("chainladder")
except ImportError:
# Fallback for Python < 3.8
from importlib_metadata import version
__version__ = version("chainladder")
__version__ = version("chainladder")
68 changes: 68 additions & 0 deletions chainladder/adjustments/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,74 @@ class BootstrapODPSample(DevelopmentBase):
A set of triangles represented by each simulation
scale_:
The scale parameter to be used in generating process risk

Examples
--------

Generate ODP bootstrap samples of the RAA sample triangle. The estimator
re-samples standardized Pearson residuals to produce ``n_sims`` synthetic
triangles stacked along the index axis of ``resampled_triangles_``, and
exposes the scale parameter ``scale_`` used to generate process risk.
``random_state`` and a small ``n_sims`` keep the output deterministic
and fast.

.. testsetup::

import warnings
warnings.filterwarnings("ignore")
import chainladder as cl

.. testcode::

raa = cl.load_sample('raa')
boot = cl.BootstrapODPSample(n_sims=100, random_state=42).fit(raa)
print(boot.resampled_triangles_.shape)
print(round(float(boot.scale_), 2))

.. testoutput::

(100, 1, 10, 10)
983.64

Because ``resampled_triangles_`` is itself a Triangle (with the simulation
index along the first axis), it can be fed straight into any downstream
reserving estimator to obtain a stochastic distribution of ultimates and
IBNR. Below, a deterministic chain-ladder is fit on the resampled triangle
and the total IBNR is summarised across the 100 simulations.

.. testcode::

sims = cl.BootstrapODPSample(
n_sims=100, random_state=42
).fit_transform(raa)
ibnr = cl.Chainladder().fit(sims).ibnr_.sum('origin')
print(ibnr.shape)
print(round(float(ibnr.mean()), 2))
print(round(float(ibnr.std()), 2))

.. testoutput::

(100, 1, 1, 1)
51301.13
16149.47

The estimator also supports a leave-one-out sensitivity check on the
residual distribution. Setting ``drop_high=True`` excludes the highest
link ratio in each development column before computing residuals, without
making any outlier judgement, so the resulting ``scale_`` measures how
influential the column maxima are on the bootstrap. For the RAA triangle
this shrinks ``scale_`` from 983.64 to 648.94.

.. testcode::

boot_dh = cl.BootstrapODPSample(
n_sims=100, random_state=42, drop_high=True
).fit(raa)
print(round(float(boot_dh.scale_), 2))

.. testoutput::

648.94
"""

def __init__(
Expand Down
10 changes: 7 additions & 3 deletions chainladder/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

from abc import ABC, abstractmethod

from chainladder import options
from chainladder import (
__dt64_unit__,
__dt64_dtype__,
options
)

from chainladder.core.common import Common
from chainladder.core.display import TriangleDisplay
Expand Down Expand Up @@ -534,12 +538,12 @@ def valuation(self):
ddim_arr = ddims - ddims[0]
origin = np.minimum(self.odims, np.datetime64(self.valuation_date))
val_array = origin.astype("datetime64[M]") + np.timedelta64(ddims[0], "M")
val_array = val_array.astype(options.DT64_DTYPE) - np.timedelta64(1, options.DT64_UNIT)
val_array = val_array.astype(__dt64_dtype__) - np.timedelta64(1, __dt64_unit__)
val_array = val_array[:, None]
s = slice(None, -1) if ddims[-1] == 9999 else slice(None, None)
val_array = (
val_array.astype("datetime64[M]") + ddim_arr[s][None, :] + 1
).astype(options.DT64_DTYPE) - np.timedelta64(1, options.DT64_UNIT)
).astype(__dt64_dtype__) - np.timedelta64(1, __dt64_unit__)
if ddims[-1] == 9999:
ult = np.repeat(np.datetime64(options.ULT_VAL), val_array.shape[0])[:, None]
val_array = np.concatenate(
Expand Down
86 changes: 86 additions & 0 deletions chainladder/core/correlation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,45 @@ class DevelopmentCorrelation:
confidence_interval: tuple
Range within which ``t_expectation`` must fall for independence assumption
to be significant.
Examples
--------
Mack (1997) lists "successive development factors are uncorrelated" as
one of the assumptions underpinning the chain-ladder method. Before
relying on a ``Chainladder`` or ``MackChainladder`` ultimate it is good
practice to test that assumption on the triangle at hand.
``DevelopmentCorrelation`` performs Mack's weighted Spearman rank test
across consecutive development columns and exposes both the test
statistic ``t_expectation`` and the no-correlation
``confidence_interval``, so the decision is visible rather than reduced
to a single boolean.
.. testsetup::
import chainladder as cl
.. testcode::
raa = cl.load_sample('raa')
dc = cl.DevelopmentCorrelation(raa, p_critical=0.5)
print(round(float(dc.t_expectation.iloc[0, 0]), 4))
print(round(float(dc.confidence_interval[0]), 4))
print(round(float(dc.confidence_interval[1]), 4))
print(bool(dc.t_critical.iloc[0, 0]))
.. testoutput::
0.0696
-0.1275
0.1275
False
The Spearman statistic ``0.0696`` lies inside the 50% confidence band
``(-0.1275, 0.1275)`` derived from ``t_variance = 2 / ((I - 2)(I - 3))``,
so the test does not reject independence and chain-ladder is appropriate
for RAA on this dimension. See the Mack chain-ladder section of the user
guide for the full assumption set.
"""

def __init__(self, triangle, p_critical: float = 0.5):
Expand Down Expand Up @@ -171,6 +210,53 @@ class ValuationCorrelation:
The expected value of Z.
z_variance : Triangle or DataFrame
The variance value of Z.
Examples
--------
Mack's second prerequisite for the chain-ladder method is that no
calendar period systematically inflates or deflates link ratios (for
example from a one-off reserve strengthening or a change in case
reserving practice). ``ValuationCorrelation`` flags any diagonal on
which the split of high versus low link ratios is unlikely under random
ordering.
.. testsetup::
import chainladder as cl
.. testcode::
raa = cl.load_sample('raa')
vc = cl.ValuationCorrelation(raa, p_critical=0.1, total=False)
print(vc.z_critical)
.. testoutput::
1982 1983 1984 1985 1986 1987 1988 1989 1990
1981 False False False False False False False False False
No diagonal crosses the 90% threshold, so the calendar-effect assumption
is supported. If any cell read ``True`` you would inspect that diagonal
before relying on Mack or chain-ladder ultimates.
The same test can be aggregated to a whole-triangle form
(``total=True``, Mack 1993) instead of the per-diagonal form
(``total=False``, Mack 1997) shown above:
.. testcode::
vc_total = cl.ValuationCorrelation(raa, p_critical=0.1, total=True)
print(round(float(vc_total.z.iloc[0, 0]), 4))
print(bool(vc_total.z_critical.iloc[0, 0]))
.. testoutput::
14.0
False
The whole-triangle ``z`` statistic also falls inside the no-effect band,
confirming the per-diagonal result.
"""

def __init__(self, triangle: Triangle, p_critical: float = 0.1, total: bool = True):
Expand Down
Loading
Loading