From 8b83973987d1a3fbe541018ff037df290ed741c1 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Thu, 17 Sep 2026 07:14:05 +0000 Subject: [PATCH 01/11] overnight dump --- chainladder/adjustments/disposal.py | 7 +++ chainladder/core/base.py | 2 + chainladder/core/common.py | 9 +-- chainladder/core/triangle.py | 13 ++++ chainladder/development/base.py | 3 - chainladder/development/development.py | 8 +-- chainladder/development/incremental.py | 61 +++++++++++++++++++ .../development/tests/test_incremental.py | 20 ++++++ 8 files changed, 105 insertions(+), 18 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index fcd3a568f..79f9ccb46 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -339,3 +339,10 @@ def fit_transform(self, X, y=None, sample_weight=None): Triangle with new full_triangle_ """ return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) + + def _param_property(self, X, params, idx): + obj = super._param_property(X, params) + obj.values = params + obj.is_disposal_rate = True + + return obj \ No newline at end of file diff --git a/chainladder/core/base.py b/chainladder/core/base.py index ac89fc131..ae20ae43b 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -28,6 +28,7 @@ from chainladder.utils.sparse import sp from chainladder.adjustments.disposal import DisposalMixin +from chainladder.development.incremental import IncrementalMixin from typing import Optional, TYPE_CHECKING @@ -50,6 +51,7 @@ class TriangleBase( Common, ABC, DisposalMixin, + IncrementalMixin, ): """This class handles the initialization of a triangle""" diff --git a/chainladder/core/common.py b/chainladder/core/common.py index 8f421cb51..53eee285d 100644 --- a/chainladder/core/common.py +++ b/chainladder/core/common.py @@ -82,7 +82,7 @@ def has_ldf(self): @property def has_zeta(self): - if hasattr(self, "zeta_"): + if hasattr(self, "_zeta_"): return True else: return False @@ -159,13 +159,6 @@ def pct_unreported_(self): ) return 1 - 1 / self.cdf_ - @property - def cum_zeta_(self): - if not self.has_zeta: - x = self.__class__.__name__ - raise AttributeError("'" + x + "' object has no attribute 'cum_zeta_'") - return self.zeta_.incr_to_cum() - @property def ibnr_(self): """ diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index a496d7edc..adfd54e58 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1046,6 +1046,19 @@ def is_disposal_rate(self) -> bool: def is_disposal_rate(self, is_dr: bool) -> None: self._is_disposal_rate = is_dr + @property + def is_additive(self) -> bool: + """ + Indicates whether the Triangle holds a normal additive ratio (which goes from tail to head) + """ + if hasattr(self, "_is_additive"): + return self._is_additive + return False + + @is_additive.setter + def is_additive(self, is_add: bool) -> None: + self._is_additive = is_add + def align_pattern( self, X: Triangle, sample_weight: Triangle | None = None ) -> Triangle: diff --git a/chainladder/development/base.py b/chainladder/development/base.py index 922c4dbc2..4c09067da 100644 --- a/chainladder/development/base.py +++ b/chainladder/development/base.py @@ -409,11 +409,8 @@ def _param_property(X: Triangle, params: np.ndarray) -> Triangle: from chainladder import options obj: Triangle = X[X.origin == X.origin.min()] - obj.values = params obj.valuation_date = pd.to_datetime(options.ULT_VAL) obj.is_pattern = True - obj.is_additive = True - obj.is_cumulative = False obj.virtual_columns.columns = {} obj._set_slicers() return obj diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 705e955fe..451a50233 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -482,16 +482,10 @@ def transform(self, X): return X_new def _param_property(self, X, params, idx): - from chainladder import options - - obj = X[X.origin == X.origin.min()] + obj = super._param_property(X, params) xp = X.get_array_module() obj.values = xp.ones(obj.shape)[..., :-1] * params[..., idx : idx + 1, :] obj.ddims = X.link_ratio.ddims - obj.valuation_date = pd.to_datetime(options.ULT_VAL) - obj.is_pattern = True obj.is_cumulative = False - obj.virtual_columns.columns = {} - obj._set_slicers() return obj diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index 0d7195af3..407074402 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -1,11 +1,65 @@ # 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/. +from __future__ import annotations + from chainladder.development import DevelopmentBase from chainladder.utils import TriangleWeight import numpy as np +import copy import warnings +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder.core import Triangle + + +class IncrementalMixin: + """ + This class provides attributes for the IncrementalAdditive development method + and transformed `Triangle` + """ + + @property + def zeta_(self) -> Triangle: + """ + Gets the estimated zeta + """ + if not hasattr(self, "_zeta_"): + x = self.__class__.__name__ + raise AttributeError("'" + x + "' object has no attribute 'zeta_'") + return self._zeta_ + + @zeta_.setter + def zeta_(self, value: Triangle) -> None: + """ + Sets zeta_ + """ + obj = copy.deepcopy(value) + obj.is_pattern = True + obj.is_additive = True + obj.is_cumulative = False + self._zeta_ = obj + + @property + def cum_zeta_(self) -> Triangle: + """ + Gets the cumulative of the estimated zeta + """ + return self.zeta_.incr_to_cum() + + @cum_zeta_.setter + def cum_zeta_(self, value: Triangle) -> None: + """ + Sets cum_zeta_ + """ + obj = copy.deepcopy(value) + obj.is_pattern = True + obj.is_additive = True + obj.is_cumulative = True + self._zeta_ = obj.cum_to_incr() + class IncrementalAdditive(DevelopmentBase): """ @@ -319,3 +373,10 @@ def transform(self, X): ]: X_new.__dict__[item] = self.__dict__[item] return X_new + + def _param_property(self, X, params, idx): + obj = super._param_property(X, params) + obj.values = params + obj.is_additive = True + + return obj \ No newline at end of file diff --git a/chainladder/development/tests/test_incremental.py b/chainladder/development/tests/test_incremental.py index 58c6ed3c1..597694b24 100644 --- a/chainladder/development/tests/test_incremental.py +++ b/chainladder/development/tests/test_incremental.py @@ -1,7 +1,14 @@ +from __future__ import annotations + import chainladder as cl import numpy as np import pytest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder.core import Triangle + def test_schmidt(): tri = cl.load_sample("ia_sample") @@ -90,3 +97,16 @@ def test_pipeline(): assert np.array_equal( dev1.zeta_.values, dev2.named_steps.drop_hilo.zeta_.values, True ) + + +def test_setting_cum() -> None: + """ + IncrementalMixin allows setting cumulative zeta. Validating that the setting function works properly. + """ + tri = cl.load_sample("ia_sample") + ia = cl.IncrementalAdditive().fit( + tri.iloc[0, 0], sample_weight=tri.iloc[0, 1].latest_diagonal + ) + orig_zeta_ = ia.zeta_ + ia.cum_zeta_ = ia.cum_zeta_ * 2 + assert ia.zeta_ == orig_zeta_ * 2 From 988e42dfd519e6ae544533757473d9bb7fa289ff Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Thu, 17 Sep 2026 08:22:11 +0000 Subject: [PATCH 02/11] bug fix --- chainladder/adjustments/disposal.py | 2 +- chainladder/development/development.py | 2 +- chainladder/development/incremental.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 79f9ccb46..fbbbe401a 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -341,7 +341,7 @@ def fit_transform(self, X, y=None, sample_weight=None): return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) def _param_property(self, X, params, idx): - obj = super._param_property(X, params) + obj = super()._param_property(X, params) obj.values = params obj.is_disposal_rate = True diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 451a50233..4a2734d66 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -482,7 +482,7 @@ def transform(self, X): return X_new def _param_property(self, X, params, idx): - obj = super._param_property(X, params) + obj = super()._param_property(X, params) xp = X.get_array_module() obj.values = xp.ones(obj.shape)[..., :-1] * params[..., idx : idx + 1, :] obj.ddims = X.link_ratio.ddims diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index 407074402..a19248f95 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -375,7 +375,7 @@ def transform(self, X): return X_new def _param_property(self, X, params, idx): - obj = super._param_property(X, params) + obj = super()._param_property(X, params) obj.values = params obj.is_additive = True From af3c621c56743febccdca1c510ed12d3ec9f569a Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Thu, 17 Sep 2026 08:31:16 +0000 Subject: [PATCH 03/11] various updates --- chainladder/adjustments/disposal.py | 4 ++-- chainladder/development/development.py | 1 - chainladder/development/incremental.py | 4 ++-- chainladder/development/tests/test_incremental.py | 7 ------- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index fbbbe401a..7b13c329a 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -340,9 +340,9 @@ def fit_transform(self, X, y=None, sample_weight=None): """ return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) - def _param_property(self, X, params, idx): + def _param_property(self, X, params): obj = super()._param_property(X, params) obj.values = params obj.is_disposal_rate = True - return obj \ No newline at end of file + return obj diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 4a2734d66..e9b0f8f04 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -4,7 +4,6 @@ from __future__ import annotations import numpy as np -import pandas as pd import warnings from chainladder.development.base import DevelopmentBase diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index a19248f95..0c83e7060 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -374,9 +374,9 @@ def transform(self, X): X_new.__dict__[item] = self.__dict__[item] return X_new - def _param_property(self, X, params, idx): + def _param_property(self, X, params): obj = super()._param_property(X, params) obj.values = params obj.is_additive = True - return obj \ No newline at end of file + return obj diff --git a/chainladder/development/tests/test_incremental.py b/chainladder/development/tests/test_incremental.py index 597694b24..751c9b3d0 100644 --- a/chainladder/development/tests/test_incremental.py +++ b/chainladder/development/tests/test_incremental.py @@ -1,14 +1,7 @@ -from __future__ import annotations - import chainladder as cl import numpy as np import pytest -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from chainladder.core import Triangle - def test_schmidt(): tri = cl.load_sample("ia_sample") From aa083d86fcb3b8e23ec3b88839ab25d3479cd5b0 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Thu, 17 Sep 2026 15:30:36 +0000 Subject: [PATCH 04/11] fixing tests --- chainladder/core/tests/test_triangle.py | 4 +- chainladder/core/triangle.py | 61 ++++++++++++++----- chainladder/development/incremental.py | 4 +- .../development/tests/test_incremental.py | 15 ++++- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 45c1e649b..c89ecb00d 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -3200,7 +3200,7 @@ def test_has_zeta_true(raa: Triangle) -> None: None """ fitted = cl.IncrementalAdditive().fit(raa, sample_weight=raa.latest_diagonal) - assert fitted.has_zeta is True + assert fitted.has_zeta def test_has_zeta_false(raa: Triangle) -> None: @@ -3216,7 +3216,7 @@ def test_has_zeta_false(raa: Triangle) -> None: ------- None """ - assert cl.Development().fit(raa).has_zeta is False + assert not cl.Development().fit(raa).has_zeta def test_cum_zeta_raises_when_no_zeta(raa: Triangle) -> None: diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index adfd54e58..c85333ecf 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1390,20 +1390,28 @@ def incr_to_cum(self, inplace=False): if inplace: xp = self.get_array_module() if not self.is_cumulative: - if self.is_pattern & (not self.is_disposal_rate): - if hasattr(self, "is_additive") and self.is_additive: - values = xp.nan_to_num(self.values[..., ::-1]) - values = num_to_value(values, 0) - self.values = ( - xp.cumsum(values, -1)[..., ::-1] * self.nan_triangle - ) - else: - values = xp.nan_to_num(self.values[..., ::-1]) - values = num_to_value(values, 1) - values = xp.cumprod(values, -1)[..., ::-1] - self.values = values * self.nan_triangle - values = num_to_value(values, self.get_array_module(values).nan) - else: + if ( + self.is_pattern + and (not self.is_additive) + and (not self.is_disposal_rate) + ): # for multiplicative ldf triangles (mult from tail to head) + values = xp.nan_to_num(self.values[..., ::-1]) + values = num_to_value(values, 1) + values = xp.cumprod(values, -1)[..., ::-1] + self.values = values * self.nan_triangle + values = num_to_value(values, self.get_array_module(values).nan) + elif ( + self.is_pattern + and self.is_additive + and (not self.is_disposal_rate) + ): # for additive factor triangles (sum from tail to head) + values = xp.nan_to_num(self.values[..., ::-1]) + values = num_to_value(values, 0) + self.values = ( + xp.cumsum(values, -1)[..., ::-1] * self.nan_triangle + ) + else: # for normal value triangles + # also for disposal rate triangles (sum from head to tail) if self.array_backend not in ["sparse", "dask"]: self.values = ( xp.cumsum(xp.nan_to_num(self.values), 3) @@ -1479,7 +1487,11 @@ def cum_to_incr(self, inplace=False): if inplace: v = self.valuation_date if self.is_cumulative or self.is_cumulative is None: - if self.is_pattern & (not self.is_disposal_rate): + if ( + self.is_pattern + and (not self.is_additive) + and (not self.is_disposal_rate) + ): # for multiplicative ldf triangles (div from head to tail) xp = self.get_array_module() self.values = num_to_value(xp.nan_to_num(self.values), 1) diff = self.iloc[..., :-1] / self.iloc[..., 1:].values @@ -1491,7 +1503,24 @@ def cum_to_incr(self, inplace=False): axis=3, ) self.values = self.values * self.nan_triangle - else: + elif ( + self.is_pattern + and self.is_additive + and (not self.is_disposal_rate) + ): # for additive factor triangles (diff from head to tail) + xp = self.get_array_module() + self.values = num_to_value(xp.nan_to_num(self.values), 1) + diff = self.iloc[..., :-1] - self.iloc[..., 1:].values + self = concat( + ( + diff, + self.iloc[..., -1], + ), + axis=3, + ) + self.values = self.values * self.nan_triangle + else: # for normal value triangles + # also for disposal rate triangles (sum from head to tail) diff = self.iloc[..., 1:] - self.iloc[..., :-1].values self = concat((self.iloc[..., 0], diff), axis=3) self.is_cumulative = False diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index 0c83e7060..b7af2e644 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -61,7 +61,7 @@ def cum_zeta_(self, value: Triangle) -> None: self._zeta_ = obj.cum_to_incr() -class IncrementalAdditive(DevelopmentBase): +class IncrementalAdditive(DevelopmentBase, IncrementalMixin): """ The Incremental Additive Method. @@ -365,7 +365,7 @@ def transform(self, X): for item in [ "ldf_", "w_", - "zeta_", + "_zeta_", "incremental_", "tri_zeta", "fit_zeta_", diff --git a/chainladder/development/tests/test_incremental.py b/chainladder/development/tests/test_incremental.py index 751c9b3d0..8c5ef4d2a 100644 --- a/chainladder/development/tests/test_incremental.py +++ b/chainladder/development/tests/test_incremental.py @@ -1,7 +1,14 @@ +from __future__ import annotations + import chainladder as cl import numpy as np import pytest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder import Triangle + def test_schmidt(): tri = cl.load_sample("ia_sample") @@ -92,7 +99,7 @@ def test_pipeline(): ) -def test_setting_cum() -> None: +def test_setting_cum(atol: float) -> None: """ IncrementalMixin allows setting cumulative zeta. Validating that the setting function works properly. """ @@ -102,4 +109,8 @@ def test_setting_cum() -> None: ) orig_zeta_ = ia.zeta_ ia.cum_zeta_ = ia.cum_zeta_ * 2 - assert ia.zeta_ == orig_zeta_ * 2 + assert np.allclose( + ia.zeta_.values, + (orig_zeta_ * 2).values, + atol=atol + ) From da1257e21ba9ef12281fac041810ae341b68fd97 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Mon, 21 Sep 2026 16:50:47 +0000 Subject: [PATCH 05/11] improving docstring and bugbot fix --- chainladder/adjustments/disposal.py | 20 +++++++++++++++++++- chainladder/core/triangle.py | 25 ++++++++----------------- chainladder/development/development.py | 18 ++++++++++++++++++ chainladder/development/incremental.py | 22 ++++++++++++++++++++-- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 7b13c329a..2e4e3fa06 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -340,7 +340,25 @@ def fit_transform(self, X, y=None, sample_weight=None): """ return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) - def _param_property(self, X, params): + def _param_property(self, X: Triangle, params: np.ndarray) -> Triangle: + """ + Private method to wrap a numpy array into a Triangle + + Uses and overrides DevelopmentBase._param_property. + + Parameters + ---------- + X : Triangle + The Triangle object that we want to place the parameter into + + params : np.ndarray + The parammeter we want to turn into a Triangle + + Returns + ------- + Triangle + A Triangle that mimics X but has params values + """ obj = super()._param_property(X, params) obj.values = params obj.is_disposal_rate = True diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index c85333ecf..fb44b75b1 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1394,7 +1394,7 @@ def incr_to_cum(self, inplace=False): self.is_pattern and (not self.is_additive) and (not self.is_disposal_rate) - ): # for multiplicative ldf triangles (mult from tail to head) + ): # for multiplicative ldf triangles (mult from left to right) values = xp.nan_to_num(self.values[..., ::-1]) values = num_to_value(values, 1) values = xp.cumprod(values, -1)[..., ::-1] @@ -1404,14 +1404,14 @@ def incr_to_cum(self, inplace=False): self.is_pattern and self.is_additive and (not self.is_disposal_rate) - ): # for additive factor triangles (sum from tail to head) + ): # for additive factor triangles (sum from left to right) values = xp.nan_to_num(self.values[..., ::-1]) values = num_to_value(values, 0) self.values = ( xp.cumsum(values, -1)[..., ::-1] * self.nan_triangle ) else: # for normal value triangles - # also for disposal rate triangles (sum from head to tail) + # also for disposal rate triangles (sum from right to left) if self.array_backend not in ["sparse", "dask"]: self.values = ( xp.cumsum(xp.nan_to_num(self.values), 3) @@ -1491,7 +1491,7 @@ def cum_to_incr(self, inplace=False): self.is_pattern and (not self.is_additive) and (not self.is_disposal_rate) - ): # for multiplicative ldf triangles (div from head to tail) + ): # for multiplicative ldf triangles (div from left to right) xp = self.get_array_module() self.values = num_to_value(xp.nan_to_num(self.values), 1) diff = self.iloc[..., :-1] / self.iloc[..., 1:].values @@ -1507,20 +1507,11 @@ def cum_to_incr(self, inplace=False): self.is_pattern and self.is_additive and (not self.is_disposal_rate) - ): # for additive factor triangles (diff from head to tail) - xp = self.get_array_module() - self.values = num_to_value(xp.nan_to_num(self.values), 1) - diff = self.iloc[..., :-1] - self.iloc[..., 1:].values - self = concat( - ( - diff, - self.iloc[..., -1], - ), - axis=3, - ) - self.values = self.values * self.nan_triangle + ): # for additive factor triangles (diff from left to right) + diff = self.iloc[..., :-1].values - self.iloc[..., 1:] + self = concat((diff, self.iloc[..., -1]), axis=3) else: # for normal value triangles - # also for disposal rate triangles (sum from head to tail) + # also for disposal rate triangles (diff from right to left) diff = self.iloc[..., 1:] - self.iloc[..., :-1].values self = concat((self.iloc[..., 0], diff), axis=3) self.is_cumulative = False diff --git a/chainladder/development/development.py b/chainladder/development/development.py index e9b0f8f04..3559d65cd 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -481,6 +481,24 @@ def transform(self, X): return X_new def _param_property(self, X, params, idx): + """ + Private method to wrap a numpy array into a Triangle + + Uses and overrides DevelopmentBase._param_property. + + Parameters + ---------- + X : Triangle + The Triangle object that we want to place the parameter into + + params : np.ndarray + The parammeter we want to turn into a Triangle + + Returns + ------- + Triangle + A Triangle that mimics X but has params values + """ obj = super()._param_property(X, params) xp = X.get_array_module() obj.values = xp.ones(obj.shape)[..., :-1] * params[..., idx : idx + 1, :] diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index b7af2e644..f809bc30b 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -24,7 +24,7 @@ class IncrementalMixin: @property def zeta_(self) -> Triangle: """ - Gets the estimated zeta + Gets the estimated incremental additive factors """ if not hasattr(self, "_zeta_"): x = self.__class__.__name__ @@ -45,7 +45,7 @@ def zeta_(self, value: Triangle) -> None: @property def cum_zeta_(self) -> Triangle: """ - Gets the cumulative of the estimated zeta + Gets the estimated cumulative additive factors """ return self.zeta_.incr_to_cum() @@ -375,6 +375,24 @@ def transform(self, X): return X_new def _param_property(self, X, params): + """ + Private method to wrap a numpy array into a Triangle + + Uses and overrides DevelopmentBase._param_property. + + Parameters + ---------- + X : Triangle + The Triangle object that we want to place the parameter into + + params : np.ndarray + The parammeter we want to turn into a Triangle + + Returns + ------- + Triangle + A Triangle that mimics X but has params values + """ obj = super()._param_property(X, params) obj.values = params obj.is_additive = True From 87228e613a23630a3b077e966a70f74f5818b8a3 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Mon, 21 Sep 2026 17:18:51 +0000 Subject: [PATCH 06/11] unittest & ruff fix --- chainladder/core/triangle.py | 17 ++++++++++++----- .../development/tests/test_incremental.py | 11 +---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index fb44b75b1..891eb6367 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1390,28 +1390,30 @@ def incr_to_cum(self, inplace=False): if inplace: xp = self.get_array_module() if not self.is_cumulative: + # fmt: off if ( self.is_pattern and (not self.is_additive) and (not self.is_disposal_rate) ): # for multiplicative ldf triangles (mult from left to right) + # fmt: on values = xp.nan_to_num(self.values[..., ::-1]) values = num_to_value(values, 1) values = xp.cumprod(values, -1)[..., ::-1] self.values = values * self.nan_triangle values = num_to_value(values, self.get_array_module(values).nan) + # fmt: off elif ( self.is_pattern and self.is_additive and (not self.is_disposal_rate) ): # for additive factor triangles (sum from left to right) + # fmt: on values = xp.nan_to_num(self.values[..., ::-1]) values = num_to_value(values, 0) - self.values = ( - xp.cumsum(values, -1)[..., ::-1] * self.nan_triangle - ) + self.values = (xp.cumsum(values, -1)[..., ::-1] * self.nan_triangle) else: # for normal value triangles - # also for disposal rate triangles (sum from right to left) + # also for disposal rate triangles (sum from right to left) if self.array_backend not in ["sparse", "dask"]: self.values = ( xp.cumsum(xp.nan_to_num(self.values), 3) @@ -1487,11 +1489,13 @@ def cum_to_incr(self, inplace=False): if inplace: v = self.valuation_date if self.is_cumulative or self.is_cumulative is None: + # fmt: off if ( self.is_pattern and (not self.is_additive) and (not self.is_disposal_rate) ): # for multiplicative ldf triangles (div from left to right) + # fmt: on xp = self.get_array_module() self.values = num_to_value(xp.nan_to_num(self.values), 1) diff = self.iloc[..., :-1] / self.iloc[..., 1:].values @@ -1503,15 +1507,18 @@ def cum_to_incr(self, inplace=False): axis=3, ) self.values = self.values * self.nan_triangle + # fmt: fn elif ( self.is_pattern and self.is_additive and (not self.is_disposal_rate) ): # for additive factor triangles (diff from left to right) + # fmt: on diff = self.iloc[..., :-1].values - self.iloc[..., 1:] + diff.development = self.iloc[..., :-1].development self = concat((diff, self.iloc[..., -1]), axis=3) else: # for normal value triangles - # also for disposal rate triangles (diff from right to left) + # also for disposal rate triangles (diff from right to left) diff = self.iloc[..., 1:] - self.iloc[..., :-1].values self = concat((self.iloc[..., 0], diff), axis=3) self.is_cumulative = False diff --git a/chainladder/development/tests/test_incremental.py b/chainladder/development/tests/test_incremental.py index 8c5ef4d2a..539a4fa69 100644 --- a/chainladder/development/tests/test_incremental.py +++ b/chainladder/development/tests/test_incremental.py @@ -4,11 +4,6 @@ import numpy as np import pytest -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from chainladder import Triangle - def test_schmidt(): tri = cl.load_sample("ia_sample") @@ -109,8 +104,4 @@ def test_setting_cum(atol: float) -> None: ) orig_zeta_ = ia.zeta_ ia.cum_zeta_ = ia.cum_zeta_ * 2 - assert np.allclose( - ia.zeta_.values, - (orig_zeta_ * 2).values, - atol=atol - ) + assert np.allclose(ia.zeta_.values, (orig_zeta_ * 2).values, atol=atol) From 36c04d893a72aad03624b7dccccf206d60d006d3 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Mon, 21 Sep 2026 17:36:30 +0000 Subject: [PATCH 07/11] bugbot fix --- chainladder/core/triangle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 891eb6367..083343f09 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1514,8 +1514,7 @@ def cum_to_incr(self, inplace=False): and (not self.is_disposal_rate) ): # for additive factor triangles (diff from left to right) # fmt: on - diff = self.iloc[..., :-1].values - self.iloc[..., 1:] - diff.development = self.iloc[..., :-1].development + diff = self.iloc[..., :-1] - self.iloc[..., 1:].values self = concat((diff, self.iloc[..., -1]), axis=3) else: # for normal value triangles # also for disposal rate triangles (diff from right to left) From 1eae41e3cdcdcbacdd9af2645ff33f8714140cef Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 4 Oct 2026 08:30:09 +0000 Subject: [PATCH 08/11] simplifying code --- chainladder/core/triangle.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 083343f09..62bb6ea5a 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1051,9 +1051,7 @@ def is_additive(self) -> bool: """ Indicates whether the Triangle holds a normal additive ratio (which goes from tail to head) """ - if hasattr(self, "_is_additive"): - return self._is_additive - return False + getattr(self, "_is_additive", False) @is_additive.setter def is_additive(self, is_add: bool) -> None: From c380a4ce79cde31c5779dee417cb078f24f20c2c Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 4 Oct 2026 08:39:27 +0000 Subject: [PATCH 09/11] bugbot fix --- chainladder/core/triangle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 62bb6ea5a..ade43f467 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1051,7 +1051,7 @@ def is_additive(self) -> bool: """ Indicates whether the Triangle holds a normal additive ratio (which goes from tail to head) """ - getattr(self, "_is_additive", False) + return getattr(self, "_is_additive", False) @is_additive.setter def is_additive(self, is_add: bool) -> None: From 44e2688a7d364d59e4867b4b66d9415d16b18063 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Mon, 5 Oct 2026 22:09:17 +0000 Subject: [PATCH 10/11] improving codecov --- chainladder/development/incremental.py | 3 +-- .../development/tests/test_incremental.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index f809bc30b..1b2386deb 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -27,8 +27,7 @@ def zeta_(self) -> Triangle: Gets the estimated incremental additive factors """ if not hasattr(self, "_zeta_"): - x = self.__class__.__name__ - raise AttributeError("'" + x + "' object has no attribute 'zeta_'") + raise AttributeError("`zeta_` has not been estimated.") return self._zeta_ @zeta_.setter diff --git a/chainladder/development/tests/test_incremental.py b/chainladder/development/tests/test_incremental.py index 539a4fa69..6a1c18b09 100644 --- a/chainladder/development/tests/test_incremental.py +++ b/chainladder/development/tests/test_incremental.py @@ -97,6 +97,15 @@ def test_pipeline(): def test_setting_cum(atol: float) -> None: """ IncrementalMixin allows setting cumulative zeta. Validating that the setting function works properly. + + Parameters + ---------- + atol: float + the absolute tolerance for this test + + Returns + ------- + None """ tri = cl.load_sample("ia_sample") ia = cl.IncrementalAdditive().fit( @@ -105,3 +114,20 @@ def test_setting_cum(atol: float) -> None: orig_zeta_ = ia.zeta_ ia.cum_zeta_ = ia.cum_zeta_ * 2 assert np.allclose(ia.zeta_.values, (orig_zeta_ * 2).values, atol=atol) + + +def test_no_zeta(raa: Triangle) -> None: + """ + calling zeta_ on a Triangle without _zeta_ raises + + Parameters + ---------- + raa: Triangle + the RAA sample dataset + + Returns + ------- + None + """ + with pytest.raises(AttributeError, match="has not been estimated"): + _ = raa.zeta_ From d461b140e3167921af769c8d9b8a1b72d88454d3 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Mon, 5 Oct 2026 22:10:50 +0000 Subject: [PATCH 11/11] ruff fix --- chainladder/development/tests/test_incremental.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chainladder/development/tests/test_incremental.py b/chainladder/development/tests/test_incremental.py index 6a1c18b09..6ff4aa0fe 100644 --- a/chainladder/development/tests/test_incremental.py +++ b/chainladder/development/tests/test_incremental.py @@ -4,6 +4,11 @@ import numpy as np import pytest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder.core import Triangle + def test_schmidt(): tri = cl.load_sample("ia_sample")