From 00951503d81ebef0bd45b28f0ddca96693db2147 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 14 May 2026 00:20:55 -0700 Subject: [PATCH 1/2] Add workflow to sync main into docs/great-docs-prototype On every push to main, GitHub Actions automatically merges main into docs/great-docs-prototype so the docs branch stays current with all code changes while keeping its doc-engine files intact. --- .github/workflows/sync-main-to-docs.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/sync-main-to-docs.yml diff --git a/.github/workflows/sync-main-to-docs.yml b/.github/workflows/sync-main-to-docs.yml new file mode 100644 index 00000000..ae11e38f --- /dev/null +++ b/.github/workflows/sync-main-to-docs.yml @@ -0,0 +1,25 @@ +name: Sync main to docs/great-docs-prototype + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge main into docs/great-docs-prototype + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout docs/great-docs-prototype + git merge origin/main --no-edit -m "chore: sync main into docs/great-docs-prototype" + git push origin docs/great-docs-prototype From 992622e9d217d9ccc4d17135eeac99c4da94aa00 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Sat, 13 Jun 2026 20:57:47 -0700 Subject: [PATCH 2/2] Document creating triangles with preexisting ultimates Add Triangle constructor examples and expand _split_ult docstring for round-trip and direct import via options.ULT_VAL. Add test_create_triangle_with_ultimates. Closes #523. Co-authored-by: Cursor --- chainladder/core/tests/test_triangle.py | 31 +++++++++++ chainladder/core/triangle.py | 71 ++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index dc1a7c4c..b8c0a1be 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -403,6 +403,37 @@ def test_create_full_triangle(raa): assert a == b +def test_create_triangle_with_ultimates(raa): + """Round-trip and direct import of triangles with ultimate values.""" + ult = cl.Chainladder().fit(raa).ultimate_ + round_tripped = cl.Triangle( + ult.to_frame(keepdims=True, origin_as_datetime=True), + origin="origin", + development="valuation", + columns="values", + cumulative=True, + ) + assert round_tripped.is_ultimate + assert round_tripped == ult + + direct = cl.Triangle( + pd.DataFrame( + { + "origin": pd.to_datetime(["1981-01-01", "1982-01-01"]), + "valuation": pd.to_datetime( + [cl.options.ULT_VAL, cl.options.ULT_VAL] + ), + "paid": [10000.0, 12000.0], + } + ), + origin="origin", + development="valuation", + columns="paid", + cumulative=True, + ) + assert direct.is_ultimate + + def test_groupby_getitem(clrd): assert ( clrd.groupby("LOB")["CumPaidLoss"].sum() diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index a6a33b52..e6e23b18 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -335,6 +335,62 @@ class Triangle(TriangleBase): 2024Q2 130.0 2024Q3 160.0 2024Q4 140.0 + + Triangles with ultimate values + ------------------------------ + + Triangles produced by reserving methods carry ultimate projections at the + sentinel valuation date ``options.ULT_VAL`` (default December 31, 2261). + Export with ``to_frame(keepdims=True)`` and reconstruct by passing + ``development='valuation'``. Rows whose valuation equals + ``options.ULT_VAL`` are recognized as ultimates and stored in the ultimate + development column (see also :attr:`is_ultimate`). + + .. testcode:: + + raa = cl.load_sample('raa') + ult = cl.Chainladder().fit(raa).ultimate_ + df = ult.to_frame(keepdims=True, origin_as_datetime=True) + tri = cl.Triangle( + df, + origin='origin', + development='valuation', + columns='values', + cumulative=True, + ) + print(tri.is_ultimate) + + .. testoutput:: + + True + + Pre-existing ultimate estimates can be supplied directly in long-format + data by setting the valuation column to ``options.ULT_VAL`` for each + origin. + + .. testcode:: + + df = pd.DataFrame( + data={ + 'origin': pd.to_datetime(['1981-01-01', '1982-01-01']), + 'valuation': pd.to_datetime([ + cl.options.ULT_VAL, cl.options.ULT_VAL + ]), + 'paid': [10000.0, 12000.0], + } + ) + tri = cl.Triangle( + df, + origin='origin', + development='valuation', + columns='paid', + cumulative=True, + ) + print(tri.is_ultimate) + + .. testoutput:: + + True """ def __init__( @@ -572,7 +628,20 @@ def _split_ult( origin: list, development: list ) -> tuple[DataFrame, Triangle]: - """Deal with triangles with ultimate values.""" + """Split ultimate valuation rows from long-format triangle data. + + Ultimate rows are those where the development column equals + ``options.ULT_VAL``. This supports round-tripping triangles exported + via :meth:`~chainladder.Triangle.to_frame` with ``keepdims=True`` and + ``development='valuation'``. It also allows importing pre-existing + ultimate estimates by marking the valuation column with + ``options.ULT_VAL``. + + Requires a single datetime development column. When ultimate rows are + present (and not every row is an ultimate), they are extracted and + merged back as the ultimate development column after the base triangle + is constructed. + """ ult = None if ( development