Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Disposal rate method - #1022

Merged
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method
Jun 30, 2026
Merged

Disposal rate method#1022
henrydingliu merged 36 commits into
mainfrom
disposal_rate_method

Conversation

@henrydingliu

@henrydingliuhenrydingliu commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Implement the disposal rate method from Friedland FreqSev Approach 3 as an adjustment estimator

Related GitHub Issue(s)

closes#931

Additional Context for Reviewers

  • Created a DisposalMixin for the new adjustment estimator and Triangle

    • Berquist-Sherman also uses disposal rate. will leave harmonization to another PR
    • We may eventually want to split Common into DevelopmentMixin, IncrementalMixin, etc.
  • Added disposal_rate_tri to Triangle, mimicking age_to_age/link_ratio

  • Added is_disposal_rate to Triangle, in order to differentiate the proper path in incr_to_cum and cum_to_incr

  • 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
Changes core triangle cum/incr behavior for a new pattern type and introduces a reserving path that overwrites projected triangles from ultimates; mistakes could affect forecast outputs but scope is isolated behind disposal-rate flags and new estimator usage.

Overview
Adds Friedland disposal-rate reserving as public DisposalRate, which fits a cumulative “% of ultimate” emergence pattern from a triangle and a required sample_weight ultimate, then projects the lower triangle via derived LDFs.

Triangle gains disposal_rate_tri, an is_disposal_rate flag, and DisposalMixin (disposal_rate_ / incr_disposal_rate_) mixed into TriangleBase. incr_to_cum / cum_to_incr now treat disposal-rate patterns as additive cumulative ratios instead of multiplicative link-ratio patterns.

MethodBase.validate_weight is refactored to a @staticmethod for reuse from DisposalRate. Tests cover Friedland textbook fidelity, chainladder parity, sparse backends, and required-weight validation; DisposalRate is wired into the public API.

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

Comment threadchainladder/adjustments/disposal.py Outdated
Comment threadchainladder/adjustments/tests/test_disposal.py Outdated
Comment threadchainladder/adjustments/disposal.py Outdated
@codecov

codecovBot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39130% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (1dddf97) to head (210ec67).
⚠️ Report is 4 commits behind head on main.

Files with missing linesPatch %Lines
chainladder/adjustments/disposal.py97.43%1 Missing and 1 partial ⚠️
chainladder/core/triangle.py96.15%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #1022 +/- ##
==========================================
+ Coverage 89.47% 89.70% +0.22% 
==========================================
Files 91 91 Lines 5284 5343 +59 Branches 681 692 +11 ==========================================
+ Hits 4728 4793 +65 + Misses 388 384 -4 + Partials 168 166 -2 
FlagCoverage Δ
unittests89.70% <97.39%> (+0.22%)⬆️

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.

Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review June 21, 2026 09:26
@genedan

Copy link
Copy Markdown
Member

Just letting you guys know that I can look through this today.

cumulative=True,
)
dr = cl.DisposalRate(n_periods = 5, average = 'simple', drop_high = 1, drop_low = 1).fit_transform(X=tri,sample_weight=ult_tri)
assert np.all(abs(dr.disposal_.round(3).values.flatten() - [.200,.433,.585,.710,.791,.862,.882,.912,1.000] <=0.001))

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 wonder if we should be using a higher level of precision than what Friedland has. That is, have the expectation values at the precision of the atol fixture.

Since this test passes, I think the numbers are correct, so we'd just change the rounding to 4 places and the expectation values at 4 decimals.

Interestingly, Friedland doesn't round on page 253 and also doesn't show the precision, so it looks like the paper isn't 100% consistent in that regard.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

i'll add a new assertion statement with round(,4).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

got to full matching at 3 decimals.

52., 45., 13., 19., 56.,
76., 49., 43., 12., 18., 54.,
67., 55., 36., 31., 9., 13., 39.,
140., 91., 75., 49., 42., 12., 18., 53.

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.

Friedland has 43 incremental claims at AY 2008 at 72 months. I calculated (609 - 127) / (1 - .2) (.862 - .791) = 42.7775.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

fat finger. will fix

Comment threadchainladder/adjustments/tests/test_disposal.py
Comment threadchainladder/adjustments/disposal.py Outdated
ult = cl.Chainladder().fit(clrd['IncurLoss']).ultimate_
dr = cl.DisposalRate().fit_transform(clrd['CumPaidLoss'],sample_weight = ult)

Once we apply this adjustment method via a `fit_transform`, we can examin the emergence pattern via `disposal_rate_tri`.

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.

Small typo in "examine"

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

will fix

Comment threadchainladder/adjustments/disposal.py Outdated
@genedan

Copy link
Copy Markdown
Member

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

@henrydingliu

Copy link
Copy Markdown
MemberAuthor

Do you think we should incorporate some of the latter steps in the method into the package - such as the severity trend selection (even though Friedland just wound up doing a judgmental 5%), and the tail selection of the incremental paid severities?

Or is the package already capable of these things for the Friedland recreation?

My current approach is to build enough basic functionality within the package such that they can be manually linked together to recreate friedland. That's why I split out TriangleWeight in #941, to handle the different regression periods for the severity trend selections. Once you help me work out the kinks on this, the next blocker is #1024. (Was really hoping one of the younger contributors could help me out on that one; reading latex formulas for #952 gave me enough headaches for this calendar year). if i run into more blockers after that, i'll create more issues.

i haven't thought too much about long term if all this piecemeal functionality should be combined into a new freqsev method. we can decide as a collective once we can see a completed chapter 11 recreation. maybe it'll be too much hacking (i tried hacking the severity regression periods but it got too ugly) and we'll want to do some more integration. but if it's like, iono, a dozen or so lines and it's all using the public API (no numpy hacking tri.values), it could be a cool example of how to be innovative with the package. right now we are building a lego model based on the instructions. but the magic comes from combining the pieces in weird and unexpected ways. i mean, that's also the whole end goal of having a well-typed, well-doc'd API.

there is one major refactor that i'm fairly certain i want to push for at some point, but can't yet clearly articulate the right requirements. and that's the entire votingchainladder and pipeline workflow folder of the package. within friedland, and also fairly common in actual practice, actuaries are using reported and paid to estimate the same loss ultimate or using reported and closed to estimate the same count ultimate. votingchainladder currently doesn't support weighing between reported triangle and paid triangle. pipeline currently doesn't support using reported ultimate to recast closed disposal rate. so i think for votingchainladder and pipeline to be more than window dressing, it needs to accommodate multi-triangle interaction. and then however we refactor votingchainladder and pipeline will likely result in some further harmonizing on all the dev transformers, adjustment transformers, and ibnr predictors. not sure if all this would be 1.0 work. we can decide after an initial friedland recreation.

@cursorcursorBot 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 335ddca. Configure here.

Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu marked this pull request as draft June 26, 2026 22:44
@genedangenedan mentioned this pull request Jun 27, 2026
6 tasks
@github-actions

github-actionsBot commented Jun 27, 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): 14.1% of exported symbols fully typed (175 / 1237)

KnownAmbiguousUnknownTotal
Project (head)1751079551237

Other symbols referenced but not exported by chainladder: 13

KnownAmbiguousUnknownTotal
Other (head)31913

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): 40.0% fully typed (12 / 30)

KnownAmbiguousUnknownTotal
Patch1201830
Patch symbol details
SymbolStatusChange
chainladder.adjustments.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalMixin.incr_disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.X_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.__init__❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.average✅ knownnew
chainladder.adjustments.disposal.DisposalRate.disposal_rate_❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.disposal_w_✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.drop_above✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_below✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_high✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_low✅ knownnew
chainladder.adjustments.disposal.DisposalRate.drop_valuation✅ knownnew
chainladder.adjustments.disposal.DisposalRate.fit❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.fit_transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.n_periods✅ knownnew
chainladder.adjustments.disposal.DisposalRate.preserve✅ knownnew
chainladder.adjustments.disposal.DisposalRate.transform❌ unknownnew
chainladder.adjustments.disposal.DisposalRate.xp❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_cl_parity❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_friedland_fidelity✅ knownnew
chainladder.adjustments.tests.test_disposal.test_no_disposal_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_no_weight_exception❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_setting_incr❌ unknownnew
chainladder.adjustments.tests.test_disposal.test_sparse_transform❌ unknownnew
chainladder.core.triangle.Triangle.disposal_rate_tri✅ knownnew
chainladder.core.triangle.Triangle.is_disposal_rate✅ knownnew

restructuring disposal attributes into mixin class
@henrydingliu
henrydingliu marked this pull request as ready for review June 28, 2026 05:19
Comment threadchainladder/adjustments/disposal.py Outdated
@henrydingliu
henrydingliu merged commit 921bfca into mainJun 30, 2026
18 checks passed
@henrydingliu
henrydingliu deleted the disposal_rate_method branch June 30, 2026 01:31
henrydingliu added a commit that referenced this pull request Jun 30, 2026
Sign up for freeto 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.

[FEAT] Disposal Rate Method from Friedland FreqSev Approach #3

2 participants

@henrydingliu@genedan