Skip to content

Commit

Permalink
Merge pull request #516 from casact/#512
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethshsu authored May 17, 2024
2 parents efe6f23 + ebdb2dc commit d21f125
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 26 deletions.
1 change: 1 addition & 0 deletions chainladder/adjustments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from chainladder.adjustments.berqsherm import BerquistSherman # noqa (API import)
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)
41 changes: 34 additions & 7 deletions chainladder/adjustments/tests/test_trend.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,40 @@
import chainladder as cl
import numpy as np


def test_trend1(clrd):
tri = clrd[['CumPaidLoss', 'EarnedPremDIR']].sum()
assert (
cl.CapeCod(.05).fit(tri['CumPaidLoss'], sample_weight=tri['EarnedPremDIR'].latest_diagonal).ibnr_ ==
cl.CapeCod().fit(cl.Trend(.05).fit_transform(tri['CumPaidLoss']), sample_weight=tri['EarnedPremDIR'].latest_diagonal).ibnr_)
tri = clrd[["CumPaidLoss", "EarnedPremDIR"]].sum()
lhs = (
cl.CapeCod(0.05)
.fit(tri["CumPaidLoss"], sample_weight=tri["EarnedPremDIR"].latest_diagonal)
.ibnr_
)
rhs = (
cl.CapeCod()
.fit(
cl.Trend(0.05).fit_transform(tri["CumPaidLoss"]),
sample_weight=tri["EarnedPremDIR"].latest_diagonal,
)
.ibnr_
)
assert np.round(lhs, 0) == np.round(rhs, 0)


def test_trend2(raa):
tri = raa
assert abs(
cl.Trend(trends=[.05, .05], dates=[(None, '1985'), ('1985', None)], axis='origin').fit(tri).trend_*tri -
tri.trend(.05, axis='origin')).sum().sum() < 1e-6
assert (
abs(
cl.Trend(
trends=[0.05, 0.05],
dates=[(None, "1985"), ("1985", None)],
axis="origin",
)
.fit(tri)
.trend_
* tri
- tri.trend(0.05, axis="origin")
)
.sum()
.sum()
< 1e-6
)
104 changes: 100 additions & 4 deletions chainladder/adjustments/trend.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,18 @@ def fit(self, X, y=None, sample_weight=None):
dates = [dates] if type(dates) is not list else dates
if type(dates[0]) is not tuple:
raise AttributeError(
'Dates must be specified as a tuple of start and end dates')
"Dates must be specified as a tuple of start and end dates"
)
self.trend_ = X.copy()
for i, trend in enumerate(trends):
self.trend_ = self.trend_.trend(
trend, self.axis,
start=dates[i][0], end=dates[i][1])
trend, self.axis, start=dates[i][0], end=dates[i][1]
)
self.trend_ = self.trend_ / X
return self

def transform(self, X, y=None, sample_weight=None):
""" If X and self are of different shapes, align self to X, else
"""If X and self are of different shapes, align self to X, else
return self.
Parameters
Expand All @@ -85,3 +86,98 @@ def transform(self, X, y=None, sample_weight=None):
setattr(X_new, item, getattr(self, item))
X_new._set_slicers()
return X_new


class TrendConstant(BaseEstimator, TransformerMixin, EstimatorIO):
# """
# Estimator to create and apply trend factors to a Triangle object. Allows
# for compound trends as well as storage of the trend matrix to be used in
# other estimators, such as `CapeCod`.

# Parameters
# ----------

# trends: list-like
# The list containing the annual trends expressed as a decimal. For example,
# 5% decrease should be stated as -0.05
# dates: list of date-likes
# A list-like of (start, end) dates to correspond to the `trend` list.
# axis: str (options: [‘origin’, ‘valuation’])
# The axis on which to apply the trend

# Attributes
# ----------

# trend_:
# A triangle representation of the trend factors

# """

def __init__(
self,
base_trend=0.0,
trend_from="mid",
trend_to_date=None,
# dates=None,
axis="origin",
):
self.base_trend = base_trend
self.trend_from = trend_from
self.trend_to_date = trend_to_date
self.axis = axis

def fit(self, X, y=None, sample_weight=None):
# """Fit the model with X.

# Parameters
# ----------
# X: Triangle-like
# Data to which the model will be applied.
# y: Ignored
# sample_weight: Ignored

# Returns
# -------
# self: object
# Returns the instance itself.
# """
print("IN TrendConstant FIT")
print("base_trend", self.base_trend)

self.trendedvalues_ = X.copy().trend(
self.base_trend, self.axis # , start=dates[i][0], end=dates[i][1]
)
print("self.trendedvalues_\n", self.trendedvalues_)

# if type(dates[0]) is not tuple:
# raise AttributeError(
# "Dates must be specified as a tuple of start and end dates"
# )
# self.trend_ = X.copy()
# for i, trend in enumerate(trends):
# self.trend_ = self.trend_.trend(
# trend, self.axis, start=dates[i][0], end=dates[i][1]
# )
self.trendfactor_ = self.trendedvalues_ / X
print("self.trendfactor_\n", self.trendfactor_)
return self

def transform(self, X, y=None, sample_weight=None):
# """ 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 = X.copy()
# triangles = ["trend_"]
# for item in triangles:
# setattr(X_new, item, getattr(self, item))
# X_new._set_slicers()
return X_new
13 changes: 10 additions & 3 deletions chainladder/core/tests/test_triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import copy
import pytest
import io
from datetime import datetime

try:
from IPython.core.display import HTML
Expand Down Expand Up @@ -244,11 +245,17 @@ def test_df_period_input(raa):

def test_trend_on_vector(raa):
d = raa.latest_diagonal
assert (
d.trend(0.05, axis=2).to_frame(origin_as_datetime=False).astype(int).iloc[0, 0]
== 29217

trend_from = datetime.strptime("12-31-1981", "%m-%d-%Y")
trend_to = datetime.strptime("12-31-1990", "%m-%d-%Y")
days_to_trend = (trend_to - trend_from).days
expected_value = np.round(
raa.latest_diagonal.to_frame().iloc[0, 0] * (1.05) ** (days_to_trend / 365.25),
0,
)

assert np.round(d.trend(0.05, axis=2).to_frame().iloc[0, 0], 0) == expected_value


def test_latest_diagonal_val_to_dev(raa):
assert raa.latest_diagonal.val_to_dev() == raa[raa.valuation == raa.valuation_date]
Expand Down
48 changes: 36 additions & 12 deletions chainladder/core/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,15 +754,15 @@ def trend(
Parameters
----------
trend : float
The annual amount of the trend. Use 1/(1+trend)-1 to detrend.
The annual amount of the trend. Use 1/(1+trend)-1 to de-trend.
axis : str (options: ['origin', 'valuation'])
The axis on which to apply the trend
start: date
The start date from which trend should be calculated. If none is
provided then the latest date of the triangle is used.
provided, then the earliest date of the triangle is used.
end: date
The end date to which the trend should be calculated. If none is
provided then the earliest period of the triangle is used.
provided, then the valuation date of the triangle is used.
ultimate_lag : int
If ultimate valuations are in the triangle, optionally set the overall
age (in months) of the ultimate to be some lag from the latest non-Ultimate
Expand All @@ -777,35 +777,59 @@ def trend(
raise ValueError(
"Only origin and valuation axes are supported for trending"
)

# print("====== BEGIN ======")
xp = self.get_array_module()

start = pd.to_datetime(start) if type(start) is str else start
start = self.valuation_date if start is None else start
start = self.origin[0].to_timestamp() if start is None else start
# print("start", start)

end = pd.to_datetime(end) if type(end) is str else end
end = self.origin[0].to_timestamp() if end is None else end
end = self.valuation_date if end is None else end
# print("end", end)

if axis in ["origin", 2, -2]:
vector = pd.DatetimeIndex(
np.tile(
self.origin.to_timestamp(how="e").values, self.shape[-1]
self.origin.to_timestamp(how="e").date, self.shape[-1]
).flatten()
)
else:
vector = self.valuation
lower, upper = (end, start) if end > start else (start, end)
# print("vector\n", vector)

upper, lower = (end, start) if end > start else (start, end)
# print("lower", lower)
# print("upper", upper)

vector = pd.DatetimeIndex(
np.maximum(
np.minimum(np.datetime64(lower), vector.values), np.datetime64(upper)
np.minimum(np.datetime64(upper), vector.values), np.datetime64(lower)
)
)
vector = (
(start.year - vector.year) * 12 + (start.month - vector.month)
).values.reshape(self.shape[-2:], order="f")
# print("vector\n", vector)
# print("vector\n", vector)
# vector = (
# (end.year - vector.year) * 12 + (end.month - vector.month)
# ).values.reshape(self.shape[-2:], order="f")
# print("vector\n", vector)

vector = ((end - vector).days).values.reshape(self.shape[-2:], order="f")
# print("days to trend\n", vector)

if self.is_ultimate and ultimate_lag is not None and vector.shape[-1] > 1:
vector[:, -1] = vector[:, -2] + ultimate_lag

trend = (
xp.array((1 + trend) ** (vector / 12))[None, None, ...] * self.nan_triangle
xp.array((1 + trend) ** (vector / 365.25))[None, None, ...]
* self.nan_triangle
)
# print("trend\n", trend)

obj = self.copy()
obj.values = obj.values * trend

return obj

def broadcast_axis(self, axis, value):
Expand Down

0 comments on commit d21f125

Please sign in to comment.