From 1850b4416b5de6bd514cc7a1734d88b8a900c073 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 4 Sep 2026 15:57:10 -0500 Subject: [PATCH 1/2] [FIX] Apply Ruff fixes to chainladder/utils. --- chainladder/utils/cupy.py | 13 +- chainladder/utils/dask.py | 13 +- chainladder/utils/sparse.py | 25 +- chainladder/utils/tests/test_sparse.py | 19 +- chainladder/utils/tests/test_utilities.py | 273 ++++++++++++---------- chainladder/utils/triangle_weight.py | 95 ++++---- chainladder/utils/utility_functions.py | 5 +- chainladder/utils/weighted_regression.py | 36 +-- pyproject.toml | 9 - 9 files changed, 258 insertions(+), 230 deletions(-) diff --git a/chainladder/utils/cupy.py b/chainladder/utils/cupy.py index df49cbec5..b92b1daf7 100644 --- a/chainladder/utils/cupy.py +++ b/chainladder/utils/cupy.py @@ -4,14 +4,13 @@ import numpy as np from chainladder import options -from chainladder.utils.sparse import sp try: import cupy as cp cp.array([1]) module = "cupy" -except: +except ImportError: if options.ARRAY_BACKEND == "cupy": import warnings @@ -22,29 +21,29 @@ def nansum(a, *args, **kwargs): - """ For cupy v0.6.0 compatibility """ + """For cupy v0.6.0 compatibility""" return cp.sum(cp.nan_to_num(a), *args, **kwargs) def nanmean(a, *args, **kwargs): - """ For cupy v0.6.0 compatibility """ + """For cupy v0.6.0 compatibility""" return cp.sum(cp.nan_to_num(a), *args, **kwargs) / cp.sum( ~cp.isnan(a), *args, **kwargs ) def nanmedian(a, *args, **kwargs): - """ For cupy v0.6.0 compatibility """ + """For cupy v0.6.0 compatibility""" return cp.array(np.nanmedian(cp.asnumpy(a), *args, **kwargs)) def nanquantile(a, *args, **kwargs): - """ For cupy v0.6.0 compatibility """ + """For cupy v0.6.0 compatibility""" return cp.array(np.nanquantile(cp.asnumpy(a), *args, **kwargs)) def unique(ar, axis=None, *args, **kwargs): - """ For cupy v0.6.0 compatibility """ + """For cupy v0.6.0 compatibility""" return cp.array(np.unique(cp.asnumpy(ar), axis=axis, *args, **kwargs)) diff --git a/chainladder/utils/dask.py b/chainladder/utils/dask.py index fad071f2e..54f6a1fbb 100644 --- a/chainladder/utils/dask.py +++ b/chainladder/utils/dask.py @@ -6,26 +6,29 @@ try: import dask.array as dp + dp.array([1]) module = "dask" -except: +except ImportError: if options.ARRAY_BACKEND == "dask": import warnings warnings.warn("Unable to load Dask. Using numpy instead.") import numpy as dp + module = "numpy" dp.nan = np.nan def expand_dims(a, axis=0): - l = [] + slices = [] for i in range(len(a.shape)): if i == axis: - l.append(None) - l.append(slice(None)) - return a.__getitem__(tuple(l)) + slices.append(None) + slices.append(slice(None)) + return a.__getitem__(tuple(slices)) + if dp != np: dp.expand_dims = expand_dims diff --git a/chainladder/utils/sparse.py b/chainladder/utils/sparse.py index 51d21099f..a02ffe37a 100644 --- a/chainladder/utils/sparse.py +++ b/chainladder/utils/sparse.py @@ -6,7 +6,8 @@ from sparse import COO as COO from sparse import elemwise -def _setitem_not_supported(self, key, value) -> None: # noqa + +def _setitem_not_supported(self, key, value) -> None: # noqa raise TypeError( """ In-place item assignment (e.g. `triangle.values[...] = value`) is not @@ -18,20 +19,20 @@ def _setitem_not_supported(self, key, value) -> None: # noqa sp.isnan = np.isnan COO.nan = np.array([1.0, np.nan])[-1] COO.__setitem__ = _setitem_not_supported -setattr(sp, 'testing', np.testing) +setattr(sp, "testing", np.testing) sp.sqrt = np.sqrt sp.log = np.log sp.exp = np.exp sp.abs = np.abs -def nan_to_num(a, nan = 0.0): +def nan_to_num(a, nan=0.0): if type(a) in [int, float, np.int64, np.float64]: return np.nan_to_num(a) if hasattr(a, "fill_value"): a = a.copy() a.data[np.isnan(a.data)] = nan - return COO(coords=a.coords, data=a.data, fill_value = nan, shape = a.shape) + return COO(coords=a.coords, data=a.data, fill_value=nan, shape=a.shape) def ones(*args, **kwargs): @@ -43,6 +44,7 @@ def nansum(a, axis=None, keepdims=None, *args, **kwargs): axis=axis, keepdims=keepdims, *args, **kwargs ) + def nanquantile(a: COO, q: float, axis: int = 0, keepdims: bool = False): """ mimics np.nanquantile @@ -71,15 +73,13 @@ def nanquantile(a: COO, q: float, axis: int = 0, keepdims: bool = False): if not keep_axes: out = np.nanquantile(a.data, q) if keepdims: - out = np.asarray(out).reshape( - tuple(1 for _ in range(a.ndim)) - ) + out = np.asarray(out).reshape(tuple(1 for _ in range(a.ndim))) return COO(out) # map every stored value to an output location keep_coords = a.coords[list(keep_axes)] group_ids = np.ravel_multi_index(keep_coords, keep_shape) - + # sort by group order = np.argsort(group_ids) group_ids = group_ids[order] @@ -97,10 +97,11 @@ def nanquantile(a: COO, q: float, axis: int = 0, keepdims: bool = False): out = out.reshape(keep_shape) if keepdims: - out = np.expand_dims(out,axis) + out = np.expand_dims(out, axis) return COO(out) + def nanmedian(a: COO, axis: int = 0, keepdims: bool = False): """ mimics np.nanmean @@ -121,6 +122,7 @@ def nanmedian(a: COO, axis: int = 0, keepdims: bool = False): """ return nanquantile(a, 0.5, axis, keepdims) + def nanmean(a, axis=None, keepdims=None): n = nansum(a, axis=axis, keepdims=keepdims) d = nansum(nan_to_num(a) != 0, axis=axis, keepdims=keepdims).astype(n.dtype) @@ -129,12 +131,13 @@ def nanmean(a, axis=None, keepdims=None): out = n / d return COO(data=out.data, coords=out.coords, fill_value=0, shape=out.shape) + def array(a, *args, **kwargs): if kwargs.get("fill_value", None) is not None: fill_value = kwargs.pop("fill_value") else: fill_value = COO.nan - if type(a) == sp.COO: + if isinstance(a, sp.COO): return COO(a, *args, **kwargs, fill_value=fill_value) else: return COO(np.array(a, *args, **kwargs), fill_value=fill_value) @@ -172,4 +175,4 @@ def floor(x: COO) -> COO: sp.nanmean = nanmean sp.sum = COO.sum sp.nanquantile = nanquantile -sp.nanmedian = nanmedian \ No newline at end of file +sp.nanmedian = nanmedian diff --git a/chainladder/utils/tests/test_sparse.py b/chainladder/utils/tests/test_sparse.py index 344c8850a..6e5a6f820 100644 --- a/chainladder/utils/tests/test_sparse.py +++ b/chainladder/utils/tests/test_sparse.py @@ -5,11 +5,12 @@ floor, COO, where, - nanquantile + nanquantile, ) from sparse import all as sparse_all + def test_array_from_list_default_fill_value() -> None: """ Tests chainladder.utils.sparse.array() when no fill value is provided. @@ -114,7 +115,8 @@ def test_floor_returns_copy() -> None: np.testing.assert_array_equal(result.todense(), [1.0, 2.0, -1.0]) np.testing.assert_array_equal(a.todense(), [1.2, 2.7, -0.3]) -def test_1D_nanquantile() -> None: + +def test_1d_nanquantile() -> None: """ Checks that nanquantile performs in 1D special case. @@ -122,9 +124,10 @@ def test_1D_nanquantile() -> None: ------- None """ - a = COO(np.array([1,2,3,4])) - assert nanquantile(a,0.5) == 2.5 - assert sparse_all(nanquantile(a,0.5,keepdims = True) == COO(np.array([2.5]))) + a = COO(np.array([1, 2, 3, 4])) + assert nanquantile(a, 0.5) == 2.5 + assert sparse_all(nanquantile(a, 0.5, keepdims=True) == COO(np.array([2.5]))) + def test_keepdims_nanquantile() -> None: """ @@ -134,5 +137,7 @@ def test_keepdims_nanquantile() -> None: ------- None """ - a = COO(np.array([[1,2,3,4],[3,4,5,6]])) - assert sparse_all(nanquantile(a,0.5,keepdims = True) == COO(np.array([[2,3,4,5]]))) \ No newline at end of file + a = COO(np.array([[1, 2, 3, 4], [3, 4, 5, 6]])) + assert sparse_all( + nanquantile(a, 0.5, keepdims=True) == COO(np.array([[2, 3, 4, 5]])) + ) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 5a0b8a422..d8d447aa5 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -9,15 +9,12 @@ import numpy as np import pandas as pd -from chainladder import ( - __dt64_unit__ -) -from chainladder.utils.utility_functions import date_delta_adjustment +from chainladder import __dt64_unit__ from chainladder.utils.data._manifest import SAMPLES from chainladder.utils.utility_functions import ( date_delta_adjustment, maximum, - minimum + minimum, ) from pathlib import Path @@ -28,6 +25,7 @@ from pytest import MonkeyPatch from chainladder import Triangle + class _FakeBag: """ Minimal stand-in for a dask bag that runs the mapped function eagerly. @@ -59,7 +57,6 @@ def from_sequence(seq): def test_triangle_json_io(clrd): - xp = clrd.get_array_module() clrd2 = cl.read_json(clrd.to_json(), array_backend=clrd.array_backend) assert clrd == clrd2 assert np.all(clrd.kdims == clrd2.kdims) @@ -153,7 +150,7 @@ def test_concat(clrd): ) -def test_model_diagnostics_erorr(raa,atol): +def test_model_diagnostics_erorr(raa, atol): with pytest.raises(ValueError): cl.model_diagnostics(raa) dev = cl.Development().fit_transform(raa) @@ -161,52 +158,62 @@ def test_model_diagnostics_erorr(raa,atol): emerg = est.full_expectation_.cum_to_incr() md = cl.model_diagnostics(est) assert np.allclose( - md['Run Off 1'].values, - emerg[emerg.valuation.year==1991].latest_diagonal.values, + md["Run Off 1"].values, + emerg[emerg.valuation.year == 1991].latest_diagonal.values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - md['Year Incremental'].values, + md["Year Incremental"].values, raa.cum_to_incr().latest_diagonal.values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - md['LDF'].values.flatten()[:0:-1], + md["LDF"].values.flatten()[:0:-1], dev.ldf_.values.flatten(), atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - md['CDF'].values.flatten()[:0:-1], + md["CDF"].values.flatten()[:0:-1], dev.cdf_.values.flatten(), atol=atol, - equal_nan=True + equal_nan=True, ) -def test_model_diagnostics_groupby(prism,atol): +def test_model_diagnostics_groupby(prism, atol): dev = cl.Development().fit(prism["Incurred"].sum()) est = cl.Chainladder().fit(dev.transform(prism["Incurred"])) - lhs = cl.model_diagnostics(est,groupby=['Line']) - rhs = cl.model_diagnostics(cl.Chainladder().fit(dev.transform(prism["Incurred"].groupby('Line').sum()))) - assert np.allclose(lhs['Ultimate'].values,rhs['Ultimate'].values,atol=atol,equal_nan=True) - assert np.allclose(np.nan_to_num(lhs['IBNR'].values),np.nan_to_num(rhs['IBNR'].values),atol=atol,equal_nan=True) + lhs = cl.model_diagnostics(est, groupby=["Line"]) + rhs = cl.model_diagnostics( + cl.Chainladder().fit(dev.transform(prism["Incurred"].groupby("Line").sum())) + ) + assert np.allclose( + lhs["Ultimate"].values, rhs["Ultimate"].values, atol=atol, equal_nan=True + ) + assert np.allclose( + np.nan_to_num(lhs["IBNR"].values), + np.nan_to_num(rhs["IBNR"].values), + atol=atol, + equal_nan=True, + ) def test_concat_immutability(raa): u = cl.Chainladder().fit(raa).ultimate_ - l = raa.latest_diagonal - u.columns = l.columns + latest = raa.latest_diagonal + u.columns = latest.columns u_new = copy.deepcopy(u) - cl.concat((l, u), axis=3) + cl.concat((latest, u), axis=3) assert u == u_new def test_to_pickle_read_pickle(raa): import tempfile import os + dev = cl.Development(average="simple", n_periods=4).fit(raa) fd, path = tempfile.mkstemp(suffix=".pkl") os.close(fd) @@ -215,20 +222,24 @@ def test_to_pickle_read_pickle(raa): restored = cl.read_pickle(path) assert restored.average == dev.average assert restored.n_periods == dev.n_periods - np.testing.assert_array_almost_equal( - restored.ldf_.values, dev.ldf_.values - ) + np.testing.assert_array_almost_equal(restored.ldf_.values, dev.ldf_.values) finally: os.remove(path) def test_maximum_minimum_1(raa): - ult_vol = cl.Chainladder().fit( - cl.Development(average="volume").fit_transform(raa) - ).ultimate_ - ult_sim = cl.Chainladder().fit( - cl.Development(average="simple").fit_transform(raa) - ).ultimate_ + ult_vol = ( + cl + .Chainladder() + .fit(cl.Development(average="volume").fit_transform(raa)) + .ultimate_ + ) + ult_sim = ( + cl + .Chainladder() + .fit(cl.Development(average="simple").fit_transform(raa)) + .ultimate_ + ) high_side = maximum(ult_vol, ult_sim) low_side = minimum(ult_vol, ult_sim) np.testing.assert_array_almost_equal( @@ -246,6 +257,7 @@ def test_invalid_sample() -> None: with pytest.raises(ValueError): cl.load_sample(key="not_a_real_sample_38473743") + def test_load_sample() -> None: """ Tests whether every sample data set declared in the manifest loads. @@ -282,7 +294,13 @@ def test_list_samples() -> None: # One row per manifest entry, indexed by sample name. assert df.index.name == "name" assert set(df.index) == set(SAMPLES) - assert {"index", "columns", "cumulative", "origin_grain", "development_grain"} <= set(df.columns) + assert { + "index", + "columns", + "cumulative", + "origin_grain", + "development_grain", + } <= set(df.columns) # The fast path skips loading data and therefore omits the grain columns. fast = cl.list_samples(include_grain=False) @@ -372,15 +390,17 @@ def test_load_sample_clrd2025() -> None: tri = cl.load_sample("clrd2025") # Six LOBs in the CAS Schedule P refresh. - expected_lobs = { - "comauto", "medmal", "othliab", "ppauto", "prodliab", "wkcomp" - } + expected_lobs = {"comauto", "medmal", "othliab", "ppauto", "prodliab", "wkcomp"} assert set(tri.index["LOB"].unique()) == expected_lobs # Modern column names (IncurredLosses rather than IncurLoss). expected_columns = { - "IncurredLosses", "CumPaidLoss", "BulkLoss", - "EarnedPremDIR", "EarnedPremCeded", "EarnedPremNet", + "IncurredLosses", + "CumPaidLoss", + "BulkLoss", + "EarnedPremDIR", + "EarnedPremCeded", + "EarnedPremNet", } assert set(str(c) for c in tri.vdims) == expected_columns @@ -388,6 +408,7 @@ def test_load_sample_clrd2025() -> None: assert str(tri.origin.min()) == "1998" assert "2007" in [str(o) for o in tri.origin] + def test_date_delta_adjustment() -> None: """ Tests the date adjustment depending on Pandas default precision, nanosecond for Pandas 2, microsecond for Pandas 3. @@ -401,6 +422,7 @@ def test_date_delta_adjustment() -> None: ) assert result == expected + def test_read_pickle_triangle(raa: Triangle, tmp_path: Path) -> None: """ Create a triangle, dump a pickle of it, and then read it back in. The ingested pickle should result @@ -424,11 +446,7 @@ def test_read_pickle_triangle(raa: Triangle, tmp_path: Path) -> None: assert cl.read_pickle(str(pkl_path)) == raa -def test_triangle_to_pickle( - raa: Triangle, - clrd: Triangle, - tmp_path: Path -) -> None: +def test_triangle_to_pickle(raa: Triangle, clrd: Triangle, tmp_path: Path) -> None: """ Dump a pickle of a triangle and read it back in. The read-in triangle should equal the one that was dumped. @@ -653,10 +671,9 @@ def test_reset_option() -> None: original_array_priority = cl.options.ARRAY_PRIORITY try: - - cl.options.set_option('ARRAY_BACKEND', 'sparse') - cl.options.set_option('AUTO_SPARSE', False) - cl.options.set_option('ARRAY_PRIORITY', ['sparse', 'dask', 'numpy', 'cupy']) + cl.options.set_option("ARRAY_BACKEND", "sparse") + cl.options.set_option("AUTO_SPARSE", False) + cl.options.set_option("ARRAY_PRIORITY", ["sparse", "dask", "numpy", "cupy"]) cl.options.reset_option() @@ -665,10 +682,10 @@ def test_reset_option() -> None: assert cl.options.ARRAY_PRIORITY == original_array_priority finally: - # Manual reset in case of test failure. - cl.options.set_option('ARRAY_BACKEND', original_backend) - cl.options.set_option('AUTO_SPARSE', original_auto_sparse) - cl.options.set_option('ARRAY_PRIORITY', original_array_priority) + # Manual reset in case of test failure. + cl.options.set_option("ARRAY_BACKEND", original_backend) + cl.options.set_option("AUTO_SPARSE", original_auto_sparse) + cl.options.set_option("ARRAY_PRIORITY", original_array_priority) def test_options_defaults() -> None: @@ -682,7 +699,7 @@ def test_options_defaults() -> None: """ options = cl.Options() assert options.ARRAY_BACKEND == "numpy" - assert options.AUTO_SPARSE == True + assert options.AUTO_SPARSE assert options.ARRAY_PRIORITY == ["dask", "sparse", "cupy", "numpy"] assert isinstance(options.ULT_VAL, str) @@ -696,10 +713,10 @@ def test_get_option() -> None: None """ - assert cl.options.get_option('ARRAY_BACKEND') == cl.options.ARRAY_BACKEND - assert cl.options.get_option('AUTO_SPARSE') == cl.options.AUTO_SPARSE - assert cl.options.get_option('ARRAY_PRIORITY') == cl.options.ARRAY_PRIORITY - assert cl.options.get_option('ULT_VAL') == cl.options.ULT_VAL + assert cl.options.get_option("ARRAY_BACKEND") == cl.options.ARRAY_BACKEND + assert cl.options.get_option("AUTO_SPARSE") == cl.options.AUTO_SPARSE + assert cl.options.get_option("ARRAY_PRIORITY") == cl.options.ARRAY_PRIORITY + assert cl.options.get_option("ULT_VAL") == cl.options.ULT_VAL def test_set_option_consistency() -> None: @@ -712,12 +729,13 @@ def test_set_option_consistency() -> None: """ try: - cl.options.set_option('ARRAY_BACKEND', 'sparse') - assert cl.options.ARRAY_BACKEND == 'sparse' - assert cl.options.get_option('ARRAY_BACKEND') == 'sparse' + cl.options.set_option("ARRAY_BACKEND", "sparse") + assert cl.options.ARRAY_BACKEND == "sparse" + assert cl.options.get_option("ARRAY_BACKEND") == "sparse" finally: # Reset the options to default if the test fails. - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") + def test_reset_single_option() -> None: """ @@ -728,11 +746,11 @@ def test_reset_single_option() -> None: None """ - cl.options.set_option('ARRAY_BACKEND', 'sparse') - assert cl.options.ARRAY_BACKEND == 'sparse' + cl.options.set_option("ARRAY_BACKEND", "sparse") + assert cl.options.ARRAY_BACKEND == "sparse" # Return backend to original state. - cl.options.reset_option('ARRAY_BACKEND') - assert cl.options.ARRAY_BACKEND == 'numpy' + cl.options.reset_option("ARRAY_BACKEND") + assert cl.options.ARRAY_BACKEND == "numpy" def test_reset_option_invalid() -> None: @@ -744,7 +762,7 @@ def test_reset_option_invalid() -> None: None """ with pytest.raises(ValueError): - cl.options.reset_option('NOT_A_REAL_OPTION') + cl.options.reset_option("NOT_A_REAL_OPTION") def test_set_option_cupy_backend_deprecated() -> None: @@ -757,9 +775,9 @@ def test_set_option_cupy_backend_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="cupy"): - cl.options.set_option('ARRAY_BACKEND', 'cupy') + cl.options.set_option("ARRAY_BACKEND", "cupy") finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_set_option_dask_backend_deprecated() -> None: @@ -772,9 +790,9 @@ def test_set_option_dask_backend_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="dask"): - cl.options.set_option('ARRAY_BACKEND', 'dask') + cl.options.set_option("ARRAY_BACKEND", "dask") finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_set_option_cupy_priority_deprecated() -> None: @@ -788,9 +806,9 @@ def test_set_option_cupy_priority_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="cupy"): - cl.options.set_option('ARRAY_PRIORITY', ['cupy', 'numpy', 'sparse', 'dask']) + cl.options.set_option("ARRAY_PRIORITY", ["cupy", "numpy", "sparse", "dask"]) finally: - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_PRIORITY") def test_set_option_dask_priority_deprecated() -> None: @@ -804,9 +822,9 @@ def test_set_option_dask_priority_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="dask"): - cl.options.set_option('ARRAY_PRIORITY', ['dask', 'numpy', 'sparse', 'cupy']) + cl.options.set_option("ARRAY_PRIORITY", ["dask", "numpy", "sparse", "cupy"]) finally: - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_PRIORITY") def test_set_option_deprecated_priority_last_no_warning(recwarn) -> None: @@ -820,10 +838,10 @@ def test_set_option_deprecated_priority_last_no_warning(recwarn) -> None: None """ try: - cl.options.set_option('ARRAY_PRIORITY', ['numpy', 'sparse', 'dask', 'cupy']) + cl.options.set_option("ARRAY_PRIORITY", ["numpy", "sparse", "dask", "cupy"]) assert not [w for w in recwarn if issubclass(w.category, DeprecationWarning)] finally: - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_PRIORITY") def test_set_option_supported_backend_no_warning(recwarn) -> None: @@ -837,12 +855,12 @@ def test_set_option_supported_backend_no_warning(recwarn) -> None: None """ try: - cl.options.set_option('ARRAY_BACKEND', 'sparse') - cl.options.set_option('ARRAY_PRIORITY', ['sparse', 'numpy']) + cl.options.set_option("ARRAY_BACKEND", "sparse") + cl.options.set_option("ARRAY_PRIORITY", ["sparse", "numpy"]) assert not [w for w in recwarn if issubclass(w.category, DeprecationWarning)] finally: - cl.options.reset_option('ARRAY_BACKEND') - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_BACKEND") + cl.options.reset_option("ARRAY_PRIORITY") def test_set_backend_cupy_deprecated(clrd) -> None: @@ -855,9 +873,10 @@ def test_set_backend_cupy_deprecated(clrd) -> None: None """ with pytest.warns(DeprecationWarning, match="cupy") as record: - clrd.set_backend('cupy', deep=True) + clrd.set_backend("cupy", deep=True) cupy_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "cupy" in str(w.message) ] # A single warning should fire at the user's call site, not once per @@ -877,14 +896,15 @@ def test_set_backend_dask_deprecated(clrd) -> None: """ with pytest.warns(DeprecationWarning, match="dask") as record: try: - clrd.set_backend('dask', deep=True) + clrd.set_backend("dask", deep=True) except Exception: # The actual conversion can fail when the optional 'dask' # dependency is not installed; we only care that the deprecation # warning fired at the public entry point. pass dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -904,6 +924,7 @@ def test_triangle_dask_input_deprecated() -> None: ------- None """ + class _FakeDaskFrame(pd.DataFrame): @property def _constructor(self): @@ -926,7 +947,8 @@ def _constructor(self): columns="values", ) dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -944,6 +966,7 @@ def test_triangle_pandas_subclass_no_dask_warning(recwarn) -> None: ------- None """ + class _PandasSubclass(pd.DataFrame): @property def _constructor(self): @@ -961,7 +984,8 @@ def _constructor(self): columns="values", ) dask_warnings = [ - w for w in recwarn + w + for w in recwarn if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert dask_warnings == [] @@ -985,7 +1009,8 @@ def test_dask_parallel_deprecated_warns_once() -> None: cl._warn_dask_parallel_deprecated() cl._warn_dask_parallel_deprecated() dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -1011,7 +1036,8 @@ def test_dask_parallel_groupby_deprecated(monkeypatch: MonkeyPatch) -> None: with pytest.warns(DeprecationWarning, match="dask") as record: sparse_clrd.groupby("LOB").sum() dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -1037,7 +1063,8 @@ def test_dask_parallel_incr_to_cum_deprecated(monkeypatch: MonkeyPatch) -> None: with pytest.warns(DeprecationWarning, match="dask") as record: incremental_sparse.incr_to_cum() dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -1046,8 +1073,8 @@ def test_dask_parallel_incr_to_cum_deprecated(monkeypatch: MonkeyPatch) -> None: def test_dask_parallel_numpy_groupby_no_warning( - monkeypatch: MonkeyPatch, - recwarn, + monkeypatch: MonkeyPatch, + recwarn, ) -> None: """ The dask 'bag' parallel-compute path is gated on the sparse backend, so a @@ -1064,7 +1091,8 @@ def test_dask_parallel_numpy_groupby_no_warning( try: numpy_clrd.groupby("LOB").sum() dask_warnings = [ - w for w in recwarn + w + for w in recwarn if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert dask_warnings == [] @@ -1087,11 +1115,12 @@ def test_describe_option(capsys: CaptureFixture[str]) -> None: None """ - cl.options.describe_option('ARRAY_BACKEND') + cl.options.describe_option("ARRAY_BACKEND") captured = capsys.readouterr() - assert 'ARRAY_BACKEND : str' in captured.out - assert '[default: numpy]' in captured.out - assert '[currently: numpy]' in captured.out + assert "ARRAY_BACKEND : str" in captured.out + assert "[default: numpy]" in captured.out + assert "[currently: numpy]" in captured.out + def test_describe_option_multi(capsys) -> None: """ @@ -1108,15 +1137,15 @@ def test_describe_option_multi(capsys) -> None: None """ - cl.options.describe_option('ARRAY_BACKEND|AUTO_SPARSE') + cl.options.describe_option("ARRAY_BACKEND|AUTO_SPARSE") captured = capsys.readouterr() - assert 'ARRAY_BACKEND : str' in captured.out - assert '[default: numpy]' in captured.out - assert '[currently: numpy]' in captured.out - assert 'AUTO_SPARSE : bool' in captured.out - assert '[default: True]' in captured.out - assert '[currently: True]' in captured.out - assert 'ARRAY_PRIORITY' not in captured.out + assert "ARRAY_BACKEND : str" in captured.out + assert "[default: numpy]" in captured.out + assert "[currently: numpy]" in captured.out + assert "AUTO_SPARSE : bool" in captured.out + assert "[default: True]" in captured.out + assert "[currently: True]" in captured.out + assert "ARRAY_PRIORITY" not in captured.out def test_describe_option_all(capsys) -> None: @@ -1150,11 +1179,11 @@ def test_describe_option_return_string() -> None: None """ - result = cl.options.describe_option('ARRAY_BACKEND', _print_desc=False) + result = cl.options.describe_option("ARRAY_BACKEND", _print_desc=False) assert isinstance(result, str) - assert 'ARRAY_BACKEND : str' in result - assert '[default: numpy]' in result - assert '[currently: numpy]' in result + assert "ARRAY_BACKEND : str" in result + assert "[default: numpy]" in result + assert "[currently: numpy]" in result def test_deprecated_option_kwarg_warns() -> None: @@ -1162,13 +1191,13 @@ def test_deprecated_option_kwarg_warns() -> None: Passing option= to get_option or set_option should emit a FutureWarning. """ with pytest.warns(FutureWarning, match="'option'"): - cl.options.get_option(option='ARRAY_BACKEND') + cl.options.get_option(option="ARRAY_BACKEND") try: with pytest.warns(FutureWarning, match="'option'"): - cl.options.set_option(option='ARRAY_BACKEND', value='numpy') + cl.options.set_option(option="ARRAY_BACKEND", value="numpy") finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_deprecated_option_kwarg_reset_option_warns() -> None: @@ -1176,12 +1205,12 @@ def test_deprecated_option_kwarg_reset_option_warns() -> None: Passing option= to reset_option should emit a FutureWarning. """ try: - cl.options.set_option('ARRAY_BACKEND', 'sparse') + cl.options.set_option("ARRAY_BACKEND", "sparse") with pytest.warns(FutureWarning, match="'option'"): - cl.options.reset_option(option='ARRAY_BACKEND') - assert cl.options.ARRAY_BACKEND == 'numpy' + cl.options.reset_option(option="ARRAY_BACKEND") + assert cl.options.ARRAY_BACKEND == "numpy" finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_get_option_missing_pat_raises() -> None: @@ -1206,9 +1235,9 @@ def test_describe_option_no_docstring_match(monkeypatch: MonkeyPatch) -> None: ------- None """ - monkeypatch.setattr(cl.Options, '__doc__', '') - result = cl.options.describe_option('ARRAY_BACKEND', _print_desc=False) - assert 'No description available.' in result + monkeypatch.setattr(cl.Options, "__doc__", "") + result = cl.options.describe_option("ARRAY_BACKEND", _print_desc=False) + assert "No description available." in result def test_describe_option_invalid() -> None: @@ -1221,7 +1250,7 @@ def test_describe_option_invalid() -> None: """ with pytest.raises(ValueError): - cl.options.describe_option('NOT_A_REAL_OPTION') + cl.options.describe_option("NOT_A_REAL_OPTION") def test_both_pat_and_option_raises() -> None: @@ -1229,7 +1258,7 @@ def test_both_pat_and_option_raises() -> None: Passing both pat and option to get_option, set_option, or reset_option should raise TypeError. """ with pytest.raises(TypeError, match="Cannot specify both"): - cl.options.get_option(pat='ARRAY_BACKEND', option='ARRAY_BACKEND') + cl.options.get_option(pat="ARRAY_BACKEND", option="ARRAY_BACKEND") def test_set_option_missing_value_raises() -> None: @@ -1237,7 +1266,7 @@ def test_set_option_missing_value_raises() -> None: Calling set_option with pat but no value should raise TypeError. """ with pytest.raises(TypeError, match="missing required argument"): - cl.options.set_option('ARRAY_BACKEND') + cl.options.set_option("ARRAY_BACKEND") def test_describe_option_invalid_regex() -> None: @@ -1245,4 +1274,4 @@ def test_describe_option_invalid_regex() -> None: Passing a malformed regular expression to describe_option should raise ValueError. """ with pytest.raises(ValueError, match="not a valid regular expression"): - cl.options.describe_option('[') + cl.options.describe_option("[") diff --git a/chainladder/utils/triangle_weight.py b/chainladder/utils/triangle_weight.py index a0ebbfae2..f62125aac 100644 --- a/chainladder/utils/triangle_weight.py +++ b/chainladder/utils/triangle_weight.py @@ -5,7 +5,6 @@ import numpy as np import pandas as pd -from chainladder.utils.sparse import sp from sklearn.base import BaseEstimator, TransformerMixin import warnings @@ -17,7 +16,8 @@ if TYPE_CHECKING: from chainladder.core.typing import TriangleLike -class TriangleWeight(BaseEstimator,TransformerMixin): + +class TriangleWeight(BaseEstimator, TransformerMixin): """ Helper class that produces a triangle of weights based on pattern selections @@ -55,11 +55,11 @@ class TriangleWeight(BaseEstimator,TransformerMixin): See order of operations below when combined with multiple drop parameters. .. note :: - + (Order of Drop Operations) - + When multiple drop parameters are used together, the weights are built in this order (steps 4 and 5 are reversed from `Development`): - + 1. ``n_periods`` — limit to the most recent origin periods. 2. ``drop`` — remove specific origin/development cells. 3. ``drop_valuation`` — remove entire valuation diagonal in the triangle. @@ -116,7 +116,7 @@ def fit(self, X: TriangleLike, y: None = None, sample_weight: None = None): Returns the instance itself. """ - self.w_ = self._set_weight_func(X=X,secondary_rank=sample_weight) + self.w_ = self._set_weight_func(X=X, secondary_rank=sample_weight) return self def transform(self, X: TriangleLike): @@ -138,10 +138,10 @@ def transform(self, X: TriangleLike): return X_new def _cascade_param( - self, - size:int, - param: bool | int | float | str | None | list[bool|int|float|str|None], - default_param: bool | int | float | str | None + self, + size: int, + param: bool | int | float | str | None | list[bool | int | float | str | None], + default_param: bool | int | float | str | None, ) -> np.ndarray: """ Internal helper function to explicitly cascade a parameter to a given triangle size @@ -151,7 +151,7 @@ def _cascade_param( size: integer the width of the triangle param: bool or int or float or str or None or list - the selected parameter, such as n_periods or drop_low, etc. + the selected parameter, such as n_periods or drop_low, etc. default_param: bool or int or float or str or None the default param to fill where unspecificied @@ -174,9 +174,7 @@ def _cascade_param( return out.astype(type(default_param)).to_numpy() def _set_weight_func( - self, - X: TriangleLike, - secondary_rank: TriangleLike | None = None + self, X: TriangleLike, secondary_rank: TriangleLike | None = None ) -> TriangleLike: """ Combines weights from all parameters @@ -230,7 +228,7 @@ def _assign_n_periods_weight_func(self, X: TriangleLike) -> TriangleLike: dev_len = X.shape[3] n_periods_param = self._cascade_param(dev_len, self.n_periods, -1) - #helper function that generates the weights for individual n_periods + # helper function that generates the weights for individual n_periods def _assign_n_periods_weight_int(X, n_periods): xp = X.get_array_module() val_offset = { @@ -253,8 +251,7 @@ def _assign_n_periods_weight_int(X, n_periods): # a dict of weights (val) by n_periods (key) dict_map = { - item: _assign_n_periods_weight_int(X, item) - for item in set(n_periods_param) + item: _assign_n_periods_weight_int(X, item) for item in set(n_periods_param) } # collection of development columns based on n_periods specified for that column conc = [ @@ -264,9 +261,7 @@ def _assign_n_periods_weight_int(X, n_periods): return xp.concatenate(tuple(conc), -1).astype(float) def _drop_n_func( - self, - X: TriangleLike, - secondary_rank: TriangleLike | None = None + self, X: TriangleLike, secondary_rank: TriangleLike | None = None ) -> TriangleLike: """ Generates weights for the `drop_high` and `drop_low` parameter @@ -282,7 +277,7 @@ def _drop_n_func( ------- A Triangle of weights - """ + """ # Preparing to set up 3D array for drop_n parameters X_val = X.values.copy() dev_len = X_val.shape[3] @@ -301,13 +296,13 @@ def _drop_n_func( # explicitly setting up 3D arrays for drop_n parameters to avoid broadcasting bugs drop_high_array = np.zeros((indices, columns, dev_len)) - drop_high_array[:, :, :] = self._cascade_param( - dev_len, self.drop_high, 0 - )[None, None] + drop_high_array[:, :, :] = self._cascade_param(dev_len, self.drop_high, 0)[ + None, None + ] drop_low_array = np.zeros((indices, columns, dev_len)) - drop_low_array[:, :, :] = self._cascade_param( - dev_len, self.drop_low, 0 - )[None, None] + drop_low_array[:, :, :] = self._cascade_param(dev_len, self.drop_low, 0)[ + None, None + ] preserve_array = np.zeros((indices, columns, dev_len)) preserve_array[:, :, :] = self._cascade_param( dev_len, self.preserve, self.preserve @@ -324,7 +319,7 @@ def _drop_n_func( # applying preserve preserve_trigger = (max_rank_unpreserve - drop_low_array) < preserve_array - + # setting up flag to produce warning warning_flag = np.any(preserve_trigger) @@ -333,9 +328,9 @@ def _drop_n_func( min_rank = np.where(preserve_trigger, 0, drop_low_array) # getting weights that are within the max and min ranks - w = ( - X_ranks < max_rank[:,:,None,:] - ) & (X_ranks > min_rank[:,:,None,:] - 1) + w = (X_ranks < max_rank[:, :, None, :]) & ( + X_ranks > min_rank[:, :, None, :] - 1 + ) # NOTE: The "Some exclusions have been ignored..." UserWarning below is # asserted by the test suite (see chainladder/development/tests/ @@ -361,7 +356,7 @@ def _drop_n_func( warnings.warn(warning) return w.astype(float) - + def _drop_func(self, X: TriangleLike) -> TriangleLike: """ Generates weights for the `drop` parameter @@ -375,7 +370,7 @@ def _drop_func(self, X: TriangleLike) -> TriangleLike: ------- A Triangle of weights - """ + """ # get the appropriate backend for nan_to_num xp = X.get_array_module() # turn single drop_valuation parameter to list if necessary @@ -391,9 +386,9 @@ def _drop_func(self, X: TriangleLike) -> TriangleLike: # create ndarray of drop_list for further operation in numpy drop_np = np.asarray(drop_list) # find indices of drop_np - origin_ind = np.where( - np.array([X.origin.astype("string")]) == drop_np[:, [0]] - )[1] + origin_ind = np.where(np.array([X.origin.astype("string")]) == drop_np[:, [0]])[ + 1 + ] dev_ind = np.where(np.array([dev_list]) == drop_np[:, [1]])[1] # set weight of dropped factors to 0 w[(origin_ind, dev_ind)] = 0 @@ -412,7 +407,7 @@ def _drop_valuation_func(self, X: TriangleLike) -> TriangleLike: ------- A Triangle of weights - """ + """ # get the appropriate backend for nan_to_num xp = X.get_array_module() # turn single drop_valuation parameter to list if necessary @@ -421,9 +416,9 @@ def _drop_valuation_func(self, X: TriangleLike) -> TriangleLike: else: drop_valuation_list = [self.drop_valuation] # turn drop_valuation to same valuation freq as X - v = pd.PeriodIndex( - drop_valuation_list, freq=X.development_grain - ).to_timestamp(how="e") + v = pd.PeriodIndex(drop_valuation_list, freq=X.development_grain).to_timestamp( + how="e" + ) # warn that some drop_valuation are outside of X if np.any(~v.isin(X.valuation)): warnings.warn("Some valuations could not be dropped.") @@ -433,7 +428,7 @@ def _drop_valuation_func(self, X: TriangleLike) -> TriangleLike: if w.sum() == 0: raise Exception("The entire triangle has been dropped via drop_valuation.") return w - + def _drop_x_func(self, X: TriangleLike) -> TriangleLike: """ Generates weights for the `drop_above` and `drop_below` parameters @@ -447,7 +442,7 @@ def _drop_x_func(self, X: TriangleLike) -> TriangleLike: ------- A Triangle of weights - """ + """ # Preparing to set up 3D array for drop_x parameters X_val = X.values.copy() dev_len = X_val.shape[3] @@ -460,9 +455,9 @@ def _drop_x_func(self, X: TriangleLike) -> TriangleLike: dev_len, self.drop_above, np.inf )[None, None] drop_below_array = np.zeros((indices, columns, dev_len)) - drop_below_array[:, :, :] = self._cascade_param( - dev_len, self.drop_below, 0.0 - )[None, None] + drop_below_array[:, :, :] = self._cascade_param(dev_len, self.drop_below, 0.0)[ + None, None + ] preserve_array = np.zeros((indices, columns, dev_len)) preserve_array[:, :, :] = self._cascade_param( dev_len, self.preserve, self.preserve @@ -472,8 +467,8 @@ def _drop_x_func(self, X: TriangleLike) -> TriangleLike: w = ~np.isnan(X_val) # weights without considering preserve - index_array_weights = (X_val < drop_above_array[:,:,None,:]) & ( - X_val > drop_below_array[:,:,None,:] + index_array_weights = (X_val < drop_above_array[:, :, None, :]) & ( + X_val > drop_below_array[:, :, None, :] ) # counting remaining factors @@ -482,7 +477,9 @@ def _drop_x_func(self, X: TriangleLike) -> TriangleLike: # applying preserve warning_flag = np.any(valid_count < preserve_array) w = np.where( - valid_count[:,:,None,:] < preserve_array[:,:,None,:], w, index_array_weights + valid_count[:, :, None, :] < preserve_array[:, :, None, :], + w, + index_array_weights, ) # NOTE: The "Some exclusions have been ignored..." UserWarning below is @@ -508,4 +505,4 @@ def _drop_x_func(self, X: TriangleLike) -> TriangleLike: ) warnings.warn(warning) - return w.astype(float) \ No newline at end of file + return w.astype(float) diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index 19536ec57..63834a54f 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -942,7 +942,8 @@ class PatsyFormula(BaseEstimator, TransformerMixin): def __init__(self, formula=None): self.formula = formula - def _check_X(self, X): + @staticmethod + def _check_X(X): # noqa: N802 from chainladder.core import Triangle if isinstance(X, Triangle): @@ -1071,7 +1072,7 @@ def model_diagnostics( return concat(triangles, 0) -def PTF_formula( +def PTF_formula( # noqa: N802 alpha: list = None, gamma: list = None, iota: list = None, dgrain: int = 12 ): """Helper formula that builds a patsy formula string for the BarnettZehnwirth diff --git a/chainladder/utils/weighted_regression.py b/chainladder/utils/weighted_regression.py index 5add4f242..60f9d9205 100644 --- a/chainladder/utils/weighted_regression.py +++ b/chainladder/utils/weighted_regression.py @@ -15,6 +15,7 @@ from typing import Literal from chainladder.core.typing import BackendArray + class WeightedRegression(BaseEstimator): """ Helper class that fits a system of regression equations @@ -24,8 +25,8 @@ class WeightedRegression(BaseEstimator): Parameters ---------- axis: integer (default = 2) - the axis along with the perform the regression; - axis of 2 is along the origin periods; + the axis along with the perform the regression; + axis of 2 is along the origin periods; axis of 3 is along the development periods; thru_orig: bool (default = False) whether the regression is forced to go through the origin @@ -43,10 +44,10 @@ class WeightedRegression(BaseEstimator): """ def __init__( - self, - axis: int = 2, - thru_orig: bool = False, - xp: ModuleType = np, + self, + axis: int = 2, + thru_orig: bool = False, + xp: ModuleType = np, ): self.axis = axis self.thru_orig = thru_orig @@ -64,11 +65,11 @@ def infer_x_w(self): return self def fit( - self, - X:BackendArray, - y:BackendArray|None=None, - sample_weight:BackendArray|None=None, - average: Literal["volume", "simple", "regression", "geometric"] | None = None + self, + X: BackendArray, + y: BackendArray | None = None, + sample_weight: BackendArray | None = None, + average: Literal["volume", "simple", "regression", "geometric"] | None = None, ): """ Fit the model with X. @@ -98,13 +99,13 @@ def fit( self.infer_x_w() if self.thru_orig: - self._fit_OLS_thru_orig() + self._fit_ols_thru_orig() else: - self._fit_OLS() + self._fit_ols() return self - def _fit_OLS_thru_orig(self): + def _fit_ols_thru_orig(self): """ Given a set of w, x, y, and an axis, this Function returns OLS slope and other statistics, while forcing an intercept of 0 @@ -138,7 +139,6 @@ def _fit_OLS_thru_orig(self): # but using the log link function and taking the differences is_geo = xp.array([a == "geometric" for a in average_[0, 0, 0]]) if is_geo.any(): - if xp.any((y == 0) & (x == 0)): warnings.warn( "Zero values present in triangle data used for geometric " @@ -165,7 +165,7 @@ def _fit_OLS_thru_orig(self): fitted_value = xp.repeat(xp.expand_dims(coef, axis), x.shape[axis], axis) fitted_value = fitted_value * x * (y * 0 + 1) - residual = (y - fitted_value) + residual = y - fitted_value wss_residual = xp.nansum(residual**2 * w, axis) mse_denom = xp.nansum((y * 0 + 1) * (xp.nan_to_num(w) != 0), axis) - 1 @@ -173,7 +173,7 @@ def _fit_OLS_thru_orig(self): mse = wss_residual / mse_denom sigma = xp.sqrt(mse) std_err = xp.sqrt(mse / denominator) - + self._w_reg = w self.slope_ = coef[..., None] @@ -182,7 +182,7 @@ def _fit_OLS_thru_orig(self): return self - def _fit_OLS(self): + def _fit_ols(self): """Given a set of w, x, y, and an axis, this Function returns OLS slope and intercept. TODO: diff --git a/pyproject.toml b/pyproject.toml index a11453772..1b6622a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,18 +147,9 @@ select = ["E2", "E4", "E7", "E9", "F", "B018", "UP034", "N802"] "chainladder/tails/bondy.py" = ["F401"] "chainladder/tails/curve.py" = ["E226", "E721", "F401"] "chainladder/tails/tests/rtest_exponential.py" = ["E722", "F401"] -"chainladder/utils/cupy.py" = ["E722", "F401"] -"chainladder/utils/dask.py" = ["E722", "E741"] -"chainladder/utils/sparse.py" = ["E231", "E251", "E721"] -"chainladder/utils/tests/test_sparse.py" = ["E231", "E251", "N802"] -"chainladder/utils/tests/test_utilities.py" = ["E225", "E231", "E712", "E741", "F811", "F841"] -"chainladder/utils/triangle_weight.py" = ["E227", "E231", "E265", "F401"] -"chainladder/utils/utility_functions.py" = ["E226", "E227", "E231", "E251", "E252", "E721", "F401", "N802"] -"chainladder/utils/weighted_regression.py" = ["E227", "E231", "E252", "N802"] "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"] "docs/friedland/chapter_10.ipynb" = ["E731", "F841"] "docs/friedland/chapter_7_part_2.ipynb" = ["N802"] "docs/friedland/chapter_9.ipynb" = ["E731"] From 42c00467b0e4f7e15a3e7fb6389c52f7b1b018f3 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 4 Sep 2026 16:18:51 -0500 Subject: [PATCH 2/2] [FIX] Apply Bugbot fix. --- chainladder/utils/cupy.py | 5 ++++- chainladder/utils/dask.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/chainladder/utils/cupy.py b/chainladder/utils/cupy.py index b92b1daf7..f7b77acee 100644 --- a/chainladder/utils/cupy.py +++ b/chainladder/utils/cupy.py @@ -10,7 +10,10 @@ cp.array([1]) module = "cupy" -except ImportError: +except (ImportError, RuntimeError): + # RuntimeError covers e.g. cupy.cuda.runtime.CUDARuntimeError, raised + # by cp.array([1]) when CuPy is installed but the GPU/CUDA runtime + # is unusable. if options.ARRAY_BACKEND == "cupy": import warnings diff --git a/chainladder/utils/dask.py b/chainladder/utils/dask.py index 54f6a1fbb..8614e46ef 100644 --- a/chainladder/utils/dask.py +++ b/chainladder/utils/dask.py @@ -9,7 +9,9 @@ dp.array([1]) module = "dask" -except ImportError: +except (ImportError, RuntimeError): + # RuntimeError covers the equivalent case of Dask being installed but + # its runtime being unusable when the dp.array([1]) probe runs. if options.ARRAY_BACKEND == "dask": import warnings