Skip to content

feat(core): deprecate vdims (#1011) and kdims (#1010) - #1303

Draft
priyam0k wants to merge 14 commits into
mainfrom
feature/deprecate-dims
Draft

priyam0k wants to merge 14 commits into
mainfrom
feature/deprecate-dims

Conversation

@priyam0k

@priyam0k priyam0k commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary of Changes

  • Deprecate vdims (Deprecate vdims #1011) and kdims (Deprecate kdims #1010) on Triangle with FutureWarnings pointing to columns/columns_label and index/key_labels
  • Update internal usage across core modules, estimators, adjustments, and tails to prevent internal warnings
  • Add test coverage verifying deprecation warnings when reading or setting both attributes
  • Address ruff lint and formatting after rebasing on main (added noqa: N802 on Clark and Munich formulas to preserve actuarial notation)

Related GitHub Issue(s)

Closes #1010, closes #1011, refs #601, refs #1216

Additional Context for Reviewers

Existing code reading or setting vdims and kdims continues to work as expected for backward compatibility. Date dimensions (odims and ddims) will follow in a separate PR.

Checklist

  • I passed tests locally for both code (uv run pytest) and documentation changes (uv run --directory docs jb build . --builder=custom --custom-builder=doctest)

Note

High Risk
This is a cross-cutting change to Triangle’s dimension model (indexing, I/O, arithmetic, and serialization); regressions could affect most user workflows even with pickle migration and broad test updates.

Overview
Migrates Triangle row/column metadata off kdims/vdims onto pandas-style index (_index DataFrame) and columns, with a new TriangleAxis descriptor backing the value dimension.

Triangle construction, slicing, arithmetic alignment, groupby/agg, concat, JSON I/O, and estimator outputs now read and write index/columns (and key_labels via _index columns) instead of numpy kdims/vdims. Pickle __setstate__ migrates legacy serialized triangles; copy() isolates _index; index/column setters validate length and refresh slicers. Bootstrap resampling builds simulation keys by extending Triangle.index with a Simulation_# column. sort_index(inplace=True) mutates values and index together; boolean Series indexing is supported.

Tests cover columns/key_labels setters, legacy pickle shapes, JSON roundtrip of _index, and concat(..., ignore_index=True) on axes 1–3.

Reviewed by Cursor Bugbot for commit 7f4e4d1. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread chainladder/core/triangle.py Outdated
@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.67136% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.25%. Comparing base (c18ee04) to head (7f4e4d1).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
chainladder/core/triangle.py 80.32% 9 Missing and 3 partials ⚠️
chainladder/core/axis.py 74.19% 4 Missing and 4 partials ⚠️
chainladder/utils/utility_functions.py 96.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1303      +/-   ##
==========================================
- Coverage   92.30%   92.25%   -0.05%     
==========================================
  Files         100      101       +1     
  Lines        5793     6029     +236     
  Branches      713      803      +90     
==========================================
+ Hits         5347     5562     +215     
- Misses        325      339      +14     
- Partials      121      128       +7     
Flag Coverage Δ
unittests 92.25% <89.67%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@priyam0k
priyam0k force-pushed the feature/deprecate-dims branch from acb0c9f to bf0e00a Compare September 5, 2026 12:29
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Pyright Type Completeness

View the full pyright --verifytypes output for this commit

Project (full chainladder package, at this PR's head): 15.2% of exported symbols fully typed (214 / 1412)

Known Ambiguous Unknown Total
Project (head) 214 111 1087 1412

Other symbols referenced but not exported by chainladder: 13

Known Ambiguous Unknown Total
Other (head) 3 1 9 13

Symbols without documentation:

  • Functions without docstring: 328
  • Functions without default param: 0
  • Classes without docstring: 10

Patch (exported symbols added or changed by this PR): 26.3% fully typed (5 / 19); 3 no longer exported

Known Ambiguous Unknown Total
Patch 5 1 13 19
Patch symbol details
Symbol Status Change
chainladder.core.axis.TriangleAxis ❌ unknown new
chainladder.core.axis.TriangleAxis.__get__ ❌ unknown new
chainladder.core.axis.TriangleAxis.__init__ ✅ known new
chainladder.core.axis.TriangleAxis.__set__ ❌ unknown new
chainladder.core.axis.TriangleAxis.fget ✅ known new
chainladder.core.axis.TriangleAxis.fset ✅ known new
chainladder.core.axis.TriangleAxis.key ✅ known new
chainladder.core.slice.TriangleSlicer.columns ⚠️ ambiguous new
chainladder.core.tests.test_triangle.test_json_roundtrip_preserves_dataframe_index ❌ unknown new
chainladder.core.tests.test_triangle.test_key_labels_setter ❌ unknown new
chainladder.core.tests.test_triangle.test_legacy_pickle_compatibility ❌ unknown new
chainladder.core.tests.test_triangle.test_series_indexing ❌ unknown new
chainladder.core.tests.test_triangle.test_sort_index_inplace ❌ unknown new
chainladder.core.tests.test_triangle.test_triangle_columns_setter ❌ unknown new
chainladder.core.tests.test_triangle.test_triangle_copy_isolated_index ❌ unknown new
chainladder.core.tests.test_triangle.test_triangle_index_setter_resets_row_index ❌ unknown new
chainladder.core.triangle.Triangle.__setstate__ ❌ unknown new
chainladder.core.triangle.Triangle.columns ✅ known changed (was ❌ unknown)
chainladder.utils.tests.test_utilities.test_concat_ignore_index_axes ❌ unknown new
chainladder.core.slice.TriangleSlicer.vdims no longer exported (was ⚠️ ambiguous)
chainladder.core.triangle.Triangle.kdims no longer exported (was ✅ known)
chainladder.core.triangle.Triangle.vdims no longer exported (was ✅ known)

@priyam0k priyam0k mentioned this pull request Sep 5, 2026
1 task
@priyam0k
priyam0k force-pushed the feature/deprecate-dims branch 3 times, most recently from acce607 to cab8a13 Compare September 9, 2026 01:33
@genedan

genedan commented Sep 9, 2026

Copy link
Copy Markdown
Member

Some general thoughts on your approach:

  • I see that kdims and vdims have been renamed _kdims and _vdims. What we'd like to do is actually get rid of these labels and define the Triangle's index and columns directly.
  • Triangle.index and Triangle.columns should no longer be properties of the Triangle, they should resolve to Index/MultiIndex data types

I have an attempt at a general approach in #601, starting with this comment. What I tried to do was examine how a Pandas DataFrame initialized its columns:

https://github.com/pandas-dev/pandas/blob/721ab260c67dd480e83d1510fa5ac08553536038/pandas/core/frame.py#L4692-L4697

And see if we can port over the logic. One strategy I would suggest trying is changing kdims and vdims to be index and columns, and then get rid of the old properties. Hope that helps.

@priyam0k

priyam0k commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

I went through your comments in #601 and the Pandas AxisProperty references.

Storing columns as a direct pd.Index on the Triangle (and replacing _vdims references with self.columns.values) makes total sense and eliminates the need for _vdims completely. I can definitely update that.

For Triangle.index, what are your thoughts on keeping it stored as a pd.DataFrame vs. migrating to pd.Index/pd.MultiIndex?

Currently, Triangle.index returns a DataFrame across the public API (which estimators and TriangleGroupBy depend on for column-based slicing and key_labels). We could store self.index directly as a DataFrame (and use self.index.values in place of _kdims), or if we want Triangle.index to become a MultiIndex, that might be a broader public API change worth tackling in a dedicated PR as part of #601.

In the meantime, I will proceed with updating columns to be stored as a pd.Index and storing index directly.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread chainladder/core/triangle.py Outdated
Comment thread chainladder/core/triangle.py Outdated
Comment thread chainladder/adjustments/bootstrap.py Outdated
@priyam0k
priyam0k force-pushed the feature/deprecate-dims branch from 35a7872 to 617fc4e Compare September 12, 2026 07:45

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread chainladder/core/triangle.py Outdated
@genedan

genedan commented Sep 12, 2026

Copy link
Copy Markdown
Member

For Triangle.index, what are your thoughts on keeping it stored as a pd.DataFrame vs. migrating to pd.Index/pd.MultiIndex?

For 0.11.0, we should retain it as a DataFrame, otherwise it'll lead to a breaking change. I think we should switch to Index/MultiIndex for 1.0.0 - you could make a new issue for that if there isn't one already. I'm not sure if there's a good way to notify the public of this change other than to have it be part of a major version bump. It would be too annoying to warn a user every time they call the index.

Anyhow, let me know when you're ready for a review.

@genedan

genedan commented Sep 12, 2026

Copy link
Copy Markdown
Member

I also don't think the dims need to go through a deprecation style, I think we can go straight to removing them. They were never meant to be part of the public API and aren't mentioned in the docs. @casact/chainladder-maintainers, let me know if you disagree.

@priyam0k

priyam0k commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

For 0.11.0, we should retain it as a DataFrame, otherwise it'll lead to a breaking change. I think we should switch to Index/MultiIndex for 1.0.0

Makes total sense. Keeping Triangle.index as a DataFrame for 0.11.0 avoids breaking anything downstream, and we can look at the Index/MultiIndex shift for 1.0.0 (maybe track that under #601).

I also don't think the dims need to go through a deprecation style, I think we can go straight to removing them.

Agreed. removal is much cleaner since they were never part of the public API, and it saves us having to maintain deprecation shims for internal attributes.

I am just clearing up a couple of CI checks on this branch (Read the Docs and coverage) and will ping you here as soon as it is ready for your review.

@priyam0k

Copy link
Copy Markdown
Contributor Author

All checks passed. @genedan this is ready for review.

Comment thread chainladder/adjustments/bootstrap.py Outdated
obj = X.copy()
if X.key_labels == ["Total"]:
obj.kdims = np.arange(self.n_sims)
obj._index = pd.DataFrame({"Simulation_#": np.arange(self.n_sims)})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should avoid accessing _index directly.

Comment thread chainladder/core/tests/test_triangle.py Outdated
assert tri.index.equals(original_index)


def test_vdims_deprecation_warning(raa):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can remove the tests for deprecation warnings since we'll be removing the dims without going through the deprecation cycle.

Comment thread chainladder/core/triangle.py Outdated
self._set_slicers()

@property
def kdims(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can remove the dims properties.

Comment thread chainladder/core/triangle.py

self.kdims, key_idx = self._set_kdims(data_agg, index)
self.vdims = np.array(columns)
kdims_arr, key_idx = self._set_kdims(data_agg, index)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

looks like the _set_kdims method still exists, we should at least rename it to _set_index (assuming we keep the function).

@genedan

genedan commented Sep 13, 2026

Copy link
Copy Markdown
Member

I've submitted some comments, but consider it a partial review to help guide you where we need to go. This is one of the larger issues to close out, so it will most likely get it take a lot of back and forth between us until we can merge it into main (more commits credited to you!). We'll have to take it step-by-step, but I think we can get it done. Some general goals:

  • The need to define internal variables _index and _columns makes me think that our current definition of of Triangle.index and Triangle.columns is lacking in some way, especially when you find the need to reference those protected variables outside the Triangle class. Once defined, we should be able to access what we need from Triangle.index and Triangle.columns without the need to reference those protected variables. This is one of the driving motivations of the refactor.
  • This leads me to believe we should get started on defining a generalized TriangleAxis class from which Triangle.index, Triangle.columns, Triangle.origin, and Triangle.development You can reference this comment for the general idea that I had: Simplify internals to prep for 1.0 release #601 (comment)
  • We can remove all vdims/kdims code, and no longer need to issue deprecation warnings.

Refactoring this block will be key to closing out the issue:

self.kdims: np.ndarray
key_idx: np.ndarray
self.vdims: np.ndarray
self.odims: np.ndarray
orig_idx: np.ndarray
self.ddims: ArrayLike
dev_idx: np.ndarray
self.kdims, key_idx = self._set_kdims(data_agg, index)
self.vdims = np.array(columns)
self.odims, orig_idx = self._set_odims(data_agg, date_axes)
self.ddims, dev_idx = self._set_ddims(data_agg, date_axes)

Ideally, we'd have something like:

self.index = TriangleAxis("index", fset=_set_index)
self.columns = TriangleAxis("columns", fset=_set_columns)
self.origin = TriangleAxis("origin", fget=_get_origin, fset=_set_origin)
self.development = TriangleAxis("development", fget=_get_development, fset=_set_development)

I have a rough sketch of what TriangleAxis should look like

  class TriangleAxis:
      """Generalized class for representing a Triangle dimension,
      analogous to Pandas AxisProperty."""

      def __init__(self, key, *, fget=None, fset=None, doc=None):
          self.key = key
          self.fget = fget   # raw -> public; identity if None
          self.fset = fset   # (obj, public) -> raw; identity if None
          self.__doc__ = doc

      def __get__(self, obj, objtype=None):
          if obj is None:
              return self                                                                                                                                                                                                                                               
          raw = obj._axes[self.key]
          return self.fget(obj, raw) if self.fget else raw

      def __set__(self, obj, value):
          raw = self.fset(obj, value) if self.fset else value
          obj._axes[self.key] = raw
          obj._set_slicers()

With these helper functions:

  def _set_index(obj, value):
      if not isinstance(value, pd.DataFrame):
          raise TypeError("index must be a pandas DataFrame")
      obj._len_check(obj.index, value)
      return value.copy().reset_index(drop=True)

  def _set_columns(obj, value):
      if isinstance(value, str):
          value = [value]                                                                                                                                                                                                                                               
      obj._len_check(obj.columns, value)
      return pd.Index(value, name="columns")

  def _get_origin(obj, raw):
      if obj.is_pattern and len(raw) == 1:
          return pd.Series(["(All)"])
      freq = {"S": "2Q", "H": "2Q"}.get(obj.origin_grain, obj.origin_grain)
      freq = freq if freq == "M" else freq + "-" + obj.origin_close
      return pd.DatetimeIndex(raw, name="origin").to_period(freq=freq)

  def _set_origin(obj, value):
      obj._len_check(obj.origin, value)
      freq = {"S": "2Q"}.get(obj.origin_grain, obj.origin_grain)
      freq = freq if freq == "M" else freq + "-" + obj.origin_close
      return pd.PeriodIndex(list(value), freq=freq).to_timestamp().values

You may need to tweak these and this might take trial and error from the both of us. How about we give it a go, create a new axis.py file to place the class in, and see if we can refactor the code for one dimension, Triangle.columns?

@priyam0k

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review and guidance,

We can remove all vdims/kdims code, and no longer need to issue deprecation warnings.

Completely agree. Removing kdims and vdims directly keeps things much cleaner and avoids carrying deprecation shims for internal attributes that were never documented or public. I'll drop those properties and their deprecation tests, and rename _set_kdims to _set_index.

The need to define internal variables _index and _columns makes me think that our current definition of Triangle.index and Triangle.columns is lacking in some way, especially when you find the need to reference those protected variables outside the Triangle class.

Spot on. Reaching into _index in bootstrap.py was a workaround for the length check during simulation reshaping; I will refactor that to use the public API instead.

How about we give it a go, create a new axis.py file to place the class in, and see if we can refactor the code for one dimension, Triangle.columns?

Nice plan. I really like the TriangleAxis descriptor design. I'll set up chainladder/core/axis.py, hook up Triangle.columns using TriangleAxis, and make sure tests pass cleanly.

Will push an update shortly so we can take a look..

@genedan

genedan commented Sep 19, 2026

Copy link
Copy Markdown
Member

Whew, looks like the PR is still intact. This was definitely one of the more significant ones that we wanted in.

image

@priyam0k

Copy link
Copy Markdown
Contributor Author

I'm back, really back. Thanos snap
image

@priyam0k
priyam0k marked this pull request as draft September 20, 2026 09:00

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f711b88. Configure here.

Comment thread chainladder/core/triangle.py
@priyam0k

Copy link
Copy Markdown
Contributor Author

@genedan I pushed an update with the first step of the descriptor refactor.

The new TriangleAxis descriptor is in axis.py, and I hooked up columns to use it in triangle.py. As discussed, I also dropped kdims and vdims directly, and cleaned up bootstrap.py so it sets the index publicly instead of touching private variables. I also safeguarded sort_index against mutating the live index.

Take a look when free. If this shape works I'll follow up with index next.

@genedan genedan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is starting to look way cleaner, good work. I left some comments requesting some changes, and I asked a few questions too.

This is going to be an awesome PR!

db = None

from typing import cast, Optional, TYPE_CHECKING
from typing import Any, cast, Optional, TYPE_CHECKING

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's put these on multiple lines.

Comment thread chainladder/core/axis.py


class TriangleAxis:
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add docstrings for:

  • The class (add parameters)
  • Methods

Comment thread chainladder/core/axis.py
*,
fget: Callable[[Triangle, Any], Any] | None = None,
fset: Callable[[Triangle, Any], Any] | None = None,
doc: str | None = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this doc parameter might be extraneous. Compare these two outputs:

import chainladder as cl
import pandas as pd

pd.DataFrame().columns.__doc__

raa = cl.load_sample('raa')
raa.columns.__doc__

The both resolve to the doc for an Index object, which is right. The raa.columns.__doc__ mentions Pandas though, I wonder if there's an easy way to replace the word "pandas" with "chainladder" without too much engineering?

Seeing "pandas" in the doc probably shouldn't be a big deal for now though, let's put it low on the priority list of changes but nice to have if you can get around to it.

*args,
**kwargs,
):
self._axes: dict[str, Any] = {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should turn this into a public property that returns a list of the access. Doing so would match the Pandas signature:

    @property
    def axes(self) -> list[Index]:
        """
        Return a list representing the axes of the DataFrame.

X._index = self._index.copy()
return X

def __setstate__(self, state: dict) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Will this block still be necessary once you're done with the PR? A passing test at the end of step 2 of this PR is enough to convince me that we haven't broken anything. We can remove this block and the associated test in a new PR step 3.

if isinstance(obj, TriangleGroupBy):

def f(k, self, obj, other):
# fmt: off

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

revert

if isinstance(obj, TriangleGroupBy):

def f(k, self, obj, other):
# fmt: off

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

revert

rows = X.index.set_index(X.key_labels).index
self.omega_ = pd.DataFrame(params[..., 0, 0], index=rows, columns=X.vdims)
self.theta_ = pd.DataFrame(params[..., 0, 1], index=rows, columns=X.vdims)
self.omega_ = pd.DataFrame(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to use columns_label here, or can we extract it from the new columns index?

fitted = xp.repeat(fitted, self.ldf_.shape[2], axis=2)
rows = X.index.set_index(X.key_labels).index
self.b_ = pd.DataFrame(self.b_[..., 0, 0], index=rows, columns=X.vdims)
self.b_ = pd.DataFrame(self.b_[..., 0, 0], index=rows, columns=X.columns_label)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as the other comment, can we extract from columns index?

rows = self.ldf_.index.set_index(self.ldf_.key_labels).index
return pd.DataFrame(
self._slope_[..., 0, 0], index=rows, columns=self.ldf_.vdims
self._slope_[..., 0, 0], index=rows, columns=self.ldf_.columns_label

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

extract from columns index?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deprecate vdims Deprecate kdims

2 participants