Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pytest_upstream_nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
run: uv run --with "pandas>=3,<4" pytest chainladder -m "not r"
31 changes: 31 additions & 0 deletions chainladder/utils/tests/test_tri_w.py
Original file line number Diff line number Diff line change
@@ -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]
10 changes: 0 additions & 10 deletions chainladder/utils/tests/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
43 changes: 43 additions & 0 deletions chainladder/utils/tests/test_wtd_reg.py
Original file line number Diff line number Diff line change
@@ -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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noob math questions from me:

  • Are these handpicked scenarios where we know the beta will come out to be 1, and we're just making sure it does when there's missing data?
  • And, even when we supply those 5 weights of 1 as input internally they get set to zero when there is a missing data point for the pair?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes to both. though for second question, you can think of it as weight of 1 being applied to a NaN cell.

you can try running the test on main. it will fail because the weights get applied incorrectly to one side of the observation pair that has a NaN.

"""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"]
)
7 changes: 6 additions & 1 deletion chainladder/utils/weighted_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ 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",
"Intended Audience :: Science/Research",
"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",
Expand All @@ -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",
Comment thread
cursor[bot] marked this conversation as resolved.
"numpy>=2.0",
"matplotlib", # Required for TriangleDisplay.heatmap()
"dill",
Expand Down
Loading