From d6cf65fe6c3d8c05b42bc1f205036dc5bbfe1be1 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 13 Aug 2026 09:38:47 -0700 Subject: [PATCH 1/4] docs: add Triangle arithmetic doctest examples (#704) Co-authored-by: Cursor --- chainladder/core/dunders.py | 130 ++++++++++++++++++ .../autosummary/class_inherited.rst | 1 + 2 files changed, 131 insertions(+) diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index fc899a8d9..946aa5bd8 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -247,6 +247,37 @@ def _arithmetic_mapper(self, obj, other, f): return concat(c, 0).sort_index() def __add__(self, other): + """Element-wise addition. + + Examples + -------- + Adding a scalar shifts every observed cell. + + .. testsetup:: + + import chainladder as cl + tri = cl.Triangle( + data={ + 'origin': [1985, 1985, 1986], + 'development': [1985, 1986, 1986], + 'paid': [100, 150, 80], + }, + origin='origin', + development='development', + columns=['paid'], + cumulative=True, + ) + + .. testcode:: + + print(tri + 10) + + .. testoutput:: + + 12 24 + 1985 110.0 160.0 + 1986 90.0 NaN + """ obj, other = self._validate_arithmetic(other) if isinstance(obj, TriangleGroupBy): def f(k, self, obj, other): @@ -262,6 +293,35 @@ def __radd__(self, other): return self if other == 0 else self.__add__(other) def __sub__(self, other): + """Element-wise subtraction. + + Examples + -------- + .. testsetup:: + + import chainladder as cl + tri = cl.Triangle( + data={ + 'origin': [1985, 1985, 1986], + 'development': [1985, 1986, 1986], + 'paid': [100, 150, 80], + }, + origin='origin', + development='development', + columns=['paid'], + cumulative=True, + ) + + .. testcode:: + + print(tri - 10) + + .. testoutput:: + + 12 24 + 1985 90.0 140.0 + 1986 70.0 NaN + """ obj, other = self._validate_arithmetic(other) if isinstance(obj, TriangleGroupBy): def f(k, self, obj, other): @@ -306,6 +366,35 @@ def __abs__(self): return obj def __mul__(self, other): + """Element-wise multiplication. + + Examples + -------- + .. testsetup:: + + import chainladder as cl + tri = cl.Triangle( + data={ + 'origin': [1985, 1985, 1986], + 'development': [1985, 1986, 1986], + 'paid': [100, 150, 80], + }, + origin='origin', + development='development', + columns=['paid'], + cumulative=True, + ) + + .. testcode:: + + print(tri * 2) + + .. testoutput:: + + 12 24 + 1985 200.0 300.0 + 1986 160.0 NaN + """ obj, other = self._validate_arithmetic(other) if isinstance(obj, TriangleGroupBy): def f(k, self, obj, other): @@ -347,6 +436,47 @@ def __truediv__(self, other: Any): other: Any The thing that divides the triangle. + + Examples + -------- + Dividing by a scalar scales the triangle. Dividing two columns is the + usual way to form a ratio triangle, such as paid-to-incurred. + + .. testsetup:: + + import chainladder as cl + tri = cl.Triangle( + data={ + 'origin': [1985, 1985, 1986], + 'development': [1985, 1986, 1986], + 'paid': [100, 150, 80], + 'incurred': [120, 160, 100], + }, + origin='origin', + development='development', + columns=['paid', 'incurred'], + cumulative=True, + ) + + .. testcode:: + + print(tri['paid'] / 2) + + .. testoutput:: + + 12 24 + 1985 50.0 75.0 + 1986 40.0 NaN + + .. testcode:: + + print(tri['paid'] / tri['incurred']) + + .. testoutput:: + + 12 24 + 1985 0.833333 0.9375 + 1986 0.800000 NaN """ obj, other = self._validate_arithmetic(other) if isinstance(obj, TriangleGroupBy): diff --git a/docs/_templates/autosummary/class_inherited.rst b/docs/_templates/autosummary/class_inherited.rst index ee45f6cc1..2b02d657b 100644 --- a/docs/_templates/autosummary/class_inherited.rst +++ b/docs/_templates/autosummary/class_inherited.rst @@ -6,4 +6,5 @@ :members: :inherited-members: :undoc-members: + :special-members: __add__, __sub__, __mul__, __truediv__ :exclude-members: set_fit_request, set_predict_request, set_score_request, set_transform_request, {{ attributes | join(', ') }} From 5dc1bf7c43927f56a6768dbd039c4deef642c7dd Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 13 Aug 2026 13:14:01 -0700 Subject: [PATCH 2/4] Move Triangle construction in arithmetic examples from testsetup into testcode. testsetup stays as the chainladder import only, matching the other API doctest examples. Co-authored-by: Cursor --- chainladder/core/dunders.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index 946aa5bd8..763ead5ca 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -256,6 +256,9 @@ def __add__(self, other): .. testsetup:: import chainladder as cl + + .. testcode:: + tri = cl.Triangle( data={ 'origin': [1985, 1985, 1986], @@ -267,9 +270,6 @@ def __add__(self, other): columns=['paid'], cumulative=True, ) - - .. testcode:: - print(tri + 10) .. testoutput:: @@ -300,6 +300,9 @@ def __sub__(self, other): .. testsetup:: import chainladder as cl + + .. testcode:: + tri = cl.Triangle( data={ 'origin': [1985, 1985, 1986], @@ -311,9 +314,6 @@ def __sub__(self, other): columns=['paid'], cumulative=True, ) - - .. testcode:: - print(tri - 10) .. testoutput:: @@ -373,6 +373,9 @@ def __mul__(self, other): .. testsetup:: import chainladder as cl + + .. testcode:: + tri = cl.Triangle( data={ 'origin': [1985, 1985, 1986], @@ -384,9 +387,6 @@ def __mul__(self, other): columns=['paid'], cumulative=True, ) - - .. testcode:: - print(tri * 2) .. testoutput:: @@ -445,6 +445,9 @@ def __truediv__(self, other: Any): .. testsetup:: import chainladder as cl + + .. testcode:: + tri = cl.Triangle( data={ 'origin': [1985, 1985, 1986], @@ -457,9 +460,6 @@ def __truediv__(self, other: Any): columns=['paid', 'incurred'], cumulative=True, ) - - .. testcode:: - print(tri['paid'] / 2) .. testoutput:: From ef001ef5c124232651979c27ffe995b351455311 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 13 Aug 2026 20:51:18 -0700 Subject: [PATCH 3/4] Fix E721/E722/F841 in TriangleDunders and align the autosummary template. The ruff workflow lints touched files with per-file ignores cleared. The template unions documented Triangle attrs and arithmetic dunders so sibling #704 PRs do not clobber each other on merge. Co-authored-by: Cursor --- chainladder/core/dunders.py | 5 ++--- docs/_templates/autosummary/class_inherited.rst | 5 ++++- pyproject.toml | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index 763ead5ca..352c2dd04 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -101,7 +101,7 @@ def _prep_index(self, x, y): x = x.sort_index() try: y = y.loc[x.index] - except: + except Exception: x = x.groupby(list(common)) y = y.groupby(list(common)) return x, y @@ -206,7 +206,7 @@ def _prep_origin_development(self, obj, other): other_arr.shape = (other.shape[0], other.shape[1], len(odims), len(ddims)) obj_arr.shape = (self.shape[0], self.shape[1], len(odims), len(ddims)) obj.odims = np.array(odims.index) - if type(obj.ddims) == pd.DatetimeIndex: + if isinstance(obj.ddims, pd.DatetimeIndex): obj.ddims = pd.DatetimeIndex(ddims.index) else: obj.ddims = np.array(ddims.index) @@ -402,7 +402,6 @@ def f(k, self, obj, other): self._slice_or_nan(other, obj, k)) obj = self._arithmetic_mapper(obj, other, f) else: - xp = obj.get_array_module() obj.values = obj.values * other return obj diff --git a/docs/_templates/autosummary/class_inherited.rst b/docs/_templates/autosummary/class_inherited.rst index 2b02d657b..41fc413c4 100644 --- a/docs/_templates/autosummary/class_inherited.rst +++ b/docs/_templates/autosummary/class_inherited.rst @@ -2,9 +2,12 @@ .. currentmodule:: {{ module }} +{% set documented_attrs = ['loc', 'iloc', 'at', 'iat', 'shape', 'empty', 'dimensionality', 'nan_triangle'] %} +{% set hidden_attrs = attributes | reject('in', documented_attrs) | list %} + .. autoclass:: {{ objname }} :members: :inherited-members: :undoc-members: :special-members: __add__, __sub__, __mul__, __truediv__ - :exclude-members: set_fit_request, set_predict_request, set_score_request, set_transform_request, {{ attributes | join(', ') }} + :exclude-members: set_fit_request, set_predict_request, set_score_request, set_transform_request{% if hidden_attrs %}, {{ hidden_attrs | join(', ') }}{% endif %} diff --git a/pyproject.toml b/pyproject.toml index 815c125f6..50dea2f92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,6 @@ select = ["E4", "E7", "E9", "F"] "chainladder/core/base.py" = ["E721"] "chainladder/core/common.py" = ["F401"] "chainladder/core/correlation.py" = ["E741"] -"chainladder/core/dunders.py" = ["E721", "E722", "F841"] "chainladder/core/io.py" = ["E731"] "chainladder/core/pandas.py" = ["E721", "F841"] "chainladder/core/slice.py" = ["E712", "E721", "E741"] From ccf54c07c704a60731fd35b0737ff19d1ec5cfad Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Fri, 14 Aug 2026 15:02:48 -0700 Subject: [PATCH 4/4] Normalize whitespace in arithmetic doctest outputs. CI pandas pads the development-age header with extra spaces; the cell values already matched. Co-authored-by: Cursor --- chainladder/core/dunders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index 352c2dd04..22e273932 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -273,6 +273,7 @@ def __add__(self, other): print(tri + 10) .. testoutput:: + :options: +NORMALIZE_WHITESPACE 12 24 1985 110.0 160.0 @@ -317,6 +318,7 @@ def __sub__(self, other): print(tri - 10) .. testoutput:: + :options: +NORMALIZE_WHITESPACE 12 24 1985 90.0 140.0 @@ -390,6 +392,7 @@ def __mul__(self, other): print(tri * 2) .. testoutput:: + :options: +NORMALIZE_WHITESPACE 12 24 1985 200.0 300.0 @@ -462,6 +465,7 @@ def __truediv__(self, other: Any): print(tri['paid'] / 2) .. testoutput:: + :options: +NORMALIZE_WHITESPACE 12 24 1985 50.0 75.0 @@ -472,6 +476,7 @@ def __truediv__(self, other: Any): print(tri['paid'] / tri['incurred']) .. testoutput:: + :options: +NORMALIZE_WHITESPACE 12 24 1985 0.833333 0.9375