Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions chainladder/adjustments/disposal.py
Original file line number Diff line number Diff line change
@@ -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
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):
"""
Expand Down Expand Up @@ -192,18 +198,22 @@ def fit(
"""
if sample_weight is None:
raise ValueError("sample_weight is required.")
#convert to numpy
#validate dimensions of sample weight
MethodBase().validate_weight(X, sample_weight)
#set backeneds to numpy
if X.array_backend == "sparse":
X = X.set_backend("numpy").incr_to_cum()
X = X.set_backend("numpy")
else:
X = X.copy().incr_to_cum()
X = X.copy()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed cumulative conversion in fit

Medium Severity

fit no longer calls incr_to_cum() on X after copying or setting the numpy backend. Disposal rates are cumulative paid (or similar) divided by ultimate; fitting on incremental values yields incorrect ratios and regression inputs for incremental triangles.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c1830d. Configure here.

if sample_weight.array_backend == "sparse":
ult = sample_weight.set_backend("numpy")
else:
ult = sample_weight.copy()
#get backend
#calculate disposal rate triangle
self.xp = X.get_array_module()
self.X_ = X.sort_index()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted X_ mismatches disposal triangle

High Severity

fit stores a sorted triangle in self.X_ but builds disposal_rate_tri from the unsorted local X. Weights and super().fit then mix sorted self.X_.values with rates and ult.values tied to the original row order, so origins can be paired incorrectly whenever the index is not already sorted.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c1830d. Configure here.

self.disposal_rate_tri = X / ult.values
#get weights for estimation
tw = TriangleWeight(
n_periods = self.n_periods,
drop_high = self.drop_high,
Expand All @@ -214,16 +224,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
Expand Down Expand Up @@ -253,10 +264,13 @@ def transform(
if sample_weight is None:
raise ValueError("sample_weight is required.")
X_new = copy.deepcopy(X)
#validate dimensions of 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
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]
Expand Down
15 changes: 14 additions & 1 deletion chainladder/methods/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):

Expand Down Expand Up @@ -138,7 +144,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]
Expand Down
Loading