From 7ce59c5d4121afbfa8e2b797b266667a4bffe0bf Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 4 Sep 2026 18:41:51 -0500 Subject: [PATCH 1/2] [FIX] Apply Ruff fix to chainladder/methods. --- chainladder/methods/base.py | 7 +- chainladder/methods/capecod.py | 24 +++-- chainladder/methods/mack.py | 32 +++--- chainladder/methods/tests/test_benktander.py | 42 ++++++-- chainladder/methods/tests/test_capecod.py | 105 ++++++++++++------- chainladder/methods/tests/test_mack.py | 75 +++++++------ chainladder/methods/tests/test_predict.py | 89 ++++++++++------ pyproject.toml | 7 -- 8 files changed, 232 insertions(+), 149 deletions(-) diff --git a/chainladder/methods/base.py b/chainladder/methods/base.py index 6fcf0537f..d573a04ca 100644 --- a/chainladder/methods/base.py +++ b/chainladder/methods/base.py @@ -3,7 +3,6 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations -import numpy as np import pandas as pd import warnings from sklearn.base import BaseEstimator @@ -21,7 +20,7 @@ class MethodBase(BaseEstimator, EstimatorIO, Common): _estimator_type = "chainladder" - def validate_X(self, X): + def validate_X(self, X): # noqa: N802 obj = X.copy() if "ldf_" not in obj: obj = Development().fit_transform(obj) @@ -31,7 +30,7 @@ def validate_X(self, X): def _align_cdf(self, X, sample_weight=None): """ Vertically align CDF to origin period latest diagonal. """ - return X.cdf_.align_pattern(X,sample_weight) + return X.cdf_.align_pattern(X, sample_weight) def _set_ult_attr(self, ultimate): """ Ultimate scaffolding """ @@ -103,7 +102,7 @@ def predict(self, X, sample_weight=None): X_new = X.val_to_dev() if sum(X_new.ddims > self.ldf_.ddims.max()) > 0: raise ValueError("X has ages that exceed those available in model.") - X_new = X_new + (self.X_.val_to_dev().iloc[0,0].sum(2) * 0) + X_new = X_new + (self.X_.val_to_dev().iloc[0, 0].sum(2) * 0) self.validate_weight(X_new, sample_weight) if sample_weight: sample_weight = sample_weight.set_backend(X_new.array_backend) diff --git a/chainladder/methods/capecod.py b/chainladder/methods/capecod.py index 4eebf8309..bf5266b1d 100644 --- a/chainladder/methods/capecod.py +++ b/chainladder/methods/capecod.py @@ -2,7 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import warnings -import numpy as np from chainladder.methods import Benktander @@ -16,12 +15,12 @@ class CapeCod(Benktander): The cape cod trend assumption. Any Trend transformer on X will override this argument. decay: float, optional (default=1.0) - The cape cod decay assumption. This parameter is required by the + The cape cod decay assumption. This parameter is required by the Generalized Cape Cod Method, as discussed in [Using Best Practices to - Determine a Best Reserve Estimate](https://www.casact.org/sites/default/files/database/forum_98fforum_struhuss.pdf) - by Struzzieri and Hussian. As the `decay` factor approaches 1 - (the default value), the result approaches the traditional Cape Cod - method. As the `decay` factor approaches 0, the result approaches + Determine a Best Reserve Estimate](https://www.casact.org/sites/default/files/database/forum_98fforum_struhuss.pdf) + by Struzzieri and Hussian. As the `decay` factor approaches 1 + (the default value), the result approaches the traditional Cape Cod + method. As the `decay` factor approaches 0, the result approaches the `Chainladder` method. n_iters: int, optional (default=1) Number of iterations to use in the Benktander model. @@ -80,9 +79,9 @@ class CapeCod(Benktander): cl.CapeCod().fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal).ibnr_ ) print(ibnr) - + .. testoutput:: - + 2261 1998 NaN 1999 88.211299 @@ -324,10 +323,13 @@ def predict(self, X, sample_weight=None): # If model was fit at a higher grain, then need to aggregate predicted aprioris too if len(set(sample_weight.key_labels) - set(self.apriori_.key_labels)) > 1: apriori_, detrended_apriori_ = self._get_capecod_aprioris( - X_new.groupby(self.apriori_.key_labels).sum(), - sample_weight.groupby(self.apriori_.key_labels).sum()) + X_new.groupby(self.apriori_.key_labels).sum(), + sample_weight.groupby(self.apriori_.key_labels).sum(), + ) else: - apriori_, detrended_apriori_ = self._get_capecod_aprioris(X_new, sample_weight) + apriori_, detrended_apriori_ = self._get_capecod_aprioris( + X_new, sample_weight + ) X_new.expectation_ = sample_weight * detrended_apriori_ X_new = super().predict(X_new, X_new.expectation_) X_new.apriori_ = apriori_ diff --git a/chainladder/methods/mack.py b/chainladder/methods/mack.py index e0405680c..98714bf02 100644 --- a/chainladder/methods/mack.py +++ b/chainladder/methods/mack.py @@ -1,13 +1,13 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -import numpy as np import pandas as pd from chainladder.methods import Chainladder class MackChainladder(Chainladder): - """ Basic stochastic chainladder method popularized by Thomas Mack + """ + Basic stochastic chainladder method popularized by Thomas Mack Parameters ---------- @@ -232,15 +232,13 @@ def predict(self, X, sample_weight=None): X_new._full_triangle_ = X_new.full_triangle_ X_new.parameter_risk_ = self._mack_recursion("parameter_risk_", X_new) X_new.process_risk_ = self._mack_recursion("process_risk_", X_new) - X_new.total_process_risk_ = (X_new.process_risk_ ** 2).sum(axis="origin").sqrt() + X_new.total_process_risk_ = (X_new.process_risk_**2).sum(axis="origin").sqrt() X_new.total_parameter_risk_ = self._mack_recursion( "total_parameter_risk_", X_new ) X_new.full_std_err_ = self._get_full_std_err_(X_new) X_new.total_mack_std_err_ = self._get_total_mack_std_err_(X_new) - X_new.mack_std_err_ = ( - X_new.parameter_risk_ ** 2 + X_new.process_risk_ ** 2 - ).sqrt() + X_new.mack_std_err_ = (X_new.parameter_risk_**2 + X_new.process_risk_**2).sqrt() del X_new._full_triangle_ return X_new @@ -295,7 +293,9 @@ def _get_full_std_err_(self, X=None): val = xp.broadcast_to(xp.array(avg + [avg[-1]]), X.shape) weight = xp.sqrt(full.values[..., : len(X.ddims)] ** (2 - val)) obj.values = X.sigma_.values / num_to_nan(weight) - w = lxp.concatenate((X.w_, lxp.ones((val.shape[0], val.shape[1], val.shape[2], 1))), 3) + w = lxp.concatenate( + (X.w_, lxp.ones((val.shape[0], val.shape[1], val.shape[2], 1))), 3 + ) w[xp.isnan(w)] = 1 obj.values = xp.nan_to_num(obj.values) * xp.array(w) obj.valuation_date = full.valuation_date @@ -335,7 +335,7 @@ def total_process_risk_(self): 72 84 9999 2007 1039.901929 1069.726277 1069.726277 """ - return (self.process_risk_ ** 2).sum(axis="origin").sqrt() + return (self.process_risk_**2).sum(axis="origin").sqrt() def _mack_recursion(self, est, X=None): obj = X.copy() @@ -348,16 +348,20 @@ def _mack_recursion(self, est, X=None): future_std_err = ( X._full_triangle_ - X[X.valuation < X.valuation_date] ).iloc[:, :, :, : X.shape[3]] * X.std_err_.values - #sum applies auto_sparse, so backend needs to be forced - t1_t = xp.nan_to_num(future_std_err.sum("origin").set_backend(backend).values) + # sum applies auto_sparse, so backend needs to be forced + t1_t = xp.nan_to_num( + future_std_err.sum("origin").set_backend(backend).values + ) obj.odims = obj.odims[0:1] else: nans = xp.nan_to_num(X.nan_triangle[None, None]) nans = 1 - xp.concatenate((nans, xp.zeros((1, 1, X.shape[2], 1))), 3) full_tri = X._full_triangle_.values[..., : len(X.ddims)] if est == "parameter_risk_": - #std_err_ is always numpy - t1_t = xp.nan_to_num(full_tri) * obj.std_err_.set_backend(backend).values + # std_err_ is always numpy + t1_t = ( + xp.nan_to_num(full_tri) * obj.std_err_.set_backend(backend).values + ) else: t1_t = xp.nan_to_num(full_tri) * self._get_full_std_err_(X).values extend = X.ldf_.shape[-1] - X.shape[-1] + 1 @@ -413,7 +417,7 @@ def mack_std_err_(self): 2012 673.828536 693.166178 693.166178 2013 876.437914 901.408385 901.408385 """ - return (self.parameter_risk_ ** 2 + self.process_risk_ ** 2).sqrt() + return (self.parameter_risk_**2 + self.process_risk_**2).sqrt() @property def total_mack_std_err_(self): @@ -452,7 +456,7 @@ def total_mack_std_err_(self): return self._get_total_mack_std_err_(self) def _get_total_mack_std_err_(self, obj): - obj = obj.total_process_risk_ ** 2 + obj.total_parameter_risk_ ** 2 + obj = obj.total_process_risk_**2 + obj.total_parameter_risk_**2 if obj.array_backend == "sparse": out = obj.set_backend("numpy").sqrt().values[..., 0, -1] else: diff --git a/chainladder/methods/tests/test_benktander.py b/chainladder/methods/tests/test_benktander.py index d7a8fed5c..4e210dcb2 100644 --- a/chainladder/methods/tests/test_benktander.py +++ b/chainladder/methods/tests/test_benktander.py @@ -3,6 +3,7 @@ import chainladder as cl import pandas as pd + @pytest.fixture def atol(): return 1e-5 @@ -19,6 +20,7 @@ def test_bk_fit_weight(): with pytest.raises(ValueError): cl.Benktander().fit(raa) + @pytest.mark.parametrize("data", data) def test_benktander_to_chainladder(data, atol): tri = cl.load_sample(data) @@ -57,6 +59,7 @@ def test_different_backends(clrd): < 1 ) + def test_odd_shaped_triangle(): df = pd.DataFrame({ "claim_year": 2000 + pd.Series([0] * 8 + [1] * 4), @@ -73,8 +76,25 @@ def test_odd_shaped_triangle(): cumulative=False, ) atr = tr.grain("OYDQ") - ult1 = cl.Benktander(apriori = 1,n_iters=10000).fit(cl.Development(average="volume").fit_transform(atr),sample_weight = atr.latest_diagonal).ultimate_.sum() - ult2 = cl.Benktander(apriori = 1,n_iters=10000).fit(cl.Development(average="volume").fit_transform(tr),sample_weight = tr.latest_diagonal).ultimate_.grain("OYDQ").sum() + ult1 = ( + cl + .Benktander(apriori=1, n_iters=10000) + .fit( + cl.Development(average="volume").fit_transform(atr), + sample_weight=atr.latest_diagonal, + ) + .ultimate_.sum() + ) + ult2 = ( + cl + .Benktander(apriori=1, n_iters=10000) + .fit( + cl.Development(average="volume").fit_transform(tr), + sample_weight=tr.latest_diagonal, + ) + .ultimate_.grain("OYDQ") + .sum() + ) assert abs(ult1 - ult2) < 1e-5 @@ -83,19 +103,21 @@ def test_bf_apriori_sigma_is_lognormal(): tri = cl.load_sample("genins") boot = cl.BootstrapODPSample(n_sims=50000, random_state=42).fit_transform(tri) w = boot.latest_diagonal.copy() - w.values = np.ones_like(w.values) # sample_weight=1 -> expectation_ == raw multiplier + w.values = np.ones_like( + w.values + ) # sample_weight=1 -> expectation_ == raw multiplier sigma = 0.8 - bf = cl.BornhuetterFerguson( - apriori=1.0, apriori_sigma=sigma, random_state=7 - ).fit(boot, sample_weight=w) + bf = cl.BornhuetterFerguson(apriori=1.0, apriori_sigma=sigma, random_state=7).fit( + boot, sample_weight=w + ) mult = np.nanmean(bf.expectation_.values[:, 0, :, 0], axis=1) mult = mult[np.isfinite(mult)] - assert (mult > 0).all() # strictly positive -> lognormal, not normal - assert abs(mult.mean() - 1.0) < 0.02 # mean preserved (E = apriori) - assert abs(mult.std() - sigma) < 0.03 # SD preserved (== apriori_sigma) + assert (mult > 0).all() # strictly positive -> lognormal, not normal + assert abs(mult.mean() - 1.0) < 0.02 # mean preserved (E = apriori) + assert abs(mult.std() - sigma) < 0.03 # SD preserved (== apriori_sigma) def test_capecod_apriori_sigma_is_positive(): @@ -108,4 +130,4 @@ def test_capecod_apriori_sigma_is_positive(): vals = cc.expectation_.values vals = vals[np.isfinite(vals)] - assert (vals > 0).all() # ~10% would be negative under the old normal \ No newline at end of file + assert (vals > 0).all() # ~10% would be negative under the old normal diff --git a/chainladder/methods/tests/test_capecod.py b/chainladder/methods/tests/test_capecod.py index 55e05f96c..2a60244ad 100644 --- a/chainladder/methods/tests/test_capecod.py +++ b/chainladder/methods/tests/test_capecod.py @@ -1,12 +1,14 @@ import chainladder as cl import numpy as np + def test_struhuss(): X = cl.load_sample("cc_sample")["loss"] X = cl.TailConstant(tail=1 / 0.85).fit_transform(cl.Development().fit_transform(X)) sample_weight = cl.load_sample("cc_sample")["exposure"].latest_diagonal ibnr = int( - cl.CapeCod(trend=0.07, decay=0.75) + cl + .CapeCod(trend=0.07, decay=0.75) .fit(X, sample_weight=sample_weight) .ibnr_.sum() ) @@ -14,20 +16,37 @@ def test_struhuss(): def test_groupby(clrd): - clrd = clrd[clrd['LOB']=='comauto'] + clrd = clrd[clrd["LOB"] == "comauto"] # But only the top 10 get their own CapeCod aprioris. Smaller companies get grouped together - top_10 = clrd['EarnedPremDIR'].groupby('GRNAME').sum().latest_diagonal - top_10 = top_10.loc[..., '1997', :].to_frame(origin_as_datetime=True).nlargest(10) - cc_groupby = clrd.index['GRNAME'].map(lambda x: x if x in top_10.index else 'Remainder') + top_10 = clrd["EarnedPremDIR"].groupby("GRNAME").sum().latest_diagonal + top_10 = top_10.loc[..., "1997", :].to_frame(origin_as_datetime=True).nlargest(10) + cc_groupby = clrd.index["GRNAME"].map( + lambda x: x if x in top_10.index else "Remainder" + ) idx = clrd.index - idx['Top 10'] = cc_groupby + idx["Top 10"] = cc_groupby clrd.index = idx # All companies share the same development factors regardless of size - X = cl.Development().fit(clrd['CumPaidLoss'].sum()).transform(clrd['CumPaidLoss']) - sample_weight=clrd['EarnedPremDIR'].latest_diagonal - a = cl.CapeCod(groupby='Top 10', decay=0.98, trend=0.02).fit(X, sample_weight=sample_weight).ibnr_.groupby('Top 10').sum().sort_index() - b = cl.CapeCod(decay=0.98, trend=0.02).fit(X.groupby('Top 10').sum(), sample_weight=sample_weight.groupby('Top 10').sum()).ibnr_.sort_index() + X = cl.Development().fit(clrd["CumPaidLoss"].sum()).transform(clrd["CumPaidLoss"]) + sample_weight = clrd["EarnedPremDIR"].latest_diagonal + a = ( + cl + .CapeCod(groupby="Top 10", decay=0.98, trend=0.02) + .fit(X, sample_weight=sample_weight) + .ibnr_.groupby("Top 10") + .sum() + .sort_index() + ) + b = ( + cl + .CapeCod(decay=0.98, trend=0.02) + .fit( + X.groupby("Top 10").sum(), + sample_weight=sample_weight.groupby("Top 10").sum(), + ) + .ibnr_.sort_index() + ) xp = a.get_array_module() b = b.set_backend(a.array_backend) xp.allclose(xp.nan_to_num(a.values), xp.nan_to_num(b.values), atol=1e-5) @@ -35,52 +54,58 @@ def test_groupby(clrd): def test_capecod_zero_tri(raa): premium = raa.latest_diagonal * 0 + 50000 - raa.at['Total','values','1987',48] = 0 - assert cl.CapeCod().fit(raa, sample_weight=premium).ultimate_.loc[:,:,'1987'].sum() > 0 + raa.at["Total", "values", "1987", 48] = 0 + assert ( + cl.CapeCod().fit(raa, sample_weight=premium).ultimate_.loc[:, :, "1987"].sum() + > 0 + ) def test_capecod_predict1(prism): - """ github issue #400 + """github issue #400 Test whether we can make predictions at a more granular level than is fitted """ - prism = prism[['reportedCount', 'Paid']] + prism = prism[["reportedCount", "Paid"]] - cc_pipe = cl.Pipeline( - [('dev', cl.Development()), - ('model', cl.CapeCod())] - ) + cc_pipe = cl.Pipeline([("dev", cl.Development()), ("model", cl.CapeCod())]) cc_pipe.fit( - X=prism.groupby('Line')['Paid'].sum(), - sample_weight=prism.groupby('Line')['reportedCount'].sum().sum('development')) + X=prism.groupby("Line")["Paid"].sum(), + sample_weight=prism.groupby("Line")["reportedCount"].sum().sum("development"), + ) + + assert ( + abs( + cc_pipe.predict( + prism["Paid"], sample_weight=prism["reportedCount"].sum("development") + ).ultimate_.sum() + - cc_pipe.named_steps.model.ultimate_.sum() + ).sum() + < 1e-6 + ) - assert abs(cc_pipe.predict(prism['Paid'], sample_weight=prism['reportedCount'].sum('development')).ultimate_.sum() - - cc_pipe.named_steps.model.ultimate_.sum()).sum() < 1e-6 - def test_capecod_predict2(prism): - """ github issue #400 + """github issue #400 Test whether predictions between groupby with estimator and groupby outside estimator match """ - prism = prism[['reportedCount', 'Paid']] + prism = prism[["reportedCount", "Paid"]] - pipe1 = cl.Pipeline( - [('dev', cl.Development(groupby='Line')), - ('model', cl.CapeCod(groupby='Line'))] - ) - pipe1.fit( - X=prism['Paid'], - sample_weight=prism['reportedCount'].sum('development')) + pipe1 = cl.Pipeline([ + ("dev", cl.Development(groupby="Line")), + ("model", cl.CapeCod(groupby="Line")), + ]) + pipe1.fit(X=prism["Paid"], sample_weight=prism["reportedCount"].sum("development")) - pipe2 = cl.Pipeline( - [('dev', cl.Development()), - ('model', cl.CapeCod())] - ) + pipe2 = cl.Pipeline([("dev", cl.Development()), ("model", cl.CapeCod())]) pipe2.fit( - X=prism.groupby('Line')['Paid'].sum(), - sample_weight=prism.groupby('Line')['reportedCount'].sum().sum('development')) + X=prism.groupby("Line")["Paid"].sum(), + sample_weight=prism.groupby("Line")["reportedCount"].sum().sum("development"), + ) pred1 = pipe1.named_steps.model.ultimate_.sum() - pred2 = pipe2.predict(prism['Paid'], sample_weight=prism['reportedCount'].sum('development')).ultimate_.sum() + pred2 = pipe2.predict( + prism["Paid"], sample_weight=prism["reportedCount"].sum("development") + ).ultimate_.sum() - assert np.nan_to_num(abs(pred1 - pred2).values).sum() <= 1e-6 \ No newline at end of file + assert np.nan_to_num(abs(pred1 - pred2).values).sum() <= 1e-6 diff --git a/chainladder/methods/tests/test_mack.py b/chainladder/methods/tests/test_mack.py index 828d9f190..82088253c 100644 --- a/chainladder/methods/tests/test_mack.py +++ b/chainladder/methods/tests/test_mack.py @@ -1,71 +1,84 @@ import chainladder as cl import numpy as np + def test_mack_to_triangle(): assert ( - cl.MackChainladder() + cl + .MackChainladder() .fit( cl.TailConstant().fit_transform( cl.Development().fit_transform(cl.load_sample("ABC")) ) ) .summary_ - == cl.MackChainladder() + == cl + .MackChainladder() .fit(cl.Development().fit_transform(cl.load_sample("ABC"))) .summary_ ) + def test_mack_malformed(raa): raa_alt = raa.copy().iloc[:, :, :-1] - x = cl.MackChainladder().fit(raa) + x = cl.MackChainladder().fit(raa) y = cl.MackChainladder().fit(raa_alt) - assert x.process_risk_.iloc[:,:,:-1] == y.process_risk_ + assert x.process_risk_.iloc[:, :, :-1] == y.process_risk_ + -def test_multi_triangle_mack(clrd,atol): - tri = clrd.loc['Agway Ins Co']['IncurLoss','CumPaidLoss'] +def test_multi_triangle_mack(clrd, atol): + tri = clrd.loc["Agway Ins Co"]["IncurLoss", "CumPaidLoss"] mack = cl.MackChainladder().fit(tri) for i in range(len(tri.index)): for j in range(len(tri.columns)): - assert np.all(abs(mack.full_std_err_.iloc[i,j].values-cl.MackChainladder().fit(tri.iloc[i,j]).full_std_err_.values) < atol) + assert np.all( + abs( + mack.full_std_err_.iloc[i, j].values + - cl.MackChainladder().fit(tri.iloc[i, j]).full_std_err_.values + ) + < atol + ) + def test_mack1999_hardcode(): """ Reconciles key MackChainladder statistics to values provided in the paper """ - #sourced from Table 1, p365 of Mack(1999) - ldf_se = [2.24,.517,.122,.051,.042,.023,.015,.012] - sigma = [1337,988.5,440.1,207.0,164.2,74.6,35.49,16.89] - #sourced from Table 2, p366 of Mack(1999) - ibnr_se = [0,61,140,319,596,1038,1298,1806,2182] + # sourced from Table 1, p365 of Mack(1999) + ldf_se = [2.24, 0.517, 0.122, 0.051, 0.042, 0.023, 0.015, 0.012] + sigma = [1337, 988.5, 440.1, 207.0, 164.2, 74.6, 35.49, 16.89] + # sourced from Table 2, p366 of Mack(1999) + ibnr_se = [0, 61, 140, 319, 596, 1038, 1298, 1806, 2182] tri = cl.load_sample("mortgage") - dev = cl.Development(sigma_interpolation = 'mack').fit_transform(tri) + dev = cl.Development(sigma_interpolation="mack").fit_transform(tri) model = cl.MackChainladder().fit(dev) ldf_rhs = dev.std_err_.values.flatten() - assert np.allclose(ldf_se[0],ldf_rhs[0],atol=0.01) - assert np.allclose(ldf_se[1:],ldf_rhs[1:],atol=0.001) + assert np.allclose(ldf_se[0], ldf_rhs[0], atol=0.01) + assert np.allclose(ldf_se[1:], ldf_rhs[1:], atol=0.001) sigma_rhs = dev.sigma_.values.flatten() - assert np.allclose(sigma[0],sigma_rhs[0],atol=1) - assert np.allclose(sigma[1:],sigma_rhs[1:],atol=0.1) - ibnr_rhs = model.summary_.values[0,0,:,-1]/1000 - assert np.allclose(ibnr_se,np.nan_to_num(ibnr_rhs,nan=0),atol=1,equal_nan=True) + assert np.allclose(sigma[0], sigma_rhs[0], atol=1) + assert np.allclose(sigma[1:], sigma_rhs[1:], atol=0.1) + ibnr_rhs = model.summary_.values[0, 0, :, -1] / 1000 + assert np.allclose(ibnr_se, np.nan_to_num(ibnr_rhs, nan=0), atol=1, equal_nan=True) + def test_mack1994_hardcode(raa): """ Reconciles key MackChainladder statistics to values provided in the paper """ - #sourced from top table on p130 of Mack(1994) - sigma_sq = [27883,1109,691,61.2,119,40.8,1.34,7.88] - #sourced from bottom table on p130 of Mack(1994) - ibnr_se = [206,623,747,1469,2002,2209,5358,6333,24566] + # sourced from top table on p130 of Mack(1994) + sigma_sq = [27883, 1109, 691, 61.2, 119, 40.8, 1.34, 7.88] + # sourced from bottom table on p130 of Mack(1994) + ibnr_se = [206, 623, 747, 1469, 2002, 2209, 5358, 6333, 24566] - dev = cl.Development(sigma_interpolation = 'mack').fit_transform(raa) + dev = cl.Development(sigma_interpolation="mack").fit_transform(raa) model = cl.MackChainladder().fit(dev) sigma_rhs = dev.sigma_.values.flatten() ** 2 - assert np.allclose(sigma_sq[:3],sigma_rhs[:3],atol=1) - assert np.allclose(sigma_sq[3:4],sigma_rhs[3:4],atol=0.1) - assert np.allclose(sigma_sq[4:5],sigma_rhs[4:5],atol=1) - assert np.allclose(sigma_sq[5:6],sigma_rhs[5:6],atol=0.1) - assert np.allclose(sigma_sq[6:8],sigma_rhs[6:8],atol=0.01) - ibnr_rhs = model.summary_.values[0,0,:,-1] - assert np.allclose(ibnr_se,ibnr_rhs[1:],atol=1) + assert np.allclose(sigma_sq[:3], sigma_rhs[:3], atol=1) + assert np.allclose(sigma_sq[3:4], sigma_rhs[3:4], atol=0.1) + assert np.allclose(sigma_sq[4:5], sigma_rhs[4:5], atol=1) + assert np.allclose(sigma_sq[5:6], sigma_rhs[5:6], atol=0.1) + assert np.allclose(sigma_sq[6:8], sigma_rhs[6:8], atol=0.01) + ibnr_rhs = model.summary_.values[0, 0, :, -1] + assert np.allclose(ibnr_se, ibnr_rhs[1:], atol=1) diff --git a/chainladder/methods/tests/test_predict.py b/chainladder/methods/tests/test_predict.py index c594c48bc..47a08c7b2 100644 --- a/chainladder/methods/tests/test_predict.py +++ b/chainladder/methods/tests/test_predict.py @@ -1,7 +1,7 @@ import chainladder as cl import numpy as np import pandas as pd -import pytest +import pytest raa = cl.load_sample("RAA") raa_1989 = raa[raa.valuation < raa.valuation_date] @@ -9,6 +9,7 @@ apriori = cl_ult * 0 + (float(cl_ult.sum()) / 10) # Mean Chainladder Ultimate apriori_1989 = apriori[apriori.origin < "1990"] + @pytest.mark.parametrize( "estimators", [ @@ -16,60 +17,67 @@ cl.BornhuetterFerguson, cl.ExpectedLoss, cl.Benktander, - cl.Chainladder + cl.Chainladder, ], ) -def test_predict_and_weights(estimators,atol): +def test_predict_and_weights(estimators, atol): est = estimators().fit(raa_1989, sample_weight=apriori_1989) pred = est.predict(raa, sample_weight=apriori) assert pred assert np.allclose( - raa_1989.latest_diagonal.values.swapaxes(0,1), - cl.model_diagnostics(est)['Latest'].values, + raa_1989.latest_diagonal.values.swapaxes(0, 1), + cl.model_diagnostics(est)["Latest"].values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - raa.latest_diagonal.values.swapaxes(0,1), - cl.model_diagnostics(pred)['Latest'].values, + raa.latest_diagonal.values.swapaxes(0, 1), + cl.model_diagnostics(pred)["Latest"].values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - est.ultimate_.values.swapaxes(0,1), - cl.model_diagnostics(est)['Ultimate'].values, + est.ultimate_.values.swapaxes(0, 1), + cl.model_diagnostics(est)["Ultimate"].values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - pred.ultimate_.values.swapaxes(0,1), - cl.model_diagnostics(pred)['Ultimate'].values, + pred.ultimate_.values.swapaxes(0, 1), + cl.model_diagnostics(pred)["Ultimate"].values, atol=atol, - equal_nan=True - ) - #Test validation of sample_weight requirement. Should raise a value error if no weight is supplied. - if estimators in [cl.CapeCod,cl.BornhuetterFerguson,cl.ExpectedLoss,cl.Benktander]: + equal_nan=True, + ) + # Test validation of sample_weight requirement. Should raise a value error if no weight is supplied. + if estimators in [ + cl.CapeCod, + cl.BornhuetterFerguson, + cl.ExpectedLoss, + cl.Benktander, + ]: assert np.allclose( - est.expectation_.values.swapaxes(0,1), - cl.model_diagnostics(est)['Apriori'].values, + est.expectation_.values.swapaxes(0, 1), + cl.model_diagnostics(est)["Apriori"].values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - pred.expectation_.values.swapaxes(0,1), - cl.model_diagnostics(pred)['Apriori'].values, + pred.expectation_.values.swapaxes(0, 1), + cl.model_diagnostics(pred)["Apriori"].values, atol=atol, - equal_nan=True - ) + equal_nan=True, + ) with pytest.raises(ValueError): estimators().fit(raa_1989) with pytest.raises(ValueError): estimators().fit(raa_1989, sample_weight=apriori_1989).predict(raa) + def test_mack_predict(): mack = cl.MackChainladder().fit(raa_1989) assert mack.predict(raa_1989) + def test_bs_random_state_predict(clrd): tri = clrd.groupby("LOB").sum().loc["wkcomp", ["CumPaidLoss", "EarnedPremNet"]] X = cl.BootstrapODPSample(random_state=100).fit_transform(tri["CumPaidLoss"]) @@ -78,7 +86,8 @@ def test_bs_random_state_predict(clrd): ) assert ( abs( - bf.predict(X, sample_weight=tri["EarnedPremNet"].latest_diagonal) + bf + .predict(X, sample_weight=tri["EarnedPremNet"].latest_diagonal) .ibnr_.sum() .sum() / bf.ibnr_.sum().sum() @@ -135,14 +144,16 @@ def test_misaligned_index2(clrd): assert abs(a - b) < 1e-5 a = bbk.ultimate_.iloc[150:153].sum().sum() b = ( - bbk.predict(clrd.iloc[150:153], sample_weight=w.iloc[150:153]) + bbk + .predict(clrd.iloc[150:153], sample_weight=w.iloc[150:153]) .ultimate_.sum() .sum() ) assert abs(a - b) < 1e-5 a = bcc.ultimate_.iloc[150:153].sum().sum() b = ( - bcc.predict(clrd.iloc[150:153], sample_weight=w.iloc[150:153]) + bcc + .predict(clrd.iloc[150:153], sample_weight=w.iloc[150:153]) .ultimate_.sum() .sum() ) @@ -153,14 +164,16 @@ def test_misaligned_index2(clrd): assert abs(a - b) < 1e-5 a = bbk.ultimate_.iloc[150:152].sum().sum() b = ( - bbk.predict(clrd.iloc[150:152], sample_weight=w.iloc[150:152]) + bbk + .predict(clrd.iloc[150:152], sample_weight=w.iloc[150:152]) .ultimate_.sum() .sum() ) assert abs(a - b) < 1e-5 a = bcc.ultimate_.iloc[150:152].sum().sum() b = ( - bcc.predict(clrd.iloc[150:152], sample_weight=w.iloc[150:152]) + bcc + .predict(clrd.iloc[150:152], sample_weight=w.iloc[150:152]) .ultimate_.sum() .sum() ) @@ -199,6 +212,7 @@ def test_check_val_tri_cl(raa): model = cl.Chainladder().fit(raa.dev_to_val()) assert model.predict(raa.latest_diagonal).ultimate_ == model.ultimate_ + def test_odd_shaped_triangle(): df = pd.DataFrame({ "claim_year": 2000 + pd.Series([0] * 8 + [1] * 4), @@ -214,6 +228,17 @@ def test_odd_shaped_triangle(): columns="payment", cumulative=False, ) - ult1 = cl.Chainladder().fit(cl.Development(average="volume").fit_transform(tr.grain("OYDQ"))).ultimate_.sum() - ult2 = cl.Chainladder().fit(cl.Development(average="volume").fit_transform(tr)).ultimate_.grain("OYDQ").sum() + ult1 = ( + cl + .Chainladder() + .fit(cl.Development(average="volume").fit_transform(tr.grain("OYDQ"))) + .ultimate_.sum() + ) + ult2 = ( + cl + .Chainladder() + .fit(cl.Development(average="volume").fit_transform(tr)) + .ultimate_.grain("OYDQ") + .sum() + ) assert abs(ult1 - ult2) < 1e-5 diff --git a/pyproject.toml b/pyproject.toml index 3799c97ea..72211897d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,14 +123,7 @@ select = ["E2", "E4", "E7", "E9", "F", "B018", "UP034", "N802"] "chainladder/development/tests/rtest_clark.py" = ["E266", "E722"] "chainladder/development/tests/rtest_development.py" = ["E266", "E722", "F401"] "chainladder/development/tests/rtest_munich.py" = ["E266", "E722", "F401"] -"chainladder/methods/base.py" = ["E231", "F401", "N802"] -"chainladder/methods/capecod.py" = ["F401"] -"chainladder/methods/mack.py" = ["E265", "F401"] "chainladder/methods/tests/rtest_mack.py" = ["E266", "E722", "F401"] -"chainladder/methods/tests/test_benktander.py" = ["E231", "E251"] -"chainladder/methods/tests/test_capecod.py" = ["E225", "E231"] -"chainladder/methods/tests/test_mack.py" = ["E226", "E231", "E251", "E265"] -"chainladder/methods/tests/test_predict.py" = ["E231", "E265"] "chainladder/tails/base.py" = ["E226", "E251"] "chainladder/tails/bondy.py" = ["F401"] "chainladder/tails/curve.py" = ["E226", "E721", "F401"] From 198832c5bca775f50aef6ccf04a820c56c86e4ee Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 4 Sep 2026 18:50:39 -0500 Subject: [PATCH 2/2] [FIX] Apply Ruff fix to chainladder/methods. --- chainladder/methods/base.py | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/chainladder/methods/base.py b/chainladder/methods/base.py index d573a04ca..7fa30956b 100644 --- a/chainladder/methods/base.py +++ b/chainladder/methods/base.py @@ -16,8 +16,8 @@ if TYPE_CHECKING: from chainladder.core import Triangle -class MethodBase(BaseEstimator, EstimatorIO, Common): +class MethodBase(BaseEstimator, EstimatorIO, Common): _estimator_type = "chainladder" def validate_X(self, X): # noqa: N802 @@ -29,11 +29,11 @@ def validate_X(self, X): # noqa: N802 return obj.val_to_dev() def _align_cdf(self, X, sample_weight=None): - """ Vertically align CDF to origin period latest diagonal. """ + """Vertically align CDF to origin period latest diagonal.""" return X.cdf_.align_pattern(X, sample_weight) def _set_ult_attr(self, ultimate): - """ Ultimate scaffolding """ + """Ultimate scaffolding""" from chainladder import options xp = ultimate.get_array_module() @@ -56,7 +56,7 @@ def latest_diagonal(self): if self.X_.is_cumulative: return self.X_.latest_diagonal else: - return self.X_.sum('development') + return self.X_.sum("development") def fit(self, X, y=None, sample_weight=None): """Applies the chainladder technique to triangle **X** @@ -109,10 +109,10 @@ def predict(self, X, sample_weight=None): X_new.ldf_ = self.ldf_ X_new, X_new.ldf_ = self.intersection(X_new, X_new.ldf_) return X_new - + def intersection(self, a, b): - """ Given two Triangles with mismatched indices, this method aligns - their indices """ + """Given two Triangles with mismatched indices, this method aligns + their indices""" if len(a) == 1 and len(b) == 1: return a, b intersection = list(set(a.key_labels).intersection(set(b.key_labels))) @@ -121,12 +121,24 @@ def intersection(self, a, b): a_idx = a.index[intersection] b_idx = b.index[intersection] idx_intersection = list( - set(a_idx.set_index(intersection).index.intersection( - b_idx.set_index(intersection).index))) + set( + a_idx.set_index(intersection).index.intersection( + b_idx.set_index(intersection).index + ) + ) + ) if (len(a) == 1 or len(b) == 1) and idx_intersection == []: return a, b - b = b.iloc[b_idx[b_idx[intersection].set_index(intersection).index.isin(idx_intersection)].index] - a = a.iloc[a_idx[a_idx[intersection].set_index(intersection).index.isin(idx_intersection)].index] + b = b.iloc[ + b_idx[ + b_idx[intersection].set_index(intersection).index.isin(idx_intersection) + ].index + ] + a = a.iloc[ + a_idx[ + a_idx[intersection].set_index(intersection).index.isin(idx_intersection) + ].index + ] return a, b def fit_predict(self, X, y=None, sample_weight=None): @@ -145,12 +157,12 @@ def _include_process_variance(self): @staticmethod def validate_weight( - X: Triangle, - sample_weight: Triangle + X: Triangle, + sample_weight: Triangle, ) -> None: - ''' + """ Checks that the a aprior has valid dimensions - ''' + """ if ( sample_weight and X.shape[:-1] != sample_weight.shape[:-1] @@ -158,4 +170,5 @@ def validate_weight( and sample_weight.shape[0] > 1 ): warnings.warn( - "X and sample_weight are not aligned. Broadcasting may occur.\n") + "X and sample_weight are not aligned. Broadcasting may occur.\n" + )