Skip to content

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

Merged
priyam0k merged 28 commits into
mainfrom
feature/deprecate-dims
Oct 19, 2026
Merged

priyam0k merged 28 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 broad axis-metadata refactor touching indexing, arithmetic, concat, and serialization; incorrect alignment could change results or break saved workflows, though extensive tests and pickle migration mitigate breakage.

Overview
Replaces Triangle’s internal kdims / vdims storage with pandas-aligned index (a DataFrame) and columns (Index), backed by a new TriangleAxis descriptor and _axes dict so axis assignment validates lengths and refreshes slicers.

key_labels is now derived from index columns (with a setter that renames them), and library code (arithmetic alignment, slicing, groupby/agg, concat, bootstrap simulation indices, JSON I/O) is updated to read/write index / columns instead of the old arrays. Legacy pickles are migrated in __setstate__ with a warning; copy deep-copies axis metadata.

Related behavior fixes: boolean Series indexing on the index axis, sort_index(inplace=True), safer column __setitem__ (scalars, duplicate labels), and relaxed concat index matching (NaNs / differing row labels on axis 1).

Reviewed by Cursor Bugbot for commit 814a2cb. 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 96.62921% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.34%. Comparing base (90c7b35) to head (814a2cb).

Files with missing lines Patch % Lines
chainladder/core/axis.py 92.85% 2 Missing and 1 partial ⚠️
chainladder/utils/utility_functions.py 93.75% 1 Missing and 2 partials ⚠️
chainladder/core/slice.py 90.90% 1 Missing and 1 partial ⚠️
chainladder/core/triangle.py 97.72% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1303      +/-   ##
==========================================
- Coverage   94.34%   94.34%   -0.01%     
==========================================
  Files          96       97       +1     
  Lines        5973     6131     +158     
  Branches      768      798      +30     
==========================================
+ Hits         5635     5784     +149     
- Misses        219      223       +4     
- Partials      119      124       +5     
Flag Coverage Δ
unittests 94.34% <96.62%> (-0.01%) ⬇️

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.9% of exported symbols fully typed (241 / 1514)

Known Ambiguous Unknown Total
Project (head) 241 106 1167 1514

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

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

Known Ambiguous Unknown Total
Patch 7 1 27 35
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.TestLegacyPickleCompatibility ❌ unknown new
chainladder.core.tests.test_triangle.TestLegacyPickleCompatibility.test_legacy_kdims_vdims_migration ❌ unknown new
chainladder.core.tests.test_triangle.TestLegacyPickleCompatibility.test_legacy_pickle_fallback_without_keys ❌ unknown new
chainladder.core.tests.test_triangle.TestLegacyPickleCompatibility.test_migrated_pickle_roundtrip ❌ unknown 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_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 done
@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.

@priyam0k
priyam0k force-pushed the feature/deprecate-dims branch from 4df8368 to 7e5ff84 Compare October 18, 2026 20:53
@genedan

genedan commented Oct 18, 2026

Copy link
Copy Markdown
Member

Last thing would be doing 1 import per line per the above comment, then I'll approve.

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

Split them into individual import lines.
Grateful for walking through this PR and appreciate your patience, learned a lot here 🙇‍♂️

Comment thread chainladder/core/tests/test_triangle.py Outdated

def test_triangle_axis_descriptor():
"""Verify TriangleAxis descriptor correctly uses integer axis keys for index and columns."""
from chainladder.core.axis import TriangleAxis, _set_columns, _set_index

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.

Done, split each into its own import line.

@genedan

genedan commented Oct 19, 2026

Copy link
Copy Markdown
Member

You can put them inside parentheses, that allows you do avoid typing chainladder.core.axis multiple times:

from chainladder.core.axis import (
    _set_columns,
    _set_index, 
    TriangleAxis, 
)

@priyam0k

Copy link
Copy Markdown
Contributor Author

You can put them inside parentheses, that allows you do avoid typing chainladder.core.axis multiple times:

from chainladder.core.axis import (
    _set_columns,
    _set_index, 
    TriangleAxis, 
)

Updated to parenthesized imports

@genedan

genedan commented Oct 19, 2026

Copy link
Copy Markdown
Member

This is probably one of the most complex PRs we've ever done. Good job!

@priyam0k

Copy link
Copy Markdown
Contributor Author

Definitely felt like a puzzle with all the moving parts, glad we made it here

@priyam0k
priyam0k merged commit bfb93b0 into main Oct 19, 2026
22 checks passed
@priyam0k
priyam0k deleted the feature/deprecate-dims branch October 19, 2026 02:16
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