Skip to content

[ENH] Define Triangle Styler - #1342

Open
genedan wants to merge 15 commits into
mainfrom
#1331-styler
Open

genedan wants to merge 15 commits into
mainfrom
#1331-styler

Conversation

@genedan

@genedan genedan commented Sep 11, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Adds functionality to style Triangles directly.

Related GitHub Issue(s)

Closes:

#1332
#1333
#1324

Partially addresses:

#1337

Additional Context for Reviewers

The docs have a stub tutorial in user_guide/style.ipynb. This allows the reviewer to visually inspect what the new styling looks like. I'll fill it out with a proper tutorial towards the end of the parent issue #1331.

I recommend the reviewer to first take a look at the notebook to confirm the feature works. pandas.py is where Triangle.style() returns the styler, which is defined in style.py (where the bulk of the formatting logic takes place).

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

Low Risk
Additive presentation API with no changes to reserving math; lower-triangle masking is guarded by shape checks and clear errors when misused.

Overview
Adds a pandas-style API for styling triangles: a public Styler subclass of pandas.io.formats.style.Styler and a Triangle.style property (mirroring DataFrame.style) that wraps to_frame() and applies default number formatting for single-dimension triangles.

The Triangle-specific highlight_lower_triangle method CSS-highlights cells beyond the latest diagonal using the wrapped triangle’s valuation grid (default valuation_date), with optional text_color, custom props, and an explicit valuation_date for fully predicted triangles like full_triangle_. It requires a styler from Triangle.style and only supports a single 2-D grid.

Styler is exported from chainladder, documented in the API and a stub user_guide/style.ipynb, with broad unit tests covering subclass behavior, masking alignment with nan_triangle, and error cases for non-triangle stylers and multi-dimensional frames.

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

@github-actions

github-actions Bot commented Sep 11, 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.5% of exported symbols fully typed (218 / 1406)

Known Ambiguous Unknown Total
Project (head) 218 111 1077 1406

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

Patch (exported symbols added or changed by this PR): 38.5% fully typed (10 / 26)

Known Ambiguous Unknown Total
Patch 10 0 16 26
Patch symbol details
Symbol Status Change
chainladder.core.Styler ❌ unknown new
chainladder.core.pandas.TrianglePandas.style ❌ unknown new
chainladder.core.style.Styler ❌ unknown new
chainladder.core.style.Styler.__init__ ❌ unknown new
chainladder.core.style.Styler.highlight_lower_triangle ✅ known new
chainladder.core.style.ValuationDateLike ✅ known new
chainladder.core.tests.test_style.df ✅ known new
chainladder.core.tests.test_style.test_chained_methods_preserve_subclass ✅ known new
chainladder.core.tests.test_style.test_highlight_lower_triangle_predicted_cells_not_highlighted_by_default ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_props_overrides_color ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_requires_a_triangle ✅ known new
chainladder.core.tests.test_style.test_highlight_lower_triangle_requires_matching_shape ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_returns_styler ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_styles_exactly_the_lower_triangle ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_text_color ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_text_color_ignored_without_default_props ❌ unknown new
chainladder.core.tests.test_style.test_highlight_lower_triangle_with_valuation_date_highlights_predicted_cells ❌ unknown new
chainladder.core.tests.test_style.test_is_pandas_styler_subclass ✅ known new
chainladder.core.tests.test_style.test_is_public ✅ known new
chainladder.core.tests.test_style.test_renders_identically_to_pandas_styler ✅ known new
chainladder.core.tests.test_style.test_triangle_style_is_a_property ❌ unknown new
chainladder.core.tests.test_style.test_triangle_style_is_fresh_each_access ❌ unknown new
chainladder.core.tests.test_style.test_triangle_style_returns_styler ❌ unknown new
chainladder.core.tests.test_style.test_triangle_style_wraps_to_frame ❌ unknown new
chainladder.core.tests.test_style.test_wraps_a_dataframe ✅ known new
chainladder.core.typing.TriangleProtocol.valuation ✅ known new

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.83%. Comparing base (d7479a4) to head (97b3771).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1342      +/-   ##
==========================================
+ Coverage   91.76%   91.83%   +0.07%     
==========================================
  Files          96       98       +2     
  Lines        5475     5525      +50     
  Branches      706      712       +6     
==========================================
+ Hits         5024     5074      +50     
  Misses        327      327              
  Partials      124      124              
Flag Coverage Δ
unittests 91.83% <100.00%> (+0.07%) ⬆️

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.

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

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 53f1edc. Configure here.

Comment thread chainladder/core/pandas.py Outdated
@genedan
genedan marked this pull request as ready for review September 11, 2026 23:41
self,
key: str | int,
value: int | float | TriangleSlicer | Callable[[Triangle], TriangleSlicer],
) -> 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.

can you confirm this is ruff formatting changes only?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, it was ruff.

Comment thread chainladder/core/style.py

def __init__(
self,
data: pd.DataFrame | pd.Series,

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.

is this needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think the triangle argument is the extraneous one. I can eliminate it, and restrict data so that it still matches the Pandas API, but only takes a Triangle since a regular DataFrame or Series doesn't have components like a lower diagonal.

styler = styler.format( # pyright: ignore[reportReturnType]
fmt_str, na_rep=""
)
return styler

@henrydingliu henrydingliu Sep 12, 2026

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.

is there any value in allowing styler on multi triangle but essentially blocking multi triangle from using any of the custom cl stylers? (i know we only have a single styler right now. but anything we might add, like highlight_diagonal, etc. would all only work on single triangles) like, what if we just let multi triangles error out right here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've updated the code to throw an exception if the triangle is multi. Although I do need your thought on e putting "2-D" in the error message. It's technically 2-D (origin x development), but it's often called 1-D throughout the code. I think "single triangle" is a little less controversial.

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.

'single'/'multi' is how we are calling it in the codebase. i'm good with single.

Comment thread chainladder/core/style.py
from chainladder.core.style import Styler

df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
Styler(df).format(precision=1)

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.

can this mimic the pandas doc and actually show an output? something like precision = 1 on a raa.age-to-age

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah it can, I'll need to get rid of this example though. We really shouldn't be allowing the Styler to take a regular DataFrame or Series since it's designed to work on the Triangle-specific attributes. I'll replace this with a precision example (will add the feature if needed).

Comment thread chainladder/core/style.py
full.style.highlight_lower_triangle(
color="lightgray", valuation_date=raa.valuation_date
)

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.

can we follow pandas and link to the tutorial notebook?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes

Comment thread chainladder/core/style.py
if text_color is not None:
props += f" color: {text_color};"

def f(_data: pd.DataFrame) -> np.ndarray:

@henrydingliu henrydingliu Sep 12, 2026

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 feel like we are gatekeeping a lot of generalizable code inside this single-purpose styler. how about overloading or replacing cl.Styler.apply with apply(indicator_triangle, true_prop, false_prop)? then highlight_lower_triangle can route through that apply.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We can get rid of this f function and create a generalized method to create an indicator triangle, then pass it to apply. Will let you know when it's done.

@henrydingliu henrydingliu self-assigned this Sep 12, 2026
@henrydingliu henrydingliu mentioned this pull request Sep 15, 2026
11 tasks
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.

2 participants