Skip to content
Open
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
52 changes: 33 additions & 19 deletions chainladder/tails/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,31 @@


class TailBase(DevelopmentBase):
""" Base class for all tail methods. Tail objects are equivalent
to development objects with an additional set of tail statistics"""
"""
Base class for all tail methods. Tail objects are equivalent
to development objects with an additional set of tail statistics
"""

def fit(self, X, y=None, sample_weight=None):
obj = X.copy()
if "ldf_" not in obj:
obj = Development().fit_transform(obj)
xp = obj.ldf_.get_array_module()
m = int(self.projection_period / 12)
self._ave_period = {"Y": (1 * m, 12), "Q": (4 * m, 3), "M": (12 * m, 1), "S": (2 * m, 6)}[
obj.development_grain
]
self._ave_period = {
"Y": (1 * m, 12),
"Q": (4 * m, 3),
"M": (12 * m, 1),
"S": (2 * m, 6),
}[obj.development_grain]
t_ddims = [
(item + 1) * self._ave_period[1] + obj.ldf_.ddims[-1]
for item in range(self._ave_period[0]+1)
for item in range(self._ave_period[0] + 1)
]
ddims = np.concatenate((obj.ldf_.ddims, t_ddims), 0,)
ddims = np.concatenate(
(obj.ldf_.ddims, t_ddims),
0,
)
self.ldf_ = obj.ldf_.copy()
tail = xp.ones(self.ldf_.shape)[..., -1:]
tail = xp.repeat(tail, self._ave_period[0] + 1, -1)
Expand All @@ -35,7 +43,7 @@ def fit(self, X, y=None, sample_weight=None):
self.sigma_.values = xp.concatenate((self.sigma_.values, zeros), -1)
self.std_err_ = getattr(obj, "std_err_").copy()
self.std_err_.values = xp.concatenate((self.std_err_.values, zeros), -1)
self.sigma_.ddims = self.std_err_.ddims = self.ldf_.ddims[:obj.shape[2]]
self.sigma_.ddims = self.std_err_.ddims = self.ldf_.ddims[: obj.shape[2]]
self.sigma_._set_slicers()
self.std_err_._set_slicers()
if hasattr(obj, "average_"):
Expand All @@ -46,7 +54,8 @@ def fit(self, X, y=None, sample_weight=None):
return self

def transform(self, X):
""" 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 Down Expand Up @@ -75,15 +84,15 @@ def _get_tail_prediction(self, tail_ldf):
return tail

def _get_initial_ldf(self, xp, tail):
""" Quadratic series expansion solution to return seed LDF for tail"""
"""Quadratic series expansion solution to return seed LDF for tail"""
arr = self.decay ** xp.arange(1000)
a = xp.sum(arr ** 2)
a = xp.sum(arr**2)
b = xp.sum(arr)
c = -xp.log(tail)
return (-b + xp.sqrt(b ** 2 - 4 * a * c)) / (2 * a)
return (-b + xp.sqrt(b**2 - 4 * a * c)) / (2 * a)

def _apply_decay(self, X, tail, attach_idx=None):
""" Created Tail vector with decay over time. """
"""Created Tail vector with decay over time."""
xp = self.ldf_.get_array_module()
if attach_idx:
decay_range = self.ldf_.shape[-1] - attach_idx
Expand All @@ -106,17 +115,21 @@ def _apply_decay(self, X, tail, attach_idx=None):
return self

def _get_tail_stats(self, X):
""" Method to approximate the tail sigma using
"""
Method to approximate the tail sigma using
log-linear extrapolation applied to tail average period
"""
from chainladder.utils.utility_functions import num_to_nan
if not hasattr(X, 'sigma_'):

if not hasattr(X, "sigma_"):
self.sigma_ = None
self.std_err_ = None
else:
time_pd = self._get_tail_weighted_time_period(X)
xp = X.sigma_.get_array_module()
reg = WeightedRegression(axis=3, xp=xp).fit(None, xp.log(X.sigma_.values), None)
reg = WeightedRegression(axis=3, xp=xp).fit(
None, xp.log(X.sigma_.values), None
)
sigma_ = xp.exp(time_pd * reg.slope_ + reg.intercept_)
y = X.std_err_.values
y = num_to_nan(y)
Expand All @@ -125,7 +138,7 @@ def _get_tail_stats(self, X):
if self.tail_.values.flatten().sum() / xp.prod(self.tail_.shape) == 1.0:
# If no tail, assume no variation
sigma_ = sigma_ * 0
std_err_ = std_err_* 0
std_err_ = std_err_ * 0
self.sigma_.values = xp.concatenate(
(self.sigma_.values[..., :-1], sigma_[..., -1:]), axis=-1
)
Expand All @@ -134,7 +147,8 @@ def _get_tail_stats(self, X):
)

def _get_tail_weighted_time_period(self, X):
""" Method to approximate the weighted-average development age of tail
"""
Method to approximate the weighted-average development age of tail
using log-linear extrapolation

Returns: float32
Expand All @@ -158,7 +172,7 @@ def _tail_(self):
== self.cdf_.development.iloc[-1 - self._ave_period[0]]
]
if np.all(df.values.min(axis=2) == df.values.max(axis=2)):
df = df.iloc[..., 0, :].to_frame(origin_as_datetime = False)
df = df.iloc[..., 0, :].to_frame(origin_as_datetime=False)
return df

@property
Expand Down
5 changes: 3 additions & 2 deletions chainladder/tails/bondy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pandas as pd
from scipy.optimize import least_squares
from chainladder.tails import TailBase
from chainladder.development import DevelopmentBase, Development
from chainladder.development import Development


class TailBondy(TailBase):
Expand Down Expand Up @@ -150,7 +150,8 @@ def fit(self, X, y=None, sample_weight=None):
else:
earliest_age = X.ddims[
int(
self.earliest_age / ({"Y": 12, "S": 6, "Q": 3, "M": 1}[X.development_grain])
self.earliest_age
/ ({"Y": 12, "S": 6, "Q": 3, "M": 1}[X.development_grain])
)
- 1
]
Expand Down
61 changes: 31 additions & 30 deletions chainladder/tails/curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,30 +128,27 @@ class TailCurve(TailBase):
"""

def __init__(
self,
curve="exponential",
fit_period=(None, None),
extrap_periods=100,
errors="ignore",
attachment_age=None,
reg_threshold=(1.00001, None),
projection_period=12
self,
curve="exponential",
fit_period=(None, None),
extrap_periods=100,
errors="ignore",
attachment_age=None,
reg_threshold=(1.00001, None),
projection_period=12,
):
# validate arguments

if curve not in [
'exponential',
'inverse_power',
'weibull'
]:
raise ValueError("Invalid curve type specified. Accepted values are 'exponential', 'inverse_power' and 'weibull'.")
if curve not in ["exponential", "inverse_power", "weibull"]:
raise ValueError(
"Invalid curve type specified. Accepted values are 'exponential', 'inverse_power' and 'weibull'."
)

if errors not in [
'ignore',
'raise'
]:
raise ValueError("Invalid value argument supplied to the errors parameter. Accepted values are 'raise' "
"and 'ignore'.")
if errors not in ["ignore", "raise"]:
raise ValueError(
"Invalid value argument supplied to the errors parameter. Accepted values are 'raise' "
"and 'ignore'."
)
self.curve = curve
self.fit_period = fit_period
self.extrap_periods = extrap_periods
Expand All @@ -175,11 +172,10 @@ def fit(self, X, y=None, sample_weight=None):
self : object
Returns the instance itself.
"""
from chainladder.utils.utility_functions import num_to_nan

X = X.copy()
xp = X.get_array_module()
if type(self.fit_period) == slice:
if type(self.fit_period) is slice:
warnings.warn(
"Slicing for fit_period is deprecated and will be removed. Please use a tuple (start_age, end_age)."
)
Expand Down Expand Up @@ -210,19 +206,22 @@ def fit(self, X, y=None, sample_weight=None):
if self.reg_threshold[0] is None:
warnings.warn(
"Lower threshold for ldfs not set. Lower threshold will be set to 1.0 to ensure"
"valid inputs for regression.")
"valid inputs for regression."
)
lower_threshold = 1
elif self.reg_threshold[0] < 1:
warnings.warn(
"Lower threshold for ldfs set too low (<1). Lower threshold will be set to 1.0 to ensure "
"valid inputs for regression.")
"valid inputs for regression."
)
lower_threshold = 1
else:
lower_threshold = self.reg_threshold[0]
if self.reg_threshold[1] is not None:
if self.reg_threshold[1] <= lower_threshold:
warnings.warn(
"Can't set upper threshold for ldfs below lower threshold. Upper threshold will be set to 'None'.")
"Can't set upper threshold for ldfs below lower threshold. Upper threshold will be set to 'None'."
)
upper_threshold = None
else:
upper_threshold = self.reg_threshold[1]
Expand Down Expand Up @@ -276,23 +275,25 @@ def _predict_tail(self, extrapolate):
if self.curve == "exponential":
tail_ldf = xp.exp(self._slope_ * extrapolate + self._intercept_)
if self.curve == "inverse_power":
tail_ldf = xp.exp(self._intercept_) * (extrapolate ** self._slope_)
tail_ldf = xp.exp(self._intercept_) * (extrapolate**self._slope_)
if self.curve == "weibull":
tail_ldf = 1/(1-xp.exp(-xp.exp(self._intercept_)
* extrapolate**self._slope_))-1
tail_ldf = (
1 / (1 - xp.exp(-xp.exp(self._intercept_) * extrapolate**self._slope_))
- 1
)
return self._get_tail_prediction(tail_ldf)

@property
def slope_(self):
""" Does not work with munich """
"""Does not work with munich"""
rows = self.ldf_.index.set_index(self.ldf_.key_labels).index
return pd.DataFrame(
self._slope_[..., 0, 0], index=rows, columns=self.ldf_.vdims
)

@property
def intercept_(self):
""" Does not work with munich """
"""Does not work with munich"""
rows = self.ldf_.index.set_index(self.ldf_.key_labels).index
return pd.DataFrame(
self._intercept_[..., 0, 0], index=rows, columns=self.ldf_.vdims
Expand Down
3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,6 @@ select = ["E2", "E4", "E7", "E9", "F", "B018", "UP034", "N802"]
"chainladder/development/tests/rtest_development.py" = ["E266", "E722", "F401"]
"chainladder/development/tests/rtest_munich.py" = ["E266", "E722", "F401"]
"chainladder/methods/tests/rtest_mack.py" = ["E266", "E722", "F401"]
"chainladder/tails/base.py" = ["E226", "E251"]
"chainladder/tails/bondy.py" = ["F401"]
"chainladder/tails/curve.py" = ["E226", "E721", "F401"]
"chainladder/tails/tests/rtest_exponential.py" = ["E722", "F401"]
"chainladder/workflow/tests/test_voting.py" = ["E231", "E731", "UP034"]
"chainladder/workflow/tests/test_workflow.py" = ["E203", "E241"]
Expand Down
Loading