From 1fa8b496379214d96d35caad2dfec79b671fc050 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Thu, 18 Jun 2026 17:25:47 +0000 Subject: [PATCH 01/24] WIP --- chainladder/adjustments/disposal.py | 227 ++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 chainladder/adjustments/disposal.py diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py new file mode 100644 index 00000000..20c99ac7 --- /dev/null +++ b/chainladder/adjustments/disposal.py @@ -0,0 +1,227 @@ +# 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 chainladder.methods.chainladder import Chainladder +from sklearn.base import BaseEstimator, TransformerMixin +import numpy as np +import copy +import warnings +from chainladder.core.io import EstimatorIO +from chainladder.utils import TriangleWeight + + +class DisposalRate(BaseEstimator, TransformerMixin, EstimatorIO): + """ + Class to alter the bottom of a full Triangle using the Disposal Rate method described + by Friedland. + + Parameters + ---------- + n_periods: integer, optional (default = -1) + number of origin periods to be used in the ldf average calculation. For + all origin periods, set n_periods = -1 + drop: tuple or list of tuples + Drops specific origin/development combination(s). See order of operations + below when combined with multiple drop parameters. + drop_high: bool, int, list of bools, or list of ints (default = None) + Drops highest (by rank) link ratio(s) from LDF calculation + If a boolean variable is passed, drop_high is set to 1, dropping only the + highest value. Protected by ``preserve``. + See order of operations below when combined with multiple drop parameters. + drop_low: bool, int, list of bools, or list of ints (default = None) + Drops lowest (by rank) link ratio(s) from LDF calculation + If a boolean variable is passed, drop_low is set to 1, dropping only the + lowest value. Protected by ``preserve``. + See order of operations below when combined with multiple drop parameters. + drop_above: float or list of floats (default = numpy.inf) + Drops all link ratio(s) above the given parameter from the LDF calculation. + Protected by ``preserve``. + See order of operations below when combined with multiple drop parameters. + drop_below: float or list of floats (default = 0.00) + Drops all link ratio(s) below the given parameter from the LDF calculation. + Protected by ``preserve``. + See order of operations below when combined with multiple drop parameters. + preserve: int (default = 1) + The minimum number of link ratio(s) required for LDF calculation. + See order of operations below when combined with multiple drop parameters. + drop_valuation: str or list of str (default = None) + Drops specific valuation periods. str must be date convertible. + See order of operations below when combined with multiple drop parameters. + + .. note :: + + (Order of Drop Operations) + + When multiple drop parameters are used together, the weights are built in this order (steps 4 and 5 are reversed from `Development`): + + 1. ``n_periods`` — limit to the most recent origin periods. + 2. ``drop`` — remove specific origin/development cells. + 3. ``drop_valuation`` — remove entire valuation diagonal in the triangle. + 4. ``drop_above`` / ``drop_below`` — remove link ratios outside a range + (Protected by``preserve``, which may relax exclusions from this step if too few ratios would remain + then this step is skipped). + 5. ``drop_high`` / ``drop_low`` — remove highest/lowest link ratios by rank + (eligible factors from ``n_periods`` are used; protected by ``preserve``, + which may relax exclusions from this step if too few ratios would remain then this step is skipped). + 6. Calculate the loss development factors using ``average`` method. + + Attributes + ---------- + disposal_rate_tri: Triangle + actual disposal rates by origin and development + + disposal_rate_: Triangle + fitted disposal rates + + Examples + -------- + ``trend`` tilts the case-adequacy adjustment before ``Incurred`` is rebuilt; + on the ``MedMal`` slice the inner diagonals of the adjusted ``Incurred`` + triangle restate materially between ``0%`` and ``15%`` annual drift, while + the latest diagonal is preserved. + + .. testsetup:: + + import chainladder as cl + import numpy as np + + .. testcode:: + + tri = cl.load_sample("berqsherm").loc["MedMal"] + base = cl.BerquistSherman( + paid_amount="Paid", + incurred_amount="Incurred", + reported_count="Reported", + closed_count="Closed", + trend=0.0, + ).fit(tri) + tilted = cl.BerquistSherman( + paid_amount="Paid", + incurred_amount="Incurred", + reported_count="Reported", + closed_count="Closed", + trend=0.15, + ).fit(tri) + print(np.round(base.adjusted_triangle_["Incurred"], 0)) + + .. testoutput:: + :options: +NORMALIZE_WHITESPACE + + 12 24 36 48 60 72 84 96 + 1969 9883293.0 27420103.0 35879085.0 43105257.0 33438702.0 30397324.0 25723694.0 23506000.0 + 1970 8641763.0 31305782.0 41543535.0 48550616.0 38203864.0 36222888.0 32216000.0 NaN + 1971 11733960.0 43887171.0 61649896.0 64917222.0 51410209.0 48377000.0 NaN NaN + 1972 13638651.0 50987209.0 66696278.0 72777529.0 61163000.0 NaN NaN NaN + 1973 14387930.0 45470590.0 56577593.0 73733000.0 NaN NaN NaN NaN + 1974 13630366.0 47189379.0 63477000.0 NaN NaN NaN NaN NaN + 1975 15036351.0 48904000.0 NaN NaN NaN NaN NaN NaN + 1976 15791000.0 NaN NaN NaN NaN NaN NaN NaN + + .. testcode:: + + print(np.round(tilted.adjusted_triangle_["Incurred"], 0)) + + .. testoutput:: + :options: +NORMALIZE_WHITESPACE + + 12 24 36 48 60 72 84 96 + 1969 3793504.0 12084942.0 18563821.0 25924316.0 23516364.0 24979245.0 24016864.0 23506000.0 + 1970 3760482.0 15830500.0 24615996.0 33169802.0 30722141.0 33362729.0 32216000.0 NaN + 1971 5982185.0 25583831.0 41384825.0 50323342.0 46191356.0 48377000.0 NaN NaN + 1972 7819355.0 33794110.0 51361061.0 64559286.0 61163000.0 NaN NaN NaN + 1973 9533246.0 34585431.0 49667342.0 73733000.0 NaN NaN NaN NaN + 1974 10348458.0 41241243.0 63477000.0 NaN NaN NaN NaN NaN + 1975 13102479.0 48904000.0 NaN NaN NaN NaN NaN NaN + 1976 15791000.0 NaN NaN NaN NaN NaN NaN NaN + + """ + + def __init__( + self, + n_periods: int = -1, + drop: tuple | list[tuple] | None = None, + drop_high: bool | int | list[bool] | list[int] | None = None, + drop_low: bool | int | list[bool] | list[int] | None = None, + preserve: int = 1, + drop_valuation: str | list[str] = None, + drop_above: float = np.inf, + drop_below: float = 0.00, + ): + self.n_periods = n_periods + self.drop_high = drop_high + self.drop_low = drop_low + self.preserve = preserve + self.drop_valuation = drop_valuation + self.drop_above = drop_above + self.drop_below = drop_below + self.drop = drop + + def fit(self, X, y=None, sample_weight=None): + #check for ultimate_ + if hasattr(X, "ultimate_"): + pass + else: + raise ValueError("X must have ultimate_") + #convert to numpy + if X.array_backend == "sparse": + X = X.set_backend("numpy").incr_to_cum() + else: + X = X.copy().incr_to_cum() + if X.ultimate_.array_backend == "sparse": + ult = X.ultimate_.set_backend("numpy") + else: + X = X.ultimate_.copy() + #get backend + self.xp = X.get_array_module() + self.disposal_rate_tri = X / ult.values + tw = TriangleWeight( + n_periods = self.n_periods, + drop_high = self.drop_high, + drop_low = self.drop_low, + drop_above = self.drop_above, + drop_below = self.drop_below, + drop_valuation = self.drop_valuation, + preserve = self.preserve, + drop = self.drop + ) + if hasattr(X, "w_"): + self.w_ = tw.fit(X=self.disposal_rate_tri * X.w_).w_.values + else: + self.w_ = tw.fit(X=self.disposal_rate_tri).w_.values + #calculate factors + super().fit(ult.values,self.disposal_rate_tri.values,self.w_) + #keep attributes + self.zeta_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) + return self + + def predict(self, X): + """ If X and self are of different shapes, align self to X, else + return self. + + Parameters + ---------- + X: Triangle + The triangle to be transformed + + Returns + ------- + X_new: New triangle with transformed attributes. + """ + X_new = copy.deepcopy(X) + X_new[self.paid_amount] = self.adjusted_triangle_[self.paid_amount] + X_new[self.incurred_amount] = self.adjusted_triangle_[self.incurred_amount] + X_new[self.reported_count] = self.adjusted_triangle_[self.reported_count] + X_new[self.closed_count] = self.adjusted_triangle_[self.closed_count] + X_new.a_ = self.a_ + X_new.b_ = self.b_ + return X_new + + def set_params(self, **params): + from chainladder.utils.utility_functions import read_json + + if type(params["reported_count_estimator"]) is str: + params["reported_count_estimator"] = read_json( + params["reported_count_estimator"] + ) + return super().set_params(**params) From 562024e45f17383457e70bd937bd11850dbc3b98 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 19 Jun 2026 01:31:15 +0000 Subject: [PATCH 02/24] making progress --- chainladder/__init__.py | 1 + chainladder/adjustments/__init__.py | 2 ++ chainladder/adjustments/disposal.py | 14 +++++++------- chainladder/tests/test_public_api.py | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/chainladder/__init__.py b/chainladder/__init__.py index ea6f1aa2..a32a7a1b 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -445,6 +445,7 @@ def describe_option(self, pat: str = "", _print_desc: bool=True) -> None | str: ParallelogramOLF, Trend, TrendConstant, + DisposalRate, ) from chainladder.tails import ( # noqa (API import) TailBase, diff --git a/chainladder/adjustments/__init__.py b/chainladder/adjustments/__init__.py index 06b6d161..ad3a660b 100644 --- a/chainladder/adjustments/__init__.py +++ b/chainladder/adjustments/__init__.py @@ -3,6 +3,7 @@ from chainladder.adjustments.parallelogram import ParallelogramOLF # noqa (API import) from chainladder.adjustments.trend import Trend # noqa (API import) from chainladder.adjustments.trend import TrendConstant # noqa (API import) +from chainladder.adjustments.disposal import DisposalRate # noqa (API import) __all__ = [ "BootstrapODPSample", @@ -10,4 +11,5 @@ "ParallelogramOLF", "Trend", "TrendConstant", + "DisposalRate" ] diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 20c99ac7..c35bad8e 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -2,16 +2,15 @@ # 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 chainladder.methods.chainladder import Chainladder -from sklearn.base import BaseEstimator, TransformerMixin +from chainladder.methods import Chainladder +from chainladder.development import DevelopmentBase import numpy as np import copy import warnings -from chainladder.core.io import EstimatorIO from chainladder.utils import TriangleWeight -class DisposalRate(BaseEstimator, TransformerMixin, EstimatorIO): +class DisposalRate(TriangleWeight): """ Class to alter the bottom of a full Triangle using the Disposal Rate method described by Friedland. @@ -140,11 +139,12 @@ class DisposalRate(BaseEstimator, TransformerMixin, EstimatorIO): def __init__( self, n_periods: int = -1, + average: str | list[str] = 'volume', drop: tuple | list[tuple] | None = None, drop_high: bool | int | list[bool] | list[int] | None = None, drop_low: bool | int | list[bool] | list[int] | None = None, preserve: int = 1, - drop_valuation: str | list[str] = None, + drop_valuation: str | list[str] | None = None, drop_above: float = np.inf, drop_below: float = 0.00, ): @@ -171,7 +171,7 @@ def fit(self, X, y=None, sample_weight=None): if X.ultimate_.array_backend == "sparse": ult = X.ultimate_.set_backend("numpy") else: - X = X.ultimate_.copy() + ult = X.ultimate_.copy() #get backend self.xp = X.get_array_module() self.disposal_rate_tri = X / ult.values @@ -190,7 +190,7 @@ def fit(self, X, y=None, sample_weight=None): else: self.w_ = tw.fit(X=self.disposal_rate_tri).w_.values #calculate factors - super().fit(ult.values,self.disposal_rate_tri.values,self.w_) + super().fit(ult,self.disposal_rate_tri,self.w_) #keep attributes self.zeta_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) return self diff --git a/chainladder/tests/test_public_api.py b/chainladder/tests/test_public_api.py index f01c629d..6ea1c331 100644 --- a/chainladder/tests/test_public_api.py +++ b/chainladder/tests/test_public_api.py @@ -56,6 +56,7 @@ "ParallelogramOLF", "Trend", "TrendConstant", + "DisposalRate", # tails "TailBase", "TailConstant", From a85c673f42bf585665bf78dba9ac7f01605339df Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 19 Jun 2026 01:44:25 +0000 Subject: [PATCH 03/24] promoting _param_property --- chainladder/development/base.py | 48 ++++++++++++++++++++++---- chainladder/development/incremental.py | 16 +-------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/chainladder/development/base.py b/chainladder/development/base.py index 47c13081..7e34fd7f 100644 --- a/chainladder/development/base.py +++ b/chainladder/development/base.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from chainladder.core.typing import TriangleLike + from chainladder.core import Triangle class DevelopmentBase( @@ -45,18 +45,18 @@ def fit(self, X, y=None, sample_weight=None): def _set_fit_groups( self, - X: TriangleLike - ) -> TriangleLike: + X: Triangle + ) -> Triangle: """ Used for assigning group_index in fit. Parameters ---------- - X: TriangleLike + X: Triangle Returns ------- - TriangleLike, after performing the groupby on it. + Triangle, after performing the groupby on it. """ backend = "numpy" if X.array_backend in ["sparse", "numpy"] else "cupy" @@ -383,4 +383,40 @@ def _drop(self, X): np.where(X.origin == item[0])[0][0], np.where(X.development == item[1])[0][0], ] = 0 - return arr[:, :-1] \ No newline at end of file + return arr[:, :-1] + + @staticmethod + def _param_property( + self, + X: Triangle, + params: np.ndarray + ) -> Triangle: + """ + Wrap an array of estimated parameters in a Triangle + + Parameters + ---------- + X: Triangle + The Triangle to wrap the parameters with + + params: np.ndarray + The parameters to be wrapped + + Returns + ------- + Triangle + The wrapped parameters + + """ + from chainladder import options + + obj: Triangle = X[X.origin == X.origin.min()] + xp = X.get_array_module() + 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/incremental.py b/chainladder/development/incremental.py index a79df253..90e80777 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -294,18 +294,4 @@ def transform(self, X): X_new = X.copy() for item in ["ldf_", "w_", "zeta_", "incremental_", "tri_zeta", "fit_zeta_", "sample_weight"]: X_new.__dict__[item] = self.__dict__[item] - return X_new - - def _param_property(self, factor, params): - from chainladder import options - - obj = factor[factor.origin == factor.origin.min()] - xp = factor.get_array_module() - 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 + return X_new \ No newline at end of file From d18939452e70f39404c3ec9ba7aa7b03bb2c676c Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 19 Jun 2026 01:58:09 +0000 Subject: [PATCH 04/24] fixing bug --- chainladder/development/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chainladder/development/base.py b/chainladder/development/base.py index 7e34fd7f..49c1192e 100644 --- a/chainladder/development/base.py +++ b/chainladder/development/base.py @@ -387,7 +387,6 @@ def _drop(self, X): @staticmethod def _param_property( - self, X: Triangle, params: np.ndarray ) -> Triangle: From 40f977f5b5879835b246baa09ccc1e0ce3d371c9 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 19 Jun 2026 06:09:50 +0000 Subject: [PATCH 05/24] full implementation w/ test --- chainladder/adjustments/disposal.py | 115 +++++++++++++----- .../adjustments/tests/test_disposal.py | 31 +++++ 2 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 chainladder/adjustments/tests/test_disposal.py diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index c35bad8e..560bb20e 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -7,12 +7,12 @@ import numpy as np import copy import warnings -from chainladder.utils import TriangleWeight +from chainladder.utils import TriangleWeight, concat +from chainladder import Triangle - -class DisposalRate(TriangleWeight): +class DisposalRate(DevelopmentBase): """ - Class to alter the bottom of a full Triangle using the Disposal Rate method described + Calculates the bottom of a fitted full_triangle_ using the Disposal Rate method described by Friedland. Parameters @@ -70,9 +70,12 @@ class DisposalRate(TriangleWeight): disposal_rate_tri: Triangle actual disposal rates by origin and development - disposal_rate_: Triangle + disposal_: Triangle fitted disposal rates + incr_disposal_: Triangle + incremental of disposal_ + Examples -------- ``trend`` tilts the case-adequacy adjustment before ``Incurred`` is rebuilt; @@ -149,6 +152,7 @@ def __init__( drop_below: float = 0.00, ): self.n_periods = n_periods + self.average = average self.drop_high = drop_high self.drop_low = drop_low self.preserve = preserve @@ -157,21 +161,41 @@ def __init__( self.drop_below = drop_below self.drop = drop - def fit(self, X, y=None, sample_weight=None): - #check for ultimate_ - if hasattr(X, "ultimate_"): - pass - else: - raise ValueError("X must have ultimate_") + def fit( + self, + X:Triangle, + y:None=None, + sample_weight:Triangle|None=None + ): + """ + Estimate disposal rate for a given Triangle and ultimate + + Parameters + ---------- + X : Triangle + Triangle to which the Disposal Rate method is applied + y : None + Ignored + sample_weight : Triangle + Ultimate + + Returns + ------- + self : object + Returns the instance itself. + + """ + if sample_weight is None: + raise ValueError("sample_weight is required.") #convert to numpy if X.array_backend == "sparse": X = X.set_backend("numpy").incr_to_cum() else: X = X.copy().incr_to_cum() - if X.ultimate_.array_backend == "sparse": - ult = X.ultimate_.set_backend("numpy") + if sample_weight.array_backend == "sparse": + ult = sample_weight.set_backend("numpy") else: - ult = X.ultimate_.copy() + ult = sample_weight.copy() #get backend self.xp = X.get_array_module() self.disposal_rate_tri = X / ult.values @@ -190,12 +214,22 @@ def fit(self, X, y=None, sample_weight=None): else: self.w_ = tw.fit(X=self.disposal_rate_tri).w_.values #calculate factors - super().fit(ult,self.disposal_rate_tri,self.w_) + super().fit(ult.values,X.values,self.w_) #keep attributes - self.zeta_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) + self.disposal_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) + self.disposal_ = concat((self.disposal_,(ult/ult).iloc[:,:,0,:].rename("development", [9999])),axis=3) + self.disposal_.is_cumulative = True + self.disposal_.is_pattern = False + self.incr_disposal_ = self.disposal_.cum_to_incr() + self.incr_disposal_.is_pattern = True + self.disposal_.is_pattern = True return self - def predict(self, X): + def transform( + self, + X: Triangle, + sample_weight: Triangle | None = None + ) -> Triangle: """ If X and self are of different shapes, align self to X, else return self. @@ -204,24 +238,45 @@ def predict(self, X): X: Triangle The triangle to be transformed + sample_weight: Triangle + Ultimate + Returns ------- X_new: New triangle with transformed attributes. """ + if sample_weight is None: + raise ValueError("sample_weight is required.") X_new = copy.deepcopy(X) - X_new[self.paid_amount] = self.adjusted_triangle_[self.paid_amount] - X_new[self.incurred_amount] = self.adjusted_triangle_[self.incurred_amount] - X_new[self.reported_count] = self.adjusted_triangle_[self.reported_count] - X_new[self.closed_count] = self.adjusted_triangle_[self.closed_count] - X_new.a_ = self.a_ - X_new.b_ = self.b_ + X_new.disposal_rate_tri = self.disposal_rate_tri + X_new.disposal_ = self.disposal_ + X_new.incr_disposal_ = self.incr_disposal_ + X_new.ultimate_ = sample_weight.latest_diagonal + ibnr_pct = 1 - X_new.disposal_.align_pattern(X_new.disposal_rate_tri) + run_off = X_new.incr_disposal_ / ibnr_pct * X_new.ibnr_ + run_off = run_off[run_off.valuation > X_new.valuation_date] + X_new.ldf_ = (X_new.cum_to_incr() + run_off).incr_to_cum().age_to_age return X_new + + def fit_transform(self, X, y=None, sample_weight=None): + """Fit and return predictions for VotingChainladder - def set_params(self, **params): - from chainladder.utils.utility_functions import read_json + Parameters + ---------- + X : Triangle + Loss data to which the model will be applied. - if type(params["reported_count_estimator"]) is str: - params["reported_count_estimator"] = read_json( - params["reported_count_estimator"] - ) - return super().set_params(**params) + y : None + Ignored + + sample_weight : Triangle, default=None + Ultimate + + Returns + ------- + X_new: Triangle + Loss data with VotingChainladder ultimate applied + """ + return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) + def _test(self, X, ult): + return 'test' \ No newline at end of file diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py new file mode 100644 index 00000000..db04959f --- /dev/null +++ b/chainladder/adjustments/tests/test_disposal.py @@ -0,0 +1,31 @@ +import chainladder as cl +import numpy as np + + +def test_disposal(): + tri = cl.load_sample('friedland_gl_insurer')['Closed Claim Counts'] + ult_tri = cl.Triangle( + data = { + 'Closed Claim Counts':[873,720,626,629,588,553,438,609], + 'ay': [2001,2002,2003,2004,2005,2006,2007,2008], + 'dev':[2008,2008,2008,2008,2008,2008,2008,2008], + }, + origin = 'ay', + development='dev', + columns='Closed Claim Counts', + cumulative=True, + ) + dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri) + assert np.all(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001) + lhs = (dr.full_triangle_.cum_to_incr()-tri.cum_to_incr()).round(0).values.flatten() + rhs = np.array([ + 77., + 24., 70., + 12., 18., 54., + 46., 13., 19., 57., + 52., 45., 13., 19., 56., + 76., 49., 43., 12., 18., 54., + 67., 55., 36., 31., 9., 13., 39., + 140., 91., 75., 49., 42., 12., 18., 53. + ]) + assert np.all(lhs[~np.isnan(lhs)] - rhs <= 1) \ No newline at end of file From 7ea90e09bd3cf00f8154eb98dda3bbdfae46102b Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 19 Jun 2026 06:42:28 +0000 Subject: [PATCH 06/24] bugbot and tests --- chainladder/adjustments/disposal.py | 9 +++------ chainladder/adjustments/tests/test_disposal.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 560bb20e..ed1fa61e 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -6,7 +6,6 @@ from chainladder.development import DevelopmentBase import numpy as np import copy -import warnings from chainladder.utils import TriangleWeight, concat from chainladder import Triangle @@ -259,7 +258,7 @@ def transform( return X_new def fit_transform(self, X, y=None, sample_weight=None): - """Fit and return predictions for VotingChainladder + """Fit and return transformed full_triangle_ based on the Disposal Rate Parameters ---------- @@ -275,8 +274,6 @@ def fit_transform(self, X, y=None, sample_weight=None): Returns ------- X_new: Triangle - Loss data with VotingChainladder ultimate applied + Triangle with new full_triangle_ """ - return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) - def _test(self, X, ult): - return 'test' \ No newline at end of file + return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight) \ No newline at end of file diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index db04959f..322c6d34 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -16,7 +16,7 @@ def test_disposal(): cumulative=True, ) dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri) - assert np.all(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001) + assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001)) lhs = (dr.full_triangle_.cum_to_incr()-tri.cum_to_incr()).round(0).values.flatten() rhs = np.array([ 77., @@ -28,4 +28,15 @@ def test_disposal(): 67., 55., 36., 31., 9., 13., 39., 140., 91., 75., 49., 42., 12., 18., 53. ]) - assert np.all(lhs[~np.isnan(lhs)] - rhs <= 1) \ No newline at end of file + assert np.all(abs(lhs[~np.isnan(lhs)] - rhs <= 1)) + +def test_disposal_no_weight(raa): + tri = raa.set_backend('sparse') + with pytest.raises(ValueError): + dr = cl.DisposalRate().fit(tri) + ult = cl.Chainladder().fit(tri).ultimate_ + dr = cl.DisposalRate().fit(tri,sample_weight=ult) + with pytest.raises(ValueError): + est = dr.transform(tri) + + \ No newline at end of file From 252878e9aa3a270a8b9454ab75c45f719ca82f94 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 19 Jun 2026 06:51:29 +0000 Subject: [PATCH 07/24] test fix --- chainladder/adjustments/tests/test_disposal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 322c6d34..fd899173 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -1,6 +1,6 @@ import chainladder as cl import numpy as np - +import pytest def test_disposal(): tri = cl.load_sample('friedland_gl_insurer')['Closed Claim Counts'] From c6588fe50fca5397cd467842d140e92c87988034 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 20 Jun 2026 23:22:33 +0000 Subject: [PATCH 08/24] docstring and additional tests --- chainladder/adjustments/disposal.py | 96 ++++++++++--------- .../adjustments/tests/test_disposal.py | 22 +++-- 2 files changed, 66 insertions(+), 52 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index ed1fa61e..4c6be658 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -77,11 +77,9 @@ class DisposalRate(DevelopmentBase): Examples -------- - ``trend`` tilts the case-adequacy adjustment before ``Incurred`` is rebuilt; - on the ``MedMal`` slice the inner diagonals of the adjusted ``Incurred`` - triangle restate materially between ``0%`` and ``15%`` annual drift, while - the latest diagonal is preserved. - + This adjustment method re-apportions future loss emergence based on a '% of ultimate' emergence pattern. + The ultimate can come from another triangle. A common use case is to forecast payment pattern based on incurred ultimate. + .. testsetup:: import chainladder as cl @@ -89,52 +87,60 @@ class DisposalRate(DevelopmentBase): .. testcode:: - tri = cl.load_sample("berqsherm").loc["MedMal"] - base = cl.BerquistSherman( - paid_amount="Paid", - incurred_amount="Incurred", - reported_count="Reported", - closed_count="Closed", - trend=0.0, - ).fit(tri) - tilted = cl.BerquistSherman( - paid_amount="Paid", - incurred_amount="Incurred", - reported_count="Reported", - closed_count="Closed", - trend=0.15, - ).fit(tri) - print(np.round(base.adjusted_triangle_["Incurred"], 0)) + clrd = cl.load_sample('clrd').sum() + ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_ + dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult) + + Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`. + + .. testcode:: + + dr.disposal_rate_tri .. testoutput:: - :options: +NORMALIZE_WHITESPACE - - 12 24 36 48 60 72 84 96 - 1969 9883293.0 27420103.0 35879085.0 43105257.0 33438702.0 30397324.0 25723694.0 23506000.0 - 1970 8641763.0 31305782.0 41543535.0 48550616.0 38203864.0 36222888.0 32216000.0 NaN - 1971 11733960.0 43887171.0 61649896.0 64917222.0 51410209.0 48377000.0 NaN NaN - 1972 13638651.0 50987209.0 66696278.0 72777529.0 61163000.0 NaN NaN NaN - 1973 14387930.0 45470590.0 56577593.0 73733000.0 NaN NaN NaN NaN - 1974 13630366.0 47189379.0 63477000.0 NaN NaN NaN NaN NaN - 1975 15036351.0 48904000.0 NaN NaN NaN NaN NaN NaN - 1976 15791000.0 NaN NaN NaN NaN NaN NaN NaN + + 12 24 36 48 60 72 84 96 108 120 + 1988 0.313923 0.619459 0.774429 0.865377 0.919077 0.948898 0.964643 0.973184 0.980224 0.983063 + 1989 0.321526 0.626023 0.781086 0.872345 0.924842 0.952533 0.967690 0.977373 0.981938 NaN + 1990 0.329567 0.634056 0.790752 0.880273 0.927029 0.952951 0.968379 0.976049 NaN NaN + 1991 0.330035 0.636233 0.791888 0.881010 0.929460 0.954694 0.968533 NaN NaN NaN + 1992 0.342613 0.650521 0.801875 0.885976 0.932865 0.956495 NaN NaN NaN NaN + 1993 0.353784 0.663303 0.810639 0.894414 0.939009 NaN NaN NaN NaN NaN + 1994 0.367530 0.670460 0.814661 0.897244 NaN NaN NaN NaN NaN NaN + 1995 0.379650 0.680979 0.821603 NaN NaN NaN NaN NaN NaN NaN + 1996 0.395603 0.688621 NaN NaN NaN NaN NaN NaN NaN NaN + 1997 0.393820 NaN NaN NaN NaN NaN NaN NaN NaN NaN + + The estimated pattern is stored in `disposal_`. .. testcode:: - print(np.round(tilted.adjusted_triangle_["Incurred"], 0)) + dr.disposal_ + + .. testoutput:: + + 12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult 132-Ult + (All) 0.112105 0.336242 0.545897 0.693774 0.812877 0.905045 0.942998 0.974365 0.990868 1.0 1.0 + `full_triangle_` now reflects the disposal-rate-based forecast. + + .. testcode:: + + dr.full_triangle_ + .. testoutput:: - :options: +NORMALIZE_WHITESPACE - - 12 24 36 48 60 72 84 96 - 1969 3793504.0 12084942.0 18563821.0 25924316.0 23516364.0 24979245.0 24016864.0 23506000.0 - 1970 3760482.0 15830500.0 24615996.0 33169802.0 30722141.0 33362729.0 32216000.0 NaN - 1971 5982185.0 25583831.0 41384825.0 50323342.0 46191356.0 48377000.0 NaN NaN - 1972 7819355.0 33794110.0 51361061.0 64559286.0 61163000.0 NaN NaN NaN - 1973 9533246.0 34585431.0 49667342.0 73733000.0 NaN NaN NaN NaN - 1974 10348458.0 41241243.0 63477000.0 NaN NaN NaN NaN NaN - 1975 13102479.0 48904000.0 NaN NaN NaN NaN NaN NaN - 1976 15791000.0 NaN NaN NaN NaN NaN NaN NaN + + 12 24 36 48 60 72 84 96 108 120 9999 + 1988 3577780.0 7.059966e+06 8.826151e+06 9.862687e+06 1.047470e+07 1.081458e+07 1.099401e+07 1.109136e+07 1.117159e+07 1.120395e+07 1.139698e+07 + 1989 4090680.0 7.964702e+06 9.937520e+06 1.109859e+07 1.176649e+07 1.211879e+07 1.231163e+07 1.243483e+07 1.249290e+07 1.251646e+07 1.272270e+07 + 1990 4578442.0 8.808486e+06 1.098535e+07 1.222900e+07 1.287854e+07 1.323867e+07 1.345299e+07 1.355956e+07 1.363458e+07 1.366101e+07 1.389229e+07 + 1991 4648756.0 8.961755e+06 1.115424e+07 1.240959e+07 1.309204e+07 1.344748e+07 1.364241e+07 1.375400e+07 1.382878e+07 1.385512e+07 1.408564e+07 + 1992 5139142.0 9.757699e+06 1.202798e+07 1.328948e+07 1.399282e+07 1.434727e+07 1.454438e+07 1.465904e+07 1.473589e+07 1.476295e+07 1.499983e+07 + 1993 5653379.0 1.059942e+07 1.295381e+07 1.429252e+07 1.500514e+07 1.533589e+07 1.553037e+07 1.564351e+07 1.571933e+07 1.574603e+07 1.597976e+07 + 1994 6246447.0 1.139496e+07 1.384576e+07 1.524933e+07 1.593547e+07 1.629529e+07 1.650686e+07 1.662994e+07 1.671242e+07 1.674147e+07 1.699574e+07 + 1995 6473843.0 1.161215e+07 1.401010e+07 1.527961e+07 1.597603e+07 1.634123e+07 1.655597e+07 1.668088e+07 1.676460e+07 1.679409e+07 1.705215e+07 + 1996 6591599.0 1.147391e+07 1.365956e+07 1.491261e+07 1.559998e+07 1.596045e+07 1.617240e+07 1.629570e+07 1.637833e+07 1.640743e+07 1.666215e+07 + 1997 6451896.0 1.106345e+07 1.330436e+07 1.458909e+07 1.529384e+07 1.566342e+07 1.588073e+07 1.600714e+07 1.609186e+07 1.612170e+07 1.638286e+07 """ @@ -216,7 +222,7 @@ def fit( super().fit(ult.values,X.values,self.w_) #keep attributes self.disposal_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) - self.disposal_ = concat((self.disposal_,(ult/ult).iloc[:,:,0,:].rename("development", [9999])),axis=3) + self.disposal_ = concat((self.disposal_,(X.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) self.disposal_.is_cumulative = True self.disposal_.is_pattern = False self.incr_disposal_ = self.disposal_.cum_to_incr() diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index fd899173..814f21e2 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -2,7 +2,7 @@ import numpy as np import pytest -def test_disposal(): +def test_friedland_fidelity(): tri = cl.load_sample('friedland_gl_insurer')['Closed Claim Counts'] ult_tri = cl.Triangle( data = { @@ -30,13 +30,21 @@ def test_disposal(): ]) assert np.all(abs(lhs[~np.isnan(lhs)] - rhs <= 1)) -def test_disposal_no_weight(raa): - tri = raa.set_backend('sparse') +def test_no_weight_exception(raa): with pytest.raises(ValueError): - dr = cl.DisposalRate().fit(tri) - ult = cl.Chainladder().fit(tri).ultimate_ - dr = cl.DisposalRate().fit(tri,sample_weight=ult) + dr = cl.DisposalRate().fit(raa) + ult = cl.Chainladder().fit(raa).ultimate_ + dr = cl.DisposalRate().fit(raa,sample_weight=ult) with pytest.raises(ValueError): - est = dr.transform(tri) + est = dr.transform(raa) +def test_cl_parity(raa): + """ + A no-tail, full-triangle, volume-weighted Chainladder estimator coincides with the disposal rate adjustment. + """ + tri = raa.set_backend('sparse') + dev = cl.Development().fit_transform(tri) + est = cl.Chainladder().fit(dev) + dr = cl.DisposalRate().fit_transform(raa,sample_weight=est.ultimate_) + assert np.all(dr.full_triangle_.round(3).values[...,:-1] == est.full_triangle_.round(3).values[...,:-2]) \ No newline at end of file From 3bb4e35a0876d8051625e247a358a7a7c05e2990 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 20 Jun 2026 23:33:03 +0000 Subject: [PATCH 09/24] bugbot proof --- chainladder/adjustments/tests/test_disposal.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 814f21e2..e830adaf 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -47,4 +47,10 @@ def test_cl_parity(raa): est = cl.Chainladder().fit(dev) dr = cl.DisposalRate().fit_transform(raa,sample_weight=est.ultimate_) assert np.all(dr.full_triangle_.round(3).values[...,:-1] == est.full_triangle_.round(3).values[...,:-2]) + +def test_sparse_transform(raa): + raa_sparse = raa.set_backend('sparse') + from chainladder.utils.sparse import sp + dr = cl.DisposalRate().fit_transform(raa,sample_weight=cl.Chainladder().fit(raa).ultimate_) + assert isinstance(dr.full_triangle_.values,sp.COO) \ No newline at end of file From 4b8dea47530274534923514af064fa11e58ded92 Mon Sep 17 00:00:00 2001 From: henrydingliu <106109320+henrydingliu@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:22:48 -0700 Subject: [PATCH 10/24] Update test_disposal.py --- chainladder/adjustments/tests/test_disposal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index e830adaf..7f745218 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -51,6 +51,6 @@ def test_cl_parity(raa): def test_sparse_transform(raa): raa_sparse = raa.set_backend('sparse') from chainladder.utils.sparse import sp - dr = cl.DisposalRate().fit_transform(raa,sample_weight=cl.Chainladder().fit(raa).ultimate_) + dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=cl.Chainladder().fit(raa_sparse).ultimate_) assert isinstance(dr.full_triangle_.values,sp.COO) - \ No newline at end of file + From 1b2fdee999edd5eb4973a69f98f9e9bc148f5bc9 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 20 Jun 2026 23:22:33 +0000 Subject: [PATCH 11/24] docstring and additional tests --- chainladder/adjustments/disposal.py | 96 ++++++++++--------- .../adjustments/tests/test_disposal.py | 22 +++-- 2 files changed, 66 insertions(+), 52 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index ed1fa61e..4c6be658 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -77,11 +77,9 @@ class DisposalRate(DevelopmentBase): Examples -------- - ``trend`` tilts the case-adequacy adjustment before ``Incurred`` is rebuilt; - on the ``MedMal`` slice the inner diagonals of the adjusted ``Incurred`` - triangle restate materially between ``0%`` and ``15%`` annual drift, while - the latest diagonal is preserved. - + This adjustment method re-apportions future loss emergence based on a '% of ultimate' emergence pattern. + The ultimate can come from another triangle. A common use case is to forecast payment pattern based on incurred ultimate. + .. testsetup:: import chainladder as cl @@ -89,52 +87,60 @@ class DisposalRate(DevelopmentBase): .. testcode:: - tri = cl.load_sample("berqsherm").loc["MedMal"] - base = cl.BerquistSherman( - paid_amount="Paid", - incurred_amount="Incurred", - reported_count="Reported", - closed_count="Closed", - trend=0.0, - ).fit(tri) - tilted = cl.BerquistSherman( - paid_amount="Paid", - incurred_amount="Incurred", - reported_count="Reported", - closed_count="Closed", - trend=0.15, - ).fit(tri) - print(np.round(base.adjusted_triangle_["Incurred"], 0)) + clrd = cl.load_sample('clrd').sum() + ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_ + dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult) + + Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`. + + .. testcode:: + + dr.disposal_rate_tri .. testoutput:: - :options: +NORMALIZE_WHITESPACE - - 12 24 36 48 60 72 84 96 - 1969 9883293.0 27420103.0 35879085.0 43105257.0 33438702.0 30397324.0 25723694.0 23506000.0 - 1970 8641763.0 31305782.0 41543535.0 48550616.0 38203864.0 36222888.0 32216000.0 NaN - 1971 11733960.0 43887171.0 61649896.0 64917222.0 51410209.0 48377000.0 NaN NaN - 1972 13638651.0 50987209.0 66696278.0 72777529.0 61163000.0 NaN NaN NaN - 1973 14387930.0 45470590.0 56577593.0 73733000.0 NaN NaN NaN NaN - 1974 13630366.0 47189379.0 63477000.0 NaN NaN NaN NaN NaN - 1975 15036351.0 48904000.0 NaN NaN NaN NaN NaN NaN - 1976 15791000.0 NaN NaN NaN NaN NaN NaN NaN + + 12 24 36 48 60 72 84 96 108 120 + 1988 0.313923 0.619459 0.774429 0.865377 0.919077 0.948898 0.964643 0.973184 0.980224 0.983063 + 1989 0.321526 0.626023 0.781086 0.872345 0.924842 0.952533 0.967690 0.977373 0.981938 NaN + 1990 0.329567 0.634056 0.790752 0.880273 0.927029 0.952951 0.968379 0.976049 NaN NaN + 1991 0.330035 0.636233 0.791888 0.881010 0.929460 0.954694 0.968533 NaN NaN NaN + 1992 0.342613 0.650521 0.801875 0.885976 0.932865 0.956495 NaN NaN NaN NaN + 1993 0.353784 0.663303 0.810639 0.894414 0.939009 NaN NaN NaN NaN NaN + 1994 0.367530 0.670460 0.814661 0.897244 NaN NaN NaN NaN NaN NaN + 1995 0.379650 0.680979 0.821603 NaN NaN NaN NaN NaN NaN NaN + 1996 0.395603 0.688621 NaN NaN NaN NaN NaN NaN NaN NaN + 1997 0.393820 NaN NaN NaN NaN NaN NaN NaN NaN NaN + + The estimated pattern is stored in `disposal_`. .. testcode:: - print(np.round(tilted.adjusted_triangle_["Incurred"], 0)) + dr.disposal_ + + .. testoutput:: + + 12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult 132-Ult + (All) 0.112105 0.336242 0.545897 0.693774 0.812877 0.905045 0.942998 0.974365 0.990868 1.0 1.0 + `full_triangle_` now reflects the disposal-rate-based forecast. + + .. testcode:: + + dr.full_triangle_ + .. testoutput:: - :options: +NORMALIZE_WHITESPACE - - 12 24 36 48 60 72 84 96 - 1969 3793504.0 12084942.0 18563821.0 25924316.0 23516364.0 24979245.0 24016864.0 23506000.0 - 1970 3760482.0 15830500.0 24615996.0 33169802.0 30722141.0 33362729.0 32216000.0 NaN - 1971 5982185.0 25583831.0 41384825.0 50323342.0 46191356.0 48377000.0 NaN NaN - 1972 7819355.0 33794110.0 51361061.0 64559286.0 61163000.0 NaN NaN NaN - 1973 9533246.0 34585431.0 49667342.0 73733000.0 NaN NaN NaN NaN - 1974 10348458.0 41241243.0 63477000.0 NaN NaN NaN NaN NaN - 1975 13102479.0 48904000.0 NaN NaN NaN NaN NaN NaN - 1976 15791000.0 NaN NaN NaN NaN NaN NaN NaN + + 12 24 36 48 60 72 84 96 108 120 9999 + 1988 3577780.0 7.059966e+06 8.826151e+06 9.862687e+06 1.047470e+07 1.081458e+07 1.099401e+07 1.109136e+07 1.117159e+07 1.120395e+07 1.139698e+07 + 1989 4090680.0 7.964702e+06 9.937520e+06 1.109859e+07 1.176649e+07 1.211879e+07 1.231163e+07 1.243483e+07 1.249290e+07 1.251646e+07 1.272270e+07 + 1990 4578442.0 8.808486e+06 1.098535e+07 1.222900e+07 1.287854e+07 1.323867e+07 1.345299e+07 1.355956e+07 1.363458e+07 1.366101e+07 1.389229e+07 + 1991 4648756.0 8.961755e+06 1.115424e+07 1.240959e+07 1.309204e+07 1.344748e+07 1.364241e+07 1.375400e+07 1.382878e+07 1.385512e+07 1.408564e+07 + 1992 5139142.0 9.757699e+06 1.202798e+07 1.328948e+07 1.399282e+07 1.434727e+07 1.454438e+07 1.465904e+07 1.473589e+07 1.476295e+07 1.499983e+07 + 1993 5653379.0 1.059942e+07 1.295381e+07 1.429252e+07 1.500514e+07 1.533589e+07 1.553037e+07 1.564351e+07 1.571933e+07 1.574603e+07 1.597976e+07 + 1994 6246447.0 1.139496e+07 1.384576e+07 1.524933e+07 1.593547e+07 1.629529e+07 1.650686e+07 1.662994e+07 1.671242e+07 1.674147e+07 1.699574e+07 + 1995 6473843.0 1.161215e+07 1.401010e+07 1.527961e+07 1.597603e+07 1.634123e+07 1.655597e+07 1.668088e+07 1.676460e+07 1.679409e+07 1.705215e+07 + 1996 6591599.0 1.147391e+07 1.365956e+07 1.491261e+07 1.559998e+07 1.596045e+07 1.617240e+07 1.629570e+07 1.637833e+07 1.640743e+07 1.666215e+07 + 1997 6451896.0 1.106345e+07 1.330436e+07 1.458909e+07 1.529384e+07 1.566342e+07 1.588073e+07 1.600714e+07 1.609186e+07 1.612170e+07 1.638286e+07 """ @@ -216,7 +222,7 @@ def fit( super().fit(ult.values,X.values,self.w_) #keep attributes self.disposal_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) - self.disposal_ = concat((self.disposal_,(ult/ult).iloc[:,:,0,:].rename("development", [9999])),axis=3) + self.disposal_ = concat((self.disposal_,(X.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) self.disposal_.is_cumulative = True self.disposal_.is_pattern = False self.incr_disposal_ = self.disposal_.cum_to_incr() diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index fd899173..814f21e2 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -2,7 +2,7 @@ import numpy as np import pytest -def test_disposal(): +def test_friedland_fidelity(): tri = cl.load_sample('friedland_gl_insurer')['Closed Claim Counts'] ult_tri = cl.Triangle( data = { @@ -30,13 +30,21 @@ def test_disposal(): ]) assert np.all(abs(lhs[~np.isnan(lhs)] - rhs <= 1)) -def test_disposal_no_weight(raa): - tri = raa.set_backend('sparse') +def test_no_weight_exception(raa): with pytest.raises(ValueError): - dr = cl.DisposalRate().fit(tri) - ult = cl.Chainladder().fit(tri).ultimate_ - dr = cl.DisposalRate().fit(tri,sample_weight=ult) + dr = cl.DisposalRate().fit(raa) + ult = cl.Chainladder().fit(raa).ultimate_ + dr = cl.DisposalRate().fit(raa,sample_weight=ult) with pytest.raises(ValueError): - est = dr.transform(tri) + est = dr.transform(raa) +def test_cl_parity(raa): + """ + A no-tail, full-triangle, volume-weighted Chainladder estimator coincides with the disposal rate adjustment. + """ + tri = raa.set_backend('sparse') + dev = cl.Development().fit_transform(tri) + est = cl.Chainladder().fit(dev) + dr = cl.DisposalRate().fit_transform(raa,sample_weight=est.ultimate_) + assert np.all(dr.full_triangle_.round(3).values[...,:-1] == est.full_triangle_.round(3).values[...,:-2]) \ No newline at end of file From 359eebb41e8f75d79ba34bc995c5861b3bea5a74 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 20 Jun 2026 23:33:03 +0000 Subject: [PATCH 12/24] bugbot proof --- chainladder/adjustments/tests/test_disposal.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 814f21e2..e830adaf 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -47,4 +47,10 @@ def test_cl_parity(raa): est = cl.Chainladder().fit(dev) dr = cl.DisposalRate().fit_transform(raa,sample_weight=est.ultimate_) assert np.all(dr.full_triangle_.round(3).values[...,:-1] == est.full_triangle_.round(3).values[...,:-2]) + +def test_sparse_transform(raa): + raa_sparse = raa.set_backend('sparse') + from chainladder.utils.sparse import sp + dr = cl.DisposalRate().fit_transform(raa,sample_weight=cl.Chainladder().fit(raa).ultimate_) + assert isinstance(dr.full_triangle_.values,sp.COO) \ No newline at end of file From d6bbdac03dd75d14ca6dbda57171f0d56aae24ae Mon Sep 17 00:00:00 2001 From: henrydingliu <106109320+henrydingliu@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:22:48 -0700 Subject: [PATCH 13/24] Update test_disposal.py --- chainladder/adjustments/tests/test_disposal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index e830adaf..7f745218 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -51,6 +51,6 @@ def test_cl_parity(raa): def test_sparse_transform(raa): raa_sparse = raa.set_backend('sparse') from chainladder.utils.sparse import sp - dr = cl.DisposalRate().fit_transform(raa,sample_weight=cl.Chainladder().fit(raa).ultimate_) + dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=cl.Chainladder().fit(raa_sparse).ultimate_) assert isinstance(dr.full_triangle_.values,sp.COO) - \ No newline at end of file + From 3726b603d9a3345080fdcd9bec078cce288706cc Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 21 Jun 2026 04:24:05 +0000 Subject: [PATCH 14/24] more bugbot fixes --- chainladder/adjustments/disposal.py | 33 +++++++++++++++-------------- chainladder/methods/base.py | 9 +++++++- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 4c6be658..b35b80c7 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -3,6 +3,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from chainladder.methods import Chainladder +from chainladder.methods.base import validate_weight from chainladder.development import DevelopmentBase import numpy as np import copy @@ -192,18 +193,14 @@ def fit( """ if sample_weight is None: raise ValueError("sample_weight is required.") - #convert to numpy - if X.array_backend == "sparse": - X = X.set_backend("numpy").incr_to_cum() - else: - X = X.copy().incr_to_cum() - if sample_weight.array_backend == "sparse": - ult = sample_weight.set_backend("numpy") - else: - ult = sample_weight.copy() - #get backend - self.xp = X.get_array_module() + #validate dimensions of sample weight + validate_weight(X, sample_weight) + #align backeneds + ult = sample_weight.set_backend(X.array_backend).sort_index() + #calculate disposal rate triangle + self.X_ = X.sort_index() self.disposal_rate_tri = X / ult.values + #get weights for estimation tw = TriangleWeight( n_periods = self.n_periods, drop_high = self.drop_high, @@ -214,16 +211,17 @@ def fit( preserve = self.preserve, drop = self.drop ) - if hasattr(X, "w_"): - self.w_ = tw.fit(X=self.disposal_rate_tri * X.w_).w_.values + if hasattr(self.X_, "w_"): + self.w_ = tw.fit(X=self.disposal_rate_tri * self.X_.w_).w_.values else: self.w_ = tw.fit(X=self.disposal_rate_tri).w_.values #calculate factors - super().fit(ult.values,X.values,self.w_) + super().fit(ult.values,self.X_.values,self.w_) #keep attributes self.disposal_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) - self.disposal_ = concat((self.disposal_,(X.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) + self.disposal_ = concat((self.disposal_,(self.X_.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) self.disposal_.is_cumulative = True + #pattern multiples from tail and additive adds from head self.disposal_.is_pattern = False self.incr_disposal_ = self.disposal_.cum_to_incr() self.incr_disposal_.is_pattern = True @@ -253,10 +251,13 @@ def transform( if sample_weight is None: raise ValueError("sample_weight is required.") X_new = copy.deepcopy(X) + #validate dimensions of sample weight + validate_weight(X, sample_weight) + #align backeneds + X_new.ultimate_ = sample_weight.set_backend(self.X_.array_backend).latest_diagonal X_new.disposal_rate_tri = self.disposal_rate_tri X_new.disposal_ = self.disposal_ X_new.incr_disposal_ = self.incr_disposal_ - X_new.ultimate_ = sample_weight.latest_diagonal ibnr_pct = 1 - X_new.disposal_.align_pattern(X_new.disposal_rate_tri) run_off = X_new.incr_disposal_ / ibnr_pct * X_new.ibnr_ run_off = run_off[run_off.valuation > X_new.valuation_date] diff --git a/chainladder/methods/base.py b/chainladder/methods/base.py index 7ad3117e..b2668b15 100644 --- a/chainladder/methods/base.py +++ b/chainladder/methods/base.py @@ -138,7 +138,14 @@ def _include_process_variance(self): process_var = None return process_var - def validate_weight(self, X, sample_weight): + @staticmethod + def validate_weight( + X: Triangle, + sample_weight: Triangle + ) -> None: + ''' + Checks that the a aprior has valid dimensions + ''' if ( sample_weight and X.shape[:-1] != sample_weight.shape[:-1] From 0ffc46ebb6efe403ae759b4f02f5039854912974 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 21 Jun 2026 04:30:40 +0000 Subject: [PATCH 15/24] bug fix --- chainladder/adjustments/disposal.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index b35b80c7..4a6c5fdd 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -2,8 +2,7 @@ # 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 chainladder.methods import Chainladder -from chainladder.methods.base import validate_weight +from chainladder.methods import Chainladder, MethodBase from chainladder.development import DevelopmentBase import numpy as np import copy @@ -194,7 +193,7 @@ def fit( if sample_weight is None: raise ValueError("sample_weight is required.") #validate dimensions of sample weight - validate_weight(X, sample_weight) + MethodBase().validate_weight(X, sample_weight) #align backeneds ult = sample_weight.set_backend(X.array_backend).sort_index() #calculate disposal rate triangle @@ -252,7 +251,7 @@ def transform( raise ValueError("sample_weight is required.") X_new = copy.deepcopy(X) #validate dimensions of sample weight - validate_weight(X, sample_weight) + MethodBase().validate_weight(X, sample_weight) #align backeneds X_new.ultimate_ = sample_weight.set_backend(self.X_.array_backend).latest_diagonal X_new.disposal_rate_tri = self.disposal_rate_tri From a232d5efb53ed8c294fe060bb631ebac2873756f Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 21 Jun 2026 04:48:43 +0000 Subject: [PATCH 16/24] fix --- chainladder/adjustments/disposal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 4a6c5fdd..82fd3135 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -197,6 +197,7 @@ def fit( #align backeneds ult = sample_weight.set_backend(X.array_backend).sort_index() #calculate disposal rate triangle + self.xp = X.get_array_module() self.X_ = X.sort_index() self.disposal_rate_tri = X / ult.values #get weights for estimation From b736bc0597953a76652573f5cab48b739b834826 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 21 Jun 2026 06:50:04 +0000 Subject: [PATCH 17/24] forcing numpy after all --- chainladder/adjustments/disposal.py | 19 ++++++++++++++++--- chainladder/methods/base.py | 6 ++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 82fd3135..cd4e811d 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -1,13 +1,19 @@ # 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.methods import Chainladder, MethodBase from chainladder.development import DevelopmentBase import numpy as np import copy from chainladder.utils import TriangleWeight, concat -from chainladder import Triangle + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder.core import Triangle + class DisposalRate(DevelopmentBase): """ @@ -194,8 +200,15 @@ def fit( raise ValueError("sample_weight is required.") #validate dimensions of sample weight MethodBase().validate_weight(X, sample_weight) - #align backeneds - ult = sample_weight.set_backend(X.array_backend).sort_index() + #set backeneds to numpy + if X.array_backend == "sparse": + X = X.set_backend("numpy") + else: + X = X.copy() + if sample_weight.array_backend == "sparse": + ult = sample_weight.set_backend("numpy") + else: + ult = sample_weight.copy() #calculate disposal rate triangle self.xp = X.get_array_module() self.X_ = X.sort_index() diff --git a/chainladder/methods/base.py b/chainladder/methods/base.py index b2668b15..6fcf0537 100644 --- a/chainladder/methods/base.py +++ b/chainladder/methods/base.py @@ -1,6 +1,8 @@ # 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 + import numpy as np import pandas as pd import warnings @@ -10,6 +12,10 @@ from chainladder.core.io import EstimatorIO from chainladder.core.common import Common +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder.core import Triangle class MethodBase(BaseEstimator, EstimatorIO, Common): From 6eb6afdbb77eed1eecdd1065e643c594462619b8 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 21 Jun 2026 07:15:36 +0000 Subject: [PATCH 18/24] more fixes --- chainladder/adjustments/disposal.py | 4 ++-- chainladder/adjustments/tests/test_disposal.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index cd4e811d..28b3e728 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -211,8 +211,8 @@ def fit( ult = sample_weight.copy() #calculate disposal rate triangle self.xp = X.get_array_module() - self.X_ = X.sort_index() - self.disposal_rate_tri = X / ult.values + self.X_ = X.incr_to_cum().sort_index() + self.disposal_rate_tri = self.X_ / ult.values #get weights for estimation tw = TriangleWeight( n_periods = self.n_periods, diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 7f745218..8b298c10 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -50,7 +50,8 @@ def test_cl_parity(raa): def test_sparse_transform(raa): raa_sparse = raa.set_backend('sparse') + ult = cl.Chainladder().fit(raa_sparse).ultimate_.set_backend('sparse') + dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=ult) from chainladder.utils.sparse import sp - dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=cl.Chainladder().fit(raa_sparse).ultimate_) assert isinstance(dr.full_triangle_.values,sp.COO) From ba539b1dfb3f160b939cdaefdcf2df0e618969f4 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 21 Jun 2026 07:15:36 +0000 Subject: [PATCH 19/24] more fixes --- chainladder/adjustments/disposal.py | 4 ++-- chainladder/adjustments/tests/test_disposal.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index cd4e811d..28b3e728 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -211,8 +211,8 @@ def fit( ult = sample_weight.copy() #calculate disposal rate triangle self.xp = X.get_array_module() - self.X_ = X.sort_index() - self.disposal_rate_tri = X / ult.values + self.X_ = X.incr_to_cum().sort_index() + self.disposal_rate_tri = self.X_ / ult.values #get weights for estimation tw = TriangleWeight( n_periods = self.n_periods, diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 7f745218..8b298c10 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -50,7 +50,8 @@ def test_cl_parity(raa): def test_sparse_transform(raa): raa_sparse = raa.set_backend('sparse') + ult = cl.Chainladder().fit(raa_sparse).ultimate_.set_backend('sparse') + dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=ult) from chainladder.utils.sparse import sp - dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=cl.Chainladder().fit(raa_sparse).ultimate_) assert isinstance(dr.full_triangle_.values,sp.COO) From 663a860be7f6fff08b0c85724afa0b311daf8f79 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 26 Jun 2026 17:37:00 +0000 Subject: [PATCH 20/24] addressing reviewer comments --- chainladder/adjustments/disposal.py | 2 +- .../adjustments/tests/test_disposal.py | 49 +++++++++++-------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 28b3e728..e80fd5f9 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -97,7 +97,7 @@ class DisposalRate(DevelopmentBase): ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_ dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult) - Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`. + Once we apply this adjustment method via a `fit_transform`, we can examine the emergence pattern via `disposal_rate_tri`. .. testcode:: diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 8b298c10..b27f73ce 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -1,23 +1,24 @@ -import chainladder as cl +mport chainladder as cl import numpy as np import pytest -def test_friedland_fidelity(): - tri = cl.load_sample('friedland_gl_insurer')['Closed Claim Counts'] - ult_tri = cl.Triangle( - data = { - 'Closed Claim Counts':[873,720,626,629,588,553,438,609], - 'ay': [2001,2002,2003,2004,2005,2006,2007,2008], - 'dev':[2008,2008,2008,2008,2008,2008,2008,2008], - }, - origin = 'ay', - development='dev', - columns='Closed Claim Counts', - cumulative=True, - ) - dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri) - assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001)) - lhs = (dr.full_triangle_.cum_to_incr()-tri.cum_to_incr()).round(0).values.flatten() +def test_friedland_fidelity() -> None: + ''' + Demonstrates + ''' + tri = cl.load_sample('friedland_gl_insurer') + ccc_dev = cl.Development(n_periods=3, average='volume').fit_transform(tri['Closed Claim Counts']) + ccc_dev.ldf_ = ccc_dev.ldf_.round(3) + ccc_dev_wtail = cl.TailConstant(tail = 1.100, projection_period = 0).fit_transform(ccc_dev) + ccc_ult = cl.Chainladder().fit(ccc_dev_wtail).ultimate_ + rcc_dev = cl.Development(n_periods=3, average='volume').fit_transform(tri['Reported Claim Counts']) + rcc_dev.ldf_ = rcc_dev.ldf_.round(3) + rcc_ult = cl.Chainladder().fit(rcc_dev).ultimate_ + ult = (ccc_ult + rcc_ult) / 2 + dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri['Closed Claim Counts'],sample_weight=ult) + assert np.all(dr.disposal_.round(3).values.flatten() == [.200,.433,.585,.710,.791,.862,.882,.912,1.000]) + #Friedland uses rounded ultimates to calculate bottom half of the triangle, which introduces some rounding discrepancies with the implementation + lhs = (dr.full_triangle_.cum_to_incr()-tri['Closed Claim Counts'].cum_to_incr()).round(0).values.flatten() rhs = np.array([ 77., 24., 70., @@ -26,11 +27,14 @@ def test_friedland_fidelity(): 52., 45., 13., 19., 56., 76., 49., 43., 12., 18., 54., 67., 55., 36., 31., 9., 13., 39., - 140., 91., 75., 49., 42., 12., 18., 53. + 140., 91., 75., 49., 43., 12., 18., 53. ]) assert np.all(abs(lhs[~np.isnan(lhs)] - rhs <= 1)) -def test_no_weight_exception(raa): +def test_no_weight_exception(raa:Triangle) -> None: + ''' + sample_weight is optional in the default sklearn API. however, we require sample_weight to provide the a priori ultimate. + ''' with pytest.raises(ValueError): dr = cl.DisposalRate().fit(raa) ult = cl.Chainladder().fit(raa).ultimate_ @@ -38,7 +42,7 @@ def test_no_weight_exception(raa): with pytest.raises(ValueError): est = dr.transform(raa) -def test_cl_parity(raa): +def test_cl_parity(raa:Triangle) -> None: """ A no-tail, full-triangle, volume-weighted Chainladder estimator coincides with the disposal rate adjustment. """ @@ -48,7 +52,10 @@ def test_cl_parity(raa): dr = cl.DisposalRate().fit_transform(raa,sample_weight=est.ultimate_) assert np.all(dr.full_triangle_.round(3).values[...,:-1] == est.full_triangle_.round(3).values[...,:-2]) -def test_sparse_transform(raa): +def test_sparse_transform(raa:Triangle) -> None: + """ + if the supplied Triangle is sparse, then the resulting full_triangle_ is also sparse + """ raa_sparse = raa.set_backend('sparse') ult = cl.Chainladder().fit(raa_sparse).ultimate_.set_backend('sparse') dr = cl.DisposalRate().fit_transform(raa_sparse,sample_weight=ult) From df8e2f9c8b18e0186fa8880115985b01fb30491a Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 26 Jun 2026 17:42:26 +0000 Subject: [PATCH 21/24] added missing dependencies --- chainladder/adjustments/tests/test_disposal.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index b27f73ce..307b3300 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -1,10 +1,17 @@ -mport chainladder as cl +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_friedland_fidelity() -> None: ''' - Demonstrates + Reconciles to Chapter 11 Exhibit 5 of the Friedland reserving textbook ''' tri = cl.load_sample('friedland_gl_insurer') ccc_dev = cl.Development(n_periods=3, average='volume').fit_transform(tri['Closed Claim Counts']) From 74655d1afbca8f1c590d4629e4c558fa41ecfe94 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sat, 27 Jun 2026 18:56:46 +0000 Subject: [PATCH 22/24] restructuring disposal attributes into mixin class --- chainladder/adjustments/disposal.py | 52 +++++++++++++++---- .../adjustments/tests/test_disposal.py | 6 ++- chainladder/core/base.py | 5 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index e80fd5f9..e9afbc6d 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -14,8 +14,45 @@ if TYPE_CHECKING: from chainladder.core import Triangle - -class DisposalRate(DevelopmentBase): +class DisposalMixin: + ''' + This class provides attributes for the DisposalRate adjustment method and transformed `Triangle` + ''' + + @property + def disposal_(self) -> Triangle: + if not hasattr(self, "_disposal_"): + x = self.__class__.__name__ + raise AttributeError("'" + x + "' object has no attribute 'disposal_'") + return self._disposal_ + + @disposal_.setter + def disposal_(self,value) -> None: + output = copy.deepcopy(value) + output.is_cumulative = True + output.is_pattern = True + self._disposal_ = output + + @property + def incr_disposal_(self) -> Triangle: + if not hasattr(self, "_disposal_"): + x = self.__class__.__name__ + raise AttributeError("'" + x + "' object has no attribute 'incr_disposal_'") + disposal_copy = copy.deepcopy(self._disposal_) + disposal_copy.is_pattern = False + output = disposal_copy.cum_to_incr() + output.is_pattern = True + return output + + @incr_disposal_.setter + def incr_disposal_(self,value) -> None: + output = copy.deepcopy(value) + output.is_pattern = False + output = output.incr_to_cum() + output.is_pattern = True + self._disposal_ = output + +class DisposalRate(DevelopmentBase, DisposalMixin): """ Calculates the bottom of a fitted full_triangle_ using the Disposal Rate method described by Friedland. @@ -231,14 +268,8 @@ def fit( #calculate factors super().fit(ult.values,self.X_.values,self.w_) #keep attributes - self.disposal_ = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) - self.disposal_ = concat((self.disposal_,(self.X_.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) - self.disposal_.is_cumulative = True - #pattern multiples from tail and additive adds from head - self.disposal_.is_pattern = False - self.incr_disposal_ = self.disposal_.cum_to_incr() - self.incr_disposal_.is_pattern = True - self.disposal_.is_pattern = True + disposal = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) + self._disposal_ = concat((disposal,(self.X_.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) return self def transform( @@ -270,7 +301,6 @@ def transform( X_new.ultimate_ = sample_weight.set_backend(self.X_.array_backend).latest_diagonal X_new.disposal_rate_tri = self.disposal_rate_tri X_new.disposal_ = self.disposal_ - X_new.incr_disposal_ = self.incr_disposal_ ibnr_pct = 1 - X_new.disposal_.align_pattern(X_new.disposal_rate_tri) run_off = X_new.incr_disposal_ / ibnr_pct * X_new.ibnr_ run_off = run_off[run_off.valuation > X_new.valuation_date] diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 307b3300..1797e23d 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -22,9 +22,11 @@ def test_friedland_fidelity() -> None: rcc_dev.ldf_ = rcc_dev.ldf_.round(3) rcc_ult = cl.Chainladder().fit(rcc_dev).ultimate_ ult = (ccc_ult + rcc_ult) / 2 - dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri['Closed Claim Counts'],sample_weight=ult) - assert np.all(dr.disposal_.round(3).values.flatten() == [.200,.433,.585,.710,.791,.862,.882,.912,1.000]) + dr_transformer = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit(X=tri['Closed Claim Counts'],sample_weight=ult) + dr_transformer.disposal_ = dr_transformer.disposal_.round(3) + assert np.all(dr_transformer.disposal_.values.flatten() == [.200,.433,.585,.710,.791,.862,.882,.912,1.000]) #Friedland uses rounded ultimates to calculate bottom half of the triangle, which introduces some rounding discrepancies with the implementation + dr = dr_transformer.transform(X=tri['Closed Claim Counts'],sample_weight=ult.round(0)) lhs = (dr.full_triangle_.cum_to_incr()-tri['Closed Claim Counts'].cum_to_incr()).round(0).values.flatten() rhs = np.array([ 77., diff --git a/chainladder/core/base.py b/chainladder/core/base.py index 9006559f..ce3aded7 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -27,6 +27,8 @@ from chainladder.utils.dask import dp from chainladder.utils.sparse import sp +from chainladder.adjustments.disposal import DisposalMixin + from typing import ( Optional, TYPE_CHECKING @@ -50,7 +52,8 @@ class TriangleBase( TriangleDunders, TrianglePandas, Common, - ABC + ABC, + DisposalMixin ): """This class handles the initialization of a triangle""" From 6487299151a0c48fc6132bedbaaedf59568d1b65 Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 28 Jun 2026 03:15:32 +0000 Subject: [PATCH 23/24] adding `is_disposal_rate` attribute to triangle --- chainladder/adjustments/disposal.py | 76 +++++++--------- .../adjustments/tests/test_disposal.py | 15 +++- chainladder/core/triangle.py | 86 ++++++++++++++++--- 3 files changed, 121 insertions(+), 56 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index e9afbc6d..530ae95b 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -20,37 +20,31 @@ class DisposalMixin: ''' @property - def disposal_(self) -> Triangle: - if not hasattr(self, "_disposal_"): + def disposal_rate_(self) -> Triangle: + if not hasattr(self, "_disposal_rate_"): x = self.__class__.__name__ - raise AttributeError("'" + x + "' object has no attribute 'disposal_'") - return self._disposal_ + raise AttributeError("'" + x + "' object has no attribute 'disposal_rate_'") + return self._disposal_rate_ - @disposal_.setter - def disposal_(self,value) -> None: - output = copy.deepcopy(value) - output.is_cumulative = True - output.is_pattern = True - self._disposal_ = output + @disposal_rate_.setter + def disposal_rate_(self,value) -> None: + obj = copy.deepcopy(value) + obj.is_pattern = True + obj.is_disposal_rate = True + obj.is_cumulative = True + self._disposal_rate_ = obj @property - def incr_disposal_(self) -> Triangle: - if not hasattr(self, "_disposal_"): - x = self.__class__.__name__ - raise AttributeError("'" + x + "' object has no attribute 'incr_disposal_'") - disposal_copy = copy.deepcopy(self._disposal_) - disposal_copy.is_pattern = False - output = disposal_copy.cum_to_incr() - output.is_pattern = True - return output - - @incr_disposal_.setter - def incr_disposal_(self,value) -> None: - output = copy.deepcopy(value) - output.is_pattern = False - output = output.incr_to_cum() - output.is_pattern = True - self._disposal_ = output + def incr_disposal_rate_(self) -> Triangle: + return self.disposal_rate_.cum_to_incr() + + @incr_disposal_rate_.setter + def incr_disposal_rate_(self,value) -> None: + obj = copy.deepcopy(value) + obj.is_pattern = True + obj.is_disposal_rate = True + obj.is_cumulative = False + self._disposal_rate_ = obj.incr_to_cum() class DisposalRate(DevelopmentBase, DisposalMixin): """ @@ -109,13 +103,10 @@ class DisposalRate(DevelopmentBase, DisposalMixin): Attributes ---------- - disposal_rate_tri: Triangle - actual disposal rates by origin and development - - disposal_: Triangle + disposal_rate_: Triangle fitted disposal rates - incr_disposal_: Triangle + incr_disposal_rate_: Triangle incremental of disposal_ Examples @@ -154,11 +145,11 @@ class DisposalRate(DevelopmentBase, DisposalMixin): 1996 0.395603 0.688621 NaN NaN NaN NaN NaN NaN NaN NaN 1997 0.393820 NaN NaN NaN NaN NaN NaN NaN NaN NaN - The estimated pattern is stored in `disposal_`. + The estimated pattern is stored in `disposal_rate_`. .. testcode:: - dr.disposal_ + dr.disposal_rate_ .. testoutput:: @@ -249,7 +240,7 @@ def fit( #calculate disposal rate triangle self.xp = X.get_array_module() self.X_ = X.incr_to_cum().sort_index() - self.disposal_rate_tri = self.X_ / ult.values + self.X_.ultimate_ = ult #get weights for estimation tw = TriangleWeight( n_periods = self.n_periods, @@ -262,14 +253,14 @@ def fit( drop = self.drop ) if hasattr(self.X_, "w_"): - self.w_ = tw.fit(X=self.disposal_rate_tri * self.X_.w_).w_.values + self.w_ = tw.fit(X=self.X_.disposal_rate_tri * self.X_.w_).w_.values else: - self.w_ = tw.fit(X=self.disposal_rate_tri).w_.values + self.w_ = tw.fit(X=self.X_.disposal_rate_tri).w_.values #calculate factors super().fit(ult.values,self.X_.values,self.w_) #keep attributes - disposal = self._param_property(self.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) - self._disposal_ = concat((disposal,(self.X_.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) + disposal = self._param_property(self.X_.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) + self.disposal_rate_ = concat((disposal,(self.X_.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) return self def transform( @@ -299,10 +290,9 @@ def transform( MethodBase().validate_weight(X, sample_weight) #align backeneds X_new.ultimate_ = sample_weight.set_backend(self.X_.array_backend).latest_diagonal - X_new.disposal_rate_tri = self.disposal_rate_tri - X_new.disposal_ = self.disposal_ - ibnr_pct = 1 - X_new.disposal_.align_pattern(X_new.disposal_rate_tri) - run_off = X_new.incr_disposal_ / ibnr_pct * X_new.ibnr_ + X_new.disposal_rate_ = self.disposal_rate_ + ibnr_pct = 1 - X_new.disposal_rate_.align_pattern(X_new.disposal_rate_tri) + run_off = X_new.incr_disposal_rate_ / ibnr_pct * X_new.ibnr_ run_off = run_off[run_off.valuation > X_new.valuation_date] X_new.ldf_ = (X_new.cum_to_incr() + run_off).incr_to_cum().age_to_age return X_new diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index 1797e23d..c4111b13 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -23,8 +23,8 @@ def test_friedland_fidelity() -> None: rcc_ult = cl.Chainladder().fit(rcc_dev).ultimate_ ult = (ccc_ult + rcc_ult) / 2 dr_transformer = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit(X=tri['Closed Claim Counts'],sample_weight=ult) - dr_transformer.disposal_ = dr_transformer.disposal_.round(3) - assert np.all(dr_transformer.disposal_.values.flatten() == [.200,.433,.585,.710,.791,.862,.882,.912,1.000]) + dr_transformer.disposal_rate_ = dr_transformer.disposal_rate_.round(3) + assert np.all(dr_transformer.disposal_rate_.values.flatten() == [.200,.433,.585,.710,.791,.862,.882,.912,1.000]) #Friedland uses rounded ultimates to calculate bottom half of the triangle, which introduces some rounding discrepancies with the implementation dr = dr_transformer.transform(X=tri['Closed Claim Counts'],sample_weight=ult.round(0)) lhs = (dr.full_triangle_.cum_to_incr()-tri['Closed Claim Counts'].cum_to_incr()).round(0).values.flatten() @@ -50,7 +50,16 @@ def test_no_weight_exception(raa:Triangle) -> None: dr = cl.DisposalRate().fit(raa,sample_weight=ult) with pytest.raises(ValueError): est = dr.transform(raa) - + +def test_no_disposal_exception(raa:Triangle) -> None: + ''' + disposal attributes are available in Triangle through the mixin, but not available before fitting + ''' + with pytest.raises(AttributeError): + _ = raa.disposal_rate_ + with pytest.raises(AttributeError): + _ = raa.incr_disposal_rate_ + def test_cl_parity(raa:Triangle) -> None: """ A no-tail, full-triangle, volume-weighted Chainladder estimator coincides with the disposal rate adjustment. diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index b0428a91..e9a931e2 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -106,8 +106,10 @@ class Triangle(TriangleBase): The grain of the development vector ('Y', 'S', 'Q', 'M') shape: tuple The 4D shape of the triangle instance with axes corresponding to (index, columns, origin, development) - link_ratio, age_to_age + link_ratio, age_to_age: Triangle Displays age-to-age ratios for the triangle. + disposal_rate_tri: Triangle + Displays actual disposal rates by origin and development; must have ultimate_ valuation_date : date The latest valuation date of the data loc: Triangle @@ -923,6 +925,19 @@ def is_pattern(self) -> bool: def is_pattern(self, pattern: bool): self._pattern = pattern + @property + def is_disposal_rate(self) -> bool: + """ + Indicates whether the Triangle holds disposal rates (additive ratios going from head to tail) + """ + if hasattr(self, "_is_disposal_rate"): + return self._is_disposal_rate + return False + + @is_disposal_rate.setter + def is_disposal_rate(self, is_dr: bool) -> None: + self._is_disposal_rate = is_dr + def align_pattern(self, X:Triangle, sample_weight:Triangle|None=None) -> Triangle: """ Vertically align a selected pattern to origin period latest diagonal. Triangle must be a selected pattern. @@ -1134,6 +1149,58 @@ def age_to_age(self): """ return self.link_ratio + @property + def disposal_rate_tri(self) -> Triangle: + """ + Displays disposal rates for the triangle. Must have ultimate_ + + Returns + ------- + + Triangle + Triangle of disposal rates + + Examples + -------- + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + clrd = cl.load_sample('clrd').sum() + ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_ + dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult) + dr.disposal_rate_tri + + .. testoutput:: + + 12 24 36 48 60 72 84 96 108 120 + 1988 0.313923 0.619459 0.774429 0.865377 0.919077 0.948898 0.964643 0.973184 0.980224 0.983063 + 1989 0.321526 0.626023 0.781086 0.872345 0.924842 0.952533 0.967690 0.977373 0.981938 NaN + 1990 0.329567 0.634056 0.790752 0.880273 0.927029 0.952951 0.968379 0.976049 NaN NaN + 1991 0.330035 0.636233 0.791888 0.881010 0.929460 0.954694 0.968533 NaN NaN NaN + 1992 0.342613 0.650521 0.801875 0.885976 0.932865 0.956495 NaN NaN NaN NaN + 1993 0.353784 0.663303 0.810639 0.894414 0.939009 NaN NaN NaN NaN NaN + 1994 0.367530 0.670460 0.814661 0.897244 NaN NaN NaN NaN NaN NaN + 1995 0.379650 0.680979 0.821603 NaN NaN NaN NaN NaN NaN NaN + 1996 0.395603 0.688621 NaN NaN NaN NaN NaN NaN NaN NaN + 1997 0.393820 NaN NaN NaN NaN NaN NaN NaN NaN NaN + """ + + obj: Triangle = self.incr_to_cum() / self.ultimate_.values + if not obj.is_full: + obj = obj[obj.valuation <= obj.valuation_date] + if hasattr(obj, "w_"): + w_ = obj.w_[..., : len(obj.odims), :] + obj = obj * w_ if obj.shape == w_.shape else obj + obj.is_pattern = True + obj.is_cumulative = True + obj.is_disposal_rate = True + obj.values = num_to_nan(obj.values) + return obj + def incr_to_cum(self, inplace=False): """Method to convert an incremental triangle into a cumulative triangle. @@ -1215,14 +1282,13 @@ def incr_to_cum(self, inplace=False): if inplace: xp = self.get_array_module() if not self.is_cumulative: - if self.is_pattern: - if hasattr(self, "is_additive"): - if 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 - ) + 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) @@ -1297,7 +1363,7 @@ 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: + if self.is_pattern & (not self.is_disposal_rate): xp = self.get_array_module() self.values = xp.nan_to_num(self.values) values = num_to_value(self.values, 1) From 432158de29d137332558dc28dc759292ae45165e Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Sun, 28 Jun 2026 04:44:13 +0000 Subject: [PATCH 24/24] improving docstring and adding codecov coverage --- chainladder/adjustments/disposal.py | 21 +++++++++++++++---- .../adjustments/tests/test_disposal.py | 9 ++++++++ chainladder/core/triangle.py | 21 +++++++++---------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/chainladder/adjustments/disposal.py b/chainladder/adjustments/disposal.py index 530ae95b..0eb7e815 100644 --- a/chainladder/adjustments/disposal.py +++ b/chainladder/adjustments/disposal.py @@ -21,6 +21,9 @@ class DisposalMixin: @property def disposal_rate_(self) -> Triangle: + ''' + Gets the estimated disposal rate + ''' if not hasattr(self, "_disposal_rate_"): x = self.__class__.__name__ raise AttributeError("'" + x + "' object has no attribute 'disposal_rate_'") @@ -28,6 +31,9 @@ def disposal_rate_(self) -> Triangle: @disposal_rate_.setter def disposal_rate_(self,value) -> None: + ''' + Sets disposal_rate_ + ''' obj = copy.deepcopy(value) obj.is_pattern = True obj.is_disposal_rate = True @@ -36,10 +42,16 @@ def disposal_rate_(self,value) -> None: @property def incr_disposal_rate_(self) -> Triangle: + ''' + Gets the incremental of the estimated disposal rate + ''' return self.disposal_rate_.cum_to_incr() @incr_disposal_rate_.setter def incr_disposal_rate_(self,value) -> None: + ''' + Sets incr_disposal_rate_ + ''' obj = copy.deepcopy(value) obj.is_pattern = True obj.is_disposal_rate = True @@ -252,12 +264,12 @@ def fit( preserve = self.preserve, drop = self.drop ) - if hasattr(self.X_, "w_"): - self.w_ = tw.fit(X=self.X_.disposal_rate_tri * self.X_.w_).w_.values + if hasattr(self.X_, "disposal_w_"): + self.disposal_w_ = tw.fit(X=self.X_.disposal_rate_tri * self.X_.disposal_w_).w_.values else: - self.w_ = tw.fit(X=self.X_.disposal_rate_tri).w_.values + self.disposal_w_ = tw.fit(X=self.X_.disposal_rate_tri).w_.values #calculate factors - super().fit(ult.values,self.X_.values,self.w_) + super().fit(ult.values,self.X_.values,self.disposal_w_) #keep attributes disposal = self._param_property(self.X_.disposal_rate_tri,self.params_.slope_[...,0][..., None, :]) self.disposal_rate_ = concat((disposal,(self.X_.latest_diagonal*0 + 1).iloc[:,:,0,:].rename("development", [9999])),axis=3) @@ -289,6 +301,7 @@ def transform( #validate dimensions of sample weight MethodBase().validate_weight(X, sample_weight) #align backeneds + X_new.disposal_w_ = self.disposal_w_ X_new.ultimate_ = sample_weight.set_backend(self.X_.array_backend).latest_diagonal X_new.disposal_rate_ = self.disposal_rate_ ibnr_pct = 1 - X_new.disposal_rate_.align_pattern(X_new.disposal_rate_tri) diff --git a/chainladder/adjustments/tests/test_disposal.py b/chainladder/adjustments/tests/test_disposal.py index c4111b13..b32febd4 100644 --- a/chainladder/adjustments/tests/test_disposal.py +++ b/chainladder/adjustments/tests/test_disposal.py @@ -80,3 +80,12 @@ def test_sparse_transform(raa:Triangle) -> None: from chainladder.utils.sparse import sp assert isinstance(dr.full_triangle_.values,sp.COO) +def test_setting_incr(raa:Triangle) -> None: + """ + DisposalMixin allows setting incremental disposal rates. Validating that the setting function works properly. + """ + ult = cl.Chainladder().fit(raa).ultimate_ + dr = cl.DisposalRate(n_periods = 4).fit(raa,sample_weight=ult) + orig_disposal_rate_ = dr.disposal_rate_ + dr.incr_disposal_rate_ = dr.incr_disposal_rate_ * 2 + assert dr.disposal_rate_ == orig_disposal_rate_ * 2 \ No newline at end of file diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index e9a931e2..bd5e710e 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1176,25 +1176,24 @@ def disposal_rate_tri(self) -> Triangle: .. testoutput:: - 12 24 36 48 60 72 84 96 108 120 - 1988 0.313923 0.619459 0.774429 0.865377 0.919077 0.948898 0.964643 0.973184 0.980224 0.983063 - 1989 0.321526 0.626023 0.781086 0.872345 0.924842 0.952533 0.967690 0.977373 0.981938 NaN - 1990 0.329567 0.634056 0.790752 0.880273 0.927029 0.952951 0.968379 0.976049 NaN NaN - 1991 0.330035 0.636233 0.791888 0.881010 0.929460 0.954694 0.968533 NaN NaN NaN - 1992 0.342613 0.650521 0.801875 0.885976 0.932865 0.956495 NaN NaN NaN NaN - 1993 0.353784 0.663303 0.810639 0.894414 0.939009 NaN NaN NaN NaN NaN + 12-Ult 24-Ult 36-Ult 48-Ult 60-Ult 72-Ult 84-Ult 96-Ult 108-Ult 120-Ult + 1988 NaN NaN NaN NaN NaN NaN 0.964643 0.973184 0.980224 0.983063 + 1989 NaN NaN NaN NaN NaN 0.952533 0.967690 0.977373 0.981938 NaN + 1990 NaN NaN NaN NaN 0.927029 0.952951 0.968379 0.976049 NaN NaN + 1991 NaN NaN NaN 0.881010 0.929460 0.954694 0.968533 NaN NaN NaN + 1992 NaN NaN 0.801875 0.885976 0.932865 0.956495 NaN NaN NaN NaN + 1993 NaN 0.663303 0.810639 0.894414 0.939009 NaN NaN NaN NaN NaN 1994 0.367530 0.670460 0.814661 0.897244 NaN NaN NaN NaN NaN NaN 1995 0.379650 0.680979 0.821603 NaN NaN NaN NaN NaN NaN NaN 1996 0.395603 0.688621 NaN NaN NaN NaN NaN NaN NaN NaN 1997 0.393820 NaN NaN NaN NaN NaN NaN NaN NaN NaN - """ + """ obj: Triangle = self.incr_to_cum() / self.ultimate_.values if not obj.is_full: obj = obj[obj.valuation <= obj.valuation_date] - if hasattr(obj, "w_"): - w_ = obj.w_[..., : len(obj.odims), :] - obj = obj * w_ if obj.shape == w_.shape else obj + if hasattr(obj, "disposal_w_"): + obj = obj * obj.disposal_w_ obj.is_pattern = True obj.is_cumulative = True obj.is_disposal_rate = True