From d3961da2f8694231a3bd7b149ac09ec3b1a4f7a1 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 10 Oct 2026 18:49:00 +0000 Subject: [PATCH 01/14] implementation + test --- chainladder/core/base.py | 10 +- chainladder/core/pandas.py | 4 +- chainladder/core/tests/test_triangle.py | 29 +++- chainladder/core/triangle.py | 167 +++++++++++++----------- 4 files changed, 130 insertions(+), 80 deletions(-) diff --git a/chainladder/core/base.py b/chainladder/core/base.py index d15d6cd16..eec756cc3 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -666,12 +666,16 @@ def get_array_module( + ", ".join([*modules]) ) from e - def _auto_sparse(self) -> Triangle: + def _auto_sparse(self) -> None: """ Auto sparsifies at 30Mb or more and 20% density or less. + + Returns + ------- + None """ if not options.AUTO_SPARSE: - return self + return None n = np.prod(list(self.shape) + [8 / 1e6]) if ( self.array_backend == "numpy" @@ -683,7 +687,7 @@ def _auto_sparse(self) -> Triangle: self.values.density < 0.2 and n > 30 ): self.set_backend("numpy", inplace=True) - return self + return None @property def valuation(self): diff --git a/chainladder/core/pandas.py b/chainladder/core/pandas.py index 56e300999..24afdc236 100644 --- a/chainladder/core/pandas.py +++ b/chainladder/core/pandas.py @@ -1313,7 +1313,7 @@ def agg_func( ) obj._set_slicers() if auto_sparse: - obj = obj._auto_sparse() + obj._auto_sparse() obj.values = cast("BackendArray", num_to_nan(obj.values)) if not keepdims and obj.shape == (1, 1, 1, 1): return obj.values[0, 0, 0, 0] @@ -1380,7 +1380,7 @@ def aggregate(i, obj, axis, v): obj.odims = odims.values obj._set_slicers() if auto_sparse: - obj = obj._auto_sparse() + obj._auto_sparse() return obj set_method(cls=cls, func=agg_func, k=k) diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index cd3515747..002738257 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -3433,7 +3433,7 @@ def test_feed_age_into_valuation_raises() -> None: def test_fill(clrd: Triangle) -> None: """ - ``Fill`` method works as intended + ``fill`` method works as intended """ fill_tri = clrd.iloc[2:4, 4:6].fill(100) # (10 + 1) * 10 / 2 is the number of valid values in one single triangle @@ -3445,8 +3445,33 @@ def test_fill(clrd: Triangle) -> None: def test_full_fill(raa: Triangle) -> None: """ - ``Fill`` method works as intended on full triangle + ``fill`` method works as intended on full triangle """ full_tri = cl.Chainladder().fit(raa).full_triangle_ fill_full_tri = full_tri.fill(200) assert np.all(fill_full_tri.values == np.broadcast_to([200], (1, 1, 10, 12))) + + +def test_dev_val_inplace(raa: Triangle) -> None: + """ + ``dev_to_val`` and ``val_to_dev`` methods respact ``inplace`` + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + raa_copy = raa.copy() + raa_copy2 = raa.copy() + assert raa_copy2.dev_to_val(True) is None + assert raa_copy == raa + assert raa_copy2 == raa_copy.dev_to_val() + raa_copy = raa.copy().dev_to_val() + raa_copy2 = raa_copy.copy() + assert raa_copy2.val_to_dev(True) is None + assert raa_copy2 == raa + assert raa_copy == raa_copy2.dev_to_val() diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 38efe3324..0cfa643e1 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -722,7 +722,7 @@ def __init__( if not options.AUTO_SPARSE or array_backend == "cupy": self.set_backend(backend=array_backend, inplace=True) else: - self = self._auto_sparse() + self._auto_sparse() self._set_slicers() # Deal with special properties if self.is_pattern: @@ -1599,37 +1599,49 @@ def _dstep(self): "Y": {"Y": 1}, } - def _val_dev(self, sign, inplace=False): + def _val_dev(self, sign: int) -> None: + """ + Helper function for mutating triangle from a development lag + triangle to a valuation triangle. + + Parameters + ---------- + sign : int (1 or -1) + Whether to mutate the existing Triangle instance or return a new + one. + + Returns + ------- + None + """ backend = self.array_backend - obj = self.set_backend("sparse") - if not inplace: - obj.values = obj.values.copy() - scale = self._dstep()[obj.development_grain][obj.origin_grain] - offset = np.arange(obj.shape[-2]) * scale + self.set_backend("sparse", True) + scale = self._dstep()[self.development_grain][self.origin_grain] + offset = np.arange(self.shape[-2]) * scale min_slide = -offset.max() - if (obj.values.coords[-2] == np.arange(1)).all(): + if (self.values.coords[-2] == np.arange(1)).all(): # Unique edge case #239 offset = offset[-1:] * sign - offset = offset[obj.values.coords[-2]] * sign # [0] - obj.values.coords[-1] = obj.values.coords[-1] + offset - ddims = obj.valuation[obj.valuation <= obj.valuation_date] + offset = offset[self.values.coords[-2]] * sign # [0] + self.values.coords[-1] = self.values.coords[-1] + offset + ddims = self.valuation[self.valuation <= self.valuation_date] ddims = len(ddims.drop_duplicates()) if ddims == 1 and sign == -1: - ddims = len(obj.odims) - if obj.values.density > 0: - if obj.values.coords[-1].min() < 0: - obj.values.coords[-1] = obj.values.coords[-1] - min( - obj.values.coords[-1].min(), min_slide + ddims = len(self.odims) + if self.values.density > 0: + if self.values.coords[-1].min() < 0: + self.values.coords[-1] = self.values.coords[-1] - min( + self.values.coords[-1].min(), min_slide ) - ddims = np.max([np.max(obj.values.coords[-1]) + 1, ddims]) - obj.values.shape = tuple(list(obj.shape[:-1]) + [ddims]) + ddims = np.max([np.max(self.values.coords[-1]) + 1, ddims]) + self.values.shape = tuple(list(self.shape[:-1]) + [ddims]) if not options.AUTO_SPARSE or backend == "cupy": - obj = obj.set_backend(backend) + self.set_backend(backend, True) else: - obj = obj._auto_sparse() - return obj + self._auto_sparse() + return None - def dev_to_val(self, inplace=False): + def dev_to_val(self, inplace: bool = False) -> Triangle | None: """ Converts triangle from a development lag triangle to a valuation triangle. @@ -1642,8 +1654,12 @@ def dev_to_val(self, inplace=False): Returns ------- - Triangle - Updated instance of the triangle with valuation periods. + Triangle | None + If ``inplace=False``, returns new instance of ``Triangle`` with + valuation periods. + + If ``inplace=True``, ``Triangle`` is mutated in place and ``None`` + is returned Examples -------- @@ -1688,34 +1704,32 @@ def dev_to_val(self, inplace=False): 2012 NaN NaN NaN NaN NaN 5102.0 9650.0 2013 NaN NaN NaN NaN NaN NaN 6283.0 """ - if self.is_val_tri: - if inplace: - return self - else: - return self.copy() - is_cumulative = self.is_cumulative - if self.is_full: - if is_cumulative: - obj = self.cum_to_incr(inplace=inplace) - else: - obj = self.copy() - if self.is_ultimate: - ultimate = obj.iloc[..., -1:] - obj = obj.iloc[..., :-1] + if inplace: + if self.is_val_tri: + return None + is_cumulative = self.is_cumulative + if self.is_full: + if is_cumulative: + _ = self.cum_to_incr(inplace=True) + if self.is_ultimate: + ultimate = self.iloc[..., -1:] + self = self.iloc[..., :-1] + self._val_dev(1) + ddims = self.valuation[self.valuation <= self.valuation_date] + self.ddims = ddims.drop_duplicates().sort_values() + if self.is_full: + if self.is_ultimate: + ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) + self = concat((self, ultimate), -1) + if is_cumulative: + _ = self.incr_to_cum(True) + return None else: - obj = self - obj = obj._val_dev(1, inplace) - ddims = obj.valuation[obj.valuation <= obj.valuation_date] - obj.ddims = ddims.drop_duplicates().sort_values() - if self.is_full: - if self.is_ultimate: - ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) - obj = concat((obj, ultimate), -1) - if is_cumulative: - obj = obj.incr_to_cum(inplace=inplace) - return obj + obj = self.copy() + obj.dev_to_val(True) + return obj - def val_to_dev(self, inplace=False): + def val_to_dev(self, inplace: bool = False) -> Triangle | None: """ Converts triangle from a valuation triangle to a development lag triangle. @@ -1728,7 +1742,12 @@ def val_to_dev(self, inplace=False): Returns ------- - Updated instance of triangle with development lags + Triangle | None + If ``inplace=False``, returns new instance of ``Triangle`` with + development lags. + + If ``inplace=True``, ``Triangle`` is mutated in place and ``None`` + is returned Examples -------- @@ -1756,30 +1775,32 @@ def val_to_dev(self, inplace=False): 2012 5102.0 9650.0 NaN NaN NaN NaN NaN 2013 6283.0 NaN NaN NaN NaN NaN NaN """ - if not self.is_val_tri: - if inplace: - return self + if inplace: + if not self.is_val_tri: + return None + if self.is_ultimate and self.shape[-1] > 1: + ultimate = self.iloc[..., -1:] + ultimate.ddims = np.array([9999]) + self.iloc[..., :-1]._val_dev(-1) else: - return self.copy() - if self.is_ultimate and self.shape[-1] > 1: - ultimate = self.iloc[..., -1:] - ultimate.ddims = np.array([9999]) - obj = self.iloc[..., :-1]._val_dev(-1, inplace) - else: - obj = self.copy()._val_dev(-1, inplace) - val_0 = obj.valuation[0] - if self.ddims.shape[-1] == 1 and self.ddims[0] == self.valuation_date: - origin_0 = pd.to_datetime(obj.odims[-1]) + self._val_dev(-1) + val_0 = self.valuation[0] + if self.ddims.shape[-1] == 1 and self.ddims[0] == self.valuation_date: + origin_0 = pd.to_datetime(self.odims[-1]) + else: + origin_0 = pd.to_datetime(self.odims[0]) + lag_0 = (val_0.year - origin_0.year) * 12 + val_0.month - origin_0.month + 1 + scale = self._dstep()["M"][self.development_grain] + self.ddims = np.arange(self.values.shape[-1]) * scale + lag_0 + prune = self[self.origin == self.origin.max()] + if self.is_ultimate and self.shape[-1] > 1: + self = self.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] + self = concat((self, ultimate), -1) + return None else: - origin_0 = pd.to_datetime(obj.odims[0]) - lag_0 = (val_0.year - origin_0.year) * 12 + val_0.month - origin_0.month + 1 - scale = self._dstep()["M"][obj.development_grain] - obj.ddims = np.arange(obj.values.shape[-1]) * scale + lag_0 - prune = obj[obj.origin == obj.origin.max()] - if self.is_ultimate and self.shape[-1] > 1: - obj = obj.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] - obj = concat((obj, ultimate), -1) - return obj + obj = self.copy() + obj.val_to_dev(True) + return obj def grain(self, grain="", trailing=False, inplace=False): """ From 3ad56a7d737177e66f98e0a101bb650520563988 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 10 Oct 2026 19:05:58 +0000 Subject: [PATCH 02/14] ruff and bugbot --- chainladder/core/triangle.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 0cfa643e1..9430b5983 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1708,17 +1708,19 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: if self.is_val_tri: return None is_cumulative = self.is_cumulative - if self.is_full: + is_full = self.is_full + is_ultimate = self.is_ultimate + if is_full: if is_cumulative: _ = self.cum_to_incr(inplace=True) - if self.is_ultimate: + if is_ultimate: ultimate = self.iloc[..., -1:] self = self.iloc[..., :-1] self._val_dev(1) ddims = self.valuation[self.valuation <= self.valuation_date] self.ddims = ddims.drop_duplicates().sort_values() - if self.is_full: - if self.is_ultimate: + if is_full: + if is_ultimate: ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) self = concat((self, ultimate), -1) if is_cumulative: @@ -1778,14 +1780,17 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: if inplace: if not self.is_val_tri: return None - if self.is_ultimate and self.shape[-1] > 1: + is_ultimate = self.is_ultimate + val_cnt = self.shape[-1] + valuation_date = self.valuation_date + if is_ultimate and val_cnt > 1: ultimate = self.iloc[..., -1:] ultimate.ddims = np.array([9999]) self.iloc[..., :-1]._val_dev(-1) else: self._val_dev(-1) val_0 = self.valuation[0] - if self.ddims.shape[-1] == 1 and self.ddims[0] == self.valuation_date: + if self.ddims.shape[-1] == 1 and self.ddims[0] == valuation_date: origin_0 = pd.to_datetime(self.odims[-1]) else: origin_0 = pd.to_datetime(self.odims[0]) @@ -1793,7 +1798,8 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: scale = self._dstep()["M"][self.development_grain] self.ddims = np.arange(self.values.shape[-1]) * scale + lag_0 prune = self[self.origin == self.origin.max()] - if self.is_ultimate and self.shape[-1] > 1: + if is_ultimate and val_cnt > 1: + prune = self[self.origin == self.origin.max()] self = self.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] self = concat((self, ultimate), -1) return None From a1b78d7c8b8a8587f067462c58fab1dbf8891fa5 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 10 Oct 2026 23:24:33 +0000 Subject: [PATCH 03/14] actual inplace mutation --- chainladder/core/base.py | 1 - chainladder/core/tests/test_triangle.py | 10 +++++----- chainladder/core/triangle.py | 22 +++++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/chainladder/core/base.py b/chainladder/core/base.py index eec756cc3..21e5a1cb4 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -32,7 +32,6 @@ from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: - from chainladder import Triangle from pandas import DataFrame, Series from numpy.typing import ArrayLike from pandas.core.indexes.datetimes import DatetimeIndex diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 002738257..3a2cd8206 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -472,10 +472,9 @@ def test_dev_to_val_inplace_on_val_tri_returns_self(qtr: Triangle) -> None: """ val_tri = qtr.dev_to_val() assert val_tri.is_val_tri - - result = val_tri.dev_to_val(inplace=True) - - assert result is val_tri + new_val_tri = val_tri.copy() + new_val_tri.dev_to_val(inplace=True) + assert new_val_tri is val_tri def test_valdev2(qtr): @@ -1011,7 +1010,8 @@ def test_auto_sparse_disabled_returns_self(prism: Triangle) -> None: dense = small_prism.set_backend("numpy") cl.options.set_option("AUTO_SPARSE", False) try: - result = dense._auto_sparse() + result = dense.copy() + result._auto_sparse() assert result is dense assert result.array_backend == "numpy" finally: diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 9430b5983..6d36f55ca 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1715,14 +1715,16 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: _ = self.cum_to_incr(inplace=True) if is_ultimate: ultimate = self.iloc[..., -1:] - self = self.iloc[..., :-1] + self.values = self.values.iloc[..., :-1] self._val_dev(1) ddims = self.valuation[self.valuation <= self.valuation_date] self.ddims = ddims.drop_duplicates().sort_values() if is_full: if is_ultimate: ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) - self = concat((self, ultimate), -1) + obj = concat((self, ultimate), -1) + self.values = obj.values + self.ddims = obj.ddims if is_cumulative: _ = self.incr_to_cum(True) return None @@ -1786,9 +1788,10 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: if is_ultimate and val_cnt > 1: ultimate = self.iloc[..., -1:] ultimate.ddims = np.array([9999]) - self.iloc[..., :-1]._val_dev(-1) + obj = self.iloc[..., :-1] else: - self._val_dev(-1) + obj = self.copy() + obj._val_dev(-1) val_0 = self.valuation[0] if self.ddims.shape[-1] == 1 and self.ddims[0] == valuation_date: origin_0 = pd.to_datetime(self.odims[-1]) @@ -1796,12 +1799,13 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: origin_0 = pd.to_datetime(self.odims[0]) lag_0 = (val_0.year - origin_0.year) * 12 + val_0.month - origin_0.month + 1 scale = self._dstep()["M"][self.development_grain] - self.ddims = np.arange(self.values.shape[-1]) * scale + lag_0 - prune = self[self.origin == self.origin.max()] + obj.ddims = np.arange(obj.values.shape[-1]) * scale + lag_0 if is_ultimate and val_cnt > 1: - prune = self[self.origin == self.origin.max()] - self = self.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] - self = concat((self, ultimate), -1) + prune = obj[obj.origin == obj.origin.max()] + obj = obj.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] + obj = concat((obj, ultimate), -1) + self.values = obj.values + self.ddims = obj.ddims return None else: obj = self.copy() From 87d6863188f2531a2888f1c07fba4f9aa46dc14c Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 10 Oct 2026 23:36:10 +0000 Subject: [PATCH 04/14] test fixes --- chainladder/core/tests/test_triangle.py | 5 ++--- chainladder/core/triangle.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 3a2cd8206..2547621d6 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -474,7 +474,7 @@ def test_dev_to_val_inplace_on_val_tri_returns_self(qtr: Triangle) -> None: assert val_tri.is_val_tri new_val_tri = val_tri.copy() new_val_tri.dev_to_val(inplace=True) - assert new_val_tri is val_tri + assert new_val_tri == val_tri def test_valdev2(qtr): @@ -1012,7 +1012,6 @@ def test_auto_sparse_disabled_returns_self(prism: Triangle) -> None: try: result = dense.copy() result._auto_sparse() - assert result is dense assert result.array_backend == "numpy" finally: cl.options.reset_option("AUTO_SPARSE") @@ -3454,7 +3453,7 @@ def test_full_fill(raa: Triangle) -> None: def test_dev_val_inplace(raa: Triangle) -> None: """ - ``dev_to_val`` and ``val_to_dev`` methods respact ``inplace`` + ``dev_to_val`` and ``val_to_dev`` methods respect ``inplace`` Parameters ---------- diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 6d36f55ca..19ba40d28 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1715,18 +1715,20 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: _ = self.cum_to_incr(inplace=True) if is_ultimate: ultimate = self.iloc[..., -1:] - self.values = self.values.iloc[..., :-1] - self._val_dev(1) - ddims = self.valuation[self.valuation <= self.valuation_date] - self.ddims = ddims.drop_duplicates().sort_values() + obj = self.iloc[..., :-1] + else: + obj = self.copy() + obj._val_dev(1) + ddims = obj.valuation[obj.valuation <= obj.valuation_date] + obj.ddims = ddims.drop_duplicates().sort_values() if is_full: if is_ultimate: ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) - obj = concat((self, ultimate), -1) - self.values = obj.values - self.ddims = obj.ddims + obj = concat((obj, ultimate), -1) if is_cumulative: - _ = self.incr_to_cum(True) + _ = obj.incr_to_cum(True) + self.values = obj.values + self.ddims = obj.ddims return None else: obj = self.copy() From 8cbd6fe8a6683569109d4706186eda08784c817f Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 10 Oct 2026 23:42:11 +0000 Subject: [PATCH 05/14] test fix --- chainladder/core/triangle.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 19ba40d28..dc555859c 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1710,14 +1710,13 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: is_cumulative = self.is_cumulative is_full = self.is_full is_ultimate = self.is_ultimate + obj = self.copy() if is_full: if is_cumulative: - _ = self.cum_to_incr(inplace=True) + _ = obj.cum_to_incr(inplace=True) if is_ultimate: ultimate = self.iloc[..., -1:] obj = self.iloc[..., :-1] - else: - obj = self.copy() obj._val_dev(1) ddims = obj.valuation[obj.valuation <= obj.valuation_date] obj.ddims = ddims.drop_duplicates().sort_values() From f50e718677d03505d35bac59b58535fffbe963b2 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 01:33:20 +0000 Subject: [PATCH 06/14] bugbox fixes --- chainladder/core/triangle.py | 46 ++++++++++++++---------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index dc555859c..fa07a2ee7 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1707,25 +1707,19 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: if inplace: if self.is_val_tri: return None - is_cumulative = self.is_cumulative - is_full = self.is_full - is_ultimate = self.is_ultimate - obj = self.copy() - if is_full: - if is_cumulative: - _ = obj.cum_to_incr(inplace=True) - if is_ultimate: - ultimate = self.iloc[..., -1:] - obj = self.iloc[..., :-1] + obj = self.cum_to_incr() + if self.is_full and self.is_ultimate: + ultimate = obj.iloc[..., -1:] + obj = obj.iloc[..., :-1] obj._val_dev(1) ddims = obj.valuation[obj.valuation <= obj.valuation_date] obj.ddims = ddims.drop_duplicates().sort_values() - if is_full: - if is_ultimate: + if self.is_full: + if self.is_ultimate: ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) obj = concat((obj, ultimate), -1) - if is_cumulative: - _ = obj.incr_to_cum(True) + if self.is_cumulative: + obj = obj.incr_to_cum() self.values = obj.values self.ddims = obj.ddims return None @@ -1783,25 +1777,21 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: if inplace: if not self.is_val_tri: return None - is_ultimate = self.is_ultimate - val_cnt = self.shape[-1] - valuation_date = self.valuation_date - if is_ultimate and val_cnt > 1: - ultimate = self.iloc[..., -1:] + obj = self.copy() + if self.is_ultimate and self.shape[-1] > 1: + ultimate = obj.iloc[..., -1:] ultimate.ddims = np.array([9999]) - obj = self.iloc[..., :-1] - else: - obj = self.copy() + obj = obj.iloc[..., :-1] obj._val_dev(-1) - val_0 = self.valuation[0] - if self.ddims.shape[-1] == 1 and self.ddims[0] == valuation_date: - origin_0 = pd.to_datetime(self.odims[-1]) + val_0 = obj.valuation[0] + if self.ddims.shape[-1] == 1 and self.ddims[0] == self.valuation_date: + origin_0 = pd.to_datetime(obj.odims[-1]) else: - origin_0 = pd.to_datetime(self.odims[0]) + origin_0 = pd.to_datetime(obj.odims[0]) lag_0 = (val_0.year - origin_0.year) * 12 + val_0.month - origin_0.month + 1 - scale = self._dstep()["M"][self.development_grain] + scale = self._dstep()["M"][obj.development_grain] obj.ddims = np.arange(obj.values.shape[-1]) * scale + lag_0 - if is_ultimate and val_cnt > 1: + if self.is_ultimate and self.shape[-1] > 1: prune = obj[obj.origin == obj.origin.max()] obj = obj.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] obj = concat((obj, ultimate), -1) From 941e828de588a4419c40a1267969b401cbe2d4cb Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 01:44:14 +0000 Subject: [PATCH 07/14] ruff fix --- chainladder/core/triangle.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index fa07a2ee7..beeb1c289 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1714,12 +1714,11 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: obj._val_dev(1) ddims = obj.valuation[obj.valuation <= obj.valuation_date] obj.ddims = ddims.drop_duplicates().sort_values() - if self.is_full: - if self.is_ultimate: - ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) - obj = concat((obj, ultimate), -1) - if self.is_cumulative: - obj = obj.incr_to_cum() + if self.is_full and self.is_ultimate: + ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) + obj = concat((obj, ultimate), -1) + if self.is_cumulative: + obj = obj.incr_to_cum() self.values = obj.values self.ddims = obj.ddims return None @@ -1777,7 +1776,7 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: if inplace: if not self.is_val_tri: return None - obj = self.copy() + obj = self.copy() if self.is_ultimate and self.shape[-1] > 1: ultimate = obj.iloc[..., -1:] ultimate.ddims = np.array([9999]) From 76cf401e602ea3928e9af0fef8ee9a64f466da44 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 03:14:13 +0000 Subject: [PATCH 08/14] fixed long-standing _val_dev backend bug --- chainladder/core/triangle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index beeb1c289..9da8300d5 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1635,10 +1635,7 @@ def _val_dev(self, sign: int) -> None: ) ddims = np.max([np.max(self.values.coords[-1]) + 1, ddims]) self.values.shape = tuple(list(self.shape[:-1]) + [ddims]) - if not options.AUTO_SPARSE or backend == "cupy": - self.set_backend(backend, True) - else: - self._auto_sparse() + self.set_backend(backend, True) return None def dev_to_val(self, inplace: bool = False) -> Triangle | None: @@ -1714,11 +1711,14 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: obj._val_dev(1) ddims = obj.valuation[obj.valuation <= obj.valuation_date] obj.ddims = ddims.drop_duplicates().sort_values() + print(type(obj.values)) if self.is_full and self.is_ultimate: ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) obj = concat((obj, ultimate), -1) + print(type(obj.values)) if self.is_cumulative: obj = obj.incr_to_cum() + print(type(obj.values)) self.values = obj.values self.ddims = obj.ddims return None From 1d44bd4a243470af63281fcb30e58ef4f294ea54 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 03:34:21 +0000 Subject: [PATCH 09/14] fixing various tests now that _val_dev preserves backend --- chainladder/core/tests/test_grain.py | 5 +++-- chainladder/core/tests/test_triangle.py | 22 ++++++++++++++++++---- chainladder/core/triangle.py | 5 +---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/chainladder/core/tests/test_grain.py b/chainladder/core/tests/test_grain.py index df395c9f3..217cfb31e 100644 --- a/chainladder/core/tests/test_grain.py +++ b/chainladder/core/tests/test_grain.py @@ -11,7 +11,6 @@ def test_grain(qtr): - # this test is dense only in practice, since grain() applies auto_sparse, which is True by default actual = qtr.iloc[0, 0].grain("OYDY") nan = np.nan expected = np.array([ @@ -28,7 +27,9 @@ def test_grain(qtr): [21, 422, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], [13, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], ]) - np.testing.assert_array_equal(actual.values[0, 0, :, :], expected) + np.testing.assert_array_equal( + actual.loc[0,0].to_frame().values[0, 0, :, :], expected + ) def test_grain_returns_valid_tri(qtr): diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 2547621d6..7294aeb7a 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -490,8 +490,22 @@ def test_valdev3(qtr): def test_valdev4(raa: Triangle) -> None: - lhs = raa.dev_to_val()[raa.dev_to_val().development >= "1989"].values.flatten() - rhs = raa[raa.valuation >= "1989"].dev_to_val().values.flatten() + """ + Tests slicing using the development axis of a valuation Triangle + + Parameters + ---------- + raa : Triangle + The raa sample dataset Triangle. + + Returns + ------- + None + """ + lhs = raa.dev_to_val()[ + raa.dev_to_val().development >= "1989" + ].to_frame().values.flatten() + rhs = raa[raa.valuation >= "1989"].dev_to_val().to_frame().values.flatten() np.testing.assert_array_equal(lhs[~np.isnan(lhs)], rhs[~np.isnan(rhs)]) @@ -1164,9 +1178,9 @@ def test_auto_sparse_converts_numpy_to_sparse(prism: Triangle) -> None: dense = small_prism.set_backend("numpy") assert dense.array_backend == "numpy" - result = dense._auto_sparse() + result = dense.copy() + result._auto_sparse() - assert result is dense assert result.array_backend == "sparse" diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 9da8300d5..bcc0e5907 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1711,14 +1711,11 @@ def dev_to_val(self, inplace: bool = False) -> Triangle | None: obj._val_dev(1) ddims = obj.valuation[obj.valuation <= obj.valuation_date] obj.ddims = ddims.drop_duplicates().sort_values() - print(type(obj.values)) if self.is_full and self.is_ultimate: ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) obj = concat((obj, ultimate), -1) - print(type(obj.values)) if self.is_cumulative: obj = obj.incr_to_cum() - print(type(obj.values)) self.values = obj.values self.ddims = obj.ddims return None @@ -1776,7 +1773,7 @@ def val_to_dev(self, inplace: bool = False) -> Triangle | None: if inplace: if not self.is_val_tri: return None - obj = self.copy() + obj = self.copy() if self.is_ultimate and self.shape[-1] > 1: ultimate = obj.iloc[..., -1:] ultimate.ddims = np.array([9999]) From 562da2328a5affe0b6261b63d1e4a8ffa9bc6246 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 03:37:39 +0000 Subject: [PATCH 10/14] ruff --- chainladder/core/tests/test_grain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/core/tests/test_grain.py b/chainladder/core/tests/test_grain.py index 217cfb31e..88d1b5acc 100644 --- a/chainladder/core/tests/test_grain.py +++ b/chainladder/core/tests/test_grain.py @@ -28,7 +28,7 @@ def test_grain(qtr): [13, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], ]) np.testing.assert_array_equal( - actual.loc[0,0].to_frame().values[0, 0, :, :], expected + actual.loc[0, 0].to_frame().values, expected ) From 09740cca1033be77b8ea0dfe9d9d269206fd2f5d Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 03:43:29 +0000 Subject: [PATCH 11/14] test fix --- chainladder/core/tests/test_grain.py | 4 +--- chainladder/core/tests/test_triangle.py | 11 +++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/chainladder/core/tests/test_grain.py b/chainladder/core/tests/test_grain.py index 88d1b5acc..52bf3264f 100644 --- a/chainladder/core/tests/test_grain.py +++ b/chainladder/core/tests/test_grain.py @@ -27,9 +27,7 @@ def test_grain(qtr): [21, 422, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], [13, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], ]) - np.testing.assert_array_equal( - actual.loc[0, 0].to_frame().values, expected - ) + np.testing.assert_array_equal(actual.loc[0, 0].to_frame().values, expected) def test_grain_returns_valid_tri(qtr): diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 7294aeb7a..60c92f26b 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -502,10 +502,13 @@ def test_valdev4(raa: Triangle) -> None: ------- None """ - lhs = raa.dev_to_val()[ - raa.dev_to_val().development >= "1989" - ].to_frame().values.flatten() - rhs = raa[raa.valuation >= "1989"].dev_to_val().to_frame().values.flatten() + lhs = ( + raa + .dev_to_val()[raa.dev_to_val().development >= "1989"] + .to_frame() + .values.flatten() + ) + rhs = (raa[raa.valuation >= "1989"].dev_to_val().to_frame().values.flatten() np.testing.assert_array_equal(lhs[~np.isnan(lhs)], rhs[~np.isnan(rhs)]) From 911de3824c561badad8e30e265c5f78160f6d18d Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 03:45:38 +0000 Subject: [PATCH 12/14] typo --- chainladder/core/tests/test_triangle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 60c92f26b..be79d12b5 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -508,7 +508,7 @@ def test_valdev4(raa: Triangle) -> None: .to_frame() .values.flatten() ) - rhs = (raa[raa.valuation >= "1989"].dev_to_val().to_frame().values.flatten() + rhs = raa[raa.valuation >= "1989"].dev_to_val().to_frame().values.flatten() np.testing.assert_array_equal(lhs[~np.isnan(lhs)], rhs[~np.isnan(rhs)]) From f56678dce5f89bdd1025b22a6d92d548fb04a472 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 03:53:39 +0000 Subject: [PATCH 13/14] test fix --- chainladder/core/tests/test_grain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/core/tests/test_grain.py b/chainladder/core/tests/test_grain.py index 52bf3264f..261463d17 100644 --- a/chainladder/core/tests/test_grain.py +++ b/chainladder/core/tests/test_grain.py @@ -27,7 +27,7 @@ def test_grain(qtr): [21, 422, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], [13, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], ]) - np.testing.assert_array_equal(actual.loc[0, 0].to_frame().values, expected) + np.testing.assert_array_equal(actual.to_frame().values, expected) def test_grain_returns_valid_tri(qtr): From 3279003f75df4dc5c308b3e7d52214cb4b3135df Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 11 Oct 2026 04:13:14 +0000 Subject: [PATCH 14/14] improving docstrings --- chainladder/core/tests/test_grain.py | 14 +++++++++++++- chainladder/core/triangle.py | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/chainladder/core/tests/test_grain.py b/chainladder/core/tests/test_grain.py index 261463d17..c732f261d 100644 --- a/chainladder/core/tests/test_grain.py +++ b/chainladder/core/tests/test_grain.py @@ -10,7 +10,19 @@ from chainladder import Triangle -def test_grain(qtr): +def test_grain(qtr: Triangle) -> None: + """ + Tests quarterly to annual grain conversion + + Parameters + ---------- + qtr : Triangle + The qtr sample dataset Triangle. + + Returns + ------- + None + """ actual = qtr.iloc[0, 0].grain("OYDY") nan = np.nan expected = np.array([ diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index bcc0e5907..bac96c082 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1601,14 +1601,14 @@ def _dstep(self): def _val_dev(self, sign: int) -> None: """ - Helper function for mutating triangle from a development lag - triangle to a valuation triangle. + Helper function for mutating triangle between development lag + and valuation. Parameters ---------- sign : int (1 or -1) - Whether to mutate the existing Triangle instance or return a new - one. + Whether to mutate the existing Triangle from development lag + to valuation (1) or from valuation to development lag (-1) Returns -------