diff --git a/chainladder/tails/__init__.py b/chainladder/tails/__init__.py index f4b09df8b..6062d2102 100644 --- a/chainladder/tails/__init__.py +++ b/chainladder/tails/__init__.py @@ -1,11 +1,12 @@ -""" tails should store all tail methodologies -""" +"""tails should store all tail methodologies""" + from chainladder.tails.base import TailBase # noqa (API import) from chainladder.tails.constant import TailConstant # noqa (API import) from chainladder.tails.curve import TailCurve # noqa (API import) from chainladder.tails.bondy import TailBondy # noqa (API import) from chainladder.tails.clark import TailClark # noqa (API import) + __all__ = [ "TailBase", "TailConstant", diff --git a/chainladder/tails/bondy.py b/chainladder/tails/bondy.py index 2120984a2..ae32f1cd8 100644 --- a/chainladder/tails/bondy.py +++ b/chainladder/tails/bondy.py @@ -9,7 +9,8 @@ class TailBondy(TailBase): - """Estimator for the Generalized Bondy tail factor. + """ + Estimator for the Generalized Bondy tail factor. .. versionadded:: 0.6.0 @@ -121,7 +122,8 @@ def __init__(self, earliest_age=None, attachment_age=None, projection_period=12) self.projection_period = projection_period def fit(self, X, y=None, sample_weight=None): - """Fit the model with X. + """ + Fit the model with X. Parameters ---------- @@ -207,7 +209,8 @@ def fit(self, X, y=None, sample_weight=None): return self def transform(self, X): - """Transform X. + """ + Transform X. Parameters ---------- diff --git a/chainladder/tails/clark.py b/chainladder/tails/clark.py index 841afc8ef..f3740f505 100644 --- a/chainladder/tails/clark.py +++ b/chainladder/tails/clark.py @@ -6,7 +6,8 @@ class TailClark(TailBase): - """Allows for extraploation of LDFs to form a tail factor. + """ + Allows for extraploation of LDFs to form a tail factor. .. versionadded:: 0.6.4 @@ -112,15 +113,21 @@ class TailClark(TailBase): """ - def __init__(self, growth="loglogistic", truncation_age=None, - attachment_age=None, projection_period=12): + def __init__( + self, + growth="loglogistic", + truncation_age=None, + attachment_age=None, + projection_period=12, + ): self.growth = growth self.truncation_age = truncation_age self.attachment_age = attachment_age self.projection_period = projection_period def fit(self, X, y=None, sample_weight=None): - """Fit the model with X. + """ + Fit the model with X. Parameters ---------- @@ -151,14 +158,17 @@ def fit(self, X, y=None, sample_weight=None): fitted.values[..., :-1] / fitted.values[..., 1:], fitted.values[..., -1:], ), - -1, + axis=-1, ) fitted = xp.repeat(fitted, self.ldf_.values.shape[2], 2) attachment_age = self.attachment_age if self.attachment_age else X.ddims[-2] - self.ldf_.values = xp.concatenate(( - self.ldf_.values[..., : sum(self.ldf_.ddims < attachment_age)], - fitted[..., -sum(self.ldf_.ddims >= attachment_age) :],), - axis=-1,) + self.ldf_.values = xp.concatenate( + ( + self.ldf_.values[..., : sum(self.ldf_.ddims < attachment_age)], + fitted[..., -sum(self.ldf_.ddims >= attachment_age) :], + ), + axis=-1, + ) self.omega_ = model.omega_ self.theta_ = model.theta_ self.G_ = model.G_ @@ -169,14 +179,17 @@ def fit(self, X, y=None, sample_weight=None): self.elr_ = model.elr_ self.norm_resid_ = model.norm_resid_ if self.truncation_age: - self.ldf_.values[..., -1:] = self.ldf_.values[..., -1:] * self.G_(self.truncation_age).values + self.ldf_.values[..., -1:] = ( + self.ldf_.values[..., -1:] * self.G_(self.truncation_age).values + ) # self._get_tail_stats(self) if backend == "cupy": self = self.set_backend("cupy", inplace=True) return self def transform(self, X): - """Transform X. + """ + Transform X. Parameters ---------- diff --git a/chainladder/tails/constant.py b/chainladder/tails/constant.py index c06c01272..4cc94957e 100644 --- a/chainladder/tails/constant.py +++ b/chainladder/tails/constant.py @@ -6,7 +6,8 @@ class TailConstant(TailBase): - """Allows for the entry of a constant tail factor to LDFs. + """ + Allows for the entry of a constant tail factor to LDFs. Parameters ---------- @@ -129,7 +130,8 @@ def __init__(self, tail=1.0, decay=0.5, attachment_age=None, projection_period=1 self.projection_period = projection_period def fit(self, X, y=None, sample_weight=None): - """Fit the model with X. + """ + Fit the model with X. Parameters ---------- diff --git a/chainladder/tails/curve.py b/chainladder/tails/curve.py index 71d2a5f15..7782f460b 100644 --- a/chainladder/tails/curve.py +++ b/chainladder/tails/curve.py @@ -9,7 +9,8 @@ class TailCurve(TailBase): - """Allows for extraploation of LDFs to form a tail factor. + """ + Allows for extraploation of LDFs to form a tail factor. Parameters ---------- @@ -158,7 +159,8 @@ def __init__( self.projection_period = projection_period def fit(self, X, y=None, sample_weight=None): - """Fit the model with X. + """ + Fit the model with X. Parameters ---------- diff --git a/chainladder/tails/tests/test_bondy.py b/chainladder/tails/tests/test_bondy.py index 79d2deccc..b14239787 100644 --- a/chainladder/tails/tests/test_bondy.py +++ b/chainladder/tails/tests/test_bondy.py @@ -1,7 +1,17 @@ +from __future__ import annotations + import chainladder as cl +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder import Triangle + -def test_bondy1(): - tri = cl.load_sample("tail_sample")["paid"] +def test_bondy1(tail_sample: Triangle) -> None: + tri = tail_sample["paid"] dev = cl.Development(average="simple").fit_transform(tri) - assert round(float(cl.TailBondy(earliest_age=12).fit(dev).cdf_.values[0, 0, 0, -2]), 3) == 1.028 + assert ( + round(float(cl.TailBondy(earliest_age=12).fit(dev).cdf_.values[0, 0, 0, -2]), 3) + == 1.028 + ) diff --git a/chainladder/tails/tests/test_clark.py b/chainladder/tails/tests/test_clark.py new file mode 100644 index 000000000..8011c51fb --- /dev/null +++ b/chainladder/tails/tests/test_clark.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import numpy as np +import chainladder as cl + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder import Triangle + + +def test_truncation_age(genins: Triangle, atol: float) -> None: + """ + Validate that sufficiently distant truncation age is equivalent to + not truncating + """ + long_truncation = ( + cl.TailClark(truncation_age=99999).fit(cl.ClarkLDF().fit_transform(genins)).cdf_ + ) + no_truncation = cl.TailClark().fit(cl.ClarkLDF().fit_transform(genins)).cdf_ + assert np.allclose( + long_truncation.values[..., -1], no_truncation.values[..., -1], atol=atol + ) diff --git a/chainladder/tails/tests/test_constant.py b/chainladder/tails/tests/test_constant.py index b742ca0fd..aa34e4583 100644 --- a/chainladder/tails/tests/test_constant.py +++ b/chainladder/tails/tests/test_constant.py @@ -1,12 +1,14 @@ import chainladder as cl + def test_constant_balances(qtr): xp = qtr.get_array_module() assert ( round( float( xp.prod( - cl.TailConstant(1.05, decay=0.8) + cl + .TailConstant(1.05, decay=0.8) .fit(qtr) .ldf_.iloc[0, 1] .values[0, 0, 0, -5:] diff --git a/chainladder/tails/tests/test_exponential.py b/chainladder/tails/tests/test_exponential.py index ccfc70b94..f4170030e 100644 --- a/chainladder/tails/tests/test_exponential.py +++ b/chainladder/tails/tests/test_exponential.py @@ -1,13 +1,20 @@ +from __future__ import annotations + import chainladder as cl import pytest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder import Triangle + -def test_fit_period(): - tri = cl.load_sample("tail_sample") - dev = cl.Development(average="simple").fit_transform(tri) +def test_fit_period(tail_sample: Triangle) -> None: + dev = cl.Development(average="simple").fit_transform(tail_sample) assert ( round( - cl.TailCurve(fit_period=(tri.ddims[-7], None), extrap_periods=10) + cl + .TailCurve(fit_period=(tail_sample.ddims[-7], None), extrap_periods=10) .fit(dev) .cdf_["paid"] .set_backend("numpy", inplace=True) @@ -18,24 +25,18 @@ def test_fit_period(): ) -def test_curve_validation(): +def test_curve_validation(tail_sample: Triangle) -> None: """ Test validation of the curve parameter. Should raise a value error if an incorrect argument is supplied. """ with pytest.raises(ValueError): - tri = cl.load_sample('tail_sample') - cl.TailCurve( - curve='Exponential' - ).fit_transform(tri) + cl.TailCurve(curve="Exponential").fit_transform(tail_sample) -def test_errors_validation(): +def test_errors_validation(tail_sample: Triangle) -> None: """ Test validation of the errors parameter. Should raise a value error if an incorrect argument is supplied. """ with pytest.raises(ValueError): - tri = cl.load_sample('tail_sample') - cl.TailCurve( - errors='Ignore' - ).fit_transform(tri) + cl.TailCurve(errors="Ignore").fit_transform(tail_sample) diff --git a/conftest.py b/conftest.py index 76e7fd43e..afd3d52f8 100644 --- a/conftest.py +++ b/conftest.py @@ -30,6 +30,10 @@ def pytest_generate_tests(metafunc): ) if "prism" in metafunc.fixturenames: metafunc.parametrize("prism", ["sparse_only_run"], indirect=True) + if "tail_sample" in metafunc.fixturenames: + metafunc.parametrize( + "tail_sample", ["normal_run", "sparse_only_run"], indirect=True + ) if "xyz" in metafunc.fixturenames: metafunc.parametrize("xyz", ["normal_run", "sparse_only_run"], indirect=True) @@ -116,6 +120,11 @@ def monthly(request): yield from _sample_fixture(request, "prism", transform=lambda t: t.sum()) +@pytest.fixture +def tail_sample(request): + yield from _sample_fixture(request, "tail_sample") + + @pytest.fixture def xyz(request): yield from _sample_fixture(request, "xyz")