Skip to content
Merged

Const #878

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
9 changes: 7 additions & 2 deletions chainladder/development/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from chainladder.development.base import DevelopmentBase
import pandas as pd
import numpy as np


class DevelopmentConstant(DevelopmentBase):
Expand Down Expand Up @@ -61,7 +62,7 @@ def fit(self, X, y=None, sample_weight=None):
obj = obj.iloc[..., :1, :-1]*0+1
if callable(self.patterns):
if self.callable_axis == 0:
ldf = obj.index.apply(self.patterns, axis=1)
ldf = obj.index.apply(self.patterns, axis=1)
ldf = (
pd.concat(ldf.apply(pd.DataFrame, index=[0]).values, axis=0)
.fillna(1)[obj.ddims].values)
Expand All @@ -75,7 +76,11 @@ def fit(self, X, y=None, sample_weight=None):
else:
raise ValueError('callable axis needs to be 0 or 1')
else:
ldf = xp.array([float(self.patterns[item]) for item in obj.ddims])
extra_dims = [x for x in self.patterns.keys() if x > np.max(obj.ddims)]
if extra_dims:
obj.values = xp.concatenate([obj.values]+[obj.iloc[...,-1:].values]*len(extra_dims), -1)
obj.ddims = np.concatenate((obj.ddims, extra_dims), 0,)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsorted extra_dims causes incorrect development pattern ordering

Medium Severity

extra_dims is built from self.patterns.keys() and preserves dictionary insertion order, but is never sorted before being concatenated to obj.ddims. If a user provides pattern keys in non-ascending order (e.g., {132: 1.1, 120: 1.1, ...}), the resulting obj.ddims will be out of order. This corrupts the CDF-to-LDF conversion (ldf[..., :-1] / ldf[..., 1:]) since it relies on consecutive development periods being in ascending order, leading to silently incorrect actuarial results.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5180fdb. Configure here.

ldf = xp.array([float(self.patterns.get(item,1)) for item in obj.ddims])
ldf = ldf[None, None, None, :]
if self.style == "cdf":
ldf = xp.concatenate((ldf[..., :-1] / ldf[..., 1:], ldf[..., -1:]), -1)
Expand Down
243 changes: 242 additions & 1 deletion chainladder/development/tests/test_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,245 @@ def paid_cdfs(x):
with pytest.raises(ValueError):
xerror = cl.DevelopmentConstant(patterns=paid_cdfs, callable_axis=2, style='cdf').fit(agway)
lhs = cl.DevelopmentConstant(patterns=paid_cdfs, callable_axis=1, style='cdf').fit(agway).cdf_
assert np.all(abs(lhs.values[0,:,0,:]-patterns.values[:,:-1]) < atol)
assert np.all(abs(lhs.values[0,:,0,:]-patterns.values[:,:-1]) < atol)

def test_constant_pattern_no_tail():
reported_patterns = {
12: 4.0,
24: 2.9,
36: 1.8,
48: 1.4,
60: 1.2,
72: 1.1,
84: 1.03,
96: 1.02,
# 108: 1.005,
}
auto_bi = cl.load_sample("friedland_auto_bi_insurer")
reported_BI_claim = cl.DevelopmentConstant(
patterns=reported_patterns, style="cdf"
).fit_transform(auto_bi["Reported Claims"])

assert np.all(
np.round(reported_BI_claim.cdf_.to_frame().values.flatten(), 6)
== np.array([4.0, 2.9, 1.8, 1.4, 1.2, 1.1, 1.03, 1.02])
)


def test_constant_pattern_has_tail():
reported_patterns = {
12: 4.0,
24: 2.9,
36: 1.8,
48: 1.4,
60: 1.2,
72: 1.1,
84: 1.03,
96: 1.02,
108: 1.005,
}
auto_bi = cl.load_sample("friedland_auto_bi_insurer")
reported_BI_claim = cl.DevelopmentConstant(
patterns=reported_patterns, style="cdf"
).fit_transform(auto_bi["Reported Claims"])

assert np.all(
np.round(reported_BI_claim.cdf_.to_frame().values.flatten(), 6)
== np.array([4.0, 2.9, 1.8, 1.4, 1.2, 1.1, 1.03, 1.02, 1.005])
)


def test_constant_pattern_exact_cdf(raa):
reported_patterns = {
12: 1.1,
24: 1.1,
36: 1.1,
48: 1.1,
60: 1.1,
72: 1.1,
84: 1.1,
96: 1.1,
108: 1.1,
120: 1.1,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="cdf"
).fit_transform(raa)

assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array([1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1])
)


def test_constant_pattern_exact_ldf(raa):
reported_patterns = {
12: 1.1,
24: 1.1,
36: 1.1,
48: 1.1,
60: 1.1,
72: 1.1,
84: 1.1,
96: 1.1,
108: 1.1,
120: 1.1,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="ldf"
).fit_transform(raa)

assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array(
[
2.593742,
2.357948,
2.143589,
1.948717,
1.771561,
1.61051,
1.4641,
1.331,
1.21,
1.1,
]
)
)


def test_constant_pattern_short_cdf(raa):
reported_patterns = {
12: 1.1,
24: 1.1,
36: 1.1,
48: 1.1,
60: 1.1,
72: 1.1,
# 84: 1.1,
# 96: 1.1,
# 108: 1.1,
# 120: 1.1,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="cdf"
).fit_transform(raa)

assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array([1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.0, 1.0, 1.0])
)


def test_constant_pattern_short_ldf(raa):
reported_patterns = {
12: 1.1,
24: 1.1,
36: 1.1,
48: 1.1,
60: 1.1,
72: 1.1,
# 84: 1.1,
# 96: 1.1,
# 108: 1.1,
# 120: 1.1,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="ldf"
).fit_transform(raa)

assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array([1.771561, 1.61051, 1.4641, 1.331, 1.21, 1.1, 1.0, 1.0, 1.0])
)


def test_constant_pattern_long_cdf(raa):
reported_patterns = {
12: 1.1,
24: 1.1,
36: 1.1,
48: 1.1,
60: 1.1,
72: 1.1,
84: 1.1,
96: 1.1,
108: 1.1,
120: 1.1,
132: 1.1,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="cdf"
).fit_transform(raa)
assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array([1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1])
)


def test_constant_pattern_long_ldf(raa):
reported_patterns = {
12: 1.1,
24: 1.1,
36: 1.1,
48: 1.1,
60: 1.1,
72: 1.1,
84: 1.1,
96: 1.1,
108: 1.1,
120: 1.1,
132: 1.1,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="ldf"
).fit_transform(raa)

assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array(
[
2.853117,
2.593742,
2.357948,
2.143589,
1.948717,
1.771561,
1.61051,
1.4641,
1.331,
1.21,
1.1
]
)
)


def test_constant_incr():
raa_incr = cl.load_sample("raa").cum_to_incr()
reported_patterns = {
12: 4.0,
24: 2.9,
36: 1.8,
48: 1.4,
60: 1.2,
72: 1.1,
84: 1.03,
96: 1.02,
108: 1.005,
}

result = cl.DevelopmentConstant(
patterns=reported_patterns, style="cdf"
).fit_transform(raa_incr)

assert np.all(
np.round(result.cdf_.to_frame().values.flatten(), 6)
== np.array([4.0, 2.9, 1.8, 1.4, 1.2, 1.1, 1.03, 1.02, 1.005])
)
Loading