Skip to content

Commit

Permalink
Merge pull request #519 from casact/#514
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethshsu authored May 21, 2024
2 parents d21f125 + 1bc0567 commit 7edfd1a
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 62 deletions.
18 changes: 18 additions & 0 deletions chainladder/core/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,10 +666,12 @@ def grain(self, grain="", trailing=False, inplace=False):
"M": ["Y", "S", "Q", "M"],
"S": ["S", "Y"],
}

if ograin_new not in valid.get(ograin_old, []) or dgrain_new not in valid.get(
dgrain_old, []
):
raise ValueError("New grain not compatible with existing grain")

if (
self.is_cumulative is None
and dgrain_old != dgrain_new
Expand All @@ -678,26 +680,35 @@ def grain(self, grain="", trailing=False, inplace=False):
raise AttributeError(
"The is_cumulative attribute must be set before using grain method."
)

if valid["M"].index(ograin_new) > valid["M"].index(dgrain_new):
raise ValueError("Origin grain must be coarser than development grain")

if self.is_full and not self.is_ultimate and not self.is_val_tri:
warnings.warn("Triangle includes extraneous development lags")

obj = self.dev_to_val()

if ograin_new != ograin_old:
freq = {"Y": "Y", "S": "2Q"}.get(ograin_new, ograin_new)

if trailing or (obj.origin.freqstr[-3:] != "DEC" and ograin_old != "M"):
origin_period_end = self.origin[-1].strftime("%b").upper()
else:
origin_period_end = "DEC"

indices = (
pd.Series(range(len(self.origin)), index=self.origin)
.resample("-".join([freq, origin_period_end]))
.indices
)

groups = pd.concat(
[pd.Series([k] * len(v), index=v) for k, v in indices.items()], axis=0
).values

obj = obj.groupby(groups, axis=2).sum()

obj.origin_close = origin_period_end
d_start = pd.Period(
obj.valuation[0],
Expand All @@ -707,6 +718,7 @@ def grain(self, grain="", trailing=False, inplace=False):
else dgrain_old + obj.origin.freqstr[-4:]
),
).to_timestamp(how="s")

if len(obj.ddims) > 1 and obj.origin.to_timestamp(how="s")[0] != d_start:
addl_ts = (
pd.period_range(obj.odims[0], obj.valuation[0], freq=dgrain_old)[
Expand All @@ -719,23 +731,29 @@ def grain(self, grain="", trailing=False, inplace=False):
addl.ddims = addl_ts
obj = concat((addl, obj), axis=-1)
obj.values = num_to_nan(obj.values)

if dgrain_old != dgrain_new and obj.shape[-1] > 1:
step = self._dstep()[dgrain_old][dgrain_new]
d = np.sort(
len(obj.development) - np.arange(0, len(obj.development), step) - 1
)

if obj.is_cumulative:
obj = obj.iloc[..., d]
else:
ddims = obj.ddims[d]
d2 = [d[0]] * (d[0] + 1) + list(np.repeat(np.array(d[1:]), step))
obj = obj.groupby(d2, axis=3).sum()
obj.ddims = ddims

obj.development_grain = dgrain_new

obj = obj.dev_to_val() if self.is_val_tri else obj.val_to_dev()

if inplace:
self = obj
return self

return obj

def trend(
Expand Down
44 changes: 36 additions & 8 deletions chainladder/development/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,30 +121,43 @@ def fit(self, X, y=None, sample_weight=None):
# Triangle must be cumulative and in "development" mode
obj = self._set_fit_groups(X).incr_to_cum().val_to_dev().copy()
xp = obj.get_array_module()

if self.fillna:
tri_array = num_to_nan((obj + self.fillna).values)
else:
tri_array = num_to_nan(obj.values.copy())
average_ = self._validate_assumption(X, self.average, axis=3)[... , :X.shape[3]-1]

average_ = self._validate_assumption(X, self.average, axis=3)[
..., : X.shape[3] - 1
]
self.average_ = average_.flatten()
n_periods_ = self._validate_assumption(X, self.n_periods, axis=3)[... , :X.shape[3]-1]
n_periods_ = self._validate_assumption(X, self.n_periods, axis=3)[
..., : X.shape[3] - 1
]
x, y = tri_array[..., :-1], tri_array[..., 1:]
exponent = xp.array(
[{"regression": 0, "volume": 1, "simple": 2}[x]
for x in average_[0, 0, 0]]
[{"regression": 0, "volume": 1, "simple": 2}[x] for x in average_[0, 0, 0]]
)
exponent = xp.nan_to_num(exponent * (y * 0 + 1))
link_ratio = y / x

if hasattr(X, "w_v2_"):
self.w_v2_ = self._set_weight_func(obj.age_to_age * X.w_v2_,obj.iloc[...,:-1,:-1])
self.w_v2_ = self._set_weight_func(
factor=obj.age_to_age * X.w_v2_,
# secondary_rank=obj.iloc[..., :-1, :-1]
)
else:
self.w_v2_ = self._set_weight_func(obj.age_to_age,obj.iloc[...,:-1,:-1])
self.w_v2_ = self._set_weight_func(
factor=obj.age_to_age,
# secondary_rank=obj.iloc[..., :-1, :-1]
)

self.w_ = self._assign_n_periods_weight(
obj, n_periods_
) * self._drop_adjustment(obj, link_ratio)
w = num_to_nan(self.w_ / (x ** (exponent)))
params = WeightedRegression(axis=2, thru_orig=True, xp=xp).fit(x, y, w)

if self.n_periods != 1:
params = params.sigma_fill(self.sigma_interpolation)
else:
Expand All @@ -153,20 +166,23 @@ def fit(self, X, y=None, sample_weight=None):
"of freedom to support calculation of all regression"
" statistics. Only LDFs have been calculated."
)

params.std_err_ = xp.nan_to_num(params.std_err_) + xp.nan_to_num(
(1 - xp.nan_to_num(params.std_err_ * 0 + 1))
* params.sigma_
/ xp.swapaxes(xp.sqrt(x ** (2 - exponent))[..., 0:1, :], -1, -2)
)

params = xp.concatenate((params.slope_, params.sigma_, params.std_err_), 3)
params = xp.swapaxes(params, 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)
resid = -obj.iloc[..., :-1] * self.ldf_.values + obj.iloc[..., 1:].values
std = xp.sqrt((1 / num_to_nan(w)) * (self.sigma_ ** 2).values)
std = xp.sqrt((1 / num_to_nan(w)) * (self.sigma_**2).values)
resid = resid / num_to_nan(std)
self.std_residuals_ = resid[resid.valuation < obj.valuation_date]

return self

def transform(self, X):
Expand All @@ -184,10 +200,21 @@ def transform(self, X):
"""
X_new = X.copy()
X_new.group_index = self._set_transform_groups(X_new)
triangles = ["std_err_", "ldf_", "sigma_","std_residuals_","average_", "w_", "sigma_interpolation","w_v2_"]
triangles = [
"std_err_",
"ldf_",
"sigma_",
"std_residuals_",
"average_",
"w_",
"sigma_interpolation",
"w_v2_",
]
for item in triangles:
setattr(X_new, item, getattr(self, item))

X_new._set_slicers()

return X_new

def _param_property(self, X, params, idx):
Expand All @@ -202,4 +229,5 @@ def _param_property(self, X, params, idx):
obj.is_cumulative = False
obj.virtual_columns.columns = {}
obj._set_slicers()

return obj
Loading

0 comments on commit 7edfd1a

Please sign in to comment.