From 4419889733c85b8c877e668e6f334f4fb3a21481 Mon Sep 17 00:00:00 2001 From: priyam0k Date: Sat, 5 Sep 2026 21:49:17 +0530 Subject: [PATCH 1/6] feat: deprecate odims and ddims in favor of origin and development (#1012, #1013) - Encapsulate internal date dimensions into _odims and _ddims - Provide backward-compatible odims and ddims properties emitting FutureWarning - Migrate legacy pickled instances with odims/ddims in Triangle.__setstate__ - Decouple internal usages across core, estimators, tails, and tests - Address ruff linting and formatting on touched files Closes #1012, closes #1013, refs #1216, refs #601 --- chainladder/adjustments/bootstrap.py | 2 +- chainladder/core/base.py | 6 +- chainladder/core/common.py | 7 +- chainladder/core/correlation.py | 4 +- chainladder/core/dunders.py | 38 ++--- chainladder/core/pandas.py | 20 +-- chainladder/core/slice.py | 15 +- chainladder/core/tests/test_arithmetic.py | 2 +- chainladder/core/tests/test_grain.py | 6 +- chainladder/core/tests/test_triangle.py | 34 ++++- chainladder/core/triangle.py | 137 +++++++++++++------ chainladder/core/typing.py | 4 +- chainladder/development/clark.py | 22 +-- chainladder/development/constant.py | 16 +-- chainladder/development/development.py | 2 +- chainladder/development/incremental.py | 4 +- chainladder/development/munich.py | 18 +-- chainladder/development/outstanding.py | 10 +- chainladder/methods/base.py | 6 +- chainladder/methods/benktander.py | 2 +- chainladder/methods/mack.py | 12 +- chainladder/tails/base.py | 8 +- chainladder/tails/bondy.py | 18 +-- chainladder/tails/clark.py | 8 +- chainladder/tails/constant.py | 4 +- chainladder/tails/curve.py | 4 +- chainladder/tails/tests/rtest_exponential.py | 2 +- chainladder/tails/tests/test_exponential.py | 2 +- chainladder/utils/tests/test_utilities.py | 4 +- chainladder/utils/utility_functions.py | 20 +-- 30 files changed, 262 insertions(+), 175 deletions(-) diff --git a/chainladder/adjustments/bootstrap.py b/chainladder/adjustments/bootstrap.py index ae4921dda..cbad9df97 100644 --- a/chainladder/adjustments/bootstrap.py +++ b/chainladder/adjustments/bootstrap.py @@ -190,7 +190,7 @@ def fit(self, X, y=None, sample_weight=None): xp = X.get_array_module() if len(X) != 1: raise ValueError("Only single index triangles are supported") - if type(X.ddims) is not np.ndarray: + if type(X._ddims) is not np.ndarray: raise ValueError("Triangle must be expressed with development lags") obj = Development( n_periods=self.n_periods, diff --git a/chainladder/core/base.py b/chainladder/core/base.py index 4f30769a1..eda9aac28 100644 --- a/chainladder/core/base.py +++ b/chainladder/core/base.py @@ -692,12 +692,12 @@ def _auto_sparse(self) -> Triangle: @property def valuation(self): - ddims = self.ddims + ddims = self._ddims if self.is_val_tri: - out = pd.DataFrame(np.repeat(self.ddims.values[None], len(self.odims), 0)) + out = pd.DataFrame(np.repeat(self._ddims.values[None], len(self._odims), 0)) return pd.DatetimeIndex(out.unstack().values) ddim_arr = ddims - ddims[0] - origin = np.minimum(self.odims, np.datetime64(self.valuation_date)) + origin = np.minimum(self._odims, np.datetime64(self.valuation_date)) val_array = origin.astype("datetime64[M]") + np.timedelta64(ddims[0], "M") val_array = val_array.astype(__dt64_dtype__) - np.timedelta64(1, __dt64_unit__) val_array = val_array[:, None] diff --git a/chainladder/core/common.py b/chainladder/core/common.py index 8b73648b8..f56ce8413 100644 --- a/chainladder/core/common.py +++ b/chainladder/core/common.py @@ -54,10 +54,11 @@ def _get_full_triangle(X, ultimate, is_cumulative=True): emergence.values = emergence.values.cumprod(axis=3) - 1 # Shifting the CDFs by development age, and renaming the last column as 9999 - emergence.ddims = ( - emergence.ddims + {"Y": 12, "Q": 3, "S": 6, "M": 1}[emergence.development_grain] + emergence._ddims = ( + emergence._ddims + + {"Y": 12, "Q": 3, "S": 6, "M": 1}[emergence.development_grain] ) - emergence.ddims[-1] = 9999 + emergence._ddims[-1] = 9999 emergence.values = emergence.values / num_to_nan(emergence.values[..., -1:]) ld = X.incr_to_cum().latest_diagonal cum_run_off = ld + emergence * (ultimate - ld) diff --git a/chainladder/core/correlation.py b/chainladder/core/correlation.py index ed832c509..3164780b3 100644 --- a/chainladder/core/correlation.py +++ b/chainladder/core/correlation.py @@ -138,7 +138,7 @@ def __init__(self, triangle, p_critical: float = 0.5): # remove last column because it was not part of the comparison with m2 numerator.values = numerator.values[..., :-1] - numerator.ddims = numerator.ddims[:-1] + numerator._ddims = numerator._ddims[:-1] # n_dev_periods is the number of development periods in the triangle ("I" in the Mack 97). n_dev_periods = len(triangle.development) @@ -333,7 +333,7 @@ def p_z_lower(z: int, n: int, p: float = 0.5) -> float: z_critical = triangle.dev_to_val().sum("origin") z_critical = z_critical[z_critical.valuation > triangle.valuation.min()] * 0 z_critical.values = np.array(self.probs) < p_critical - z_critical.odims = triangle.odims[0:1] + z_critical._odims = triangle._odims[0:1] self.z_critical = z_critical self.z = self.z_critical.copy() self.z.values = z diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index da39ae1a6..395cee1a0 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -165,36 +165,36 @@ def _prep_origin_development(self, obj, other): (m == n) or (m == 1) or (n == 1) for m, n in zip(obj.shape[-2:][::-1], other.shape[-2:][::-1]) ) - if (len(obj.odims), len(obj.ddims)) == (len(other.odims), len(other.ddims)): - if np.all(obj.ddims != other.ddims) and len(obj.ddims) > 1: + if (len(obj._odims), len(obj._ddims)) == (len(other._odims), len(other._ddims)): + if np.all(obj._ddims != other._ddims) and len(obj._ddims) > 1: is_broadcastable = False - if np.all(obj.odims != other.odims) and len(obj.odims) > 1: + if np.all(obj._odims != other._odims) and len(obj._odims) > 1: is_broadcastable = False - if len(other.odims) == 1 and len(obj.odims) > 1: - other.odims = obj.odims + if len(other._odims) == 1 and len(obj._odims) > 1: + other._odims = obj._odims other.origin_grain = obj.origin_grain - elif len(obj.odims) == 1 and len(other.odims) > 1: - obj.odims = other.odims + elif len(obj._odims) == 1 and len(other._odims) > 1: + obj._odims = other._odims obj.origin_grain = other.origin_grain - if len(other.ddims) == 1 and len(obj.ddims) > 1: - other.ddims = obj.ddims + if len(other._ddims) == 1 and len(obj._ddims) > 1: + other._ddims = obj._ddims other.development_grain = obj.development_grain - elif len(obj.ddims) == 1 and len(other.ddims) > 1: - obj.ddims = other.ddims + elif len(obj._ddims) == 1 and len(other._ddims) > 1: + obj._ddims = other._ddims obj.development_grain = other.development_grain if not is_broadcastable: # If broadcasting doesn't work, union axes similar to pandas ddims = pd.concat( ( - pd.Series(obj.ddims, index=obj.ddims), - pd.Series(other.ddims, index=other.ddims), + pd.Series(obj._ddims, index=obj._ddims), + pd.Series(other._ddims, index=other._ddims), ), axis=1, ).sort_index() odims = pd.concat( ( - pd.Series(obj.odims, index=obj.odims), - pd.Series(other.odims, index=other.odims), + pd.Series(obj._odims, index=obj._odims), + pd.Series(other._odims, index=other._odims), ), axis=1, sort=False, @@ -240,11 +240,11 @@ def _prep_origin_development(self, obj, other): len(ddims), ) obj_arr.shape = (self.shape[0], self.shape[1], len(odims), len(ddims)) - obj.odims = np.array(odims.index) - if isinstance(obj.ddims, pd.DatetimeIndex): - obj.ddims = pd.DatetimeIndex(ddims.index) + obj._odims = np.array(odims.index) + if isinstance(obj._ddims, pd.DatetimeIndex): + obj._ddims = pd.DatetimeIndex(ddims.index) else: - obj.ddims = np.array(ddims.index) + obj._ddims = np.array(ddims.index) obj.values = obj_arr other.values = other_arr return obj, other diff --git a/chainladder/core/pandas.py b/chainladder/core/pandas.py index 061f47390..0e518f294 100644 --- a/chainladder/core/pandas.py +++ b/chainladder/core/pandas.py @@ -106,10 +106,10 @@ def to_frame( out["columns"] = obj.columns[values.coords[1]] missing_cols: list = list(set(self.columns) - set(out["columns"])) if origin_as_datetime: - out["origin"] = obj.odims[values.coords[2]] + out["origin"] = obj._odims[values.coords[2]] else: out["origin"] = obj.origin[values.coords[2]] - out["development"] = obj.ddims[values.coords[3]] + out["development"] = obj._ddims[values.coords[3]] out["values"] = values.data out: DataFrame = pd.pivot_table( out, index=obj.key_labels + ["origin", "development"], columns="columns" @@ -121,8 +121,8 @@ def to_frame( valuation_series = pd.DataFrame( obj.valuation.values.reshape(obj.shape[-2:], order="F"), - index=obj.odims if origin_as_datetime else obj.origin, - columns=obj.ddims, + index=obj._odims if origin_as_datetime else obj.origin, + columns=obj._ddims, ).unstack() valuation_series.name = "valuation" valuation: DataFrame = valuation_series.reset_index().rename( @@ -960,7 +960,7 @@ def drop( keep = self._validate_contiguous_drop( result.development, ax_labels, "development", errors ) - result = result._slice(keep, "ddims") + result = result._slice(keep, "_ddims") if result.is_val_tri: result.valuation_date = min( result.valuation.max(), result.valuation_date @@ -1313,11 +1313,11 @@ def agg_func( ) if axis == 1 and obj.values.shape[axis] == 1 and len(obj.columns) > 1: obj.columns = pd.Index([0], name="columns") - if axis == 2 and obj.values.shape[axis] == 1 and len(obj.odims) > 1: - obj.odims = obj.odims[0:1] + if axis == 2 and obj.values.shape[axis] == 1 and len(obj._odims) > 1: + obj._odims = obj._odims[0:1] # If axis is development, set the ddims to be the valuation date. - if axis == 3 and obj.values.shape[axis] == 1 and len(obj.ddims) > 1: - obj.ddims = pd.DatetimeIndex( + if axis == 3 and obj.values.shape[axis] == 1 and len(obj._ddims) > 1: + obj._ddims = pd.DatetimeIndex( [self.valuation_date], dtype=__dt64_dtype__, freq=None ) obj._set_slicers() @@ -1385,7 +1385,7 @@ def aggregate(i, obj, axis, v): obj.origin_grain = self.obj._get_grain(odims) split = obj.origin_grain.split("-") obj.origin_grain = {"2Q": "S"}.get(split[0], split[0]) - obj.odims = odims.values + obj._odims = odims.values obj._set_slicers() if auto_sparse: obj = obj._auto_sparse() diff --git a/chainladder/core/slice.py b/chainladder/core/slice.py index 440870e2c..b67ca62a5 100644 --- a/chainladder/core/slice.py +++ b/chainladder/core/slice.py @@ -66,7 +66,7 @@ def get_idx(self, idx: tuple[_AxisKey, _AxisKey, _AxisKey, _AxisKey]) -> Triangl # Set the new dimension values. obj.index = obj.index.iloc[i_idx].reset_index(drop=True) obj.columns = obj.columns[c_idx] - obj.odims, obj.ddims = obj.odims[o_idx], obj.ddims[d_idx] + obj._odims, obj._ddims = obj._odims[o_idx], obj._ddims[d_idx] obj.valuation_date = cast( pd.Timestamp, np.minimum(obj.valuation.max(), obj.valuation_date) ) @@ -788,14 +788,14 @@ def __getitem__( # Case development axis. if isinstance(key, pd.Series) and key.name == "development": - return self._slice(key, "ddims") + return self._slice(key, "_ddims") # Case ndarray, could be valuation or origin. if isinstance(key, np.ndarray): # Case valuation, inferred by size of ndarray obtained by filtering on valuation date. if len(key) == np.prod(self.shape[-2:]) and self.shape[-1] > 1: return self._slice_valuation(key) # Otherwise, assume case origin. - return self._slice(key, "odims") + return self._slice(key, "_odims") # Case index. if isinstance(key, pd.Series): if pd.api.types.is_bool_dtype(key): @@ -990,7 +990,7 @@ def _slice_valuation(self: TriangleProtocol, key: np.ndarray) -> Triangle: def _slice( self: TriangleProtocol, key: pd.Series | np.ndarray, - axis: Literal["ddims", "odims"], + axis: Literal["ddims", "odims", "_ddims", "_odims"], ) -> Triangle: """ Private method for handling of origin/development slicing. @@ -999,16 +999,17 @@ def _slice( ---------- key: pd.Series | np.ndarray Array specifying the slice to extract. - axis: Literal['ddims', 'odims'] + axis: Literal['ddims', 'odims', '_ddims', '_odims'] The axis to which the slice would apply, `ddims` for development, `odims` for origin. """ # Update the axis, then the values. obj = self.copy() - setattr(obj, axis, getattr(obj, axis)[key]) + attr = f"_{axis}" if not axis.startswith("_") else axis + setattr(obj, attr, getattr(obj, attr)[key]) # noinspection PyProtectedMember slicer = ..., _LocBase._contig_slice(np.arange(len(key))[key]) - if axis == "odims": + if axis in ("odims", "_odims"): slicer = tuple(list(slicer) + [slice(None)]) obj.values = obj.values[slicer] return obj diff --git a/chainladder/core/tests/test_arithmetic.py b/chainladder/core/tests/test_arithmetic.py index 60bab9a3c..d29c74818 100644 --- a/chainladder/core/tests/test_arithmetic.py +++ b/chainladder/core/tests/test_arithmetic.py @@ -114,7 +114,7 @@ def test_arithmetic_union_val_tri(raa: Triangle) -> None: a = val_raa[val_raa.valuation < "1987"] b = val_raa[val_raa.valuation >= "1987"] result = a + b - assert isinstance(result.ddims, pd.DatetimeIndex) + assert isinstance(result._ddims, pd.DatetimeIndex) assert result.shape == val_raa.shape assert result == val_raa diff --git a/chainladder/core/tests/test_grain.py b/chainladder/core/tests/test_grain.py index df395c9f3..78a8e7b8a 100644 --- a/chainladder/core/tests/test_grain.py +++ b/chainladder/core/tests/test_grain.py @@ -98,12 +98,12 @@ def test_annual_trailing(prism): # (limit data to November) tri = tri[tri.valuation < tri.valuation_date].incr_to_cum() tri = tri.grain("OQDQ", trailing=True).grain("OYDY") - assert np.all(tri.ddims[:4] == np.array([12, 24, 36, 48])) + assert np.all(tri.development[:4] == np.array([12, 24, 36, 48])) def test_development_age(raa: Triangle) -> None: - assert (raa.ddims == list(range(12, 121, 12))).all() + assert (raa.development == list(range(12, 121, 12))).all() def test_development_age_quarterly(qtr: Triangle) -> None: - assert (qtr.ddims == list(range(3, 136, 3))).all() + assert (qtr.development == list(range(3, 136, 3))).all() diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 542579f39..bea17b628 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -331,7 +331,7 @@ def test_origin_and_value_setters(raa): assert np.all(( np.all(raa2.origin == raa.origin), np.all(raa2.development == raa.development), - np.all(raa2.odims == raa.odims), + np.all(raa2._odims == raa._odims), np.all(raa2.columns == raa.columns), )) @@ -667,17 +667,43 @@ def test_series_indexing(raa): assert sliced.index.iloc[0]["Company"] == "A" +def test_odims_deprecation_warning(raa): + """Accessing or setting odims should emit a FutureWarning.""" + with pytest.warns(FutureWarning, match="'odims' attribute is deprecated"): + o = raa.odims + assert np.all(o == raa._odims) + + raa2 = raa.copy() + with pytest.warns(FutureWarning, match="'odims' attribute is deprecated"): + raa2.odims = raa._odims + assert np.all(raa2._odims == raa._odims) + + +def test_ddims_deprecation_warning(raa): + """Accessing or setting ddims should emit a FutureWarning.""" + with pytest.warns(FutureWarning, match="'ddims' attribute is deprecated"): + d = raa.ddims + assert np.all(d == raa._ddims) + + raa2 = raa.copy() + with pytest.warns(FutureWarning, match="'ddims' attribute is deprecated"): + raa2.ddims = raa._ddims + assert np.all(raa2._ddims == raa._ddims) + + def test_legacy_pickle_compatibility(raa): - """Pickles saved prior to kdims/vdims migration should unpickle cleanly with warning.""" + """Pickles saved prior to kdims/vdims/odims/ddims migration should unpickle cleanly with warning.""" import pickle - # 1. Simulate oldest serialized Triangle containing kdims and vdims + # 1. Simulate oldest serialized Triangle containing kdims, vdims, odims, and ddims state = raa.__dict__.copy() state.pop("_axes", None) state.pop("_index", None) state.pop("_columns", None) state["kdims"] = raa.index.values state["vdims"] = raa.columns.values + state["odims"] = state.pop("_odims") + state["ddims"] = state.pop("_ddims") restored = cl.Triangle.__new__(cl.Triangle) with pytest.warns( @@ -689,6 +715,8 @@ def test_legacy_pickle_compatibility(raa): assert isinstance(restored.index, pd.DataFrame) assert restored.index.equals(raa.index) assert list(restored.columns) == list(raa.columns) + assert restored.origin.equals(raa.origin) + assert (restored.development == raa.development).all() assert restored.loc["Total"].shape == raa.loc["Total"].shape assert restored == raa assert 0 in restored._axes diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 850e47185..f257a0db2 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -638,16 +638,16 @@ def __init__( data_agg[index[0]] = "Total" index_idx: np.ndarray - self.odims: np.ndarray + self._odims: np.ndarray orig_idx: np.ndarray - self.ddims: ArrayLike + self._ddims: ArrayLike dev_idx: np.ndarray index_values, index_idx = self._factorize_index(data_agg, index) self.index = pd.DataFrame(list(index_values), columns=index) self.columns = columns - self.odims, orig_idx = self._set_odims(data_agg, date_axes) - self.ddims, dev_idx = self._set_ddims(data_agg, date_axes) + self._odims, orig_idx = self._set_odims(data_agg, date_axes) + self._ddims, dev_idx = self._set_ddims(data_agg, date_axes) # Set remaining triangle properties. val_date: Timestamp = data_agg["__development__"].max() @@ -687,7 +687,7 @@ def __init__( check_origin: np.ndarray = ( pd .period_range( - start=self.odims.min(), + start=self._odims.min(), end=self.valuation_date, freq=self.origin_grain.replace("S", "2Q") + ("" if self.origin_grain == "M" else "-" + self.origin_close), @@ -697,11 +697,11 @@ def __init__( ) if ( - len(check_origin) != len(self.odims) + len(check_origin) != len(self._odims) and pd.to_datetime(options.ULT_VAL) != self.valuation_date and not self.is_pattern ): - self.odims: np.ndarray = check_origin + self._odims: np.ndarray = check_origin # Set the Triangle values. coords: np.ndarray @@ -728,8 +728,8 @@ def __init__( shape=( len(self.index), len(self.columns), - len(self.odims), - len(self.ddims), + len(self._odims), + len(self._ddims), ), ) ), @@ -746,14 +746,14 @@ def __init__( # Deal with special properties if self.is_pattern: obj = self.dropna() - self.odims = obj.odims - self.ddims = obj.ddims + self._odims = obj._odims + self._ddims = obj._ddims self.values = obj.values if ult: - obj = concat((self.dev_to_val().iloc[..., : len(ult.odims), :], ult), -1) + obj = concat((self.dev_to_val().iloc[..., : len(ult._odims), :], ult), -1) obj = obj.val_to_dev() - self.odims = obj.odims - self.ddims = obj.ddims + self._odims = obj._odims + self._ddims = obj._ddims self.values = obj.values self.valuation_date = pd.Timestamp(options.ULT_VAL) @@ -793,7 +793,7 @@ def _split_ult( index=index, cumulative=cumulative, ) - ult.ddims = pd.DatetimeIndex([options.ULT_VAL]) + ult._ddims = pd.DatetimeIndex([options.ULT_VAL]) data = data[data[valuation[0]] != options.ULT_VAL] return data, ult @@ -865,7 +865,7 @@ def origin(self): True """ - if self.is_pattern and len(self.odims) == 1: + if self.is_pattern and len(self._odims) == 1: return pd.Series(["(All)"]) else: freq = { @@ -873,7 +873,7 @@ def origin(self): "H": "2Q", }.get(self.origin_grain, self.origin_grain) freq = freq if freq == "M" else freq + "-" + self.origin_close - return pd.DatetimeIndex(self.odims, name="origin").to_period(freq=freq) + return pd.DatetimeIndex(self._odims, name="origin").to_period(freq=freq) @origin.setter def origin(self, value) -> None: @@ -883,7 +883,27 @@ def origin(self, value) -> None: }.get(self.origin_grain, self.origin_grain) freq = freq if freq == "M" else freq + "-" + self.origin_close value = pd.PeriodIndex(list(value), freq=freq) - self.odims = value.to_timestamp().values + self._odims = value.to_timestamp().values + + @property + def odims(self): + warnings.warn( + "The 'odims' attribute is deprecated and will be removed in a future release. " + "Use 'Triangle.origin' instead.", + FutureWarning, + stacklevel=2, + ) + return self._odims + + @odims.setter + def odims(self, value): + warnings.warn( + "The 'odims' attribute is deprecated and will be removed in a future release. " + "Use 'Triangle.origin' instead.", + FutureWarning, + stacklevel=2, + ) + self._odims = value @property def development(self): @@ -939,7 +959,7 @@ def development(self): ['12-24', '24-36', '36-48', '48-60', '60-72', '72-84'] """ - ddims = self.ddims.copy() + ddims = self._ddims.copy() if self.is_val_tri: formats = {"Y": "%Y", "S": "%YQ%q", "Q": "%YQ%q", "M": "%Y-%m"} ddims = ddims.to_period( @@ -961,7 +981,27 @@ def development(self): @development.setter def development(self, value): self._len_check(self.development, value) - self.ddims = np.array([value] if type(value) is str else value) + self._ddims = np.array([value] if type(value) is str else value) + + @property + def ddims(self): + warnings.warn( + "The 'ddims' attribute is deprecated and will be removed in a future release. " + "Use 'Triangle.development' instead.", + FutureWarning, + stacklevel=2, + ) + return self._ddims + + @ddims.setter + def ddims(self, value): + warnings.warn( + "The 'ddims' attribute is deprecated and will be removed in a future release. " + "Use 'Triangle.development' instead.", + FutureWarning, + stacklevel=2, + ) + self._ddims = value @property def style(self) -> Styler: @@ -1044,7 +1084,7 @@ def is_val_tri(self): True """ - return isinstance(self.ddims, pd.DatetimeIndex) + return isinstance(self._ddims, pd.DatetimeIndex) @property def is_full(self) -> bool: @@ -1333,7 +1373,7 @@ def link_ratio(self) -> Triangle: if not obj.is_full: obj = obj[obj.valuation < obj.valuation_date] if hasattr(obj, "w_"): - w_ = obj.w_[..., : len(obj.odims), :] + w_ = obj.w_[..., : len(obj._odims), :] obj = obj * w_ if obj.shape == w_.shape else obj obj.is_pattern = True obj.is_cumulative = False @@ -1628,7 +1668,7 @@ def _val_dev(self, sign, inplace=False): ddims = obj.valuation[obj.valuation <= obj.valuation_date] ddims = len(ddims.drop_duplicates()) if ddims == 1 and sign == -1: - ddims = len(obj.odims) + ddims = len(obj._odims) if obj.values.density > 0: if obj.values.coords[-1].min() < 0: obj.values.coords[-1] = obj.values.coords[-1] - min( @@ -1719,10 +1759,10 @@ def dev_to_val(self, inplace=False): obj = self obj = obj._val_dev(1, inplace) ddims = obj.valuation[obj.valuation <= obj.valuation_date] - obj.ddims = ddims.drop_duplicates().sort_values() + obj._ddims = ddims.drop_duplicates().sort_values() if self.is_full: if self.is_ultimate: - ultimate.ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) + ultimate._ddims = pd.DatetimeIndex(ultimate.valuation[0:1]) obj = concat((obj, ultimate), -1) if is_cumulative: obj = obj.incr_to_cum(inplace=inplace) @@ -1776,18 +1816,18 @@ def val_to_dev(self, inplace=False): return self.copy() if self.is_ultimate and self.shape[-1] > 1: ultimate = self.iloc[..., -1:] - ultimate.ddims = np.array([9999]) + ultimate._ddims = np.array([9999]) obj = self.iloc[..., :-1]._val_dev(-1, inplace) else: obj = self.copy()._val_dev(-1, inplace) val_0 = obj.valuation[0] - if self.ddims.shape[-1] == 1 and self.ddims[0] == self.valuation_date: - origin_0 = pd.to_datetime(obj.odims[-1]) + if self._ddims.shape[-1] == 1 and self._ddims[0] == self.valuation_date: + origin_0 = pd.to_datetime(obj._odims[-1]) else: - origin_0 = pd.to_datetime(obj.odims[0]) + origin_0 = pd.to_datetime(obj._odims[0]) lag_0 = (val_0.year - origin_0.year) * 12 + val_0.month - origin_0.month + 1 scale = self._dstep()["M"][obj.development_grain] - obj.ddims = np.arange(obj.values.shape[-1]) * scale + lag_0 + obj._ddims = np.arange(obj.values.shape[-1]) * scale + lag_0 prune = obj[obj.origin == obj.origin.max()] if self.is_ultimate and self.shape[-1] > 1: obj = obj.iloc[..., : (prune.valuation <= prune.valuation_date).sum()] @@ -1976,11 +2016,11 @@ def grain(self, grain="", trailing=False, inplace=False): if dgrain_old == "S": d_start = d_start + pd.DateOffset(months=-3) - if len(obj.ddims) > 1 and obj.origin.to_timestamp(how="s")[0] != d_start: + if len(obj._ddims) > 1 and obj.origin.to_timestamp(how="s")[0] != d_start: addl_ts = ( pd .period_range( - obj.odims[0], + obj._odims[0], obj.valuation[0], freq=dgrain_old.replace("S", "2Q"), )[:-1] @@ -1988,7 +2028,7 @@ def grain(self, grain="", trailing=False, inplace=False): .values ) addl = obj.iloc[..., -len(addl_ts) :] * 0 - addl.ddims = addl_ts + addl._ddims = addl_ts obj = concat((addl, obj), axis=-1) obj.values = num_to_nan(obj.values) @@ -2001,10 +2041,10 @@ def grain(self, grain="", trailing=False, inplace=False): if obj.is_cumulative: obj = obj.iloc[..., d] else: - ddims = obj.ddims[d] + ddims = obj._ddims[d] d2 = [d[0]] * (d[0] + 1) + list(np.repeat(np.array(d[1:]), step)) obj = obj.groupby(d2, axis=3).sum() - obj.ddims = ddims + obj._ddims = ddims obj.development_grain = dgrain_new @@ -2168,9 +2208,18 @@ def copy(self): return X def __setstate__(self, state: dict) -> None: - """Migrate legacy pickled instances with 'kdims'/'_kdims' to '_index' and 'vdims'/'_vdims'/'_columns' to '_axes'.""" + """Migrate legacy pickled instances with 'kdims'/'_kdims' to '_axes[0]', 'vdims'/'_vdims'/'_columns' to '_axes[1]', and 'odims'/'ddims' to private equivalents.""" has_legacy_keys = any( - k in state for k in ("kdims", "_kdims", "vdims", "_vdims", "_columns") + k in state + for k in ( + "kdims", + "_kdims", + "vdims", + "_vdims", + "_columns", + "odims", + "ddims", + ) ) if has_legacy_keys: warnings.warn( @@ -2213,6 +2262,12 @@ def __setstate__(self, state: dict) -> None: state["_axes"][1] = pd.Index(raw_cols, name="columns") else: state["_axes"][1] = pd.Index(["values"], name="columns") + for old_key, new_key in [ + ("odims", "_odims"), + ("ddims", "_ddims"), + ]: + if old_key in state and new_key not in state: + state[new_key] = state.pop(old_key) self.__dict__.update(state) def development_correlation(self, p_critical=0.5): @@ -2494,15 +2549,15 @@ def sort_axis(self, axis): obj.values = obj.values[:, list(sort), ...] obj.columns = self.columns[list(sort)] if axis == 2: - sort = pd.Series(self.odims).sort_values().index - if np.any(sort != pd.Series(self.odims).index): + sort = pd.Series(self._odims).sort_values().index + if np.any(sort != pd.Series(self._odims).index): obj.values = obj.values[..., list(sort), :] - obj.odims = obj.odims[list(sort)] + obj._odims = obj._odims[list(sort)] if axis == 3: sort = self.development.sort_values().index if np.any(sort != self.development.index): obj.values = obj.values[..., list(sort)] - obj.ddims = obj.ddims[list(sort)] + obj._ddims = obj._ddims[list(sort)] return obj def reindex(self, columns=None, fill_value=np.nan): diff --git a/chainladder/core/typing.py b/chainladder/core/typing.py index 29496b61c..e37a5d301 100644 --- a/chainladder/core/typing.py +++ b/chainladder/core/typing.py @@ -122,7 +122,9 @@ def drop( def val_to_dev(self) -> Triangle: ... def _repr_format(self, origin_as_datetime: bool = False) -> pd.DataFrame: ... def _slice( - self, key: pd.Series | np.ndarray, axis: Literal["ddims", "odims"] + self, + key: pd.Series | np.ndarray, + axis: Literal["ddims", "odims", "_ddims", "_odims"], ) -> Triangle: ... def _slice_valuation(self, key: np.ndarray) -> Triangle: ... def to_frame( diff --git a/chainladder/development/clark.py b/chainladder/development/clark.py index f254a6490..86fcb411e 100644 --- a/chainladder/development/clark.py +++ b/chainladder/development/clark.py @@ -213,9 +213,9 @@ def G_(self, age): # noqa: N802 if isinstance(age, list): age = xp.array([age]).astype("float64") obj = self.incremental_act_.copy() - obj.odims = obj.odims[0:1] + obj._odims = obj._odims[0:1] obj.values = 1 / self._G(age) - obj.ddims = age + obj._ddims = age return obj def fit(self, X, y=None, sample_weight=None): @@ -256,13 +256,13 @@ def fit(self, X, y=None, sample_weight=None): age_offset = {"Y": 6.0, "S": 3, "Q": 1.5, "M": 0.5}[X.development_grain] age_interval = {"Y": 12.0, "S": 6.0, "Q": 3.0, "M": 1.0}[X.development_grain] nans = nan_triangle.reshape(1, -1)[0] - age = xp.tile(X.ddims, len(X.odims))[~xp.isnan(nans)].astype("float64") + age = xp.tile(X._ddims, len(X._odims))[~xp.isnan(nans)].astype("float64") age_end = age - age_offset age_start = xp.maximum(age_end - age_interval, 0).astype("float64") - origin = np.repeat(X.odims, len(X.ddims))[~xp.isnan(nans)] + origin = np.repeat(X._odims, len(X._ddims))[~xp.isnan(nans)] latest_diagonal = obj[X.valuation == X.valuation_date].sum("origin") - latest_age = latest_diagonal.ddims.astype("float64") - latest_origin = X.odims + latest_age = latest_diagonal._ddims.astype("float64") + latest_origin = X._odims latest_diagonal = latest_diagonal.values[..., 0, :] index = xp.argsort(latest_origin) @@ -321,8 +321,8 @@ def ldf(age): latest_age - age_offset, theta=params[..., 1:2], omega=params[..., 0:1] ) obj.values = cdf[..., :-1] / cdf[..., 1:] - obj.ddims = X.link_ratio.ddims - obj.odims = obj.odims[0:1] + obj._ddims = X.link_ratio._ddims + obj._odims = obj._odims[0:1] obj.is_pattern = True obj.is_cumulative = False obj._set_slicers() @@ -346,8 +346,8 @@ def ldf(age): self.incremental_fits_.array_backend = "numpy" self.incremental_fits_.values = ( ( - 1 / self._G(X.ddims - age_offset) - - 1 / self._G(xp.maximum(X.ddims - age_offset - age_interval, 0)) + 1 / self._G(X._ddims - age_offset) + - 1 / self._G(xp.maximum(X._ddims - age_offset - age_interval, 0)) ) * ultimate_[..., ::-1] * nan_triangle @@ -403,7 +403,7 @@ def scale_(self): ) df = xp.nansum(self.incremental_fits_.nan_triangle) - 2 if self.method_ == "ldf": - df = df - len(self.incremental_fits_.odims) + df = df - len(self.incremental_fits_._odims) else: df = df - 1 if scale.shape != (): diff --git a/chainladder/development/constant.py b/chainladder/development/constant.py index 64ea8cfda..4956c862e 100644 --- a/chainladder/development/constant.py +++ b/chainladder/development/constant.py @@ -157,23 +157,23 @@ def fit(self, X, y=None, sample_weight=None): pattern_ddims = sorted(self.patterns.keys()) # pattern supplied is much shorter than the triangle - if pattern_length < len(obj.ddims) - 1: + if pattern_length < len(obj._ddims) - 1: obj = obj.iloc[..., 0, :-1] * 0 + 1 # pattern supplied is exactly one short of the triangle - elif pattern_length == len(obj.ddims) - 1: + elif pattern_length == len(obj._ddims) - 1: obj = obj.iloc[..., 0, :-1] * 0 + 1 # pattern supplied is exactly the same length as the triangle - elif pattern_length == len(obj.ddims): + elif pattern_length == len(obj._ddims): obj = obj.iloc[..., 0, :] * 0 + 1 # pattern supplied is longer than the triangle else: obj = obj.iloc[..., 0, :] * 0 + 1 - extra = len(pattern_ddims) - len(obj.ddims) + extra = len(pattern_ddims) - len(obj._ddims) if extra > 0: tail = xp.ones(obj.shape)[..., -1:] tail = xp.repeat(tail, extra, -1) obj.values = xp.concatenate((obj.values, tail), -1) - obj.ddims = np.array(pattern_ddims) + obj._ddims = np.array(pattern_ddims) obj._set_slicers() if callable(self.patterns): @@ -182,7 +182,7 @@ def fit(self, X, y=None, sample_weight=None): ldf = ( pd .concat(ldf.apply(pd.DataFrame, index=[0]).values, axis=0) - .fillna(1)[obj.ddims] + .fillna(1)[obj._ddims] .values ) ldf = xp.array(ldf[:, None, None, :]) @@ -191,14 +191,14 @@ def fit(self, X, y=None, sample_weight=None): ldf = ( pd .concat(ldf.apply(pd.DataFrame, index=[0]).values, axis=0) - .fillna(1)[obj.ddims] + .fillna(1)[obj._ddims] .values ) ldf = xp.array(ldf[None, :, None, :]) else: raise ValueError("callable axis needs to be 0 or 1") else: - ldf = xp.array([self.patterns.get(item, 1.0) for item in obj.ddims]) + ldf = xp.array([self.patterns.get(item, 1.0) for item in obj._ddims]) ldf = ldf[None, None, None, :] if self.style == "cdf": diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 540ba1b51..84c003016 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -490,7 +490,7 @@ def _param_property(self, X, params, idx): obj = X[X.origin == X.origin.min()] xp = X.get_array_module() obj.values = xp.ones(obj.shape)[..., :-1] * params[..., idx : idx + 1, :] - obj.ddims = X.link_ratio.ddims + obj._ddims = X.link_ratio._ddims obj.valuation_date = pd.to_datetime(options.ULT_VAL) obj.is_pattern = True obj.is_cumulative = False diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index 0d7195af3..d5c007ee9 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -211,7 +211,7 @@ def fit(self, X, y=None, sample_weight=None): Returns the instance itself. """ # check dev lag - if not isinstance(X.ddims, np.ndarray): + if not isinstance(X._ddims, np.ndarray): raise ValueError("Triangle must be expressed with development lags") # convert to numpy if X.array_backend == "sparse": @@ -263,7 +263,7 @@ def fit(self, X, y=None, sample_weight=None): self.zeta_ = self._param_property(x, self.params_.slope_[..., 0][..., None, :]) # to consolidate under full_triangle_ - y_ = xp.repeat(self.zeta_.values, len(x.odims), -2) + y_ = xp.repeat(self.zeta_.values, len(x._odims), -2) obj = x.copy() keeps = ( 1 diff --git a/chainladder/development/munich.py b/chainladder/development/munich.py index cad2efd34..15f7a0eb0 100644 --- a/chainladder/development/munich.py +++ b/chainladder/development/munich.py @@ -354,8 +354,8 @@ def _get_mcl_cdf(self, X, munich_full_triangle_): """ xp = X.get_array_module() obj = X.cdf_.copy() - obj.values = xp.repeat(obj.values, len(X.odims), 2) - obj.odims = X.odims + obj.values = xp.repeat(obj.values, len(X._odims), 2) + obj._odims = X._odims if type(self.paid_to_incurred) is tuple: p_to_i = [self.paid_to_incurred] else: @@ -380,7 +380,7 @@ def _set_ldf(self, X, cdf): ldf_tri = ldf_tri[..., :-1] / ldf_tri[..., 1:] obj = cdf.copy() obj.values = ldf_tri - obj.ddims = X.link_ratio.ddims + obj._ddims = X.link_ratio._ddims obj.is_pattern = True obj.is_cumulative = False obj._set_slicers() @@ -400,8 +400,8 @@ def _reshape(self, measure): @property def lambda_(self): obj = self.ldf_.copy() - obj.odims = obj.odims[0:1] - obj.ddims = obj.ddims[0:1] + obj._odims = obj._odims[0:1] + obj._ddims = obj._ddims[0:1] obj.values = self._reshape("lambda_coef_") return obj.to_frame(origin_as_datetime=False) @@ -421,13 +421,13 @@ def basic_sigma_(self): def resids_(self): obj = self.ldf_.copy() obj.values = self._reshape("residual_") - obj.odims = self.cdf_.odims[: obj.values.shape[2]] + obj._odims = self.cdf_._odims[: obj.values.shape[2]] return obj @property def q_(self): obj = self.rho_.copy() - obj.odims = self.cdf_.odims + obj._odims = self.cdf_._odims obj.values = self._reshape("q_f_") return obj @@ -437,6 +437,6 @@ def q_resids_(self): obj.values = self._reshape("q_resid_")[ ..., : self.residual_.shape[-2], : self.residual_.shape[-1] ] - obj.odims = obj.odims[: obj.values.shape[2]] - obj.ddims = obj.ddims[: obj.values.shape[3]] + obj._odims = obj._odims[: obj.values.shape[2]] + obj._ddims = obj._ddims[: obj.values.shape[3]] return obj diff --git a/chainladder/development/outstanding.py b/chainladder/development/outstanding.py index 3cc73cc8a..98fc515f3 100644 --- a/chainladder/development/outstanding.py +++ b/chainladder/development/outstanding.py @@ -179,13 +179,13 @@ def _set_ldf(self, X): xp = case_ldf_.get_array_module() # Broadcast triangle shape case_ldf_ = case_ldf_ * case_tri.latest_diagonal / case_tri.latest_diagonal - case_ldf_.odims = case_tri.odims + case_ldf_._odims = case_tri._odims case_ldf_.is_pattern = False case_ldf_.values = xp.concatenate( (xp.ones(list(case_ldf_.shape[:-1]) + [1]), case_ldf_.values), axis=-1 ) - case_ldf_.ddims = case_tri.ddims + case_ldf_._ddims = case_tri._ddims case_ldf_.valuation_date = case_ldf_.valuation.max() case_ldf_ = case_ldf_.dev_to_val().set_backend(self.case_ldf_.array_backend) @@ -205,7 +205,7 @@ def _set_ldf(self, X): case_tri = ( (case_ldf_ * nans.values * case_tri.latest_diagonal.values) .val_to_dev() - .iloc[..., : len(case_tri.ddims)] + .iloc[..., : len(case_tri._ddims)] ) ld = ( case_tri[case_tri.valuation == X.valuation_date] @@ -217,7 +217,7 @@ def _set_ldf(self, X): self.paid_ldf_ * ld ).values paid = case_tri.iloc[..., :-1] * patterns - paid.ddims = case_tri.ddims[1:] + paid._ddims = case_tri._ddims[1:] paid.valuation_date = pd.Timestamp(options.ULT_VAL) # Create a full triangle of incurrds to support a multiplicative LDF paid = (paid_tri.cum_to_incr() + paid).incr_to_cum() @@ -237,7 +237,7 @@ def _set_ldf(self, X): # Convert the paid/incurred to multiplicative LDF dev = (dev.iloc[..., -1] / dev).iloc[..., :-1] dev.valuation_date = pd.Timestamp(options.ULT_VAL) - dev.ddims = X.link_ratio.ddims + dev._ddims = X.link_ratio._ddims dev.is_pattern = True dev.is_cumulative = True diff --git a/chainladder/methods/base.py b/chainladder/methods/base.py index 543d8c815..3cd45a0c5 100644 --- a/chainladder/methods/base.py +++ b/chainladder/methods/base.py @@ -24,7 +24,7 @@ def validate_X(self, X): # noqa: N802 obj = X.copy() if "ldf_" not in obj: obj = Development().fit_transform(obj) - if len(obj.ddims) - len(obj.ldf_.ddims) == 1: + if len(obj._ddims) - len(obj.ldf_._ddims) == 1: obj = TailConstant().fit_transform(obj) return obj.val_to_dev() @@ -39,7 +39,7 @@ def _set_ult_attr(self, ultimate): xp = ultimate.get_array_module() if ultimate.array_backend != "sparse": ultimate.values[~xp.isfinite(ultimate.values)] = xp.nan - ultimate.ddims = pd.DatetimeIndex([options.ULT_VAL]) + ultimate._ddims = pd.DatetimeIndex([options.ULT_VAL]) ultimate.virtual_columns.columns = {} ultimate.is_cumulative = True ultimate._set_slicers() @@ -102,7 +102,7 @@ def predict(self, X, sample_weight=None): """ X_new = X.val_to_dev() - if sum(X_new.ddims > self.ldf_.ddims.max()) > 0: + if sum(X_new._ddims > self.ldf_._ddims.max()) > 0: raise ValueError("X has ages that exceed those available in model.") # Before the line below, which borrows self.X_'s index when both sides # are a single row and so would erase what the caller actually passed. diff --git a/chainladder/methods/benktander.py b/chainladder/methods/benktander.py index add140083..0652c05c9 100644 --- a/chainladder/methods/benktander.py +++ b/chainladder/methods/benktander.py @@ -298,5 +298,5 @@ def _get_ultimate(self, X, expectation): b = cdf[-1, ...] * xp.nan_to_num(expectation.set_backend(backend).values) ultimate.values = num_to_nan(a + b) ultimate.array_backend = backend - ultimate.ddims = self.cdf_.ddims[: ultimate.shape[-1]] + ultimate._ddims = self.cdf_._ddims[: ultimate.shape[-1]] return self._set_ult_attr(ultimate) diff --git a/chainladder/methods/mack.py b/chainladder/methods/mack.py index 96e4a4081..7fc3c0e77 100644 --- a/chainladder/methods/mack.py +++ b/chainladder/methods/mack.py @@ -293,7 +293,7 @@ def _get_full_std_err_(self, X=None): avg = {"regression": 0, "volume": 1, "simple": 2} avg = [avg.get(item, item) for item in X.average_] val = xp.broadcast_to(xp.array(avg + [avg[-1]]), X.shape) - weight = xp.sqrt(full.values[..., : len(X.ddims)] ** (2 - val)) + weight = xp.sqrt(full.values[..., : len(X._ddims)] ** (2 - val)) obj.values = X.sigma_.values / num_to_nan(weight) w = lxp.concatenate( (X.w_, lxp.ones((val.shape[0], val.shape[1], val.shape[2], 1))), 3 @@ -354,11 +354,11 @@ def _mack_recursion(self, est, X=None): t1_t = xp.nan_to_num( future_std_err.sum("origin").set_backend(backend).values ) - obj.odims = obj.odims[0:1] + obj._odims = obj._odims[0:1] else: nans = xp.nan_to_num(X.nan_triangle[None, None]) nans = 1 - xp.concatenate((nans, xp.zeros((1, 1, X.shape[2], 1))), 3) - full_tri = X._full_triangle_.values[..., : len(X.ddims)] + full_tri = X._full_triangle_.values[..., : len(X._ddims)] if est == "parameter_risk_": # std_err_ is always numpy t1_t = ( @@ -367,7 +367,7 @@ def _mack_recursion(self, est, X=None): else: t1_t = xp.nan_to_num(full_tri) * self._get_full_std_err_(X).values extend = X.ldf_.shape[-1] - X.shape[-1] + 1 - ldf = X.ldf_.values[..., : len(X.ddims) - 1] + ldf = X.ldf_.values[..., : len(X._ddims) - 1] tail = X.cdf_.values[..., -extend : -extend + 1] ldf = xp.array(X.ldf_.get_array_module().concatenate((ldf, tail), -1)) # Recursive Mack Formula @@ -379,7 +379,7 @@ def _mack_recursion(self, est, X=None): t_tot = t_tot * nans[..., i + 1 : i + 2] risk_arr = xp.concatenate((risk_arr, xp.nan_to_num(t_tot)), 3) obj.values = risk_arr - obj.ddims = X._full_triangle_.ddims[list(range(X.shape[-1])) + [-1]] + obj._ddims = X._full_triangle_._ddims[list(range(X.shape[-1])) + [-1]] obj.valuation_date = X._full_triangle_.valuation_date obj._set_slicers() return obj @@ -512,6 +512,6 @@ def summary_(self): self.mack_std_err_.set_backend(backend).values[..., -1:], ) obj.values = obj.get_array_module().concatenate(cols, 3) - obj.ddims = ["Latest", "IBNR", "Ultimate", "Mack Std Err"] + obj._ddims = ["Latest", "IBNR", "Ultimate", "Mack Std Err"] obj._set_slicers() return obj diff --git a/chainladder/tails/base.py b/chainladder/tails/base.py index 4797f5f15..3f5c6b443 100644 --- a/chainladder/tails/base.py +++ b/chainladder/tails/base.py @@ -25,25 +25,25 @@ def fit(self, X, y=None, sample_weight=None): "S": (2 * m, 6), }[obj.development_grain] t_ddims = [ - (item + 1) * self._ave_period[1] + obj.ldf_.ddims[-1] + (item + 1) * self._ave_period[1] + obj.ldf_._ddims[-1] for item in range(self._ave_period[0] + 1) ] ddims = np.concatenate( - (obj.ldf_.ddims, t_ddims), + (obj.ldf_._ddims, t_ddims), 0, ) self.ldf_ = obj.ldf_.copy() tail = xp.ones(self.ldf_.shape)[..., -1:] tail = xp.repeat(tail, self._ave_period[0] + 1, -1) self.ldf_.values = xp.concatenate((self.ldf_.values, tail), -1) - self.ldf_.ddims = ddims + self.ldf_._ddims = ddims if hasattr(obj, "sigma_"): zeros = (obj.sigma_.iloc[..., -1:] * 0).values self.sigma_ = getattr(obj, "sigma_").copy() self.sigma_.values = xp.concatenate((self.sigma_.values, zeros), -1) self.std_err_ = getattr(obj, "std_err_").copy() self.std_err_.values = xp.concatenate((self.std_err_.values, zeros), -1) - self.sigma_.ddims = self.std_err_.ddims = self.ldf_.ddims[: obj.shape[2]] + self.sigma_._ddims = self.std_err_._ddims = self.ldf_._ddims[: obj.shape[2]] self.sigma_._set_slicers() self.std_err_._set_slicers() if hasattr(obj, "average_"): diff --git a/chainladder/tails/bondy.py b/chainladder/tails/bondy.py index 5c1072d88..91f31853d 100644 --- a/chainladder/tails/bondy.py +++ b/chainladder/tails/bondy.py @@ -148,19 +148,19 @@ def fit(self, X, y=None, sample_weight=None): super().fit(X, y, sample_weight) if self.earliest_age is None: - earliest_age = X.ddims[-2] + earliest_age = X._ddims[-2] else: - earliest_age = X.ddims[ + earliest_age = X._ddims[ int( self.earliest_age / ({"Y": 12, "S": 6, "Q": 3, "M": 1}[X.development_grain]) ) - 1 ] - attachment_age = self.attachment_age if self.attachment_age else X.ddims[-2] + attachment_age = self.attachment_age if self.attachment_age else X._ddims[-2] obj = Development().fit_transform(X) if "ldf_" not in X else X b_optimized = [] - initial = xp.where(obj.ddims == earliest_age)[0][0] if earliest_age else 0 + initial = xp.where(obj._ddims == earliest_age)[0][0] if earliest_age else 0 for num in range(len(obj.columns)): b0 = (xp.ones(obj.shape[0]) * 0.5)[:, None] data = xp.log(obj.ldf_.values[:, num, 0, initial:]) @@ -178,14 +178,14 @@ def fit(self, X, y=None, sample_weight=None): [item.reshape(-1, 2)[:, 1:2] for item in b_optimized], axis=1 )[..., None, None] ) - if sum(X.ddims > earliest_age) > 1: - tail = xp.exp(self.earliest_ldf_ * self.b_ ** (len(obj.ldf_.ddims) - 1)) + if sum(X._ddims > earliest_age) > 1: + tail = xp.exp(self.earliest_ldf_ * self.b_ ** (len(obj.ldf_._ddims) - 1)) else: tail = self.ldf_.values[..., 0, initial] tail = tail ** (self.b_ / (1 - self.b_)) f0 = self.ldf_.values[..., 0:1, initial : initial + 1] fitted = f0 ** ( - self.b_ ** (np.arange(sum(X.ddims >= earliest_age))[None, None, None, :]) + self.b_ ** (np.arange(sum(X._ddims >= earliest_age))[None, None, None, :]) ) fitted = xp.concatenate( (fitted, fitted[..., -1:] ** (self.b_ / (1 - self.b_))), axis=-1 @@ -200,8 +200,8 @@ def fit(self, X, y=None, sample_weight=None): ) self.ldf_.values = xp.concatenate( ( - self.ldf_.values[..., : sum(X.ddims <= attachment_age)], - fitted[..., -sum(X.ddims >= attachment_age) :], + self.ldf_.values[..., : sum(X._ddims <= attachment_age)], + fitted[..., -sum(X._ddims >= attachment_age) :], ), axis=-1, ) diff --git a/chainladder/tails/clark.py b/chainladder/tails/clark.py index f3740f505..2f18c45e1 100644 --- a/chainladder/tails/clark.py +++ b/chainladder/tails/clark.py @@ -152,7 +152,7 @@ def fit(self, X, y=None, sample_weight=None): model = ClarkLDF(growth=self.growth).fit(X, sample_weight=sample_weight) xp = X.get_array_module() age_offset = {"Y": 6.0, "S": 3, "Q": 1.5, "M": 0.5}[X.development_grain] - fitted = 1 / model.G_(self.ldf_.ddims - age_offset) + fitted = 1 / model.G_(self.ldf_._ddims - age_offset) fitted = xp.concatenate( ( fitted.values[..., :-1] / fitted.values[..., 1:], @@ -161,11 +161,11 @@ def fit(self, X, y=None, sample_weight=None): axis=-1, ) fitted = xp.repeat(fitted, self.ldf_.values.shape[2], 2) - attachment_age = self.attachment_age if self.attachment_age else X.ddims[-2] + attachment_age = self.attachment_age if self.attachment_age else X._ddims[-2] self.ldf_.values = xp.concatenate( ( - self.ldf_.values[..., : sum(self.ldf_.ddims < attachment_age)], - fitted[..., -sum(self.ldf_.ddims >= attachment_age) :], + self.ldf_.values[..., : sum(self.ldf_._ddims < attachment_age)], + fitted[..., -sum(self.ldf_._ddims >= attachment_age) :], ), axis=-1, ) diff --git a/chainladder/tails/constant.py b/chainladder/tails/constant.py index 4cc94957e..a8250d0a9 100644 --- a/chainladder/tails/constant.py +++ b/chainladder/tails/constant.py @@ -149,9 +149,9 @@ def fit(self, X, y=None, sample_weight=None): xp = self.ldf_.get_array_module() tail = self.tail if self.attachment_age: - attach_idx = xp.min(xp.where(X.ddims >= self.attachment_age)) + attach_idx = xp.min(xp.where(X._ddims >= self.attachment_age)) else: - attach_idx = len(X.ddims) - 1 + attach_idx = len(X._ddims) - 1 self = self._apply_decay(X, tail, attach_idx) obj = Development().fit_transform(X) if "ldf_" not in X else X self._get_tail_stats(obj) diff --git a/chainladder/tails/curve.py b/chainladder/tails/curve.py index bb90db9fa..4d528b381 100644 --- a/chainladder/tails/curve.py +++ b/chainladder/tails/curve.py @@ -252,9 +252,9 @@ def fit(self, X, y=None, sample_weight=None): ) tail = self._predict_tail(extrapolate) if self.attachment_age: - attach_idx = xp.min(xp.where(X.ddims >= self.attachment_age)) + attach_idx = xp.min(xp.where(X._ddims >= self.attachment_age)) else: - attach_idx = len(X.ddims) - 1 + attach_idx = len(X._ddims) - 1 self.ldf_.values = xp.concatenate( (self.ldf_.values[..., :attach_idx], tail[..., attach_idx:]), -1 ) diff --git a/chainladder/tails/tests/rtest_exponential.py b/chainladder/tails/tests/rtest_exponential.py index 5ca870571..0d580d125 100644 --- a/chainladder/tails/tests/rtest_exponential.py +++ b/chainladder/tails/tests/rtest_exponential.py @@ -102,7 +102,7 @@ def test_tail_doesnt_mutate_ldf_(data, averages, est_sigma): xp = p.get_array_module() p_no_tail = mack_p_no_tail(data, averages[0], est_sigma[0]).ldf_.values xp.testing.assert_array_equal( - p_no_tail, p.values[..., : len(cl.load_sample(data).ddims) - 1] + p_no_tail, p.values[..., : len(cl.load_sample(data).development) - 1] ) diff --git a/chainladder/tails/tests/test_exponential.py b/chainladder/tails/tests/test_exponential.py index 9ef7efa32..c9b407e45 100644 --- a/chainladder/tails/tests/test_exponential.py +++ b/chainladder/tails/tests/test_exponential.py @@ -18,7 +18,7 @@ def test_fit_period(tail_sample: Triangle) -> None: assert ( round( cl - .TailCurve(fit_period=(tail_sample.ddims[-7], None), extrap_periods=10) + .TailCurve(fit_period=(tail_sample.development.iloc[-7], None), extrap_periods=10) .fit(dev) .cdf_["paid"] .set_backend("numpy", inplace=True) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index ae355634e..5c46b9495 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -61,8 +61,8 @@ def test_triangle_json_io(clrd): assert clrd == clrd2 assert clrd.index.equals(clrd2.index) assert np.all(clrd.columns == clrd2.columns) - assert np.all(clrd.odims == clrd2.odims) - assert np.all(clrd.ddims == clrd2.ddims) + assert np.all(clrd.origin == clrd2.origin) + assert np.all(clrd.development == clrd2.development) assert np.all(clrd.valuation == clrd2.valuation) diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index 0b9401392..a2fb2b84b 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -720,10 +720,10 @@ def concat( ) ) if axis != 2: - a = np.array([obj.odims for obj in objs]) + a = np.array([obj._odims for obj in objs]) assert np.all(a == a[0]) if axis != 3: - a = np.array([obj.ddims for obj in objs]) + a = np.array([obj._ddims for obj in objs]) assert np.all(a == a[0]) out = copy.deepcopy(objs[0]) @@ -752,20 +752,20 @@ def concat( out.columns = new_axis elif axis == 2: if ignore_index: - new_axis = np.arange(sum([len(obj.odims) for obj in objs])) + new_axis = np.arange(sum([len(obj._odims) for obj in objs])) else: - new_axis = np.concatenate([obj.odims for obj in objs]) + new_axis = np.concatenate([obj._odims for obj in objs]) assert len(new_axis) == len(set(new_axis)) - out.odims = new_axis + out._odims = new_axis elif axis == 3: if ignore_index: - new_axis = np.arange(sum([len(obj.ddims) for obj in objs])) + new_axis = np.arange(sum([len(obj._ddims) for obj in objs])) else: - new_axis = np.concatenate([obj.ddims for obj in objs]) + new_axis = np.concatenate([obj._ddims for obj in objs]) assert len(new_axis) == len(set(new_axis)) - out.ddims = new_axis - if out.ddims.dtype == __dt64_dtype__ and type(out.ddims) is np.ndarray: - out.ddims = pd.DatetimeIndex(out.ddims) + out._ddims = new_axis + if out._ddims.dtype == __dt64_dtype__ and type(out._ddims) is np.ndarray: + out._ddims = pd.DatetimeIndex(out._ddims) out.valuation_date = pd.Series([obj.valuation_date for obj in objs]).max() out._set_slicers() From c2602438fc05655923c2de7a177030658f07a62f Mon Sep 17 00:00:00 2001 From: priyam0k Date: Sat, 5 Sep 2026 22:48:21 +0530 Subject: [PATCH 2/6] fix ruff lints --- chainladder/tails/tests/rtest_exponential.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chainladder/tails/tests/rtest_exponential.py b/chainladder/tails/tests/rtest_exponential.py index 0d580d125..37e2060b2 100644 --- a/chainladder/tails/tests/rtest_exponential.py +++ b/chainladder/tails/tests/rtest_exponential.py @@ -1,4 +1,3 @@ -import numpy as np import pytest import chainladder as cl @@ -7,7 +6,7 @@ from rpy2.robjects import r CL = importr("ChainLadder") -except ImportError: +except Exception: pass From a9f69608755ba9d8af803d9d348fe1c7cd0d199c Mon Sep 17 00:00:00 2001 From: priyam0k Date: Sun, 6 Sep 2026 17:12:38 +0530 Subject: [PATCH 3/6] fix(development): check is_cumulative is False --- chainladder/development/constant.py | 2 +- chainladder/tails/tests/rtest_exponential.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chainladder/development/constant.py b/chainladder/development/constant.py index 4956c862e..0b645f35e 100644 --- a/chainladder/development/constant.py +++ b/chainladder/development/constant.py @@ -131,7 +131,7 @@ def fit(self, X, y=None, sample_weight=None): """ from chainladder import options - if not X.is_cumulative: + if X.is_cumulative is False: obj = self._set_fit_groups(X).incr_to_cum().val_to_dev().copy() else: obj = self._set_fit_groups(X).val_to_dev().copy() diff --git a/chainladder/tails/tests/rtest_exponential.py b/chainladder/tails/tests/rtest_exponential.py index 37e2060b2..5e16e3909 100644 --- a/chainladder/tails/tests/rtest_exponential.py +++ b/chainladder/tails/tests/rtest_exponential.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import chainladder as cl From 180d5fb1cf64e78bbffb769435a5c1a7078e703d Mon Sep 17 00:00:00 2001 From: priyam0k Date: Fri, 2 Oct 2026 02:53:38 +0530 Subject: [PATCH 4/6] Fix rtest exception handling --- chainladder/tails/tests/rtest_exponential.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/tails/tests/rtest_exponential.py b/chainladder/tails/tests/rtest_exponential.py index 5e16e3909..c979f8d33 100644 --- a/chainladder/tails/tests/rtest_exponential.py +++ b/chainladder/tails/tests/rtest_exponential.py @@ -7,7 +7,7 @@ from rpy2.robjects import r CL = importr("ChainLadder") -except Exception: +except Exception: # noqa: BLE001 pass From 0db5fd50541c7327eb11850f21c025442b16e684 Mon Sep 17 00:00:00 2001 From: priyam0k Date: Sat, 3 Oct 2026 04:48:03 +0530 Subject: [PATCH 5/6] Format exponential tail test --- chainladder/tails/tests/test_exponential.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chainladder/tails/tests/test_exponential.py b/chainladder/tails/tests/test_exponential.py index c9b407e45..69302da9a 100644 --- a/chainladder/tails/tests/test_exponential.py +++ b/chainladder/tails/tests/test_exponential.py @@ -18,7 +18,9 @@ def test_fit_period(tail_sample: Triangle) -> None: assert ( round( cl - .TailCurve(fit_period=(tail_sample.development.iloc[-7], None), extrap_periods=10) + .TailCurve( + fit_period=(tail_sample.development.iloc[-7], None), extrap_periods=10 + ) .fit(dev) .cdf_["paid"] .set_backend("numpy", inplace=True) From 5668b6dabf1589d99791f73f47fd10b78cb25ddc Mon Sep 17 00:00:00 2001 From: priyam0k Date: Sat, 3 Oct 2026 13:55:02 +0530 Subject: [PATCH 6/6] Restore cumulative truthiness check --- chainladder/development/constant.py | 2 +- .../development/tests/test_constant.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/chainladder/development/constant.py b/chainladder/development/constant.py index 0b645f35e..4956c862e 100644 --- a/chainladder/development/constant.py +++ b/chainladder/development/constant.py @@ -131,7 +131,7 @@ def fit(self, X, y=None, sample_weight=None): """ from chainladder import options - if X.is_cumulative is False: + if not X.is_cumulative: obj = self._set_fit_groups(X).incr_to_cum().val_to_dev().copy() else: obj = self._set_fit_groups(X).val_to_dev().copy() diff --git a/chainladder/development/tests/test_constant.py b/chainladder/development/tests/test_constant.py index f9c27f43c..8b9f66f13 100644 --- a/chainladder/development/tests/test_constant.py +++ b/chainladder/development/tests/test_constant.py @@ -476,3 +476,28 @@ def test_constant_incr(): np.round(result.cdf_.to_frame().values.flatten(), 6) == np.array([4.0, 2.9, 1.8, 1.4, 1.2, 1.1, 1.03, 1.02, 1.005]) ) + + +def test_constant_incr_cumulative_none(): + raa_incr = cl.load_sample("raa").cum_to_incr() + raa_incr.is_cumulative = None + reported_patterns = { + 12: 4.0, + 24: 2.9, + 36: 1.8, + 48: 1.4, + 60: 1.2, + 72: 1.1, + 84: 1.03, + 96: 1.02, + 108: 1.005, + } + + result = cl.DevelopmentConstant( + patterns=reported_patterns, style="cdf" + ).fit_transform(raa_incr) + + assert np.all( + np.round(result.cdf_.to_frame().values.flatten(), 6) + == np.array([4.0, 2.9, 1.8, 1.4, 1.2, 1.1, 1.03, 1.02, 1.005]) + )