From 8cd4c0b67758cf898f4949ed847119678ca339a8 Mon Sep 17 00:00:00 2001 From: danielfong-act Date: Mon, 20 Apr 2026 14:16:50 -0700 Subject: [PATCH 1/4] added _pattern attribute to Triangle. Converted is_pattern to property with setter. Added is_pattern and is_ultimate abstract properties to TriangleBase --- chainladder/core/base.py | 18 ++++++++++++++++-- chainladder/core/triangle.py | 10 +++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/chainladder/core/base.py b/chainladder/core/base.py index e50bb7f2..514595c0 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -9,6 +9,8 @@ import numpy as np import warnings +from abc import ABC, abstractmethod + from chainladder import options from chainladder.core.common import Common @@ -44,7 +46,8 @@ class TriangleBase( TriangleSlicer, TriangleDunders, TrianglePandas, - Common + Common, + ABC ): """This class handles the initialization of a triangle""" @@ -309,6 +312,16 @@ def _get_date_axes( ) return c[c["__development__"] > c["__origin__"]] + + @property + @abstractmethod + def is_pattern(self): + raise NotImplementedError + + @property + @abstractmethod + def is_ultimate(self): + raise NotImplementedError @property def nan_triangle(self): @@ -317,7 +330,8 @@ def nan_triangle(self): This becomes useful when managing array arithmetic. """ xp = self.get_array_module() - if min(self.values.shape[2:]) == 1: +# if min(self.values.shape[2:]) == 1: + if self.is_pattern or self.is_ultimate: return xp.ones(self.values.shape[2:], dtype="float16") val_array = np.array(self.valuation).reshape(self.shape[-2:], order="f") nan_triangle = np.array(pd.DataFrame(val_array) > self.valuation_date) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 09a3361b..30c9b9bf 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -251,7 +251,7 @@ def __init__( self.is_cumulative: bool = cumulative self.virtual_columns = VirtualColumns(self) - self.is_pattern: bool = pattern + self._pattern: bool = pattern split: list[str] = self.origin_grain.split("-") self.origin_grain: str = {"A": "Y", "2Q": "S"}.get(split[0], split[0]) @@ -484,6 +484,14 @@ def is_full(self) -> bool: return self.nan_triangle.sum().sum() == np.prod(self.shape[-2:]) + + @property + def is_pattern(self): + return self._pattern + @is_pattern.setter + def is_pattern(self,pattern): + self._pattern = pattern + @property def is_ultimate(self): return sum(self.valuation >= options.ULT_VAL[:4]) > 0 From 88011ac907addd3898a82a27556e8acc95ecfd04 Mon Sep 17 00:00:00 2001 From: danielfong-act Date: Mon, 20 Apr 2026 14:27:02 -0700 Subject: [PATCH 2/4] remove commented out line --- chainladder/core/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chainladder/core/base.py b/chainladder/core/base.py index 514595c0..6a59c514 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -330,7 +330,6 @@ def nan_triangle(self): This becomes useful when managing array arithmetic. """ xp = self.get_array_module() -# if min(self.values.shape[2:]) == 1: if self.is_pattern or self.is_ultimate: return xp.ones(self.values.shape[2:], dtype="float16") val_array = np.array(self.valuation).reshape(self.shape[-2:], order="f") From 2efef59ded5acba8b1748b164081ed107fe517f4 Mon Sep 17 00:00:00 2001 From: danielfong-act Date: Tue, 21 Apr 2026 22:28:18 -0700 Subject: [PATCH 3/4] added annotations to properties --- chainladder/core/triangle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 30c9b9bf..856d0500 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -486,14 +486,15 @@ def is_full(self) -> bool: @property - def is_pattern(self): + def is_pattern(self) -> bool: return self._pattern + @is_pattern.setter - def is_pattern(self,pattern): + def is_pattern(self, pattern: bool): self._pattern = pattern @property - def is_ultimate(self): + def is_ultimate(self) -> np.bool: return sum(self.valuation >= options.ULT_VAL[:4]) > 0 @property From a25ea4c88a4fa5ca4b66d5abdbb5f506898adf34 Mon Sep 17 00:00:00 2001 From: danielfong-act Date: Wed, 22 Apr 2026 14:30:55 -0700 Subject: [PATCH 4/4] added annotations to TriangleBase properties. Added test for 2x2 triangles calling cum_to_incr(). --- chainladder/core/base.py | 4 ++-- chainladder/core/tests/test_triangle.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/chainladder/core/base.py b/chainladder/core/base.py index 6a59c514..d05e29cd 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -315,12 +315,12 @@ def _get_date_axes( @property @abstractmethod - def is_pattern(self): + def is_pattern(self) -> bool: raise NotImplementedError @property @abstractmethod - def is_ultimate(self): + def is_ultimate(self) -> bool: raise NotImplementedError @property diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 4422c614..aee7834d 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -949,4 +949,22 @@ def test_fillzero(): raa = cl.load_sample('raa') zero = raa - raa[raa.origin=='1982'] filled = zero.fillzero() - assert (filled[filled.origin == '1982'][filled.development == 24].values.flatten()[0]) == 0 \ No newline at end of file + assert (filled[filled.origin == '1982'][filled.development == 24].values.flatten()[0]) == 0 + +def test_2x2_triangle(): + + df = pd.DataFrame(data={ + 'origin': [2022, 2022, 2023], + 'development': [2022, 2023, 2023], + 'reported': [78000, 222000, 78000]} + ) + tri_from_df = cl.Triangle( + data=df, + origin='origin', + development='development', + columns=['reported'], + cumulative=True + ) + tri_from_df + assert np.array_equal(tri_from_df.cum_to_incr().values,np.array([[[[ 78000., 144000.], + [ 78000., np.float64(np.nan)]]]]), equal_nan=True) \ No newline at end of file