Skip to content

Friedland Chapter 11 - #1347

Open
henrydingliu wants to merge 10 commits into
casact:mainfrom
henrydingliu:chapter_11_only
Open

henrydingliu wants to merge 10 commits into
casact:mainfrom
henrydingliu:chapter_11_only

Conversation

@henrydingliu

@henrydingliu henrydingliu commented Sep 11, 2026

Copy link
Copy Markdown
Member

Summary of Changes

Related GitHub Issue(s)

Additional Context for Reviewers

Checklist

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

Note

Low Risk
Documentation-only addition (notebook + TOC); no runtime or API changes to the library.

Overview
Adds Friedland Chapter 11 to the Jupyter Book as docs/friedland/chapter_11.ipynb and links it in docs/_toc.yml after Chapter 10.

The notebook walks through frequency–severity reserving without a dedicated library API: it chains Development, Chainladder, TailConstant, Trend, TriangleWeight, DisposalRate, and ParallelogramOLF with triangle arithmetic to match the textbook exhibits. Exhibits I–II use Approach 1 (separate count and severity development, then multiply). III–IV extend Approach 2 with trended frequency/severity, payroll/on-level premium, and tort reform. V–VI use Approach 3 (disposal rates to complete incremental counts, incremental paid severity with regression/trend, projected paid ultimates).

Shared helpers format exhibit tables; each exhibit section ends with assert checks against Friedland reference values to catch regressions when the notebook is executed.

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

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Pyright Type Completeness

View the full pyright --verifytypes output for this commit

Project (full chainladder package, at this PR's head): 15.1% of exported symbols fully typed (208 / 1380)

Known Ambiguous Unknown Total
Project (head) 208 111 1061 1380

Other symbols referenced but not exported by chainladder: 13

Known Ambiguous Unknown Total
Other (head) 3 1 9 13

Symbols without documentation:

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

Patch (exported symbols added or changed by this PR): no exported symbol type-completeness changes detected.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread docs/friedland/chapter_11.ipynb
Comment thread docs/friedland/chapter_11.ipynb
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.76%. Comparing base (d7479a4) to head (42476e0).
⚠️ Report is 26 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1347   +/-   ##
=======================================
  Coverage   91.76%   91.76%           
=======================================
  Files          96       96           
  Lines        5475     5475           
  Branches      706      706           
=======================================
  Hits         5024     5024           
  Misses        327      327           
  Partials      124      124           
Flag Coverage Δ
unittests 91.76% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread docs/friedland/chapter_11.ipynb

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6f77eee. Configure here.

Comment thread docs/friedland/chapter_11.ipynb Outdated
@henrydingliu
henrydingliu marked this pull request as ready for review September 12, 2026 04:15
@henrydingliu

Copy link
Copy Markdown
Member Author

@kennethshsu carrying over the conversation from #1176

On the "selected LDFs", in my exhibit, I actually did it both ways, that one is "selected" using the cl.Development(), and the other equivalent using cl.DevelopmentConstant(). Exactly same workflow, but it shows one analysis is continuous and the other is like "pause" and "wait for input". I know some people absolutely likes the first while the other likes the second, but either way it's only 1 more like to add and I would recommend that.

I'm not following. Can you provide a more specific example?

I basically provided two workflows to get the "selected LDFs" in. The first way is to actually "select" the averaging method, and the second way is to "select" (or input) the LDFs manually using cl.DevelopmentConstant(). I know some users prefer the 2nd way (the old me), and just recently switched over to the 1st way. For example:

# the first way
dev_reported = cl.Development(average="simple").fit(reported)
display(np.round(dev_reported.ldf_.to_frame(), 3))

# the second way
selected_ldf = {
    12: 2.532,
    24: 1.921,
    36: 1.503,
    48: 1.170,
    60: 1.206,
    72: 1.052,
    84: 1.027,
    96: 1.000,
}
avg_ldf = np.round(dev_reported.ldf_.to_frame().values.flatten(), 3)
selected_ldf = dict(zip([12, 24, 36, 48, 60, 72, 84, 96], [*avg_ldf, 1.000]))
reported_dev = cl.DevelopmentConstant(patterns=selected_ldf, style="ldf").fit(reported)

I gotta be honest here. I don't think the second way is what we should be doing, outside of spots where no triangles were given. running two half-marathons (triangle to ldf, override ldf to ultimate) is not a full marathon (triangle to ultimate).

Exhibit I Sheet 1 (and others) "Percent Closed" 1 / e1_ccc_selected.cdf_.round(3), rounding need to be on the outside.

So Friedland actually rounds twice, once on the CDF for the calc then once more on the PCT for display. If I round inside, the resulting 4 decimal pct always reconcile to the text. If I round outside, there are corner cases where the resulting 3 decimal pct doesn't actually reconcile to the text.

Can we do this? Does this reconcile? Basically just follow the text to actually recreate the numbers.

(1 / e1_ccc_selected.cdf_.round(3)).round(3)

i don't know how we got here. when i started chapter 7, your initial feedback was, should we even need to do any rounding? and now we are trying to match every single number. these numbers clearly reconcile.
image
image

Traspose Exhibit III Sheet 10?

There is no way to have different number formats in the same column. See Exhibit IV Sheet 3. Let me know which one you prefer.

Oh good point... Ehh... I wonder in this case if it's easier to just break them up into mini DFs, one per row? There are pros and cons to everything. I don't feel strongly about this either way, but it did take me a second to realize that the table is just transposed.

I used stylers to make it work

Exhibit V Sheet 7 asserts: one is checking latest 4, the other latest 6, why?

Sampling different averaging periods

Fine haha, if it were me I would probably check all 4 rows in both scenarios.

added

@kennethshsu

Copy link
Copy Markdown
Member

@kennethshsu carrying over the conversation from #1176

On the "selected LDFs", in my exhibit, I actually did it both ways, that one is "selected" using the cl.Development(), and the other equivalent using cl.DevelopmentConstant(). Exactly same workflow, but it shows one analysis is continuous and the other is like "pause" and "wait for input". I know some people absolutely likes the first while the other likes the second, but either way it's only 1 more like to add and I would recommend that.

I'm not following. Can you provide a more specific example?

I basically provided two workflows to get the "selected LDFs" in. The first way is to actually "select" the averaging method, and the second way is to "select" (or input) the LDFs manually using cl.DevelopmentConstant(). I know some users prefer the 2nd way (the old me), and just recently switched over to the 1st way. For example:

# the first way
dev_reported = cl.Development(average="simple").fit(reported)
display(np.round(dev_reported.ldf_.to_frame(), 3))

# the second way
selected_ldf = {
    12: 2.532,
    24: 1.921,
    36: 1.503,
    48: 1.170,
    60: 1.206,
    72: 1.052,
    84: 1.027,
    96: 1.000,
}
avg_ldf = np.round(dev_reported.ldf_.to_frame().values.flatten(), 3)
selected_ldf = dict(zip([12, 24, 36, 48, 60, 72, 84, 96], [*avg_ldf, 1.000]))
reported_dev = cl.DevelopmentConstant(patterns=selected_ldf, style="ldf").fit(reported)

I gotta be honest here. I don't think the second way is what we should be doing, outside of spots where no triangles were given. running two half-marathons (triangle to ldf, override ldf to ultimate) is not a full marathon (triangle to ultimate).

I agree, except that this is the "traditional" way of doing things, even Friedland herself. After the age-to-age factors are studied, we make a selection, so there's always a pause to select LDFs (i.e. a half marathon). If you don't want to show it, that's fine, I just think it would be helpful to the readers.

Exhibit I Sheet 1 (and others) "Percent Closed" 1 / e1_ccc_selected.cdf_.round(3), rounding need to be on the outside.

So Friedland actually rounds twice, once on the CDF for the calc then once more on the PCT for display. If I round inside, the resulting 4 decimal pct always reconcile to the text. If I round outside, there are corner cases where the resulting 3 decimal pct doesn't actually reconcile to the text.

Can we do this? Does this reconcile? Basically just follow the text to actually recreate the numbers.

(1 / e1_ccc_selected.cdf_.round(3)).round(3)

i don't know how we got here. when i started chapter 7, your initial feedback was, should we even need to do any rounding? and now we are trying to match every single number. these numbers clearly reconcile. image image

I still prefer the rounding to be perfect, you either don't round and say the difference is due to rounding, or round and get the numbers to match exactly. I don't think it's ok to round, and still don't get the numbers to match exactly (i.e. in this case to 3 decimal vs 4 decimal, which is just truncated by display). But will approve and let it up to you! Not gonna argue with you over rounding procedures :)

kennethshsu
kennethshsu previously approved these changes Sep 14, 2026
@henrydingliu

Copy link
Copy Markdown
Member Author

@kennethshsu carrying over the conversation from #1176

On the "selected LDFs", in my exhibit, I actually did it both ways, that one is "selected" using the cl.Development(), and the other equivalent using cl.DevelopmentConstant(). Exactly same workflow, but it shows one analysis is continuous and the other is like "pause" and "wait for input". I know some people absolutely likes the first while the other likes the second, but either way it's only 1 more like to add and I would recommend that.

I'm not following. Can you provide a more specific example?

I basically provided two workflows to get the "selected LDFs" in. The first way is to actually "select" the averaging method, and the second way is to "select" (or input) the LDFs manually using cl.DevelopmentConstant(). I know some users prefer the 2nd way (the old me), and just recently switched over to the 1st way. For example:

# the first way
dev_reported = cl.Development(average="simple").fit(reported)
display(np.round(dev_reported.ldf_.to_frame(), 3))

# the second way
selected_ldf = {
    12: 2.532,
    24: 1.921,
    36: 1.503,
    48: 1.170,
    60: 1.206,
    72: 1.052,
    84: 1.027,
    96: 1.000,
}
avg_ldf = np.round(dev_reported.ldf_.to_frame().values.flatten(), 3)
selected_ldf = dict(zip([12, 24, 36, 48, 60, 72, 84, 96], [*avg_ldf, 1.000]))
reported_dev = cl.DevelopmentConstant(patterns=selected_ldf, style="ldf").fit(reported)

I gotta be honest here. I don't think the second way is what we should be doing, outside of spots where no triangles were given. running two half-marathons (triangle to ldf, override ldf to ultimate) is not a full marathon (triangle to ultimate).

I agree, except that this is the "traditional" way of doing things, even Friedland herself. After the age-to-age factors are studied, we make a selection, so there's always a pause to select LDFs (i.e. a half marathon). If you don't want to show it, that's fine, I just think it would be helpful to the readers.

i'm really not following. ldf_ is the selected ldf based on the parameters fed into Development. in this snippet, you are hardcoding selected_ldf and then immediately overriding it. we don't do anything like this elsewhere in the tutorials or user guides. how would this be useful to the reader?

@kennethshsu

Copy link
Copy Markdown
Member

I''m just saying, typically, one would analyze the age-to-age triangles and come up with many different estimated LDFs using different averages, you then "select" something, and by definition, this is a pause in the workflow. By using cl.DevelopmentConstant(...), you allow for this pause a bit more naturally. If you don't want to include this workflow because you think we should discourage it, that's fine. I think letting the LDFs flow through with fit_transform() is the better way anyways, but some might wonder how to make this "selection" manually.

@henrydingliu

Copy link
Copy Markdown
Member Author

I''m just saying, typically, one would analyze the age-to-age triangles and come up with many different estimated LDFs using different averages, you then "select" something, and by definition, this is a pause in the workflow. By using cl.DevelopmentConstant(...), you allow for this pause a bit more naturally. If you don't want to include this workflow because you think we should discourage it, that's fine. I think letting the LDFs flow through with fit_transform() is the better way anyways, but some might wonder how to make this "selection" manually.

hmmm, this is making me think i can't accept your approval.

in the text, multiple averages are calculated.
image

this is also happening in the recreation.
image

i'm doing this very explicitly in the code

# loading data and assumptions
e1_tri = cl.load_sample("friedland_auto_freq_sev")
e1_cnt_assumptions = {}
e1_cnt_assumptions["simple_5"] = {"n_periods": 5, "average": "simple"}
e1_cnt_assumptions["simple_3"] = {"n_periods": 3, "average": "simple"}
e1_cnt_assumptions["medial_5x1"] = {
    "n_periods": 5,
    "average": "simple",
    "drop_high": 1,
    "drop_low": 1,
}
...
# developing closed claim counts
e1_ccc_devs = average_dev(e1_tri["Closed Claim Counts"], e1_cnt_assumptions)
e1_ccc_selected = cl.TailConstant(tail=1.0, projection_period=0).fit_transform(
    e1_ccc_devs["simple_3"]
)

what am i not doing that your snippet is doing? the actual typing out of selected ldfs, is that it?

@kennethshsu

Copy link
Copy Markdown
Member

Yap, that's it.

There can be a scenario where you are selecting different averages, for example switching between simple_5 and going to volume_3. In the text it looks like we are just copying simple_5, but do we know for sure? It is a selection after all. Yes I know there's a way to chain multiple "selections" inside the package with fit_transform(), but using cl.DevelopmenConstant() is just a more traditional way of making actual LDF selections.

@henrydingliu

Copy link
Copy Markdown
Member Author

@kennethshsu i updated the rounding and somehow this isn't triggering a re-review. is this what you would expect?

@kennethshsu

Copy link
Copy Markdown
Member

I think that should fix it, I migrated to the new GitHub rulesets a few weeks ago and I think you guys had that other issue in the GitHub folder right? This should fix it.

kennethshsu
kennethshsu previously approved these changes Sep 15, 2026
@kennethshsu

Copy link
Copy Markdown
Member

Don't know if you want to make another dummy commit to test

@henrydingliu

Copy link
Copy Markdown
Member Author

Don't know if you want to make another dummy commit to test

seems to be working! thanks!

In the text it looks like we are just copying simple_5, but do we know for sure?

lol, were you one of those who just read the study notes and never read the text? Friedland tells us exactly which selections she's making
image

but using cl.DevelopmenConstant() is just a more traditional way of making actual LDF selections

iono dude. using DevelopmentConstant is basically like hardcoding values in Excel. Feel more inefficient than traditional

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants