Skip to content

Pandas annotations#1035

Open
genedan wants to merge 11 commits into
mainfrom
pandas_annotations
Open

Pandas annotations#1035
genedan wants to merge 11 commits into
mainfrom
pandas_annotations

Conversation

@genedan

@genedan genedan commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary of Changes

Add type annotations to pandas.py. Add API reference examples to TrianglePandas.dropna(). This method really needed examples to illustrate its complex behavior. I'll try to stick to just 1 or 2 methods next time if they turn out to be as complex.

Related GitHub Issue(s)

#486
#704

Additional Context for Reviewers

Removed line 303:

self.values = np.where((xp.nan_to_num(self.values) == 0) * (self.nan_triangle == 1), self.nan_triangle * 0, self.values)

This line didn't do anything, it simplifies to self.values = self.values, so I removed it.

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

Note

Medium Risk
fillna no longer accepts implicit None and uses a different inplace value merge, which can change runtime behavior for callers that relied on the old path; other changes are mostly typing and documentation.

Overview
Adds static typing across TrianglePandas by typing mixin methods with TriangleProtocol, tightening signatures (e.g. _get_axis, plot/hvplot), and extending TriangleProtocol in typing.py with the properties and methods those pandas mirrors need.

Triangle.dropna() gains a large doctest-backed API reference that spells out edge-only trimming (origins/development at the ends, multi-index aggregation), plus small inline comments in the implementation. to_frame(keepdims=True) gets minor cleanup: COO cast for sparse values, valuation built via a named Series, and removal of invalid variable type annotations on DataFrame column assignments.

fillna now requires an explicit fill value (TypeError if value is None) and simplifies inplace updates to self.values = (self + fill).values instead of the prior frame = self + value * 0 path. fillzero drops a redundant np.where assignment that was effectively a no-op and keeps the real NaN-mask fill logic.

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

@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.34884% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.33%. Comparing base (468f11a) to head (53fffc4).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
chainladder/core/pandas.py 95.34% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1035      +/-   ##
==========================================
- Coverage   89.38%   89.33%   -0.05%     
==========================================
  Files          91       89       -2     
  Lines        5266     5177      -89     
  Branches      678      662      -16     
==========================================
- Hits         4707     4625      -82     
+ Misses        391      387       -4     
+ Partials      168      165       -3     
Flag Coverage Δ
unittests 89.33% <95.34%> (-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.

@genedan

genedan commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

I'll fix that codecov comment in another PR, I think this one's already kind of big.

Comment thread chainladder/core/pandas.py
Comment thread chainladder/core/pandas.py
Comment thread chainladder/core/pandas.py
Comment thread chainladder/core/pandas.py
self.development <= ddim.max())
]
return obj[(self.origin >= min_odim) & (self.origin <= max_odim)]
obj = cast("TriangleProtocol", cast(object, obj))

@henrydingliu henrydingliu Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

two casts (i guess technically 3) in 2 lines. why the differences?

  • one upcasts to object but ones doesn't
  • one casts to triangleprotocal one casts to triangle

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The double-cast pattern is used to eliminate a certain IDE warning. If I were to do a single cast:

cast("TriangleProtocol", obj)

I'd get this warning from my IDE:

Cast of type 'Triangle | Series' to type 'TriangleProtocol' may be a mistake because they are not in the same inheritance hierarchy. If this was intentional, cast the expression to 'object' first.

The next cast doesn't throw the warning since we're casting to Triangle, which is one of Triangle | Series. I wonder if casting is a crutch to cover up for some weaknesses in the code. I have a hunch that if we had clearer branching in the functions, the return type would be unambiguous and the type checker would know which one of Triangle | Series it'd be. But that's just a hunch, I guess I've gotten used to dynamic typing for far too long...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hmmm, where is your IDE getting Triangle|Series from? self is typed to TriangleProtocol at the top.

and when should we type to Triangle and when should we type to TriangleProtocol?

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