From 6ea3360a34378a567a0f8ec37e4a9d4d4ec835d4 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 13:34:45 -0700 Subject: [PATCH 01/27] Implemented policy_length --- chainladder/adjustments/parallelogram.py | 5 ++ chainladder/utils/utility_functions.py | 95 ++++++++++-------------- 2 files changed, 43 insertions(+), 57 deletions(-) diff --git a/chainladder/adjustments/parallelogram.py b/chainladder/adjustments/parallelogram.py index 1c7a758a..ddb90bee 100644 --- a/chainladder/adjustments/parallelogram.py +++ b/chainladder/adjustments/parallelogram.py @@ -25,6 +25,8 @@ class ParallelogramOLF(BaseEstimator, TransformerMixin, EstimatorIO): 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. + 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 @@ -45,12 +47,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): @@ -86,6 +90,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, ) diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index c7d61b90..ea934e81 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -376,12 +376,27 @@ def read_json(json_str, array_backend=None): return cl.__dict__[json_dict["__class__"]]().set_params(**json_dict["params"]) +def _parallelogram_midpoint_average(cum_rate_changes: pd.Series, window: int) -> pd.Series: + """Rolling mean of cumulative rate level, averaged with a one-step shift (parallelogram).""" + avg = cum_rate_changes.rolling(window).mean() + return (avg + avg.shift(1).values) / 2 + + +def _parallelogram_olf_by_origin(series: pd.Series, grain: str, crl: float) -> pd.DataFrame: + tbl = series.groupby(series.index.to_period(grain)).mean().reset_index() + tbl.columns = ["Origin", "OLF"] + tbl["Origin"] = tbl["Origin"].astype(str) + tbl["OLF"] = crl / tbl["OLF"] + return tbl + + def parallelogram_olf( values, date, start_date=None, end_date=None, grain="Y", + policy_length=12, approximation_grain="M", vertical_line=False, ): @@ -412,71 +427,37 @@ def parallelogram_olf( cum_rate_changes = pd.Series(cum_rate_changes, rate_changes.index) crl = cum_rate_changes.iloc[-1] - cum_avg_rate_non_leaps = cum_rate_changes - cum_avg_rate_leaps = cum_rate_changes + win_m = int(policy_length) + win_d = int(round(365 * policy_length / 12)) + win_d_leap = win_d + 1 + + # Monthly calendar: leap vs non-leap is not used (same as legacy second branch discarded). + if approximation_grain == "M": + if vertical_line: + trimmed = cum_rate_changes.iloc[win_m:] + else: + trimmed = _parallelogram_midpoint_average(cum_rate_changes, win_m).iloc[win_m:] + out = _parallelogram_olf_by_origin(trimmed, grain, crl) + return out.set_index("Origin") + # Daily calendar: 365- vs 366-day policy windows; pick by origin calendar year (leap or not). if not vertical_line: - rolling_num = { - "M": 12, - "D": 365, - } - - cum_avg_rate_non_leaps = cum_rate_changes.rolling( - rolling_num[approximation_grain] - ).mean() - cum_avg_rate_non_leaps = ( - cum_avg_rate_non_leaps + cum_avg_rate_non_leaps.shift(1).values - ) / 2 - - cum_avg_rate_leaps = cum_rate_changes.rolling( - rolling_num[approximation_grain] + 1 - ).mean() - cum_avg_rate_leaps = ( - cum_avg_rate_leaps + cum_avg_rate_leaps.shift(1).values - ) / 2 - - dropdates_num = { - "M": 12, - "D": 366, - } - cum_avg_rate_non_leaps = cum_avg_rate_non_leaps.iloc[ - dropdates_num[approximation_grain] : - ] - cum_avg_rate_leaps = cum_avg_rate_leaps.iloc[ - dropdates_num[approximation_grain] + 1 : - ] + s_nonleap = _parallelogram_midpoint_average(cum_rate_changes, win_d).iloc[win_d + 1 :] + s_leap = _parallelogram_midpoint_average(cum_rate_changes, win_d_leap).iloc[win_d_leap + 1 :] + else: + s_nonleap = cum_rate_changes.iloc[win_d + 1 :] + s_leap = cum_rate_changes.iloc[win_d_leap + 1 :] - fcrl_non_leaps = ( - cum_avg_rate_non_leaps.groupby(cum_avg_rate_non_leaps.index.to_period(grain)) - .mean() - .reset_index() - ) - fcrl_non_leaps.columns = ["Origin", "OLF"] - fcrl_non_leaps["Origin"] = fcrl_non_leaps["Origin"].astype(str) - fcrl_non_leaps["OLF"] = crl / fcrl_non_leaps["OLF"] - - fcrl_leaps = ( - cum_avg_rate_leaps.groupby(cum_avg_rate_leaps.index.to_period(grain)) - .mean() - .reset_index() - ) - fcrl_leaps.columns = ["Origin", "OLF"] - fcrl_leaps["Origin"] = fcrl_leaps["Origin"].astype(str) - fcrl_leaps["OLF"] = crl / fcrl_leaps["OLF"] + fcrl_non_leaps = _parallelogram_olf_by_origin(s_nonleap, grain, crl) + fcrl_leaps = _parallelogram_olf_by_origin(s_leap, grain, crl) combined = fcrl_non_leaps.join(fcrl_leaps, lsuffix="_non_leaps", rsuffix="_leaps") combined["is_leap"] = pd.to_datetime( combined["Origin_non_leaps"], format="%Y" + ("-%M" if grain == "M" else "") ).dt.is_leap_year - - - if approximation_grain == "M": - combined["final_OLF"] = combined["OLF_non_leaps"] - else: - combined["final_OLF"] = np.where( - combined["is_leap"], combined["OLF_leaps"], combined["OLF_non_leaps"] - ) - + combined["OLF"] = np.where( + combined["is_leap"], combined["OLF_leaps"], combined["OLF_non_leaps"] + ) combined.drop( ["OLF_non_leaps", "Origin_leaps", "OLF_leaps", "is_leap"], axis=1, From 3ca76bdaeb8c69c3273f001a194daa9621bb8a01 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 13:35:53 -0700 Subject: [PATCH 02/27] undo the refactor --- chainladder/utils/utility_functions.py | 94 ++++++++++++++++---------- 1 file changed, 57 insertions(+), 37 deletions(-) diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index ea934e81..8d4c042f 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -376,20 +376,6 @@ def read_json(json_str, array_backend=None): return cl.__dict__[json_dict["__class__"]]().set_params(**json_dict["params"]) -def _parallelogram_midpoint_average(cum_rate_changes: pd.Series, window: int) -> pd.Series: - """Rolling mean of cumulative rate level, averaged with a one-step shift (parallelogram).""" - avg = cum_rate_changes.rolling(window).mean() - return (avg + avg.shift(1).values) / 2 - - -def _parallelogram_olf_by_origin(series: pd.Series, grain: str, crl: float) -> pd.DataFrame: - tbl = series.groupby(series.index.to_period(grain)).mean().reset_index() - tbl.columns = ["Origin", "OLF"] - tbl["Origin"] = tbl["Origin"].astype(str) - tbl["OLF"] = crl / tbl["OLF"] - return tbl - - def parallelogram_olf( values, date, @@ -427,37 +413,71 @@ def parallelogram_olf( cum_rate_changes = pd.Series(cum_rate_changes, rate_changes.index) crl = cum_rate_changes.iloc[-1] - win_m = int(policy_length) - win_d = int(round(365 * policy_length / 12)) - win_d_leap = win_d + 1 - - # Monthly calendar: leap vs non-leap is not used (same as legacy second branch discarded). - if approximation_grain == "M": - if vertical_line: - trimmed = cum_rate_changes.iloc[win_m:] - else: - trimmed = _parallelogram_midpoint_average(cum_rate_changes, win_m).iloc[win_m:] - out = _parallelogram_olf_by_origin(trimmed, grain, crl) - return out.set_index("Origin") + cum_avg_rate_non_leaps = cum_rate_changes + cum_avg_rate_leaps = cum_rate_changes - # Daily calendar: 365- vs 366-day policy windows; pick by origin calendar year (leap or not). if not vertical_line: - s_nonleap = _parallelogram_midpoint_average(cum_rate_changes, win_d).iloc[win_d + 1 :] - s_leap = _parallelogram_midpoint_average(cum_rate_changes, win_d_leap).iloc[win_d_leap + 1 :] - else: - s_nonleap = cum_rate_changes.iloc[win_d + 1 :] - s_leap = cum_rate_changes.iloc[win_d_leap + 1 :] + rolling_num = { + "M": policy_length, + "D": 365*policy_length/12, + } + + cum_avg_rate_non_leaps = cum_rate_changes.rolling( + rolling_num[approximation_grain] + ).mean() + cum_avg_rate_non_leaps = ( + cum_avg_rate_non_leaps + cum_avg_rate_non_leaps.shift(1).values + ) / 2 + + cum_avg_rate_leaps = cum_rate_changes.rolling( + rolling_num[approximation_grain] + 1 + ).mean() + cum_avg_rate_leaps = ( + cum_avg_rate_leaps + cum_avg_rate_leaps.shift(1).values + ) / 2 + + dropdates_num = { + "M": 12, + "D": 366, + } + cum_avg_rate_non_leaps = cum_avg_rate_non_leaps.iloc[ + dropdates_num[approximation_grain] : + ] + cum_avg_rate_leaps = cum_avg_rate_leaps.iloc[ + dropdates_num[approximation_grain] + 1 : + ] - fcrl_non_leaps = _parallelogram_olf_by_origin(s_nonleap, grain, crl) - fcrl_leaps = _parallelogram_olf_by_origin(s_leap, grain, crl) + fcrl_non_leaps = ( + cum_avg_rate_non_leaps.groupby(cum_avg_rate_non_leaps.index.to_period(grain)) + .mean() + .reset_index() + ) + fcrl_non_leaps.columns = ["Origin", "OLF"] + fcrl_non_leaps["Origin"] = fcrl_non_leaps["Origin"].astype(str) + fcrl_non_leaps["OLF"] = crl / fcrl_non_leaps["OLF"] + + fcrl_leaps = ( + cum_avg_rate_leaps.groupby(cum_avg_rate_leaps.index.to_period(grain)) + .mean() + .reset_index() + ) + fcrl_leaps.columns = ["Origin", "OLF"] + fcrl_leaps["Origin"] = fcrl_leaps["Origin"].astype(str) + fcrl_leaps["OLF"] = crl / fcrl_leaps["OLF"] combined = fcrl_non_leaps.join(fcrl_leaps, lsuffix="_non_leaps", rsuffix="_leaps") combined["is_leap"] = pd.to_datetime( combined["Origin_non_leaps"], format="%Y" + ("-%M" if grain == "M" else "") ).dt.is_leap_year - combined["OLF"] = np.where( - combined["is_leap"], combined["OLF_leaps"], combined["OLF_non_leaps"] - ) + + + if approximation_grain == "M": + combined["final_OLF"] = combined["OLF_non_leaps"] + else: + combined["final_OLF"] = np.where( + combined["is_leap"], combined["OLF_leaps"], combined["OLF_non_leaps"] + ) + combined.drop( ["OLF_non_leaps", "Origin_leaps", "OLF_leaps", "is_leap"], axis=1, From 72300e49f121305b3fd5f482b5cedbcbed9f9c41 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 13:59:31 -0700 Subject: [PATCH 03/27] added tests for policy_length --- chainladder/utils/tests/test_utilities.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index d3989920..86537bf7 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -110,6 +110,19 @@ def test_vertical_line(): true_olf = 1.2 / ((1 - 184 / 365) * 1.0 + (184 / 365) * 1.2) assert abs(olf.loc["2017"].iloc[0] - true_olf) < 0.00001 +def test_policy_length(): + prem_tri = cl.ParallelogramOLF( + rate_history, change_col="RateChange", date_col="EffDate", policy_length = 12 + ).fit_transform(prem_tri) + + assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.136348, 1.043056, 0.992792, 0.999684, 1]).all() + + prem_tri = cl.ParallelogramOLF( + rate_history, change_col="RateChange", date_col="EffDate", policy_length = 6 + ).fit_transform(prem_tri) + assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.129333, 1.013023, 0.994975, 1, 1]).all() + + def test_triangle_json_io(clrd): xp = clrd.get_array_module() From 9f99218f8fdfde2a8fdd573633b71031117b334c Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 13:59:41 -0700 Subject: [PATCH 04/27] Fixed bug to round D to int --- chainladder/utils/utility_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index 8d4c042f..86d0f676 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -419,7 +419,7 @@ def parallelogram_olf( if not vertical_line: rolling_num = { "M": policy_length, - "D": 365*policy_length/12, + "D": 365*int(policy_length/12), } cum_avg_rate_non_leaps = cum_rate_changes.rolling( From c72b1e8345166e72faec94eaeeffd05def08e7ca Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 14:10:11 -0700 Subject: [PATCH 05/27] Fixed an issue on the tests --- chainladder/utils/tests/test_utilities.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 86537bf7..e8ed93de 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -111,6 +111,17 @@ def test_vertical_line(): assert abs(olf.loc["2017"].iloc[0] - true_olf) < 0.00001 def test_policy_length(): + rate_history = pd.DataFrame( + { + "EffDate": ["2010-07-01", "2011-01-01", "2012-04-01"], + "RateChange": [0.05, 0.1, -0.01], + } + ) + data = pd.DataFrame( + {"Year": [2010, 2011, 2012, 2013, 2014], "EarnedPremium": [10_000] * 5} + ) + prem_tri = cl.Triangle(data, origin="Year", columns="EarnedPremium", cumulative = True) + prem_tri = cl.ParallelogramOLF( rate_history, change_col="RateChange", date_col="EffDate", policy_length = 12 ).fit_transform(prem_tri) From b1f1bbab7487bdb4036d8f8de016c177928ec1ed Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 14:16:12 -0700 Subject: [PATCH 06/27] Removed blank line --- chainladder/utils/tests/test_utilities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index e8ed93de..d4c1450c 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -125,7 +125,6 @@ def test_policy_length(): prem_tri = cl.ParallelogramOLF( rate_history, change_col="RateChange", date_col="EffDate", policy_length = 12 ).fit_transform(prem_tri) - assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.136348, 1.043056, 0.992792, 0.999684, 1]).all() prem_tri = cl.ParallelogramOLF( From 98521f25a95e232cc1c73aa280663c88f0562146 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Tue, 21 Apr 2026 14:17:44 -0700 Subject: [PATCH 07/27] Formatted with black --- chainladder/utils/tests/test_utilities.py | 52 ++++++++++++++--------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index d4c1450c..a8b2729c 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -110,6 +110,7 @@ def test_vertical_line(): true_olf = 1.2 / ((1 - 184 / 365) * 1.0 + (184 / 365) * 1.2) assert abs(olf.loc["2017"].iloc[0] - true_olf) < 0.00001 + def test_policy_length(): rate_history = pd.DataFrame( { @@ -120,18 +121,25 @@ def test_policy_length(): data = pd.DataFrame( {"Year": [2010, 2011, 2012, 2013, 2014], "EarnedPremium": [10_000] * 5} ) - prem_tri = cl.Triangle(data, origin="Year", columns="EarnedPremium", cumulative = True) - + prem_tri = cl.Triangle( + data, origin="Year", columns="EarnedPremium", cumulative=True + ) + prem_tri = cl.ParallelogramOLF( - rate_history, change_col="RateChange", date_col="EffDate", policy_length = 12 + rate_history, change_col="RateChange", date_col="EffDate", policy_length=12 ).fit_transform(prem_tri) - assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.136348, 1.043056, 0.992792, 0.999684, 1]).all() + assert ( + np.round(prem_tri.olf_.values.flatten(), 6) + == [1.136348, 1.043056, 0.992792, 0.999684, 1] + ).all() prem_tri = cl.ParallelogramOLF( - rate_history, change_col="RateChange", date_col="EffDate", policy_length = 6 + rate_history, change_col="RateChange", date_col="EffDate", policy_length=6 ).fit_transform(prem_tri) - assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.129333, 1.013023, 0.994975, 1, 1]).all() - + assert ( + np.round(prem_tri.olf_.values.flatten(), 6) + == [1.129333, 1.013023, 0.994975, 1, 1] + ).all() def test_triangle_json_io(clrd): @@ -179,33 +187,38 @@ def test_json_df(): ) assert abs(cl.read_json(x.to_json()).lambda_ - x.lambda_).sum() < 1e-5 + def test_read_csv_single(raa): # Test the read_csv function for a single dimensional input. - + # Read in the csv file. from pathlib import Path + raa_csv_path = Path(__file__).parent.parent / "data" / "raa.csv" assert raa == cl.read_csv( filepath_or_buffer=raa_csv_path, - origin = "origin", - development = "development", - columns = ["values"], - index = None, - cumulative = True) + origin="origin", + development="development", + columns=["values"], + index=None, + cumulative=True, + ) + def test_read_csv_multi(clrd): # Test the read_csv function for multidimensional input. # Read in the csv file. from pathlib import Path + clrd_csv_path = Path(__file__).parent.parent / "data" / "clrd.csv" assert clrd == cl.read_csv( filepath_or_buffer=clrd_csv_path, - origin = "AccidentYear", - development = "DevelopmentYear", - columns = [ + origin="AccidentYear", + development="DevelopmentYear", + columns=[ "IncurLoss", "CumPaidLoss", "BulkLoss", @@ -213,9 +226,10 @@ def test_read_csv_multi(clrd): "EarnedPremCeded", "EarnedPremNet", ], - index = ["GRNAME","LOB"], - cumulative = True - ) + index=["GRNAME", "LOB"], + cumulative=True, + ) + def test_concat(clrd): tri = clrd.groupby("LOB").sum() From f65af55a3f3b708ad17a16ea5b80fc8b709555a3 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Wed, 6 May 2026 17:10:39 -0500 Subject: [PATCH 08/27] REFACTOR: Reorganize type hierarchy of sparse.py. Move array-level functions to sparse.COO, and keep module-level functions to sp. Replaces xp() with xp.COO() to create sparse arrays when xp used to be callable and was used as a constructor. --- chainladder/core/dunders.py | 2 +- chainladder/core/slice.py | 4 +- chainladder/core/triangle.py | 2 +- chainladder/utils/sparse.py | 51 +++++++++++--------------- chainladder/utils/utility_functions.py | 4 +- 5 files changed, 27 insertions(+), 36 deletions(-) diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index e8799703..78226045 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -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() diff --git a/chainladder/core/slice.py b/chainladder/core/slice.py index 55c29a82..d7394a16 100644 --- a/chainladder/core/slice.py +++ b/chainladder/core/slice.py @@ -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) @@ -238,7 +238,7 @@ 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( + self.values = xp.COO( coords, data, shape=self.shape, prune=True, fill_value=xp.nan ) else: diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index dd80c21e..a6a33b52 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -524,7 +524,7 @@ def __init__( # Construct Sparse multidimensional array. self.values: COO = num_to_nan( - sp( + sp.COO( coords, amts, prune=True, diff --git a/chainladder/utils/sparse.py b/chainladder/utils/sparse.py index 50533d98..b47b555f 100644 --- a/chainladder/utils/sparse.py +++ b/chainladder/utils/sparse.py @@ -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 @@ -31,27 +21,27 @@ 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=sp.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: @@ -59,13 +49,13 @@ def array(a, *args, **kwargs): else: fill_value = sp.nan if type(a) == sp: - return sp(a, *args, **kwargs, fill_value=fill_value) + 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): @@ -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 @@ -90,5 +80,6 @@ def floor(x, *args, **kwargs): sp.array = array sp.nan_to_num = nan_to_num sp.ones = ones -sp.cumprod = cumprod +COO.cumprod = cumprod sp.nanmean = nanmean +sp.sum = COO.sum diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index 37c381f1..be8b0970 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -730,14 +730,14 @@ def num_to_value( arr.coords = arr.coords[:, arr.data != 0] arr.data = arr.data[arr.data != 0] - arr: COO = sp( + arr: COO = sp.COO( coords=arr.coords, data=arr.data, fill_value=sp.nan, # noqa shape=arr.shape ) else: - arr: COO = sp( + arr: COO = sp.COO( num_to_nan(np.nan_to_num(arr.todense())), fill_value=value ) From a33f181e1be37eb60f782f017fc5328803c74e7f Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Wed, 6 May 2026 18:21:14 -0500 Subject: [PATCH 09/27] FIX: Apply bugbot fixes. --- chainladder/core/slice.py | 2 +- chainladder/core/triangle.py | 2 +- chainladder/utils/sparse.py | 6 +++--- chainladder/utils/weighted_regression.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chainladder/core/slice.py b/chainladder/core/slice.py index d7394a16..4c5b98e8 100644 --- a/chainladder/core/slice.py +++ b/chainladder/core/slice.py @@ -239,7 +239,7 @@ def __setitem__(self, key, value): coords = np.concatenate((before.coords, value.values.coords), axis=1) data = np.concatenate((before.data, value.values.data)) self.values = xp.COO( - coords, data, shape=self.shape, prune=True, fill_value=xp.nan + coords, data, shape=self.shape, prune=True, fill_value=xp.COO.nan ) else: if isinstance(value, TriangleSlicer): diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index a6a33b52..7e2d54aa 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1185,7 +1185,7 @@ def incr_to_cum(self, inplace=False): else: values = xp.nan_to_num(self.values[..., ::-1]) values = num_to_value(values, 1) - values = xp.cumprod(values, -1)[..., ::-1] + values = xp.COO.cumprod(values, -1)[..., ::-1] self.values = values * self.nan_triangle values = num_to_value(values, self.get_array_module(values).nan) else: diff --git a/chainladder/utils/sparse.py b/chainladder/utils/sparse.py index b47b555f..c90c25c8 100644 --- a/chainladder/utils/sparse.py +++ b/chainladder/utils/sparse.py @@ -25,7 +25,7 @@ def nan_to_num(a): def ones(*args, **kwargs): - return COO(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): @@ -47,8 +47,8 @@ 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: + fill_value = COO.nan + if type(a) == sp.COO: return COO(a, *args, **kwargs, fill_value=fill_value) else: return COO(np.array(a, *args, **kwargs), fill_value=fill_value) diff --git a/chainladder/utils/weighted_regression.py b/chainladder/utils/weighted_regression.py index aec67d22..12a5ef7e 100644 --- a/chainladder/utils/weighted_regression.py +++ b/chainladder/utils/weighted_regression.py @@ -53,7 +53,7 @@ def _fit_OLS(self): y[w == 0] = xp.nan else: w2 = w.copy() - w2 = sp(data=w2.data, coords=w2.coords, fill_value=sp.nan, shape=w2.shape) + w2 = sp.COO(data=w2.data, coords=w2.coords, fill_value=sp.nan, shape=w2.shape) x, y = x * w2, y * w2 with warnings.catch_warnings(): From 41bf857045c6b58e9269d234f53cc0297b66d449 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Wed, 6 May 2026 18:31:03 -0500 Subject: [PATCH 10/27] FIX: Apply bugbot fixes. --- chainladder/core/triangle.py | 2 +- chainladder/utils/sparse.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 7e2d54aa..a6a33b52 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1185,7 +1185,7 @@ def incr_to_cum(self, inplace=False): else: values = xp.nan_to_num(self.values[..., ::-1]) values = num_to_value(values, 1) - values = xp.COO.cumprod(values, -1)[..., ::-1] + values = xp.cumprod(values, -1)[..., ::-1] self.values = values * self.nan_triangle values = num_to_value(values, self.get_array_module(values).nan) else: diff --git a/chainladder/utils/sparse.py b/chainladder/utils/sparse.py index c90c25c8..2de836b8 100644 --- a/chainladder/utils/sparse.py +++ b/chainladder/utils/sparse.py @@ -80,6 +80,7 @@ def floor(x): sp.array = array sp.nan_to_num = nan_to_num sp.ones = ones +sp.cumprod = cumprod COO.cumprod = cumprod sp.nanmean = nanmean sp.sum = COO.sum From e48a28e78d5ab851c7dfcaf699749ad62d881f34 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Wed, 6 May 2026 18:32:16 -0500 Subject: [PATCH 11/27] FIX: Apply bugbot fixes. --- chainladder/utils/utility_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index be8b0970..e044f9ca 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -733,7 +733,7 @@ def num_to_value( arr: COO = sp.COO( coords=arr.coords, data=arr.data, - fill_value=sp.nan, # noqa + fill_value=sp.COO.nan, # noqa shape=arr.shape ) else: From 07157937e7c600abb2881f7cb771c72f6835663a Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Wed, 6 May 2026 20:59:39 -0500 Subject: [PATCH 12/27] TEST: Add unit tests to chainladder.utils.sparse to cover missing lines. --- chainladder/utils/tests/test_sparse.py | 110 +++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 chainladder/utils/tests/test_sparse.py diff --git a/chainladder/utils/tests/test_sparse.py b/chainladder/utils/tests/test_sparse.py new file mode 100644 index 00000000..c8a2b211 --- /dev/null +++ b/chainladder/utils/tests/test_sparse.py @@ -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 From 4c21eb30f82e787e20bcd0c7ad83c5297563b144 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 13:10:29 -0700 Subject: [PATCH 13/27] v.0.9.2 release notes --- docs/library/releases.md | 47 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/library/releases.md b/docs/library/releases.md index 9a4c967a..e383aaa4 100644 --- a/docs/library/releases.md +++ b/docs/library/releases.md @@ -2,6 +2,53 @@ ## Version 0.9 +### Version 0.9.2 + +Release Date: May 8, 2026 + +**What's Changed** +* Bump nbconvert from 7.16.6 to 7.17.0 by @dependabot[bot] in [#672](https://github.com/casact/chainladder-python/pull/672) +* Adding dict support for renaming columns by @henrydingliu in [#671](https://github.com/casact/chainladder-python/pull/671) +* Drop python3.9 by @kennethshsu in [#675](https://github.com/casact/chainladder-python/pull/675) +* Bump pillow from 11.3.0 to 12.1.1 by @dependabot[bot] in [#673](https://github.com/casact/chainladder-python/pull/673) +* Various fix by @henrydingliu in [#676](https://github.com/casact/chainladder-python/pull/676) +* Build(deps): Bump tornado from 6.5.2 to 6.5.5 by @dependabot[bot] in [#677](https://github.com/casact/chainladder-python/pull/677) +* Build(deps): Bump requests from 2.32.5 to 2.33.0 by @dependabot[bot] in [#685](https://github.com/casact/chainladder-python/pull/685) +* Build(deps): Bump pygments from 2.19.2 to 2.20.0 by @dependabot[bot] in [#688](https://github.com/casact/chainladder-python/pull/688) +* Build(deps): Bump pillow from 12.1.1 to 12.2.0 by @dependabot[bot] in [#697](https://github.com/casact/chainladder-python/pull/697) +* Build(deps): Bump pytest from 8.4.2 to 9.0.3 by @dependabot[bot] in [#698](https://github.com/casact/chainladder-python/pull/698) +* [#689](https://github.com/casact/chainladder-python/issues/689) by @kennethshsu in [#690](https://github.com/casact/chainladder-python/pull/690) +* Improve docstring for approximation_grain in ParallelogramOLF by @kennethshsu in [#687](https://github.com/casact/chainladder-python/pull/687) +* Addressed wheel vulnerability by @kennethshsu in [#699](https://github.com/casact/chainladder-python/pull/699) +* Build(deps): Bump nbconvert from 7.17.0 to 7.17.1 by @dependabot[bot] in [#703](https://github.com/casact/chainladder-python/pull/703) +* DOCS: Begin work on examples. by @genedan in [#700](https://github.com/casact/chainladder-python/pull/700) +* Build(deps): Bump lxml from 6.0.2 to 6.1.0 by @dependabot[bot] in [#705](https://github.com/casact/chainladder-python/pull/705) +* nan_triangle 1D logic overhaul by @danielfong-act in [#702](https://github.com/casact/chainladder-python/pull/702) +* Add links to object source code in API Reference section by @genedan in [#710](https://github.com/casact/chainladder-python/pull/710) +* [#707](https://github.com/casact/chainladder-python/issues/707) by @kennethshsu in [#711](https://github.com/casact/chainladder-python/pull/711) +* Pr template by @kennethshsu in [#712](https://github.com/casact/chainladder-python/pull/712) +* DOCS: Expand Triangle constructor examples (#704) by @EKtheSage in [#714](https://github.com/casact/chainladder-python/pull/714) +* FEAT: Initialize triangle from dict. by @genedan in [#706](https://github.com/casact/chainladder-python/pull/706) +* DOCS: Add examples to Triangle methods and properties (#704) by @EKtheSage in [#719](https://github.com/casact/chainladder-python/pull/719) +* DOCS: Add examples to deterministic IBNR methods (#704) by @EKtheSage in [#721](https://github.com/casact/chainladder-python/pull/721) +* Docs/issue 704 tailconstant examples by @priyam0k in [#722](https://github.com/casact/chainladder-python/pull/722) +* Add Friedland datasets to cl.load_sample() by @genedan in [#730](https://github.com/casact/chainladder-python/pull/730) +* CHORE: Update workflows to test docs examples. by @genedan in [#713](https://github.com/casact/chainladder-python/pull/713) +* Added an example and improved docstring for load_sample by @kennethshsu in [#715](https://github.com/casact/chainladder-python/pull/715) +* Missing testoutput by @kennethshsu in [#731](https://github.com/casact/chainladder-python/pull/731) +* Fix test indentation and add print statement by @kennethshsu in [#732](https://github.com/casact/chainladder-python/pull/732) +* Fixed doc site build fails by @kennethshsu in [#733](https://github.com/casact/chainladder-python/pull/733) +* ENH: Continue work on type hinting, add pyright to check type coverage. by @genedan in [#735](https://github.com/casact/chainladder-python/pull/735) +* Rtd branch fix by @henrydingliu in [#741](https://github.com/casact/chainladder-python/pull/741) +* fix(tests): use assert in test_n_periods so failures actually fail by @SaguaroDev in [#744](https://github.com/casact/chainladder-python/pull/744) +* Build(deps): Bump mistune from 3.1.4 to 3.2.1 by @dependabot[bot] in [#748](https://github.com/casact/chainladder-python/pull/748) + +**New Contributors** +* @priyam0k made their first contribution in [#722](https://github.com/casact/chainladder-python/pull/722) +* @SaguaroDev made their first contribution in [#744](https://github.com/casact/chainladder-python/pull/744) + +**Full Changelog**: https://github.com/casact/chainladder-python/compare/v0.9.1...v0.9.2 + ### Version 0.9.1 Release Date: Jan 30, 2026 diff --git a/pyproject.toml b/pyproject.toml index a81b7460..f57f3e99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "chainladder" -version = "0.9.1" +version = "0.9.2" authors = [ {name = "John Bogaardt", email = "jbogaardt@gmail.com"}, ] From 9b0dac7b59d65a40cb09d65aaf12e53733027e1d Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 13:16:38 -0700 Subject: [PATCH 14/27] Added a last minute PR --- docs/library/releases.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/library/releases.md b/docs/library/releases.md index e383aaa4..dee7f738 100644 --- a/docs/library/releases.md +++ b/docs/library/releases.md @@ -42,10 +42,12 @@ Release Date: May 8, 2026 * Rtd branch fix by @henrydingliu in [#741](https://github.com/casact/chainladder-python/pull/741) * fix(tests): use assert in test_n_periods so failures actually fail by @SaguaroDev in [#744](https://github.com/casact/chainladder-python/pull/744) * Build(deps): Bump mistune from 3.1.4 to 3.2.1 by @dependabot[bot] in [#748](https://github.com/casact/chainladder-python/pull/748) +* Update pyproject.toml - Add numpy ([#738](https://github.com/casact/chainladder-python/issues/738)) by @wendy-w2029 in [#750](https://github.com/casact/chainladder-python/pull/750) **New Contributors** * @priyam0k made their first contribution in [#722](https://github.com/casact/chainladder-python/pull/722) * @SaguaroDev made their first contribution in [#744](https://github.com/casact/chainladder-python/pull/744) +* @wendy-w2029 made their first contribution in [#750](https://github.com/casact/chainladder-python/pull/750) **Full Changelog**: https://github.com/casact/chainladder-python/compare/v0.9.1...v0.9.2 From 4dd89ec481b8f558400f54e2da4d07f27f1c88c1 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 13:31:40 -0700 Subject: [PATCH 15/27] Added more releaes notes from older versions that were not included in the doc site --- docs/library/releases.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/library/releases.md b/docs/library/releases.md index dee7f738..5b3d1e51 100644 --- a/docs/library/releases.md +++ b/docs/library/releases.md @@ -43,6 +43,12 @@ Release Date: May 8, 2026 * fix(tests): use assert in test_n_periods so failures actually fail by @SaguaroDev in [#744](https://github.com/casact/chainladder-python/pull/744) * Build(deps): Bump mistune from 3.1.4 to 3.2.1 by @dependabot[bot] in [#748](https://github.com/casact/chainladder-python/pull/748) * Update pyproject.toml - Add numpy ([#738](https://github.com/casact/chainladder-python/issues/738)) by @wendy-w2029 in [#750](https://github.com/casact/chainladder-python/pull/750) +* Add `.github/CODEOWNERS` by @kennethshsu +* Limit pytest GitHub Actions workflow to run on pull requests (not every push) by @kennethshsu +* Clarify first-time vs ongoing `uv` setup in contributor docs; ignore local virtualenv doc build dirs in `.gitignore`; refine dates in the multi-trend example by @kennethshsu +* DOCS: Update VotingChainladder gallery example to agreed `testcode` / `testoutput` format by @genedan +* Refresh getting-started tutorial notebooks, `docs/library/contributing.md`, and `README.rst` by @henrydingliu +* Remove `/en/` locale segment from documentation URLs by @kennethshsu **New Contributors** * @priyam0k made their first contribution in [#722](https://github.com/casact/chainladder-python/pull/722) @@ -192,7 +198,7 @@ Release Date: May 24, 2024 Release Date: Apr 10, 2024 **What's Changed** -* Various bug fixes and improvements +* Fix for [\#509](https://github.com/casact/chainladder-python/issues/509) (triangle / core initialization and packaging metadata). ### Version 0.8.19 @@ -223,7 +229,9 @@ Release Date: Sep 18, 2023 Release Date: Jun 17, 2023 **What's Changed** -* Bump of 0.8.16 +* Relax the `pandas<2.0` upper bound in CI/deps metadata. +* Update `environment-latest.yaml` and `pytest_upstream_nightly.yml` ([#442](https://github.com/casact/chainladder-python/pull/442)). +* Adjust pytest-related dependency pins. ### Version 0.8.16 @@ -234,7 +242,7 @@ Release Date: Jun 17, 2023 * fix for [\#411](https://github.com/casact/chainladder-python/issues/411) * fix for [\#438](https://github.com/casact/chainladder-python/issues/438) -## New Contributors +**New Contributors** * [@MatthewCaseres](https://github.com/MatthewCaseres) made their first contribution. * [@andrejakobsen](https://github.com/andrejakobsen) made their first contribution. @@ -275,7 +283,18 @@ Release Date: Apr 11, 2023 Release Date: Nov 25, 2022 - +**What's Changed** +* Major documentation refresh for Jupyter Book and CAS Annual Meeting materials: user guide and gallery restructure, new exercises and demos, usage/Colab notes, and bibliography updates (including [#311](https://github.com/casact/chainladder-python/pull/311) and related annual-meeting prep PRs [#335](https://github.com/casact/chainladder-python/pull/335)–[#361](https://github.com/casact/chainladder-python/pull/361)). +* Additional Friedland and other sample datasets for `load_sample` ([#347](https://github.com/casact/chainladder-python/pull/347), [#353](https://github.com/casact/chainladder-python/pull/353), [#355](https://github.com/casact/chainladder-python/pull/355), [#357](https://github.com/casact/chainladder-python/pull/357), [#358](https://github.com/casact/chainladder-python/pull/358), [#359](https://github.com/casact/chainladder-python/pull/359), [#362](https://github.com/casact/chainladder-python/pull/362), [#363](https://github.com/casact/chainladder-python/pull/363)). +* `Development` transformer: `std_residuals_` ([#352](https://github.com/casact/chainladder-python/pull/352)). +* Four-dimensional triangles: extend `drop_high` / `drop_low` (including `drop_above` / `drop_below`) ([#375](https://github.com/casact/chainladder-python/pull/375), [#381](https://github.com/casact/chainladder-python/pull/381)). +* Semi-annual key for tail handling ([#384](https://github.com/casact/chainladder-python/pull/384)). +* Triangle `to_frame()` updates and reduced reliance on `origin_as_datetime` inside `to_frame()` ([#360](https://github.com/casact/chainladder-python/pull/360)). +* Bug fixes and hardening for `Development` and triangles ([#373](https://github.com/casact/chainladder-python/pull/373), [#371](https://github.com/casact/chainladder-python/pull/371), [#368](https://github.com/casact/chainladder-python/pull/368), [#366](https://github.com/casact/chainladder-python/pull/366)); follow-ups by @henrydingliu ([#370](https://github.com/casact/chainladder-python/pull/370)). +* `DevelopmentCorrelation` / valuation correlation: annotations, intermediate diagnostics, and `p_critical` validation ([#342](https://github.com/casact/chainladder-python/pull/342)). +* README, GitHub issue templates, and docs environment updates; reduce `to_datetime` deprecation noise. + +**Full Changelog**: https://github.com/casact/chainladder-python/compare/v0.8.13...v0.8.14 ### Version 0.8.13 @@ -299,6 +318,8 @@ Release Date: Jun 27, 2022 Release Date: Mar 8, 2022 +*Note:* There was no `0.8.11` stable release on PyPI; only pre-release tags `v0.8.11-alpha` and `v0.8.11-beta` were published. + **Bug Fixes** - [\#254](https://github.com/casact/chainladder-python/issues/254) Fixed an undesired mutation when using cl.concat From d1f2a25aa7f10594d4000ec80154761afa8f8a25 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 17:54:19 -0700 Subject: [PATCH 16/27] Bumping pandas to 2.3.3 in preparation of pandas 3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f57f3e99..c69a12ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] keywords = ["actuarial", "reserving", "insurance", "chainladder", "IBNR"] dependencies = [ - "pandas >=2.0, <3.0", + "pandas >=2.3.3, <3.0", "scikit-learn>1.4.2", "numba>0.54", "sparse>=0.9", From 0ed707a472fb3b73175588b86bea3a63e32edef2 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 18:03:32 -0700 Subject: [PATCH 17/27] Small typo, thanks AF! --- docs/getting_started/tutorials/triangle-tutorial.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started/tutorials/triangle-tutorial.ipynb b/docs/getting_started/tutorials/triangle-tutorial.ipynb index e2d3a67a..e413faad 100644 --- a/docs/getting_started/tutorials/triangle-tutorial.ipynb +++ b/docs/getting_started/tutorials/triangle-tutorial.ipynb @@ -6085,7 +6085,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`Pandas` has wonderful datetime inference functionality that the `Triangle` heavily uses to infer origin and development granularity. Even still, there are occassions where date format inferences can fail. It is often better to explicitly tell the triangle the date format, and is usually good pratice to explicitly state the date format instead." + "`Pandas` has wonderful datetime inference functionality that the `Triangle` heavily uses to infer origin and development granularity. Even still, there are occasions where date format inferences can fail. It is often better to explicitly tell the triangle the date format, and is usually good practice to explicitly state the date format instead." ] }, { From 70a7be1a02c395de1e62816e6d9f8819806c8ce4 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 18:08:18 -0700 Subject: [PATCH 18/27] One more approved PR (docstrings improvement) --- docs/library/releases.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/library/releases.md b/docs/library/releases.md index 5b3d1e51..69f1873d 100644 --- a/docs/library/releases.md +++ b/docs/library/releases.md @@ -43,6 +43,7 @@ Release Date: May 8, 2026 * fix(tests): use assert in test_n_periods so failures actually fail by @SaguaroDev in [#744](https://github.com/casact/chainladder-python/pull/744) * Build(deps): Bump mistune from 3.1.4 to 3.2.1 by @dependabot[bot] in [#748](https://github.com/casact/chainladder-python/pull/748) * Update pyproject.toml - Add numpy ([#738](https://github.com/casact/chainladder-python/issues/738)) by @wendy-w2029 in [#750](https://github.com/casact/chainladder-python/pull/750) +* Enhance docstrings and examples for improved clarity for many estimators by @kennethshsu in [#747](https://github.com/casact/chainladder-python/pull/747) * Add `.github/CODEOWNERS` by @kennethshsu * Limit pytest GitHub Actions workflow to run on pull requests (not every push) by @kennethshsu * Clarify first-time vs ongoing `uv` setup in contributor docs; ignore local virtualenv doc build dirs in `.gitignore`; refine dates in the multi-trend example by @kennethshsu From 0f9bf4e938551ad56a44de0530be3076f8e7f9a6 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Fri, 8 May 2026 18:09:34 -0700 Subject: [PATCH 19/27] bumping pandas --- uv.lock | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 7f73c005..e79a85dc 100644 --- a/uv.lock +++ b/uv.lock @@ -188,12 +188,14 @@ wheels = [ [[package]] name = "chainladder" -version = "0.9.1" +version = "0.9.2" source = { editable = "." } dependencies = [ { name = "dill" }, { name = "matplotlib" }, { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas" }, { name = "patsy" }, { name = "scikit-learn" }, @@ -283,8 +285,9 @@ requires-dist = [ { name = "nbmake", marker = "extra == 'test'" }, { name = "nbsphinx", marker = "extra == 'docs'" }, { name = "numba", specifier = ">0.54" }, + { name = "numpy" }, { name = "numpydoc", marker = "extra == 'docs'" }, - { name = "pandas", specifier = ">=2.0,<3.0" }, + { name = "pandas", specifier = ">=2.3.3,<3.0" }, { name = "parso", marker = "extra == 'docs'", specifier = ">=0.8" }, { name = "patsy" }, { name = "polars", marker = "extra == 'docs'" }, @@ -921,6 +924,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, @@ -931,6 +935,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, @@ -941,6 +946,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -951,6 +957,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -961,6 +968,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, From ce3ed47c90f312bed0d5db75e35d2bdf975fe030 Mon Sep 17 00:00:00 2001 From: Kevin <74339271+SaguaroDev@users.noreply.github.com> Date: Sun, 10 May 2026 19:36:34 -0400 Subject: [PATCH 20/27] Fix Adjustments API page linking to chainladder.workflow (#762) Closes #757 The Adjustments section header in docs/library/api.md was a copy-paste of the Workflow section. The :mod: link and .. automodule:: directive both pointed at chainladder.workflow, so the rendered RTD page for Adjustments hyperlinked to the Workflow module page. The four classes listed under Adjustments (BootstrapODPSample, BerquistSherman, Trend, ParallelogramOLF) all live in chainladder.adjustments, which is the correct target. Co-authored-by: Nick Kinney --- docs/library/api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/library/api.md b/docs/library/api.md index 0e412b07..d0985fab 100644 --- a/docs/library/api.md +++ b/docs/library/api.md @@ -107,9 +107,9 @@ Classes .. _adjustments_ref: -:mod:`chainladder.workflow`: Adjustments -======================================== -.. automodule:: chainladder.workflow +:mod:`chainladder.adjustments`: Adjustments +=========================================== +.. automodule:: chainladder.adjustments :no-members: :no-inherited-members: From f7ee445fa19235e8759b36c5d1ebb30d28d9f399 Mon Sep 17 00:00:00 2001 From: Kevin <74339271+SaguaroDev@users.noreply.github.com> Date: Sun, 10 May 2026 20:37:41 -0400 Subject: [PATCH 21/27] Annotate matplotlib dependency as required for TriangleDisplay.heatmap() (#761) Per maintainer feedback on #758/#761: matplotlib is needed at runtime for TriangleDisplay.heatmap() even though it isn't imported directly, so it must remain a core dependency. Add an inline comment so future contributors don't try to move it again. Co-authored-by: Nick Kinney --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 07231fe5..635e2372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "numba>0.54", "sparse>=0.9", "numpy", - "matplotlib", + "matplotlib", # Required for TriangleDisplay.heatmap() "dill", "patsy", ] From 02d148001f2b26cff2edbfcf299d92a7d6fd2484 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:45:00 +0000 Subject: [PATCH 22/27] Build(deps): Bump urllib3 from 2.6.3 to 2.7.0 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.7.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 7f73c005..31fa1b71 100644 --- a/uv.lock +++ b/uv.lock @@ -194,6 +194,8 @@ dependencies = [ { name = "dill" }, { name = "matplotlib" }, { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas" }, { name = "patsy" }, { name = "scikit-learn" }, @@ -283,6 +285,7 @@ requires-dist = [ { name = "nbmake", marker = "extra == 'test'" }, { name = "nbsphinx", marker = "extra == 'docs'" }, { name = "numba", specifier = ">0.54" }, + { name = "numpy" }, { name = "numpydoc", marker = "extra == 'docs'" }, { name = "pandas", specifier = ">=2.0,<3.0" }, { name = "parso", marker = "extra == 'docs'", specifier = ">=0.8" }, @@ -3580,11 +3583,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From 9df6a5ef8a397ebf37f14a6a0818c011e6132dff Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Mon, 11 May 2026 10:55:51 -0700 Subject: [PATCH 23/27] Updated date and some final merged PRs --- docs/library/releases.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/library/releases.md b/docs/library/releases.md index 69f1873d..e2be91e9 100644 --- a/docs/library/releases.md +++ b/docs/library/releases.md @@ -4,7 +4,7 @@ ### Version 0.9.2 -Release Date: May 8, 2026 +Release Date: May 11, 2026 **What's Changed** * Bump nbconvert from 7.16.6 to 7.17.0 by @dependabot[bot] in [#672](https://github.com/casact/chainladder-python/pull/672) @@ -44,12 +44,10 @@ Release Date: May 8, 2026 * Build(deps): Bump mistune from 3.1.4 to 3.2.1 by @dependabot[bot] in [#748](https://github.com/casact/chainladder-python/pull/748) * Update pyproject.toml - Add numpy ([#738](https://github.com/casact/chainladder-python/issues/738)) by @wendy-w2029 in [#750](https://github.com/casact/chainladder-python/pull/750) * Enhance docstrings and examples for improved clarity for many estimators by @kennethshsu in [#747](https://github.com/casact/chainladder-python/pull/747) -* Add `.github/CODEOWNERS` by @kennethshsu -* Limit pytest GitHub Actions workflow to run on pull requests (not every push) by @kennethshsu -* Clarify first-time vs ongoing `uv` setup in contributor docs; ignore local virtualenv doc build dirs in `.gitignore`; refine dates in the multi-trend example by @kennethshsu -* DOCS: Update VotingChainladder gallery example to agreed `testcode` / `testoutput` format by @genedan -* Refresh getting-started tutorial notebooks, `docs/library/contributing.md`, and `README.rst` by @henrydingliu -* Remove `/en/` locale segment from documentation URLs by @kennethshsu +* Fix Adjustments API page linking to chainladder.workflow (#757) by @SaguaroDev in [#762](https://github.com/casact/chainladder-python/pull/762) +* Annotate matplotlib dependency as required for TriangleDisplay.heatmap() (#758) by @SaguaroDev in [#761](https://github.com/casact/chainladder-python/pull/761) +* REFACTOR: Reorganize type hierarchy of sparse.py. Move array-level fu… by @genedan in [#739](https://github.com/casact/chainladder-python/pull/739) +* Build(deps): Bump urllib3 from 2.6.3 to 2.7.0 by @dependabot[bot] in [#767](https://github.com/casact/chainladder-python/pull/767) **New Contributors** * @priyam0k made their first contribution in [#722](https://github.com/casact/chainladder-python/pull/722) From a627a2c9940c3a04a853d94ce251b5776d1ee020 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Mon, 11 May 2026 11:57:34 -0700 Subject: [PATCH 24/27] Add more installation options --- docs/getting_started/install.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/getting_started/install.md b/docs/getting_started/install.md index a53ba6c4..b1374a2d 100644 --- a/docs/getting_started/install.md +++ b/docs/getting_started/install.md @@ -3,15 +3,18 @@ We strongly encourage users to install chainladder in a dedicated virtual environment. ## General Installation -Install the chainladder package using `pip`: +We recommend **uv** for installing `chainladder`, but you can use any of the managers below: [![](https://pepy.tech/badge/chainladder)](https://pepy.tech/project/chainladder) -Installing `chainladder` using `pip`: - -`pip install chainladder` +| Manager | Command | Source | +|:---|:---|:---| +| uv (recommended) | `uv add chainladder` | PyPI | +| pip | `pip install chainladder` | PyPI | +| pixi | `pixi add chainladder` | conda-forge | +| conda | `conda install -c conda-forge chainladder` | conda-forge | Alternatively, if you have git and want to enjoy unreleased features, you can install directly from `Github`: From c2c748ce09c006c87eea6f29d9d69b8e0b2a9489 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Mon, 11 May 2026 12:10:21 -0700 Subject: [PATCH 25/27] Added uv run in the pytest --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f359f621..749641af 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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`) From fca98e6be051f93f6492f28f384fd81d502ad112 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Mon, 11 May 2026 14:12:25 -0700 Subject: [PATCH 26/27] Added more tests --- chainladder/utils/tests/test_utilities.py | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 19ad491f..472aad63 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -142,6 +142,35 @@ def test_policy_length(): == [1.129333, 1.013023, 0.994975, 1, 1] ).all() + rate_history = pd.DataFrame( + { + "EffDate": ["2010-07-01", "2011-10-01", "2012-04-01"], + "RateChange": [0.35, 0.149, -0.095], + } + ) + data = pd.DataFrame( + {"Year": [2010, 2011, 2012, 2013, 2014], "EarnedPremium": [10_000] * 5} + ) + prem_tri = cl.Triangle(data, origin="Year", columns="EarnedPremium", cumulative=True) + + prem_tri = cl.ParallelogramOLF( + rate_history, + change_col="RateChange", + date_col="EffDate", + policy_length=12, + approximation_grain="M", + ).fit_transform(prem_tri) + assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.344949, 1.069526, 0.966045, 0.996730, 1]).all() + + prem_tri = cl.ParallelogramOLF( + rate_history, + change_col="RateChange", + date_col="EffDate", + policy_length=6, + approximation_grain="M", + ).fit_transform(prem_tri) + assert (np.round(prem_tri.olf_.values.flatten(),6) == [1.290842, 1.030251, 0.958285, 1, 1]).all() + def test_triangle_json_io(clrd): xp = clrd.get_array_module() From 57ddc61a72b4e641ba00b8916ed13ae3f83fe314 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Mon, 11 May 2026 16:53:03 -0700 Subject: [PATCH 27/27] clarification --- chainladder/adjustments/parallelogram.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/chainladder/adjustments/parallelogram.py b/chainladder/adjustments/parallelogram.py index ddb90bee..7c225019 100644 --- a/chainladder/adjustments/parallelogram.py +++ b/chainladder/adjustments/parallelogram.py @@ -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 @@ -21,10 +21,13 @@ 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: