From e220034125a2157bfc9f68cc574bc7d662514eca Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 29 May 2026 07:28:41 +0000 Subject: [PATCH 1/2] alternative implementation --- chainladder/development/constant.py | 9 +- .../development/tests/test_constant.py | 393 +++++++++++++++++- 2 files changed, 377 insertions(+), 25 deletions(-) diff --git a/chainladder/development/constant.py b/chainladder/development/constant.py index e223c25b..b423c109 100644 --- a/chainladder/development/constant.py +++ b/chainladder/development/constant.py @@ -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): @@ -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) @@ -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,) + 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) diff --git a/chainladder/development/tests/test_constant.py b/chainladder/development/tests/test_constant.py index 1c84fc41..d8bfb832 100644 --- a/chainladder/development/tests/test_constant.py +++ b/chainladder/development/tests/test_constant.py @@ -23,36 +23,383 @@ def test_constant_ldf(raa): dev_c = cl.DevelopmentConstant(patterns=link_ratios, style="ldf").fit(raa) assert xp.allclose(dev.ldf_.values, dev_c.ldf_.values, atol=1e-5) + def test_constant_callable_axis0(clrd, atol): - agway = clrd.loc['Agway Ins Co', 'CumPaidLoss'] + agway = clrd.loc["Agway Ins Co", "CumPaidLoss"] + def paid_cdfs(x): - """ A function that returns different CDFs depending on a specified LOB """ + """A function that returns different CDFs depending on a specified LOB""" cdfs = { - 'comauto': [3.832, 1.874, 1.386, 1.181, 1.085, 1.043, 1.022, 1.013, 1.007, 1], - 'medmal': [24.168, 4.127, 2.103, 1.528, 1.275, 1.161, 1.088, 1.047, 1.018, 1], - 'othliab': [10.887, 3.416, 1.957, 1.433, 1.231, 1.119, 1.06, 1.031, 1.011, 1], - 'ppauto': [2.559, 1.417, 1.181, 1.084, 1.04, 1.019, 1.009, 1.004, 1.001, 1], - 'prodliab': [13.703, 5.613, 2.92, 1.765, 1.385, 1.177, 1.072, 1.034, 1.008, 1], - 'wkcomp': [4.106, 1.865, 1.418, 1.234, 1.141, 1.09, 1.056, 1.03, 1.01, 1]} + "comauto": [ + 3.832, + 1.874, + 1.386, + 1.181, + 1.085, + 1.043, + 1.022, + 1.013, + 1.007, + 1, + ], + "medmal": [ + 24.168, + 4.127, + 2.103, + 1.528, + 1.275, + 1.161, + 1.088, + 1.047, + 1.018, + 1, + ], + "othliab": [ + 10.887, + 3.416, + 1.957, + 1.433, + 1.231, + 1.119, + 1.06, + 1.031, + 1.011, + 1, + ], + "ppauto": [2.559, 1.417, 1.181, 1.084, 1.04, 1.019, 1.009, 1.004, 1.001, 1], + "prodliab": [ + 13.703, + 5.613, + 2.92, + 1.765, + 1.385, + 1.177, + 1.072, + 1.034, + 1.008, + 1, + ], + "wkcomp": [4.106, 1.865, 1.418, 1.234, 1.141, 1.09, 1.056, 1.03, 1.01, 1], + } patterns = pd.DataFrame(cdfs, index=range(12, 132, 12)).T - return patterns.loc[x.loc['LOB']].to_dict() - model = cl.DevelopmentConstant(patterns=paid_cdfs, callable_axis=0, style='cdf') - assert abs(model.fit_transform(agway).cdf_.loc['comauto'].iloc[..., 0].sum() - 3.832) < atol + return patterns.loc[x.loc["LOB"]].to_dict() + + model = cl.DevelopmentConstant(patterns=paid_cdfs, callable_axis=0, style="cdf") + assert ( + abs(model.fit_transform(agway).cdf_.loc["comauto"].iloc[..., 0].sum() - 3.832) + < atol + ) + def test_constant_callable_axis1(clrd, atol): - agway = clrd.loc['Agway Ins Co', 'comauto'] + agway = clrd.loc["Agway Ins Co", "comauto"] cdfs = { - 'IncurLoss': [3.832, 1.874, 1.386, 1.181, 1.085, 1.043, 1.022, 1.013, 1.007, 1], - 'CumPaidLoss': [24.168, 4.127, 2.103, 1.528, 1.275, 1.161, 1.088, 1.047, 1.018, 1], - 'BulkLoss': [10.887, 3.416, 1.957, 1.433, 1.231, 1.119, 1.06, 1.031, 1.011, 1], - 'EarnedPremDIR': [2.559, 1.417, 1.181, 1.084, 1.04, 1.019, 1.009, 1.004, 1.001, 1], - 'EarnedPremCeded': [13.703, 5.613, 2.92, 1.765, 1.385, 1.177, 1.072, 1.034, 1.008, 1], - 'EarnedPremNet': [4.106, 1.865, 1.418, 1.234, 1.141, 1.09, 1.056, 1.03, 1.01, 1]} + "IncurLoss": [3.832, 1.874, 1.386, 1.181, 1.085, 1.043, 1.022, 1.013, 1.007, 1], + "CumPaidLoss": [ + 24.168, + 4.127, + 2.103, + 1.528, + 1.275, + 1.161, + 1.088, + 1.047, + 1.018, + 1, + ], + "BulkLoss": [10.887, 3.416, 1.957, 1.433, 1.231, 1.119, 1.06, 1.031, 1.011, 1], + "EarnedPremDIR": [ + 2.559, + 1.417, + 1.181, + 1.084, + 1.04, + 1.019, + 1.009, + 1.004, + 1.001, + 1, + ], + "EarnedPremCeded": [ + 13.703, + 5.613, + 2.92, + 1.765, + 1.385, + 1.177, + 1.072, + 1.034, + 1.008, + 1, + ], + "EarnedPremNet": [ + 4.106, + 1.865, + 1.418, + 1.234, + 1.141, + 1.09, + 1.056, + 1.03, + 1.01, + 1, + ], + } patterns = pd.DataFrame(cdfs, index=range(12, 132, 12)).T + def paid_cdfs(x): - """ A function that returns different CDFs depending on a specified column """ - return patterns.loc[x.loc['columns']].to_dict() + """A function that returns different CDFs depending on a specified column""" + return patterns.loc[x.loc["columns"]].to_dict() + 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) \ No newline at end of file + 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) + + +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]) + ) \ No newline at end of file From 5180fdb96aaaaf5697bb27a5aa83f1069cf56e8c Mon Sep 17 00:00:00 2001 From: "henrydingliu@gmail.com" Date: Fri, 29 May 2026 07:33:45 +0000 Subject: [PATCH 2/2] update test --- .../development/tests/test_constant.py | 152 +++--------------- 1 file changed, 23 insertions(+), 129 deletions(-) diff --git a/chainladder/development/tests/test_constant.py b/chainladder/development/tests/test_constant.py index d8bfb832..fc541c34 100644 --- a/chainladder/development/tests/test_constant.py +++ b/chainladder/development/tests/test_constant.py @@ -23,145 +23,39 @@ def test_constant_ldf(raa): dev_c = cl.DevelopmentConstant(patterns=link_ratios, style="ldf").fit(raa) assert xp.allclose(dev.ldf_.values, dev_c.ldf_.values, atol=1e-5) - def test_constant_callable_axis0(clrd, atol): - agway = clrd.loc["Agway Ins Co", "CumPaidLoss"] - + agway = clrd.loc['Agway Ins Co', 'CumPaidLoss'] def paid_cdfs(x): - """A function that returns different CDFs depending on a specified LOB""" + """ A function that returns different CDFs depending on a specified LOB """ cdfs = { - "comauto": [ - 3.832, - 1.874, - 1.386, - 1.181, - 1.085, - 1.043, - 1.022, - 1.013, - 1.007, - 1, - ], - "medmal": [ - 24.168, - 4.127, - 2.103, - 1.528, - 1.275, - 1.161, - 1.088, - 1.047, - 1.018, - 1, - ], - "othliab": [ - 10.887, - 3.416, - 1.957, - 1.433, - 1.231, - 1.119, - 1.06, - 1.031, - 1.011, - 1, - ], - "ppauto": [2.559, 1.417, 1.181, 1.084, 1.04, 1.019, 1.009, 1.004, 1.001, 1], - "prodliab": [ - 13.703, - 5.613, - 2.92, - 1.765, - 1.385, - 1.177, - 1.072, - 1.034, - 1.008, - 1, - ], - "wkcomp": [4.106, 1.865, 1.418, 1.234, 1.141, 1.09, 1.056, 1.03, 1.01, 1], - } + 'comauto': [3.832, 1.874, 1.386, 1.181, 1.085, 1.043, 1.022, 1.013, 1.007, 1], + 'medmal': [24.168, 4.127, 2.103, 1.528, 1.275, 1.161, 1.088, 1.047, 1.018, 1], + 'othliab': [10.887, 3.416, 1.957, 1.433, 1.231, 1.119, 1.06, 1.031, 1.011, 1], + 'ppauto': [2.559, 1.417, 1.181, 1.084, 1.04, 1.019, 1.009, 1.004, 1.001, 1], + 'prodliab': [13.703, 5.613, 2.92, 1.765, 1.385, 1.177, 1.072, 1.034, 1.008, 1], + 'wkcomp': [4.106, 1.865, 1.418, 1.234, 1.141, 1.09, 1.056, 1.03, 1.01, 1]} patterns = pd.DataFrame(cdfs, index=range(12, 132, 12)).T - return patterns.loc[x.loc["LOB"]].to_dict() - - model = cl.DevelopmentConstant(patterns=paid_cdfs, callable_axis=0, style="cdf") - assert ( - abs(model.fit_transform(agway).cdf_.loc["comauto"].iloc[..., 0].sum() - 3.832) - < atol - ) - + return patterns.loc[x.loc['LOB']].to_dict() + model = cl.DevelopmentConstant(patterns=paid_cdfs, callable_axis=0, style='cdf') + assert abs(model.fit_transform(agway).cdf_.loc['comauto'].iloc[..., 0].sum() - 3.832) < atol def test_constant_callable_axis1(clrd, atol): - agway = clrd.loc["Agway Ins Co", "comauto"] + agway = clrd.loc['Agway Ins Co', 'comauto'] cdfs = { - "IncurLoss": [3.832, 1.874, 1.386, 1.181, 1.085, 1.043, 1.022, 1.013, 1.007, 1], - "CumPaidLoss": [ - 24.168, - 4.127, - 2.103, - 1.528, - 1.275, - 1.161, - 1.088, - 1.047, - 1.018, - 1, - ], - "BulkLoss": [10.887, 3.416, 1.957, 1.433, 1.231, 1.119, 1.06, 1.031, 1.011, 1], - "EarnedPremDIR": [ - 2.559, - 1.417, - 1.181, - 1.084, - 1.04, - 1.019, - 1.009, - 1.004, - 1.001, - 1, - ], - "EarnedPremCeded": [ - 13.703, - 5.613, - 2.92, - 1.765, - 1.385, - 1.177, - 1.072, - 1.034, - 1.008, - 1, - ], - "EarnedPremNet": [ - 4.106, - 1.865, - 1.418, - 1.234, - 1.141, - 1.09, - 1.056, - 1.03, - 1.01, - 1, - ], - } + 'IncurLoss': [3.832, 1.874, 1.386, 1.181, 1.085, 1.043, 1.022, 1.013, 1.007, 1], + 'CumPaidLoss': [24.168, 4.127, 2.103, 1.528, 1.275, 1.161, 1.088, 1.047, 1.018, 1], + 'BulkLoss': [10.887, 3.416, 1.957, 1.433, 1.231, 1.119, 1.06, 1.031, 1.011, 1], + 'EarnedPremDIR': [2.559, 1.417, 1.181, 1.084, 1.04, 1.019, 1.009, 1.004, 1.001, 1], + 'EarnedPremCeded': [13.703, 5.613, 2.92, 1.765, 1.385, 1.177, 1.072, 1.034, 1.008, 1], + 'EarnedPremNet': [4.106, 1.865, 1.418, 1.234, 1.141, 1.09, 1.056, 1.03, 1.01, 1]} patterns = pd.DataFrame(cdfs, index=range(12, 132, 12)).T - def paid_cdfs(x): - """A function that returns different CDFs depending on a specified column""" - return patterns.loc[x.loc["columns"]].to_dict() - + """ A function that returns different CDFs depending on a specified column """ + return patterns.loc[x.loc['columns']].to_dict() 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) - + 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) def test_constant_pattern_no_tail(): reported_patterns = {