diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f089ef0e..319bfea5 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest'] - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v6 - name: Install uv diff --git a/.github/workflows/pytest_upstream_nightly.yml b/.github/workflows/pytest_upstream_nightly.yml index 359ad0bf..0b13c1c6 100644 --- a/.github/workflows/pytest_upstream_nightly.yml +++ b/.github/workflows/pytest_upstream_nightly.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest'] - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v6 - name: Install uv @@ -42,4 +42,4 @@ jobs: - name: Install dependencies run: uv sync --extra test - name: Run tests - run: uv run --with "pandas>=3,<4" pytest chainladder -m "not r" \ No newline at end of file + run: uv run --with "pandas>=3,<4" pytest chainladder -m "not r" diff --git a/chainladder/utils/tests/test_tri_w.py b/chainladder/utils/tests/test_tri_w.py new file mode 100644 index 00000000..8f075b41 --- /dev/null +++ b/chainladder/utils/tests/test_tri_w.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import chainladder as cl + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder import Triangle + + +class TestFullTri: + """Test weight generation on full triangles""" + + def test_triangleweight_full_triangle(self, raa: Triangle) -> None: + """ + Testing new path that allows weights on full triangles + """ + ult = cl.Chainladder().fit(raa) + tw = cl.TriangleWeight(n_periods=4).fit(raa) + tw_full = cl.TriangleWeight(n_periods=4).fit(ult.full_triangle_) + assert tw.w_.iloc[:, :, :, 0] == tw_full.w_.iloc[:, :, :, 0] + + def test_triangleweight_full_irregular_triangle(self) -> None: + """ + Testing unequal grains + """ + prism = cl.load_sample("prism_oydq")["Paid"] + ult = cl.Chainladder().fit(prism) + tw = cl.TriangleWeight(n_periods=4).fit(prism) + tw_full = cl.TriangleWeight(n_periods=4).fit(ult.full_triangle_) + assert tw.w_.iloc[:, :, :, 0] == tw_full.w_.iloc[:, :, :, 0] diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 34171be9..fe9410b5 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -1296,13 +1296,3 @@ def test_triangleweight_drop_valuation_all(raa: Triangle) -> None: "1990", ] ).fit(raa) - - -def test_triangleweight_full_triangle(raa: Triangle) -> None: - """ - Testing new path that allows weights on full triangles - """ - ult = cl.Chainladder().fit(raa) - tw = cl.TriangleWeight(n_periods=4).fit(raa) - tw_full = cl.TriangleWeight(n_periods=4).fit(ult.full_triangle_) - assert tw.w_.iloc[:, :, :, 0] == tw_full.w_.iloc[:, :, :, 0] diff --git a/chainladder/utils/tests/test_wtd_reg.py b/chainladder/utils/tests/test_wtd_reg.py new file mode 100644 index 00000000..18b981e2 --- /dev/null +++ b/chainladder/utils/tests/test_wtd_reg.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import numpy as np +from chainladder.utils.sparse import sp +from chainladder.utils.weighted_regression import WeightedRegression + + +class TestOLS: + """Test the OLS calculations""" + + def test_missing_data(self) -> None: + """Check that having nan in X and/or y still results in the right OLS coefficients.""" + data = [ + { + "module": np, + "X": [ + np.array([[[[1.0], [2.0], [3.0], [4.0], [5.0]]]]), + np.array([[[[1.0], [np.nan], [3.0], [4.0], [5.0]]]]), + ], + "y": [ + np.array([[[[1.0], [2.0], [3.0], [4.0], [5.0]]]]), + np.array([[[[1.0], [2.0], [np.nan], [4.0], [5.0]]]]), + ], + "w": np.array([[[[1.0], [1.0], [1.0], [1.0], [1.0]]]]), + "slope": np.array([[[[1.0]]]]), + } + ] + data.append({ + "module": sp, + "X": [sp.COO.from_numpy(i, fill_value=np.nan) for i in data[0]["X"]], + "y": [sp.COO.from_numpy(i, fill_value=np.nan) for i in data[0]["y"]], + "w": sp.COO.from_numpy(data[0]["w"]), + "slope": sp.COO.from_numpy(data[0]["slope"]), + }) + for i in data: + for x in i["X"]: + for y in i["y"]: + assert i["module"].all( + WeightedRegression(xp=i["module"]) + .fit(x, y, i["w"], "regression") + .slope_ + == i["slope"] + ) diff --git a/chainladder/utils/weighted_regression.py b/chainladder/utils/weighted_regression.py index 6973e337..05296c64 100644 --- a/chainladder/utils/weighted_regression.py +++ b/chainladder/utils/weighted_regression.py @@ -195,12 +195,17 @@ def _fit_ols(self): if xp != sp: x[w == 0] = xp.nan y[w == 0] = xp.nan + w[np.isnan(x)] = 0 + w[np.isnan(y)] = 0 else: - w2 = w.copy() + x2, y2, w2 = x.copy(), y.copy(), w.copy() w2 = sp.COO( data=w2.data, coords=w2.coords, fill_value=sp.nan, shape=w2.shape ) x, y = x * w2, y * w2 + x2 = sp.COO(data=1.0, coords=x2.coords, fill_value=sp.nan, shape=x2.shape) + y2 = sp.COO(data=1.0, coords=y2.coords, fill_value=sp.nan, shape=y2.shape) + w = w * x2 * y2 with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) diff --git a/pyproject.toml b/pyproject.toml index 1c2a57af..2be3f829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ maintainers = [ description = "Chainladder Package - P&C Loss Reserving package" readme = "README.rst" license = {text = "MPL-2.0"} -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -22,7 +22,6 @@ classifiers = [ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -32,7 +31,7 @@ keywords = ["actuarial", "reserving", "insurance", "chainladder", "IBNR"] dependencies = [ "pandas >=2.3.3, !=3.0.4", "scikit-learn>1.4.2", - "sparse>=0.9", + "sparse>=0.18", "numpy>=2.0", "matplotlib", # Required for TriangleDisplay.heatmap() "dill",