Skip to content

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

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

priyam0k wants to merge 25 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, pointing users to columns and index.
  • Move columns to use the new TriangleAxis helper, along with centralized axis lookups in typing.py and utility_functions.py.
  • Update internal library calls so they don't trigger deprecation warnings.
  • Keep support for loading older pickled triangles with a warning to re-save.
  • Add unit tests for the changes and deprecation warnings.

Related GitHub Issue(s)

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

AI/LLM Usage

Used basic model for initial unit test drafting and lint checks.

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 core Triangle metadata refactor touching indexing, concat, arithmetic, and serialization; regressions could misalign segments or break saved objects despite pickle migration and broad test coverage.

Overview
Replaces Triangle kdims/vdims with pandas-style index (DataFrame) and columns (Index), backed by a new TriangleAxis descriptor and internal _axes storage. Construction, slicing, arithmetic alignment, groupby/agg, column assignment, and concat now read and write these axes instead of numpy key/value dimension arrays; key_labels is derived from index columns.

Adds axis helpers (_get_axis_number / _get_axis_name), Triangle.axes, stricter index/columns setters with length checks, boolean Series indexing, sort_index(inplace=True), and scalar broadcast on __setitem__. Pickle/JSON paths gain __setstate__ migration from legacy kdims/vdims (with warning) and fixes so index round-trips as a DataFrame. Library estimators (bootstrap, correlation, tails, etc.) and tests are updated to the new API.

Reviewed by Cursor Bugbot for commit 4df8368. 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 94.34783% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.05%. Comparing base (856f87b) to head (4df8368).

Files with missing lines Patch % Lines
chainladder/utils/utility_functions.py 90.00% 2 Missing and 3 partials ⚠️
chainladder/core/axis.py 92.85% 2 Missing and 1 partial ⚠️
chainladder/core/triangle.py 94.73% 1 Missing and 2 partials ⚠️
chainladder/core/slice.py 90.90% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1303      +/-   ##
==========================================
- Coverage   94.15%   94.05%   -0.10%     
==========================================
  Files          96       97       +1     
  Lines        5852     5976     +124     
  Branches      739      776      +37     
==========================================
+ Hits         5510     5621     +111     
- Misses        221      227       +6     
- Partials      121      128       +7     
Flag Coverage Δ
unittests 94.05% <94.34%> (-0.10%) ⬇️

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 mentioned this pull request Sep 5, 2026
@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.6% of exported symbols fully typed (232 / 1483)

Known Ambiguous Unknown Total
Project (head) 232 108 1143 1483

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: 326
  • Functions without default param: 0
  • Classes without docstring: 8

Patch (exported symbols added or changed by this PR): 21.9% fully typed (7 / 32); 3 no longer exported

Known Ambiguous Unknown Total
Patch 7 1 24 32
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.axis ✅ known 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_axis_mappings ❌ unknown new
chainladder.core.tests.test_triangle.test_concat_axis0_index ❌ unknown new
chainladder.core.tests.test_triangle.test_duplicate_columns_assignment ❌ unknown 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_prep_index_single_element_different_key_labels ❌ unknown new
chainladder.core.tests.test_triangle.test_series_indexing ❌ unknown new
chainladder.core.tests.test_triangle.test_sort_axis_columns_out_of_order ❌ unknown new
chainladder.core.tests.test_triangle.test_sort_index_inplace ❌ unknown new
chainladder.core.tests.test_triangle.test_triangle_axes ❌ unknown new
chainladder.core.tests.test_triangle.test_triangle_axis_descriptor ❌ 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_property ❌ 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.axes ❌ unknown new
chainladder.core.triangle.Triangle.columns ✅ known changed (was ❌ unknown)
chainladder.core.triangle.Triangle.index ✅ known changed (was ❌ unknown)
chainladder.utils.tests.test_utilities.test_concat_axis_1_index_dtype_relaxed ❌ unknown new
chainladder.utils.tests.test_utilities.test_concat_axis_1_index_nan_preserved ❌ unknown new
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

@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 Outdated
Comment thread chainladder/core/triangle.py Outdated

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Left this for now since we focused on columns first. I'll clean it up when we tackle index next (and avoid stepping on the existing _set_index helper in base.py).

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 try not to define the self._index attribute. In a similar way to how we defined the _set_columns helper in axis.py, let's advance to defining _set_index.

Ideally we'll want to have index = TriangleAxis(0, fset=_set_index, doc="Represents the index axis of the triangle.").

Anyhow, this step might be a bit tricky, let me know how it goes.

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 rename the following terms to remove the "kdims" terminology:

  • kdims_arr -> index_values
  • key_idx -> index_idx
  • _set_index -> _factorize_index

The first and last of these are consistent with Pandas terminology. The middle one is pretty iffy to me, if you have a strong opinion on it, let me know. Within the existing _set_index function there is a local variable called kdims which I think should be renamed to unique_keys.

@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.

Stale Bugbot comment from a previous run.

Comment thread chainladder/core/triangle.py Outdated
@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 commented Oct 8, 2026

Copy link
Copy Markdown
Member

And, one thing I wanted to add relevant to the last comment is that the key_labels should also be deprecated, see #1474. But yeah, we won't be able to do that until we switch to the index/multiindex.

@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.

Reviewed by Cursor Bugbot for commit 9f18cf4. Configure here.

Comment thread chainladder/utils/utility_functions.py Outdated

@priyam0k priyam0k left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Scheduling MultiIndex and key_labels deprecation (#1474) for 2.0.0 works well. Keeping index as a DataFrame through 1.0 keeps the descriptor migration focused and avoids disrupting downstream estimators.

self.resampled_triangles_, self.scale_ = self._get_simulation(
X, exp_incr_triangle
)
# n_obs = xp.nansum(self.w_)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Restored the degrees of freedom comment block in bootstrap.py.

@priyam0k
priyam0k force-pushed the feature/deprecate-dims branch from 25108c4 to 9bf1b65 Compare October 9, 2026 14:38
@priyam0k
priyam0k force-pushed the feature/deprecate-dims branch from 9bf1b65 to 4df8368 Compare October 11, 2026 17:10
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