diff --git a/chainladder/adjustments/tests/test_bootstrap.py b/chainladder/adjustments/tests/test_bootstrap.py index 31e463d9..a0d683e5 100644 --- a/chainladder/adjustments/tests/test_bootstrap.py +++ b/chainladder/adjustments/tests/test_bootstrap.py @@ -6,19 +6,31 @@ def test_bs_sample(raa): tri = raa a = ( - cl.Development() + cl + .Development() .fit(cl.BootstrapODPSample(n_sims=40000).fit_transform(tri).mean()) .ldf_ ) b = cl.Development().fit_transform(tri).ldf_ assert tri.get_array_module().all(abs(((a - b) / b).values) < 0.005) + def test_bs_multiple_cols(): assert cl.BootstrapODPSample().fit_transform( - cl.load_sample('berqsherm').iloc[0]).shape == (1000, 4, 8, 8) + cl.load_sample("berqsherm").iloc[0] + ).shape == (1000, 4, 8, 8) + def test_multi_index(clrd): - tri = clrd['CumPaidLoss'].sum() + tri = clrd["CumPaidLoss"].sum() resampled_triangles = cl.BootstrapODPSample().fit(tri).resampled_triangles_ - resampled_triangles.index - assert np.all(resampled_triangles.index == pd.DataFrame(np.concat((np.array([['(All)','(All)']] * 1000),np.arange(1000).reshape(-1,1)),axis=1),columns=['GRNAME','LOB','Simulation_#'])) \ No newline at end of file + assert np.all( + resampled_triangles.index + == pd.DataFrame( + np.concat( + (np.array([["(All)", "(All)"]] * 1000), np.arange(1000).reshape(-1, 1)), + axis=1, + ), + columns=["GRNAME", "LOB", "Simulation_#"], + ) + ) diff --git a/chainladder/core/display.py b/chainladder/core/display.py index 0a80c07d..e2015b5f 100644 --- a/chainladder/core/display.py +++ b/chainladder/core/display.py @@ -17,23 +17,19 @@ IPython = None if TYPE_CHECKING: - from pandas import ( - DataFrame, - IndexSlice, - Series - ) + from pandas import DataFrame, IndexSlice, Series -class TriangleDisplay: +class TriangleDisplay: def __repr__(self) -> str | DataFrame: # If values hasn't been defined yet, return an empty triangle. - if self._dimensionality == 'empty': + if self._dimensionality == "empty": return "Empty Triangle." # For triangles with a single segment, containing a single triangle, return the # DataFrame of the values. - elif self._dimensionality == 'single': + elif self._dimensionality == "single": data: DataFrame = self._repr_format() return data.to_string() @@ -72,15 +68,16 @@ def _repr_html_(self) -> str: """ # Case empty triangle. - if self._dimensionality == 'empty': + if self._dimensionality == "empty": return "Empty Triangle." # Case single-dimensional triangle. - elif self._dimensionality == 'single': + elif self._dimensionality == "single": data = self._repr_format() fmt_str = self._get_format_str(data=data) default = ( - data.to_html( + data + .to_html( max_rows=pd.options.display.max_rows, max_cols=pd.options.display.max_columns, float_format=fmt_str.format, @@ -115,10 +112,7 @@ def _get_format_str(data: DataFrame) -> str: else: return "{:,.0f}" - def _repr_format( - self, - origin_as_datetime: bool = False - ) -> DataFrame: + def _repr_format(self, origin_as_datetime: bool = False) -> DataFrame: """ Prepare triangle values for printing as a DataFrame. Mainly used with single-dimensional triangles. @@ -137,7 +131,8 @@ def _repr_format( origin_formatted = [""] * len(origin) for origin_index in range(len(origin)): origin_formatted[origin_index] = ( - origin.astype("str")[origin_index] + origin + .astype("str")[origin_index] .replace("Q1", "H1") .replace("Q3", "H2") ) @@ -147,12 +142,12 @@ def _repr_format( return pd.DataFrame(out, index=origin, columns=development) def heatmap( - self, - cmap: str = "coolwarm", - low: float = 0, - high: float = 0, - axis: int | str = 0, - subset: IndexSlice=None + self, + cmap: str = "coolwarm", + low: float = 0, + high: float = 0, + axis: int | str = 0, + subset: IndexSlice = None, ) -> Any: """ Color the background in a gradient according to the data in each @@ -179,7 +174,7 @@ def heatmap( ------- Ipython.display.HTML """ - if self._dimensionality == 'single': + if self._dimensionality == "single": data = self._repr_format() fmt_str = self._get_format_str(data) @@ -193,7 +188,8 @@ def heatmap( ) + 1 gmap = gmap.replace(np.nan, (shape_size + 1) / 2) default_output = ( - data.style.format(fmt_str) + data.style + .format(fmt_str) .background_gradient( cmap=cmap, low=low, @@ -222,13 +218,11 @@ def _dimensionality(self) -> str: ------- str """ - try: - self.values - except AttributeError: - return 'empty' + if not hasattr(self, "values"): + return "empty" if (self.values.shape[0], self.values.shape[1]) == (1, 1): - return 'single' + return "single" - else : - return 'multi' \ No newline at end of file + else: + return "multi" diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index fea9ad3e..0d42fff4 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -1876,7 +1876,7 @@ def test_single_entry(): cl_dev_constant_fit = cl_dev_constant.fit(cl_tri.val_to_dev()) # aim - cl.Chainladder().fit(cl_dev_constant_fit.transform(cl_tri)).ultimate_ + _ = cl.Chainladder().fit(cl_dev_constant_fit.transform(cl_tri)).ultimate_ def test_origin_as_datetime_arg(clrd): @@ -2429,7 +2429,6 @@ def test_2x2_triangle(): columns=["reported"], cumulative=True, ) - tri_from_df assert np.array_equal( tri_from_df.cum_to_incr().values, np.array([[[[78000.0, 144000.0], [78000.0, np.float64(np.nan)]]]]), diff --git a/chainladder/development/clark.py b/chainladder/development/clark.py index 413e5420..1081185b 100644 --- a/chainladder/development/clark.py +++ b/chainladder/development/clark.py @@ -142,7 +142,7 @@ class ClarkLDF(DevelopmentBase): 775 CumPaidLoss - LOB + LOB comauto 1.08 medmal 1.89 othliab 1.47 @@ -150,7 +150,7 @@ class ClarkLDF(DevelopmentBase): prodliab 1.44 wkcomp 1.11 CumPaidLoss - LOB + LOB comauto 20.48 medmal 35.13 othliab 37.75 @@ -160,20 +160,11 @@ class ClarkLDF(DevelopmentBase): """ - def __init__( - self, - growth: str = "loglogistic", - groupby=None - ): + def __init__(self, growth: str = "loglogistic", groupby=None): self.growth: str = growth self.groupby = groupby - def _G( - self, - age, - theta: float = None, - omega: float = None - ): + def _G(self, age, theta: float = None, omega: float = None): """Growth function. Parameters @@ -192,12 +183,12 @@ def _G( omega = self.omega_.values[..., None, None] age[age == 0.0] = xp.nan if self.growth == "loglogistic": - out = 1 + (theta ** omega) * (age ** (-omega)) + out = 1 + (theta**omega) * (age ** (-omega)) elif self.growth == "weibull": out = 1 / (1 - xp.exp(-((age / theta) ** omega))) else: ValueError(str(self.growth) + "is an invalid growth curve.") - out[xp.isnan(out)] = xp.inf # noqa + out[xp.isnan(out)] = xp.inf # noqa return out def G_(self, age): @@ -215,9 +206,9 @@ def G_(self, age): A Triangle object with growth curve values """ xp = self.incremental_act_.get_array_module() - if type(age) in [int, float, xp.int64, xp.float64]: + if isinstance(age, (int, float, xp.int64, xp.float64)): age = xp.array([age]).astype("float64") - if type(age) == list: + if isinstance(age, list): age = xp.array([age]).astype("float64") obj = self.incremental_act_.copy() obj.odims = obj.odims[0:1] @@ -286,8 +277,11 @@ def fit(self, X, y=None, sample_weight=None): for col in range(len(X.columns)): def solver(x: ndarray): - """ Solve Loglogistic MLE""" - ldf = lambda age: self._G(age, theta=x[..., 1], omega=x[..., 0]) + """Solve Loglogistic MLE""" + + def ldf(age): + return self._G(age, theta=x[..., 1], omega=x[..., 0]) + if sample_weight: ult = ( sample_weight.values[idx : idx + 1, col : col + 1, ::-1, 0] @@ -305,7 +299,7 @@ def solver(x: ndarray): increments[idx : idx + 1, col : col + 1] * xp.log(increment_fit) - increment_fit ) - return -xp.sum((xp.nan_to_num(mle.flatten()))) + return -xp.sum(xp.nan_to_num(mle.flatten())) if sample_weight: x0 = xp.array([[[[1.0, age_interval, 1.0]]]]) @@ -314,7 +308,9 @@ def solver(x: ndarray): x0 = xp.array([[[[1.0, age_interval]]]]) bounds = ((1e-6, None), (1e-6, None)) idx_params.append( - minimize(fun=solver, x0=x0.flatten(), bounds=bounds).x.reshape(1, 1, 1, -1) + minimize(fun=solver, x0=x0.flatten(), bounds=bounds).x.reshape( + 1, 1, 1, -1 + ) ) params.append(xp.concatenate(idx_params, axis=1)) params = xp.concatenate(params, axis=0) @@ -335,8 +331,7 @@ def solver(x: ndarray): if sample_weight: self.elr_ = pd.DataFrame(params[..., 0, 2], index=rows, columns=X.vdims) ultimate_ = ( - self._G(age=(latest_age - age_offset)[::-1]).swapaxes(-1, -2) - * ld.values + self._G(age=(latest_age - age_offset)[::-1]).swapaxes(-1, -2) * ld.values ) self.incremental_fits_ = X.copy() self.incremental_fits_.array_backend = "numpy" @@ -354,7 +349,7 @@ def solver(x: ndarray): 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 diff --git a/chainladder/development/munich.py b/chainladder/development/munich.py index 9e823bf8..a14f732d 100644 --- a/chainladder/development/munich.py +++ b/chainladder/development/munich.py @@ -150,7 +150,7 @@ 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 @@ -345,8 +345,8 @@ def _get_munich_full_triangle_( return self._p_to_i_concate(full_paid, full_incurred, xp) def _get_mcl_cdf(self, X, munich_full_triangle_): - """ needs to be an attribute that gets assigned. requires we overwrite - the cdf and ldf methods with + """needs to be an attribute that gets assigned. requires we overwrite + the cdf and ldf methods with """ xp = X.get_array_module() obj = X.cdf_.copy() @@ -379,7 +379,7 @@ def _set_ldf(self, X, cdf): obj.ddims = X.link_ratio.ddims obj.is_pattern = True obj.is_cumulative = False - obj._set_slicers + obj._set_slicers() return obj def _reshape(self, measure): diff --git a/chainladder/development/tests/test_munich.py b/chainladder/development/tests/test_munich.py index df76c289..868b09b1 100644 --- a/chainladder/development/tests/test_munich.py +++ b/chainladder/development/tests/test_munich.py @@ -4,11 +4,11 @@ def test_mcl_ult(): mcl = cl.load_sample("mcl") dev = cl.Development().fit_transform(mcl) - cl_traditional = cl.Chainladder().fit(dev).ultimate_ + _ = cl.Chainladder().fit(dev).ultimate_ dev_munich = cl.MunichAdjustment( paid_to_incurred=[("paid", "incurred")] ).fit_transform(dev) - cl_munich = cl.Chainladder().fit(dev_munich).ultimate_ + _ = cl.Chainladder().fit(dev_munich).ultimate_ def test_mcl_rollforward(): @@ -16,4 +16,4 @@ def test_mcl_rollforward(): mcl_prior = mcl[mcl.valuation < mcl.valuation_date] munich = cl.MunichAdjustment(paid_to_incurred=[("paid", "incurred")]).fit(mcl_prior) new = munich.transform(mcl) - cl.Chainladder().fit(new).ultimate_ + _ = cl.Chainladder().fit(new).ultimate_ diff --git a/chainladder/methods/benktander.py b/chainladder/methods/benktander.py index 4667c433..8f757344 100644 --- a/chainladder/methods/benktander.py +++ b/chainladder/methods/benktander.py @@ -221,7 +221,7 @@ def predict(self, X, sample_weight=None): current Triangle and a refreshed apriori. .. testsetup:: - + import chainladder as cl .. testcode:: @@ -263,8 +263,8 @@ def _get_benktander_aprioris(self, X, sample_weight): random_state = xp.random.RandomState(self.random_state) # Draw from lognormal with E[apriori] = self.apriori and SD = self.apriori_sigma. cov = self.apriori_sigma / self.apriori - sigma_log = np.sqrt(np.log1p(cov ** 2)) - mu_log = np.log(self.apriori) - 0.5 * sigma_log ** 2 + sigma_log = np.sqrt(np.log1p(cov**2)) + mu_log = np.log(self.apriori) - 0.5 * sigma_log**2 apriori = random_state.lognormal(mu_log, sigma_log, X.shape[0]) apriori = apriori.reshape(X.shape[0], -1)[..., None, None] apriori = sample_weight * apriori @@ -290,7 +290,7 @@ def _get_ultimate(self, X, expectation): cdf = (1 - 1 / num_to_nan(cdf.values))[None] exponents = xp.arange(self.n_iters + 1) exponents = xp.reshape(exponents, tuple([len(exponents)] + [1] * 4)) - cdf = cdf ** (((cdf + 1e-16) / (cdf + 1e-16) * exponents)) + cdf = cdf ** ((cdf + 1e-16) / (cdf + 1e-16) * exponents) cdf = xp.nan_to_num(cdf) a = xp.sum(cdf[:-1, ...], 0) * xp.nan_to_num(ld.set_backend(backend).values) b = cdf[-1, ...] * xp.nan_to_num(expectation.set_backend(backend).values) diff --git a/pyproject.toml b/pyproject.toml index 49e29aca..028e9524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ preview = true extend-exclude = ["docs/getting_started/online_sandbox/sandbox_workbook_blank.ipynb"] [tool.ruff.lint] -select = ["E2", "E4", "E7", "E9", "F"] +select = ["E2", "E4", "E7", "E9", "F", "B018", "UP034"] # Grandfathered violations that predate enabling this rule set. New and # newly-touched code is still held to the full ruleset above; these files @@ -117,11 +117,9 @@ select = ["E2", "E4", "E7", "E9", "F"] "chainladder/adjustments/bootstrap.py" = ["E231", "E721", "E722", "F841"] "chainladder/adjustments/disposal.py" = ["E226", "E227", "E231", "E251", "E252", "E265", "F401"] "chainladder/adjustments/tests/test_berqsherm.py" = ["F841"] -"chainladder/adjustments/tests/test_bootstrap.py" = ["E231"] "chainladder/adjustments/tests/test_disposal.py" = ["E226", "E231", "E241", "E251", "E265", "F841"] "chainladder/adjustments/trend.py" = ["F401"] "chainladder/core/correlation.py" = ["E741"] -"chainladder/core/display.py" = ["E203", "E252"] "chainladder/core/slice.py" = ["E225"] "chainladder/core/tests/rtest_correlation.py" = ["E266", "E722", "F821"] "chainladder/core/tests/test_arithmetic.py" = ["E222", "E225", "E226", "E711"] @@ -129,20 +127,18 @@ select = ["E2", "E4", "E7", "E9", "F"] "chainladder/core/tests/test_grain.py" = ["E265", "F401", "F841"] "chainladder/core/tests/test_slicing.py" = ["E203", "E225"] "chainladder/development/barnzehn.py" = ["E201", "E202", "E231", "E251", "E275"] -"chainladder/development/clark.py" = ["E721", "E731"] "chainladder/development/constant.py" = ["E712"] "chainladder/development/glm.py" = ["E225", "E231", "E251", "F401"] -"chainladder/development/incremental.py" = ["E226", "E231", "E251", "E265", "E721", "F401"] +"chainladder/development/incremental.py" = ["E226", "E231", "E251", "E265", "E721", "F401", "UP034"] "chainladder/development/learning.py" = ["E225", "E226", "E231", "E265", "E711", "F401"] "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/development/tests/test_barnzehn.py" = ["E225", "E226", "E231", "E251"] "chainladder/development/tests/test_constant.py" = ["F841"] -"chainladder/development/tests/test_development.py" = ["E231", "E251", "E265", "E722"] +"chainladder/development/tests/test_development.py" = ["B018", "E231", "E251", "E265", "E722"] "chainladder/development/tests/test_glm.py" = ["E225", "E226", "E231"] "chainladder/development/tests/test_incremental.py" = ["F841"] -"chainladder/development/tests/test_munich.py" = ["F841"] "chainladder/methods/base.py" = ["E231", "F401"] "chainladder/methods/capecod.py" = ["F401"] "chainladder/methods/mack.py" = ["E265", "F401"] @@ -163,7 +159,7 @@ select = ["E2", "E4", "E7", "E9", "F"] "chainladder/utils/triangle_weight.py" = ["E227", "E231", "E265", "F401"] "chainladder/utils/utility_functions.py" = ["E226", "E227", "E231", "E251", "E252", "E721", "F401"] "chainladder/utils/weighted_regression.py" = ["E227", "E231", "E252"] -"chainladder/workflow/tests/test_voting.py" = ["E231", "E731"] +"chainladder/workflow/tests/test_voting.py" = ["E231", "E731", "UP034"] "chainladder/workflow/tests/test_workflow.py" = ["E203", "E241"] "chainladder/workflow/voting.py" = ["E231", "E252", "E265"] ".github/scripts/pytest_parallel.py" = ["E241", "E702"] @@ -176,6 +172,7 @@ select = ["E2", "E4", "E7", "E9", "F"] "docs/gallery/plot_bootstrap.ipynb" = ["E231", "F811"] "docs/gallery/plot_bootstrap_comparison.ipynb" = ["E226", "E266"] "docs/gallery/plot_capecod.ipynb" = ["E225", "E226", "E231"] +"docs/gallery/plot_capecod_onlevel.ipynb" = ["B018"] "docs/gallery/plot_clarkldf_resid.ipynb" = ["E231"] "docs/gallery/plot_development_periods.ipynb" = ["E231"] "docs/gallery/plot_elrf_resid.ipynb" = ["E231", "E703"] @@ -195,7 +192,7 @@ select = ["E2", "E4", "E7", "E9", "F"] "docs/user_guide/adjustments.ipynb" = ["E226", "E231", "E251"] "docs/user_guide/development.ipynb" = ["E225", "E226", "E231", "F401"] "docs/user_guide/methods.ipynb" = ["E225", "E226", "E261"] -"docs/user_guide/tails.ipynb" = ["E226", "E231", "E251", "E703", "F541"] +"docs/user_guide/tails.ipynb" = ["E226", "E231", "E251", "E703", "F541", "UP034"] "docs/user_guide/triangle.ipynb" = ["E203", "E225", "E226", "E227", "E231"] "docs/user_guide/workflow.ipynb" = ["E203", "E225", "E226", "E231", "E251", "E261", "E731"]