Skip to content
24 changes: 24 additions & 0 deletions chainladder/adjustments/disposal.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,27 @@ def fit_transform(self, X, y=None, sample_weight=None):
Triangle with new full_triangle_
"""
return self.fit(X, y, sample_weight).transform(X, sample_weight=sample_weight)

def _param_property(self, X: Triangle, params: np.ndarray) -> Triangle:
"""
Private method to wrap a numpy array into a Triangle

Uses and overrides DevelopmentBase._param_property.

Parameters
----------
X : Triangle
The Triangle object that we want to place the parameter into

params : np.ndarray
The parammeter we want to turn into a Triangle

Returns
-------
Triangle
A Triangle that mimics X but has params values
"""
obj = super()._param_property(X, params)
obj.is_disposal_rate = True

return obj
2 changes: 2 additions & 0 deletions chainladder/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from chainladder.utils.sparse import sp

from chainladder.adjustments.disposal import DisposalMixin
from chainladder.development.incremental import IncrementalMixin

from typing import Optional, TYPE_CHECKING

Expand All @@ -50,6 +51,7 @@ class TriangleBase(
Common,
ABC,
DisposalMixin,
IncrementalMixin,
):
"""This class handles the initialization of a triangle"""

Expand Down
9 changes: 1 addition & 8 deletions chainladder/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def has_ldf(self):

@property
def has_zeta(self):
if hasattr(self, "zeta_"):
if hasattr(self, "_zeta_"):
return True
else:
return False
Expand Down Expand Up @@ -159,13 +159,6 @@ def pct_unreported_(self):
)
return 1 - 1 / self.cdf_

@property
def cum_zeta_(self):
if not self.has_zeta:
x = self.__class__.__name__
raise AttributeError("'" + x + "' object has no attribute 'cum_zeta_'")
return self.zeta_.incr_to_cum()

@property
def ibnr_(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions chainladder/core/tests/test_triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -3200,7 +3200,7 @@ def test_has_zeta_true(raa: Triangle) -> None:
None
"""
fitted = cl.IncrementalAdditive().fit(raa, sample_weight=raa.latest_diagonal)
assert fitted.has_zeta is True
assert fitted.has_zeta


def test_has_zeta_false(raa: Triangle) -> None:
Expand All @@ -3216,7 +3216,7 @@ def test_has_zeta_false(raa: Triangle) -> None:
-------
None
"""
assert cl.Development().fit(raa).has_zeta is False
assert not cl.Development().fit(raa).has_zeta


def test_cum_zeta_raises_when_no_zeta(raa: Triangle) -> None:
Expand Down
69 changes: 53 additions & 16 deletions chainladder/core/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,17 @@ def is_disposal_rate(self) -> bool:
def is_disposal_rate(self, is_dr: bool) -> None:
self._is_disposal_rate = is_dr

@property
def is_additive(self) -> bool:
"""
Indicates whether the Triangle holds a normal additive ratio (which goes from tail to head)
"""
return getattr(self, "_is_additive", False)

@is_additive.setter
def is_additive(self, is_add: bool) -> None:
self._is_additive = is_add

def align_pattern(
self, X: Triangle, sample_weight: Triangle | None = None
) -> Triangle:
Expand Down Expand Up @@ -1377,20 +1388,30 @@ def incr_to_cum(self, inplace=False):
if inplace:
xp = self.get_array_module()
if not self.is_cumulative:
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)
values = xp.cumprod(values, -1)[..., ::-1]
self.values = values * self.nan_triangle
values = num_to_value(values, self.get_array_module(values).nan)
else:
# fmt: off
if (
self.is_pattern
and (not self.is_additive)
and (not self.is_disposal_rate)
): # for multiplicative ldf triangles (mult from tail to head)
# fmt: on
values = xp.nan_to_num(self.values[..., ::-1])
values = num_to_value(values, 1)
values = xp.cumprod(values, -1)[..., ::-1]
self.values = values * self.nan_triangle
values = num_to_value(values, self.get_array_module(values).nan)
# fmt: off
elif (
self.is_pattern
and self.is_additive
and (not self.is_disposal_rate)
): # for additive factor triangles (sum from tail to head)
# fmt: on
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: # for normal value triangles
# also for disposal rate triangles (sum from head to tail)
if self.array_backend not in ["sparse", "dask"]:
self.values = (
xp.cumsum(xp.nan_to_num(self.values), 3)
Expand Down Expand Up @@ -1466,7 +1487,13 @@ 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 & (not self.is_disposal_rate):
# fmt: off
if (
self.is_pattern
and (not self.is_additive)
and (not self.is_disposal_rate)
): # for multiplicative ldf triangles (div from head to tail)
# fmt: on
xp = self.get_array_module()
self.values = num_to_value(xp.nan_to_num(self.values), 1)
diff = self.iloc[..., :-1] / self.iloc[..., 1:].values
Expand All @@ -1478,7 +1505,17 @@ def cum_to_incr(self, inplace=False):
axis=3,
)
self.values = self.values * self.nan_triangle
else:
# fmt: fn
elif (
self.is_pattern
and self.is_additive
and (not self.is_disposal_rate)
): # for additive factor triangles (diff from head to tail)
# fmt: on
diff = self.iloc[..., :-1] - self.iloc[..., 1:].values
self = concat((diff, self.iloc[..., -1]), axis=3)
Comment thread
cursor[bot] marked this conversation as resolved.
else: # for normal value triangles
# also for disposal rate triangles (diff from tail to head)
diff = self.iloc[..., 1:] - self.iloc[..., :-1].values
self = concat((self.iloc[..., 0], diff), axis=3)
self.is_cumulative = False
Expand Down
4 changes: 1 addition & 3 deletions chainladder/development/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,9 @@ def _param_property(X: Triangle, params: np.ndarray) -> Triangle:
from chainladder import options

obj: Triangle = X[X.origin == X.origin.min()]
obj.values = params
obj.values = params.copy()
Comment thread
cursor[bot] marked this conversation as resolved.
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
38 changes: 22 additions & 16 deletions chainladder/development/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from __future__ import annotations

import numpy as np
import pandas as pd
import warnings

from chainladder.development.base import DevelopmentBase
Expand Down Expand Up @@ -434,12 +433,9 @@ def fit(self, X: TriangleLike, y: None = None, sample_weight: None = None):
)
w_reg = params._w_reg

params = xp.concatenate((params.slope_, params.sigma_, params.std_err_), 3)
params = params.swapaxes(2, 3)

self.ldf_ = self._param_property(obj, params, 0)
self.sigma_ = self._param_property(obj, params, 1)
self.std_err_ = self._param_property(obj, params, 2)
self.ldf_ = self._param_property(obj, params.slope_.swapaxes(2, 3))
self.sigma_ = self._param_property(obj, params.sigma_.swapaxes(2, 3))
self.std_err_ = self._param_property(obj, params.std_err_.swapaxes(2, 3))

resid = -obj.iloc[..., :-1] * self.ldf_.values + obj.iloc[..., 1:].values
std = xp.sqrt((1 / num_to_nan(w_reg)) * (self.sigma_**2).values)
Expand Down Expand Up @@ -481,17 +477,27 @@ def transform(self, X):

return X_new

def _param_property(self, X, params, idx):
from chainladder import options
def _param_property(self, X, params):
"""
Private method to wrap a numpy array into a Triangle

Uses and overrides DevelopmentBase._param_property.

obj = X[X.origin == X.origin.min()]
xp = X.get_array_module()
obj.values = xp.ones(obj.shape)[..., :-1] * params[..., idx : idx + 1, :]
Parameters
----------
X : Triangle
The Triangle object that we want to place the parameter into

params : np.ndarray
The parammeter we want to turn into a Triangle

Returns
-------
Triangle
A Triangle that mimics X but has params values
"""
obj = super()._param_property(X, params)
obj.ddims = X.link_ratio.ddims
obj.valuation_date = pd.to_datetime(options.ULT_VAL)
obj.is_pattern = True
obj.is_cumulative = False
obj.virtual_columns.columns = {}
obj._set_slicers()

return obj
81 changes: 79 additions & 2 deletions chainladder/development/incremental.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
# 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.development import DevelopmentBase
from chainladder.utils import TriangleWeight
import numpy as np
import copy
import warnings

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from chainladder.core import Triangle

class IncrementalAdditive(DevelopmentBase):

class IncrementalMixin:
"""
This class provides attributes for the IncrementalAdditive development method
and transformed `Triangle`
"""

@property
def zeta_(self) -> Triangle:
"""
Gets the estimated incremental additive factors
"""
if not hasattr(self, "_zeta_"):
raise AttributeError("`zeta_` has not been estimated.")
return self._zeta_

@zeta_.setter
def zeta_(self, value: Triangle) -> None:
"""
Sets zeta_
"""
obj = copy.deepcopy(value)
obj.is_pattern = True
obj.is_additive = True
obj.is_cumulative = False
self._zeta_ = obj

@property
def cum_zeta_(self) -> Triangle:
"""
Gets the estimated cumulative additive factors
"""
return self.zeta_.incr_to_cum()

@cum_zeta_.setter
def cum_zeta_(self, value: Triangle) -> None:
"""
Sets cum_zeta_
"""
obj = copy.deepcopy(value)
obj.is_pattern = True
obj.is_additive = True
obj.is_cumulative = True
self._zeta_ = obj.cum_to_incr()


class IncrementalAdditive(DevelopmentBase, IncrementalMixin):
"""
The Incremental Additive Method.

Expand Down Expand Up @@ -311,11 +364,35 @@ def transform(self, X):
for item in [
"ldf_",
"w_",
"zeta_",
"_zeta_",
"incremental_",
"tri_zeta",
"fit_zeta_",
"sample_weight",
]:
X_new.__dict__[item] = self.__dict__[item]
return X_new

def _param_property(self, X, params):
"""
Private method to wrap a numpy array into a Triangle

Uses and overrides DevelopmentBase._param_property.

Parameters
----------
X : Triangle
The Triangle object that we want to place the parameter into

params : np.ndarray
The parammeter we want to turn into a Triangle

Returns
-------
Triangle
A Triangle that mimics X but has params values
"""
obj = super()._param_property(X, params)
obj.is_additive = True

return obj
2 changes: 1 addition & 1 deletion chainladder/development/tests/test_development.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def fit(self, X, y: None = None, sample_weight: None = None):
params = cl.WeightedRegression(axis=2, thru_orig=True, xp=xp).fit(
reg_x, reg_y, self.w_.values, average_param
)
self.ldf_ = self.dev._param_property(obj, params.slope_.swapaxes(2, 3), 0)
self.ldf_ = self.dev._param_property(obj, params.slope_.swapaxes(2, 3))
return self


Expand Down
Loading
Loading