Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6ea3360
Implemented policy_length
kennethshsu Apr 21, 2026
3ca76bd
undo the refactor
kennethshsu Apr 21, 2026
72300e4
added tests for policy_length
kennethshsu Apr 21, 2026
9f99218
Fixed bug to round D to int
kennethshsu Apr 21, 2026
c72b1e8
Fixed an issue on the tests
kennethshsu Apr 21, 2026
b1f1bba
Removed blank line
kennethshsu Apr 21, 2026
98521f2
Formatted with black
kennethshsu Apr 21, 2026
7dc5b91
Merge branch 'main' of https://github.com/casact/chainladder-python i…
kennethshsu Apr 22, 2026
f65af55
REFACTOR: Reorganize type hierarchy of sparse.py. Move array-level fu…
genedan May 6, 2026
a33f181
FIX: Apply bugbot fixes.
genedan May 6, 2026
41bf857
FIX: Apply bugbot fixes.
genedan May 6, 2026
e48a28e
FIX: Apply bugbot fixes.
genedan May 6, 2026
0715793
TEST: Add unit tests to chainladder.utils.sparse to cover missing lines.
genedan May 7, 2026
4c21eb3
v.0.9.2 release notes
kennethshsu May 8, 2026
9b0dac7
Added a last minute PR
kennethshsu May 8, 2026
4dd89ec
Added more releaes notes from older versions that were not included i…
kennethshsu May 8, 2026
d1f2a25
Bumping pandas to 2.3.3 in preparation of pandas 3.0
kennethshsu May 9, 2026
d318b33
Merge branch 'main' of https://github.com/casact/chainladder-python i…
kennethshsu May 9, 2026
0ed707a
Small typo, thanks AF!
kennethshsu May 9, 2026
70a7be1
One more approved PR (docstrings improvement)
kennethshsu May 9, 2026
0f9bf4e
bumping pandas
kennethshsu May 9, 2026
ce3ed47
Fix Adjustments API page linking to chainladder.workflow (#762)
SaguaroDev May 10, 2026
f7ee445
Annotate matplotlib dependency as required for TriangleDisplay.heatma…
SaguaroDev May 11, 2026
51fb041
Merge branch 'main' of https://github.com/casact/chainladder-python i…
kennethshsu May 11, 2026
c5ab6f4
Merge pull request #739 from casact/#737-sparse-hierarchy
genedan May 11, 2026
02d1480
Build(deps): Bump urllib3 from 2.6.3 to 2.7.0
dependabot[bot] May 11, 2026
4a611e6
Merge pull request #767 from casact/dependabot/uv/urllib3-2.7.0
kennethshsu May 11, 2026
482508b
Merge branch 'main' of https://github.com/casact/chainladder-python i…
kennethshsu May 11, 2026
9df6a5e
Updated date and some final merged PRs
kennethshsu May 11, 2026
2f6ea12
Merge pull request #751 from casact/0.9.2-release-prep
kennethshsu May 11, 2026
a627a2c
Add more installation options
kennethshsu May 11, 2026
c2c748c
Added uv run in the pytest
kennethshsu May 11, 2026
ab6aa1c
Merge branch 'main' of https://github.com/casact/chainladder-python i…
kennethshsu May 11, 2026
6d6e4f6
Merge pull request #769 from casact/#717
kennethshsu May 11, 2026
fca98e6
Added more tests
kennethshsu May 11, 2026
26a542e
Merge branch 'main' of https://github.com/casact/chainladder-python i…
kennethshsu May 11, 2026
57ddc61
clarification
kennethshsu May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
## Additional Context for Reviewers


- [ ] I passed tests locally for both code (`pytest`) and documentation changes (`uv run jb build docs --builder=custom --custom-builder=doctest`)
- [ ] I passed tests locally for both code (`uv run pytest`) and documentation changes (`uv run jb build docs --builder=custom --custom-builder=doctest`)
20 changes: 14 additions & 6 deletions chainladder/adjustments/parallelogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@


class ParallelogramOLF(BaseEstimator, TransformerMixin, EstimatorIO):
"""
Estimator to create and apply on-level factors to a Triangle object. This
f"""
Estimator to create and apply on-level factors to a Triangle object. This
is commonly used for premium vectors expressed as a Triangle object.

Parameters
Expand All @@ -21,10 +21,15 @@ class ParallelogramOLF(BaseEstimator, TransformerMixin, EstimatorIO):
5% decrease should be stated as -0.05
date_col: str
A list-like set of effective dates corresponding to each of the changes
approximation_grain: str
The resolution of the internal calendar used for calculating the on-level factors:
monthly ('M') or daily ('D'). Daily is finer and adjusts for leap years when assigning
factors to origin periods.
approximation_grain: str {"M", "D"} (default="M")
The resolution of the internal calendar spacing used for calculating the
on-level factors: monthly ('M') or daily ('D'). In each `approximation_grain`,
they are treated as a period, and a weighted current rate level is estimated.
While in daily mode, each day is treated as a full period. Daily is finer
and adjusts for leap years when assigning factors to origin periods.
The Friedland text uses monthly, but daily is more accurate.
policy_length: int (default=12)
The length of the policy in months.
vertical_line:
Rates are typically stated on an effective date basis and premiums on
and earned basis. By default, this argument is False and produces
Expand All @@ -45,12 +50,14 @@ def __init__(
change_col="",
date_col="",
approximation_grain="M",
policy_length=12,
vertical_line=False,
):
self.rate_history = rate_history
self.change_col = change_col
self.date_col = date_col
self.approximation_grain = approximation_grain
self.policy_length = policy_length
self.vertical_line = vertical_line

def fit(self, X, y=None, sample_weight=None):
Expand Down Expand Up @@ -86,6 +93,7 @@ def fit(self, X, y=None, sample_weight=None):
start_date=X.origin[0].to_timestamp(how="s"),
end_date=X.origin[-1].to_timestamp(how="e"),
grain=X.origin_grain,
policy_length=self.policy_length,
vertical_line=self.vertical_line,
approximation_grain=self.approximation_grain,
)
Expand Down
2 changes: 1 addition & 1 deletion chainladder/core/dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def _validate_arithmetic(self, other: Any) -> tuple:
if isinstance(other, np.ndarray) and self.array_backend != 'numpy':
obj = self.copy()
other = obj.get_array_module().array(other)
elif isinstance(other, sp) and self.array_backend != 'sparse':
elif isinstance(other, sp.COO) and self.array_backend != 'sparse':
obj = self.set_backend('sparse')
else:
obj = self.copy()
Expand Down
6 changes: 3 additions & 3 deletions chainladder/core/slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _sparse_setitem(self, key, values):
(self.obj.values.coords, np.array(key)[:, None]), 1)
self.obj.values.data = np.concatenate(
(self.obj.values.data, np.array([values])), 0)
self.obj.values = self.obj.get_array_module()(
self.obj.values = self.obj.get_array_module().COO(
self.obj.values.coords, self.obj.values.data, prune=True,
has_duplicates=False, shape=self.obj.shape,
fill_value=self.obj.values.fill_value)
Expand Down Expand Up @@ -238,8 +238,8 @@ def __setitem__(self, key, value):
value.values.coords[1] = i
coords = np.concatenate((before.coords, value.values.coords), axis=1)
data = np.concatenate((before.data, value.values.data))
self.values = xp(
coords, data, shape=self.shape, prune=True, fill_value=xp.nan
self.values = xp.COO(
coords, data, shape=self.shape, prune=True, fill_value=xp.COO.nan
)
else:
if isinstance(value, TriangleSlicer):
Expand Down
2 changes: 1 addition & 1 deletion chainladder/core/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def __init__(

# Construct Sparse multidimensional array.
self.values: COO = num_to_nan(
sp(
sp.COO(
coords,
amts,
prune=True,
Expand Down
54 changes: 23 additions & 31 deletions chainladder/utils/sparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@
# 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 sparse
from sparse import COO as sp
import sparse as sp
from sparse import COO as COO
from sparse import elemwise
import pandas as pd
import copy

sp.isnan = np.isnan
sp.newaxis = np.newaxis
sp.nan = np.array([1.0, np.nan])[-1]
sp.testing = np.testing
sp.nansum = sparse.nansum
sp.nanmin = sparse.nanmin
sp.nanmax = sparse.nanmax
sp.concatenate = sparse.concatenate
sp.diagonal = sparse.diagonal
sp.zeros = sparse.zeros
sp.testing.assert_array_equal = np.testing.assert_equal
COO.nan = np.array([1.0, np.nan])[-1]
setattr(sp, 'testing', np.testing)
sp.sqrt = np.sqrt
sp.log = np.log
sp.exp = np.exp
Expand All @@ -31,41 +21,41 @@ def nan_to_num(a):
if hasattr(a, "fill_value"):
a = a.copy()
a.data[np.isnan(a.data)] = 0.0
return sp(coords=a.coords, data=a.data, fill_value=0.0, shape=a.shape)
return COO(coords=a.coords, data=a.data, fill_value=0.0, shape=a.shape)


def ones(*args, **kwargs):
return sp(np.ones(*args, **kwargs), fill_value=sp.nan)
return COO(np.ones(*args, **kwargs), fill_value=COO.nan)


def nansum(a, axis=None, keepdims=None, *args, **kwargs):
return sp(data=a.data, coords=a.coords, fill_value=0.0, shape=a.shape).sum(
return COO(data=a.data, coords=a.coords, fill_value=0.0, shape=a.shape).sum(
axis=axis, keepdims=keepdims, *args, **kwargs
)
sp.nansum = nansum


def nanmean(a, axis=None, keepdims=None, *args, **kwargs):
n = sp.nansum(a, axis=axis, keepdims=keepdims)
d = sp.nansum(sp.nan_to_num(a) != 0, axis=axis, keepdims=keepdims).astype(n.dtype)
n = sp(data=n.data, coords=n.coords, fill_value=np.nan, shape=n.shape)
d = sp(data=d.data, coords=d.coords, fill_value=np.nan, shape=d.shape)

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)
n = COO(data=n.data, coords=n.coords, fill_value=np.nan, shape=n.shape)
d = COO(data=d.data, coords=d.coords, fill_value=np.nan, shape=d.shape)
out = n / d
return sp(data=out.data, coords=out.coords, fill_value=0, shape=out.shape)
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 = sp.nan
if type(a) == sp:
return sp(a, *args, **kwargs, fill_value=fill_value)
fill_value = COO.nan
if type(a) == sp.COO:
return COO(a, *args, **kwargs, fill_value=fill_value)
else:
return sp(np.array(a, *args, **kwargs), fill_value=fill_value)
return COO(np.array(a, *args, **kwargs), fill_value=fill_value)


def arange(*args, **kwargs):
return sparse.COO.from_numpy(np.arange(*args, **kwargs))
return COO.from_numpy(np.arange(*args, **kwargs))


def where(*args, **kwargs):
Expand All @@ -76,12 +66,12 @@ def cumprod(a, axis=None, dtype=None, out=None):
return array(np.cumprod(a.todense(), axis=axis, dtype=dtype, out=out))


def floor(x, *args, **kwargs):
def floor(x):
x.data = np.floor(x.data)
return x



sp.nansum = nansum
sp.minimum = np.minimum
sp.maximum = np.maximum
sp.floor = floor
Expand All @@ -91,4 +81,6 @@ def floor(x, *args, **kwargs):
sp.nan_to_num = nan_to_num
sp.ones = ones
sp.cumprod = cumprod
COO.cumprod = cumprod
sp.nanmean = nanmean
sp.sum = COO.sum
110 changes: 110 additions & 0 deletions chainladder/utils/tests/test_sparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import numpy as np

from chainladder.utils.sparse import (
array,
floor,
COO,
where
)


def test_array_from_list_default_fill_value() -> None:
"""
Tests chainladder.utils.sparse.array() when no fill value is provided.
Checks whether the default nan is filled.

Returns
-------
None

"""
result: COO = array([1.0, 2.0, 3.0])
assert isinstance(result, COO)
assert np.isnan(result.fill_value)


def test_array_from_list_explicit_fill_value() -> None:
"""
Tests chainladder.utils.sparse.array() when a fill value of 0 is provided.
Checks whether the 0 is filled.

Returns
-------

"""
result: COO = array([1, 2, 3], fill_value=0)
assert isinstance(result, COO)
assert result.fill_value == 0


def test_array_from_coo_default_fill_value() -> None:
"""
Tests chainladder.utils.sparse.array() when initializing from a sparse array with a default fill value.

Returns
-------
None

"""
coo = COO.from_numpy(np.array([1.0, 2.0, 3.0]))
result: COO = array(coo)
assert isinstance(result, COO)
assert np.isnan(result.fill_value)


def test_array_from_coo_explicit_fill_value() -> None:
"""
Tests chainladder.utils.sparse.array() when initializing from a sparse array with an explicit fill value.

Returns
-------
None

"""
coo = COO.from_numpy(np.array([1, 2, 3]))
result: COO = array(coo, fill_value=0)
assert isinstance(result, COO)
assert result.fill_value == 0


def test_where_selects_from_two_arrays() -> None:
"""
Tests element-wise where across sparse arrays. Calls np.where on each element triplet
(cond[i], a[i], b[i]) - returning a[i] where the condition is True and b[i] where it's False.

Returns
-------
None
"""
a: COO = array([1.0, 2.0, 3.0])
b: COO = array([10.0, 20.0, 30.0])
cond: COO = array([True, False, True])
result: COO = where(cond, a, b)
assert isinstance(result, COO)
np.testing.assert_array_equal(result.todense(), [1.0, 20.0, 3.0])


def test_floor_rounds_down() -> None:
"""
Checks floor function rounding down with positive and negative floats.

Returns
-------
None
"""
a: COO = array([1.2, 2.7, -0.3])
result: COO = floor(a)
np.testing.assert_array_equal(result.todense(), [1.0, 2.0, -1.0])


def test_floor_mutates_in_place() -> None:
"""
Checks in-place mutation of floor function.

Returns
-------
None
"""
a = array([1.2, 2.7, -0.3])
result: COO = floor(a)
assert result is a
Loading
Loading