Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
baf68b9
call _set_slicers() on the MunichAdjustment ldf triangle instead of j…
Abhayindia Aug 29, 2026
65d8c69
run ruff format on munich.py
Abhayindia Aug 29, 2026
73f1c8e
bind the DevelopmentConstant smoke-check result to _ in test_single_e…
Abhayindia Aug 29, 2026
3e27d19
remove a leftover no-op expression line in test_2x2_triangle
Abhayindia Aug 29, 2026
c1d835f
bind the munich smoke-check results to _ instead of unused locals
Abhayindia Aug 29, 2026
44cefec
drop the redundant parentheses around the Benktander cdf exponent
Abhayindia Aug 29, 2026
807f7ef
run ruff format on benktander.py
Abhayindia Aug 29, 2026
2ee5332
use isinstance instead of exact type comparison in ClarkLDF._growth_c…
Abhayindia Aug 29, 2026
9f6e3e2
define ldf as a nested function rather than assigning a lambda in the…
Abhayindia Aug 29, 2026
27fab01
drop the redundant parentheses around the ClarkLDF MLE sum argument
Abhayindia Aug 29, 2026
7cc1c67
run ruff format on clark.py
Abhayindia Aug 29, 2026
30c8ebf
check for the empty triangle with hasattr instead of catching Attribu…
Abhayindia Aug 29, 2026
3bae205
fix keyword-equals and else-colon whitespace in display.py
Abhayindia Aug 29, 2026
f607dd5
run ruff format on display.py
Abhayindia Aug 29, 2026
915adff
remove a stray index expression in test_multi_index
Abhayindia Aug 29, 2026
5e0fb82
add whitespace after commas in test_bootstrap.py
Abhayindia Aug 29, 2026
45c7f5a
run ruff format on test_bootstrap.py
Abhayindia Aug 29, 2026
947c56f
drop clark.py, display.py, test_bootstrap.py and test_munich.py from …
Abhayindia Aug 29, 2026
353c3fe
add the B018 useless-expression rule to the ruff rule set
Abhayindia Aug 29, 2026
14bbc4d
add the UP034 extraneous-parentheses rule to the ruff rule set
Abhayindia Aug 29, 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
22 changes: 17 additions & 5 deletions chainladder/adjustments/tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,31 @@
def test_bs_sample(raa):
tri = raa
a = (
cl.Development()
cl
.Development()
.fit(cl.BootstrapODPSample(n_sims=40000).fit_transform(tri).mean())
.ldf_
)
b = cl.Development().fit_transform(tri).ldf_
assert tri.get_array_module().all(abs(((a - b) / b).values) < 0.005)


def test_bs_multiple_cols():
assert cl.BootstrapODPSample().fit_transform(
cl.load_sample('berqsherm').iloc[0]).shape == (1000, 4, 8, 8)
cl.load_sample("berqsherm").iloc[0]
).shape == (1000, 4, 8, 8)


def test_multi_index(clrd):
tri = clrd['CumPaidLoss'].sum()
tri = clrd["CumPaidLoss"].sum()
resampled_triangles = cl.BootstrapODPSample().fit(tri).resampled_triangles_
resampled_triangles.index
assert np.all(resampled_triangles.index == pd.DataFrame(np.concat((np.array([['(All)','(All)']] * 1000),np.arange(1000).reshape(-1,1)),axis=1),columns=['GRNAME','LOB','Simulation_#']))
assert np.all(
resampled_triangles.index
== pd.DataFrame(
np.concat(
(np.array([["(All)", "(All)"]] * 1000), np.arange(1000).reshape(-1, 1)),
axis=1,
),
columns=["GRNAME", "LOB", "Simulation_#"],
)
)
56 changes: 25 additions & 31 deletions chainladder/core/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,19 @@
IPython = None

if TYPE_CHECKING:
from pandas import (
DataFrame,
IndexSlice,
Series
)
from pandas import DataFrame, IndexSlice, Series

class TriangleDisplay:

class TriangleDisplay:
def __repr__(self) -> str | DataFrame:

# If values hasn't been defined yet, return an empty triangle.
if self._dimensionality == 'empty':
if self._dimensionality == "empty":
return "Empty Triangle."

# For triangles with a single segment, containing a single triangle, return the
# DataFrame of the values.
elif self._dimensionality == 'single':
elif self._dimensionality == "single":
data: DataFrame = self._repr_format()
return data.to_string()

Expand Down Expand Up @@ -72,15 +68,16 @@ def _repr_html_(self) -> str:
"""

# Case empty triangle.
if self._dimensionality == 'empty':
if self._dimensionality == "empty":
return "Empty Triangle."

# Case single-dimensional triangle.
elif self._dimensionality == 'single':
elif self._dimensionality == "single":
data = self._repr_format()
fmt_str = self._get_format_str(data=data)
default = (
data.to_html(
data
.to_html(
max_rows=pd.options.display.max_rows,
max_cols=pd.options.display.max_columns,
float_format=fmt_str.format,
Expand Down Expand Up @@ -115,10 +112,7 @@ def _get_format_str(data: DataFrame) -> str:
else:
return "{:,.0f}"

def _repr_format(
self,
origin_as_datetime: bool = False
) -> DataFrame:
def _repr_format(self, origin_as_datetime: bool = False) -> DataFrame:
"""
Prepare triangle values for printing as a DataFrame. Mainly used with single-dimensional triangles.

Expand All @@ -137,7 +131,8 @@ def _repr_format(
origin_formatted = [""] * len(origin)
for origin_index in range(len(origin)):
origin_formatted[origin_index] = (
origin.astype("str")[origin_index]
origin
.astype("str")[origin_index]
.replace("Q1", "H1")
.replace("Q3", "H2")
)
Expand All @@ -147,12 +142,12 @@ def _repr_format(
return pd.DataFrame(out, index=origin, columns=development)

def heatmap(
self,
cmap: str = "coolwarm",
low: float = 0,
high: float = 0,
axis: int | str = 0,
subset: IndexSlice=None
self,
cmap: str = "coolwarm",
low: float = 0,
high: float = 0,
axis: int | str = 0,
subset: IndexSlice = None,
) -> Any:
"""
Color the background in a gradient according to the data in each
Expand All @@ -179,7 +174,7 @@ def heatmap(
-------
Ipython.display.HTML
"""
if self._dimensionality == 'single':
if self._dimensionality == "single":
data = self._repr_format()
fmt_str = self._get_format_str(data)

Expand All @@ -193,7 +188,8 @@ def heatmap(
) + 1
gmap = gmap.replace(np.nan, (shape_size + 1) / 2)
default_output = (
data.style.format(fmt_str)
data.style
.format(fmt_str)
.background_gradient(
cmap=cmap,
low=low,
Expand Down Expand Up @@ -222,13 +218,11 @@ def _dimensionality(self) -> str:
-------
str
"""
try:
self.values
except AttributeError:
return 'empty'
if not hasattr(self, "values"):
return "empty"

if (self.values.shape[0], self.values.shape[1]) == (1, 1):
return 'single'
return "single"

else :
return 'multi'
else:
return "multi"
3 changes: 1 addition & 2 deletions chainladder/core/tests/test_triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1876,7 +1876,7 @@ def test_single_entry():
cl_dev_constant_fit = cl_dev_constant.fit(cl_tri.val_to_dev())

# aim
cl.Chainladder().fit(cl_dev_constant_fit.transform(cl_tri)).ultimate_
_ = cl.Chainladder().fit(cl_dev_constant_fit.transform(cl_tri)).ultimate_


def test_origin_as_datetime_arg(clrd):
Expand Down Expand Up @@ -2429,7 +2429,6 @@ def test_2x2_triangle():
columns=["reported"],
cumulative=True,
)
tri_from_df
assert np.array_equal(
tri_from_df.cum_to_incr().values,
np.array([[[[78000.0, 144000.0], [78000.0, np.float64(np.nan)]]]]),
Expand Down
43 changes: 19 additions & 24 deletions chainladder/development/clark.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,15 @@ class ClarkLDF(DevelopmentBase):

775
CumPaidLoss
LOB
LOB
comauto 1.08
medmal 1.89
othliab 1.47
ppauto 1.15
prodliab 1.44
wkcomp 1.11
CumPaidLoss
LOB
LOB
comauto 20.48
medmal 35.13
othliab 37.75
Expand All @@ -160,20 +160,11 @@ class ClarkLDF(DevelopmentBase):

"""

def __init__(
self,
growth: str = "loglogistic",
groupby=None
):
def __init__(self, growth: str = "loglogistic", groupby=None):
self.growth: str = growth
self.groupby = groupby

def _G(
self,
age,
theta: float = None,
omega: float = None
):
def _G(self, age, theta: float = None, omega: float = None):
"""Growth function.

Parameters
Expand All @@ -192,12 +183,12 @@ def _G(
omega = self.omega_.values[..., None, None]
age[age == 0.0] = xp.nan
if self.growth == "loglogistic":
out = 1 + (theta ** omega) * (age ** (-omega))
out = 1 + (theta**omega) * (age ** (-omega))
elif self.growth == "weibull":
out = 1 / (1 - xp.exp(-((age / theta) ** omega)))
else:
ValueError(str(self.growth) + "is an invalid growth curve.")
out[xp.isnan(out)] = xp.inf # noqa
out[xp.isnan(out)] = xp.inf # noqa
return out

def G_(self, age):
Expand All @@ -215,9 +206,9 @@ def G_(self, age):
A Triangle object with growth curve values
"""
xp = self.incremental_act_.get_array_module()
if type(age) in [int, float, xp.int64, xp.float64]:
if isinstance(age, (int, float, xp.int64, xp.float64)):
age = xp.array([age]).astype("float64")
if type(age) == list:
if isinstance(age, list):
age = xp.array([age]).astype("float64")
obj = self.incremental_act_.copy()
obj.odims = obj.odims[0:1]
Expand Down Expand Up @@ -286,8 +277,11 @@ def fit(self, X, y=None, sample_weight=None):
for col in range(len(X.columns)):

def solver(x: ndarray):
""" Solve Loglogistic MLE"""
ldf = lambda age: self._G(age, theta=x[..., 1], omega=x[..., 0])
"""Solve Loglogistic MLE"""

def ldf(age):
return self._G(age, theta=x[..., 1], omega=x[..., 0])

if sample_weight:
ult = (
sample_weight.values[idx : idx + 1, col : col + 1, ::-1, 0]
Expand All @@ -305,7 +299,7 @@ def solver(x: ndarray):
increments[idx : idx + 1, col : col + 1] * xp.log(increment_fit)
- increment_fit
)
return -xp.sum((xp.nan_to_num(mle.flatten())))
return -xp.sum(xp.nan_to_num(mle.flatten()))

if sample_weight:
x0 = xp.array([[[[1.0, age_interval, 1.0]]]])
Expand All @@ -314,7 +308,9 @@ def solver(x: ndarray):
x0 = xp.array([[[[1.0, age_interval]]]])
bounds = ((1e-6, None), (1e-6, None))
idx_params.append(
minimize(fun=solver, x0=x0.flatten(), bounds=bounds).x.reshape(1, 1, 1, -1)
minimize(fun=solver, x0=x0.flatten(), bounds=bounds).x.reshape(
1, 1, 1, -1
)
)
params.append(xp.concatenate(idx_params, axis=1))
params = xp.concatenate(params, axis=0)
Expand All @@ -335,8 +331,7 @@ def solver(x: ndarray):
if sample_weight:
self.elr_ = pd.DataFrame(params[..., 0, 2], index=rows, columns=X.vdims)
ultimate_ = (
self._G(age=(latest_age - age_offset)[::-1]).swapaxes(-1, -2)
* ld.values
self._G(age=(latest_age - age_offset)[::-1]).swapaxes(-1, -2) * ld.values
)
self.incremental_fits_ = X.copy()
self.incremental_fits_.array_backend = "numpy"
Expand All @@ -354,7 +349,7 @@ def solver(x: ndarray):
return self

def transform(self, X):
""" If X and self are of different shapes, align self to X, else
"""If X and self are of different shapes, align self to X, else
return self.

Parameters
Expand Down
8 changes: 4 additions & 4 deletions chainladder/development/munich.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def fit(self, X, y=None, sample_weight=None):
return self

def transform(self, X):
""" If X and self are of different shapes, align self to X, else
"""If X and self are of different shapes, align self to X, else
return self.

Parameters
Expand Down Expand Up @@ -345,8 +345,8 @@ def _get_munich_full_triangle_(
return self._p_to_i_concate(full_paid, full_incurred, xp)

def _get_mcl_cdf(self, X, munich_full_triangle_):
""" needs to be an attribute that gets assigned. requires we overwrite
the cdf and ldf methods with
"""needs to be an attribute that gets assigned. requires we overwrite
the cdf and ldf methods with
"""
xp = X.get_array_module()
obj = X.cdf_.copy()
Expand Down Expand Up @@ -379,7 +379,7 @@ def _set_ldf(self, X, cdf):
obj.ddims = X.link_ratio.ddims
obj.is_pattern = True
obj.is_cumulative = False
obj._set_slicers
obj._set_slicers()
return obj

def _reshape(self, measure):
Expand Down
6 changes: 3 additions & 3 deletions chainladder/development/tests/test_munich.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
def test_mcl_ult():
mcl = cl.load_sample("mcl")
dev = cl.Development().fit_transform(mcl)
cl_traditional = cl.Chainladder().fit(dev).ultimate_
_ = cl.Chainladder().fit(dev).ultimate_
dev_munich = cl.MunichAdjustment(
paid_to_incurred=[("paid", "incurred")]
).fit_transform(dev)
cl_munich = cl.Chainladder().fit(dev_munich).ultimate_
_ = cl.Chainladder().fit(dev_munich).ultimate_


def test_mcl_rollforward():
mcl = cl.load_sample("mcl")
mcl_prior = mcl[mcl.valuation < mcl.valuation_date]
munich = cl.MunichAdjustment(paid_to_incurred=[("paid", "incurred")]).fit(mcl_prior)
new = munich.transform(mcl)
cl.Chainladder().fit(new).ultimate_
_ = cl.Chainladder().fit(new).ultimate_
8 changes: 4 additions & 4 deletions chainladder/methods/benktander.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def predict(self, X, sample_weight=None):
current Triangle and a refreshed apriori.

.. testsetup::

import chainladder as cl

.. testcode::
Expand Down Expand Up @@ -263,8 +263,8 @@ def _get_benktander_aprioris(self, X, sample_weight):
random_state = xp.random.RandomState(self.random_state)
# Draw from lognormal with E[apriori] = self.apriori and SD = self.apriori_sigma.
cov = self.apriori_sigma / self.apriori
sigma_log = np.sqrt(np.log1p(cov ** 2))
mu_log = np.log(self.apriori) - 0.5 * sigma_log ** 2
sigma_log = np.sqrt(np.log1p(cov**2))
mu_log = np.log(self.apriori) - 0.5 * sigma_log**2
apriori = random_state.lognormal(mu_log, sigma_log, X.shape[0])
apriori = apriori.reshape(X.shape[0], -1)[..., None, None]
apriori = sample_weight * apriori
Expand All @@ -290,7 +290,7 @@ def _get_ultimate(self, X, expectation):
cdf = (1 - 1 / num_to_nan(cdf.values))[None]
exponents = xp.arange(self.n_iters + 1)
exponents = xp.reshape(exponents, tuple([len(exponents)] + [1] * 4))
cdf = cdf ** (((cdf + 1e-16) / (cdf + 1e-16) * exponents))
cdf = cdf ** ((cdf + 1e-16) / (cdf + 1e-16) * exponents)
cdf = xp.nan_to_num(cdf)
a = xp.sum(cdf[:-1, ...], 0) * xp.nan_to_num(ld.set_backend(backend).values)
b = cdf[-1, ...] * xp.nan_to_num(expectation.set_backend(backend).values)
Expand Down
Loading
Loading