From 00951503d81ebef0bd45b28f0ddca96693db2147 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 14 May 2026 00:20:55 -0700 Subject: [PATCH 1/3] 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 28e5c3a7fc9be9628844f3f01d602edc5ea93949 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 14 May 2026 01:37:36 -0700 Subject: [PATCH 2/3] docs: add doctest Examples for development estimators and fix Mack directives Add Examples sections using .. testsetup::, .. testcode::, and .. testoutput:: for BarnettZehnwirth, ClarkLDF, IncrementalAdditive, TweedieGLM, and DevelopmentML. Place import chainladder in testsetup and other imports in testcode. Correct MackChainladder class and full_std_err_ docstrings to use double-colon directives and .. testoutput:: so Sphinx doctests run them. Co-authored-by: Cursor --- chainladder/development/barnzehn.py | 24 ++++++++++++++++ chainladder/development/clark.py | 24 ++++++++++++++++ chainladder/development/glm.py | 26 +++++++++++++++++ chainladder/development/incremental.py | 22 +++++++++++++++ chainladder/development/learning.py | 39 ++++++++++++++++++++++++++ chainladder/methods/mack.py | 12 ++++---- 6 files changed, 141 insertions(+), 6 deletions(-) diff --git a/chainladder/development/barnzehn.py b/chainladder/development/barnzehn.py index 20ae0b79..49b3f383 100644 --- a/chainladder/development/barnzehn.py +++ b/chainladder/development/barnzehn.py @@ -33,6 +33,30 @@ class BarnettZehnwirth(TweedieGLM): gamma: list of int iota: list of int + Examples + -------- + Fit a Probabilistic Trend Family model with a patsy formula on the ``abc`` + sample triangle. Coefficients summarize origin and development effects on + logged incremental losses. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import numpy as np + + tri = cl.load_sample("abc") + model = cl.BarnettZehnwirth(formula="C(origin)+C(development)").fit(tri) + print(repr(model)) + print([float(x) for x in np.round(model.coef_.values.flatten()[:3], 3)]) + + .. testoutput:: + + BarnettZehnwirth(formula='C(origin)+C(development)') + [11.837, 0.179, 0.345] + """ def __init__(self, drop=None,drop_valuation=None,formula=None, response=None, alpha=None, gamma=None, iota=None): diff --git a/chainladder/development/clark.py b/chainladder/development/clark.py index f8e1db17..ee0f14b9 100644 --- a/chainladder/development/clark.py +++ b/chainladder/development/clark.py @@ -54,6 +54,30 @@ class ClarkLDF(DevelopmentBase): norm_resid_: Triangle The "Normalized" Residuals of the model according to Clark. + Examples + -------- + Fit Clark's LDF curve to a cumulative triangle. The estimator recovers + smooth age-to-age factors implied by a loglogistic (or Weibull) growth + curve. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import numpy as np + + tri = cl.load_sample("ukmotor") + model = cl.ClarkLDF(growth="loglogistic").fit(tri) + print(float(np.round(model.ldf_.values[0, 0, 0, 0], 6))) + print(float(np.round(model.theta_.values[0, 0], 4))) + + .. testoutput:: + + 1.917318 + 30.3322 + """ def __init__( diff --git a/chainladder/development/glm.py b/chainladder/development/glm.py index c44bb0d5..4ad7df49 100644 --- a/chainladder/development/glm.py +++ b/chainladder/development/glm.py @@ -76,6 +76,32 @@ class TweedieGLM(DevelopmentBase): ---------- model_: sklearn.Pipeline A scikit-learn Pipeline of the GLM + + Examples + -------- + With the default one-hot design on origin and development, a Poisson + (``power=1``) GLM on incremental counts produces development factors that + align closely with the volume-weighted chainladder for typical triangles. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tri = cl.load_sample("genins") + glm = cl.TweedieGLM().fit(tri) + print(repr(glm)) + dev = glm.transform(tri) + ult_glm = cl.Chainladder().fit(dev).ultimate_.values[0, 0, 0, -1] + ult_cl = cl.Chainladder().fit(tri).ultimate_.values[0, 0, 0, -1] + print(ult_glm == ult_cl) + + .. testoutput:: + + TweedieGLM() + True + """ def __init__(self, design_matrix='C(development) + C(origin)', diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index fb0537e0..88a4e440 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -68,6 +68,28 @@ class IncrementalAdditive(DevelopmentBase): incremental_: Triangle A triangle of full incremental values. + Examples + -------- + The incremental additive method expresses each incremental as a share of + exposure, then applies averaged patterns. Pass cumulative losses as ``X`` + and an exposure triangle (often latest-diagonal weights) as + ``sample_weight``. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tri = cl.load_sample("ia_sample") + model = cl.IncrementalAdditive().fit( + tri["loss"], sample_weight=tri["exposure"].latest_diagonal + ) + print(model.ldf_.shape) + + .. testoutput:: + + (1, 1, 6, 5) """ diff --git a/chainladder/development/learning.py b/chainladder/development/learning.py index 041a9abc..b46c1f6d 100644 --- a/chainladder/development/learning.py +++ b/chainladder/development/learning.py @@ -50,6 +50,45 @@ class DevelopmentML(DevelopmentBase): The estimated loss development patterns. cdf_: Triangle The estimated cumulative development patterns. + + Examples + -------- + Wrap a scikit-learn ``Pipeline`` whose first step builds a design matrix + (here via :class:`~chainladder.utils.utility_functions.PatsyFormula`) and + whose last step is a regression with no intercept. The fitted ``ldf_`` + summarizes implied link ratios. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import numpy as np + from sklearn.linear_model import LinearRegression + from sklearn.pipeline import Pipeline + + from chainladder.utils.utility_functions import PatsyFormula + + tri = cl.load_sample("genins") + est = cl.DevelopmentML( + Pipeline( + steps=[ + ("design_matrix", PatsyFormula("C(development)")), + ("model", LinearRegression(fit_intercept=False)), + ] + ), + y_ml=[tri.columns[0]], + fit_incrementals=False, + ).fit(tri) + print(est.ldf_.shape) + print(float(np.round(est.ldf_.values[0, 0, 0, 0], 6))) + + .. testoutput:: + + (1, 1, 10, 9) + 3.515035 + """ def __init__(self, estimator_ml=None, y_ml=None, autoregressive=False, diff --git a/chainladder/methods/mack.py b/chainladder/methods/mack.py index cfcf71e0..44e63acc 100644 --- a/chainladder/methods/mack.py +++ b/chainladder/methods/mack.py @@ -46,17 +46,17 @@ class MackChainladder(Chainladder): which combines the deterministic chainladder estimate with Mack's stochastic standard error. - .. testsetup: + .. testsetup:: import chainladder as cl - .. testcode: + .. testcode:: tr = cl.load_sample('ukmotor') model = cl.MackChainladder().fit(tr) print(model.summary_) - .. testoutput: + .. testoutput:: Latest IBNR Ultimate Mack Std Err 2007 12690.0 NaN 12690.000000 NaN @@ -71,11 +71,11 @@ class MackChainladder(Chainladder): :class:`Chainladder`. Mack's contribution is the stochastic standard error in the rightmost column, which can be aggregated across origins. - .. testcode: + .. testcode:: print(model.total_mack_std_err_) - .. testoutput: + .. testoutput:: columns values (Total,) 1424.531543 @@ -217,7 +217,7 @@ def full_std_err_(self): model = cl.MackChainladder().fit(tr) print(model.full_std_err_) - .. testoutput + .. testoutput:: 12 24 36 48 60 72 84 2007 0.047826 0.040745 0.031412 0.010337 0.001431 0.001523 0.0 From 1df94ecc97f35d1c4e3ec8eed552be96a1ee7454 Mon Sep 17 00:00:00 2001 From: Ethan Kang Date: Thu, 14 May 2026 09:50:38 -0700 Subject: [PATCH 3/3] docs: expand doctest examples for parameter effects, fix Mack directives Restore original class Parameters/Attributes text from main. Add Examples with .. testsetup:: / .. testcode:: / .. testoutput:: showing how outputs change for ClarkLDF (growth, sample_weight, groupby), BarnettZehnwirth (formula vs PTF), IncrementalAdditive (shape and future_trend), TweedieGLM (design_matrix and power/link), DevelopmentML (fit_incrementals and weighted_step), and MackChainladder (Development averaging before fit). Fix class-level Mack doctest directives to use :: and repair full_std_err_ testoutput::; add blank line after testsetup in total_process_risk example. Co-authored-by: Cursor --- chainladder/development/barnzehn.py | 21 +++++--- chainladder/development/clark.py | 42 +++++++++++++--- chainladder/development/glm.py | 38 +++++++++----- chainladder/development/incremental.py | 27 ++++++++-- chainladder/development/learning.py | 68 ++++++++++++++++++++------ chainladder/methods/mack.py | 19 +++++++ 6 files changed, 169 insertions(+), 46 deletions(-) diff --git a/chainladder/development/barnzehn.py b/chainladder/development/barnzehn.py index 49b3f383..d4253ab3 100644 --- a/chainladder/development/barnzehn.py +++ b/chainladder/development/barnzehn.py @@ -35,9 +35,9 @@ class BarnettZehnwirth(TweedieGLM): Examples -------- - Fit a Probabilistic Trend Family model with a patsy formula on the ``abc`` - sample triangle. Coefficients summarize origin and development effects on - logged incremental losses. + A patsy ``formula`` and the built-in ``alpha`` / ``gamma`` / ``iota`` PTF + specification can both fit the same triangle; the leading fitted + coefficient differs because the design matrices differ. .. testsetup:: @@ -48,14 +48,19 @@ class BarnettZehnwirth(TweedieGLM): import numpy as np tri = cl.load_sample("abc") - model = cl.BarnettZehnwirth(formula="C(origin)+C(development)").fit(tri) - print(repr(model)) - print([float(x) for x in np.round(model.coef_.values.flatten()[:3], 3)]) + m_formula = cl.BarnettZehnwirth( + formula="C(origin)+C(development)" + ).fit(tri) + m_ptf = cl.BarnettZehnwirth( + alpha=[0, 5], gamma=[0, 2, 5], iota=[0, 7, 11] + ).fit(tri) + print(float(np.round(m_formula.coef_.values.flatten()[0], 3))) + print(float(np.round(m_ptf.coef_.values.flatten()[0], 3))) .. testoutput:: - BarnettZehnwirth(formula='C(origin)+C(development)') - [11.837, 0.179, 0.345] + 11.837 + 12.151 """ diff --git a/chainladder/development/clark.py b/chainladder/development/clark.py index ee0f14b9..e4ff9177 100644 --- a/chainladder/development/clark.py +++ b/chainladder/development/clark.py @@ -56,9 +56,8 @@ class ClarkLDF(DevelopmentBase): Examples -------- - Fit Clark's LDF curve to a cumulative triangle. The estimator recovers - smooth age-to-age factors implied by a loglogistic (or Weibull) growth - curve. + ``growth`` selects the incremental curve; the first LDF cell moves slightly + between ``loglogistic`` (default) and ``weibull``. .. testsetup:: @@ -69,14 +68,43 @@ class ClarkLDF(DevelopmentBase): import numpy as np tri = cl.load_sample("ukmotor") - model = cl.ClarkLDF(growth="loglogistic").fit(tri) - print(float(np.round(model.ldf_.values[0, 0, 0, 0], 6))) - print(float(np.round(model.theta_.values[0, 0], 4))) + m_log = cl.ClarkLDF(growth="loglogistic").fit(tri) + m_wei = cl.ClarkLDF(growth="weibull").fit(tri) + print(float(np.round(m_log.ldf_.values[0, 0, 0, 0], 6))) + print(float(np.round(m_wei.ldf_.values[0, 0, 0, 0], 6))) .. testoutput:: 1.917318 - 30.3322 + 1.911706 + + Passing ``sample_weight`` switches to Cape Cod: ``method_`` becomes + ``cape_cod`` and ``elr_`` is estimated. + + .. testcode:: + + tri = cl.load_sample("ukmotor") + m = cl.ClarkLDF().fit(tri, sample_weight=tri * 0 + 1e7) + print(m.method_) + print(float(np.round(m.elr_.values[0, 0], 6))) + + .. testoutput:: + + cape_cod + 0.002002 + + ``groupby`` pools index levels before fitting so one parameter set is + returned per group (here, line of business on ``clrd``). + + .. testcode:: + + clrd = cl.load_sample("clrd").groupby("LOB")[["IncurLoss"]].sum() + m = cl.ClarkLDF(groupby="LOB").fit(clrd) + print(m.theta_.shape) + + .. testoutput:: + + (6, 1) """ diff --git a/chainladder/development/glm.py b/chainladder/development/glm.py index 4ad7df49..a3d50f56 100644 --- a/chainladder/development/glm.py +++ b/chainladder/development/glm.py @@ -79,9 +79,8 @@ class TweedieGLM(DevelopmentBase): Examples -------- - With the default one-hot design on origin and development, a Poisson - (``power=1``) GLM on incremental counts produces development factors that - align closely with the volume-weighted chainladder for typical triangles. + ``design_matrix`` controls which patsy terms enter the GLM; dropping + ``C(origin)`` changes the first fitted LDF. .. testsetup:: @@ -89,18 +88,35 @@ class TweedieGLM(DevelopmentBase): .. testcode:: + import numpy as np + + tri = cl.load_sample("genins") + m_full = cl.TweedieGLM( + power=1, design_matrix="C(development) + C(origin)" + ).fit(tri) + m_dev = cl.TweedieGLM(power=1, design_matrix="C(development)").fit(tri) + print(float(np.round(m_full.ldf_.values[0, 0, 0, 0], 4))) + print(float(np.round(m_dev.ldf_.values[0, 0, 0, 0], 4))) + + .. testoutput:: + + 3.491 + 3.5085 + + ``power`` and ``link`` select the Tweedie family; a Normal GLM + (``power=0`` with ``link='identity'``) yields a different pattern. + + .. testcode:: + + import numpy as np + tri = cl.load_sample("genins") - glm = cl.TweedieGLM().fit(tri) - print(repr(glm)) - dev = glm.transform(tri) - ult_glm = cl.Chainladder().fit(dev).ultimate_.values[0, 0, 0, -1] - ult_cl = cl.Chainladder().fit(tri).ultimate_.values[0, 0, 0, -1] - print(ult_glm == ult_cl) + m = cl.TweedieGLM(power=0, link="identity").fit(tri) + print(float(np.round(m.ldf_.values[0, 0, 0, 0], 2))) .. testoutput:: - TweedieGLM() - True + 2.31 """ diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index 88a4e440..82266857 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -68,12 +68,10 @@ class IncrementalAdditive(DevelopmentBase): incremental_: Triangle A triangle of full incremental values. + Examples -------- - The incremental additive method expresses each incremental as a share of - exposure, then applies averaged patterns. Pass cumulative losses as ``X`` - and an exposure triangle (often latest-diagonal weights) as - ``sample_weight``. + Basic fit on ``ia_sample`` with exposure on the latest diagonal. .. testsetup:: @@ -91,6 +89,27 @@ class IncrementalAdditive(DevelopmentBase): (1, 1, 6, 5) + ``future_trend`` (when non-zero) changes extrapolated incrementals in the + lower triangle even when ``trend`` is held at zero; here the summed + fitted incrementals increase. + + .. testcode:: + + import numpy as np + + tri = cl.load_sample("ia_sample") + loss = tri["loss"] + sw = tri["exposure"].latest_diagonal + m0 = cl.IncrementalAdditive(trend=0, future_trend=0).fit(loss, sample_weight=sw) + m1 = cl.IncrementalAdditive(trend=0, future_trend=0.1).fit(loss, sample_weight=sw) + print(float(np.round(np.nansum(m0.incremental_.values), 1))) + print(float(np.round(np.nansum(m1.incremental_.values), 1))) + + .. testoutput:: + + 30988.1 + 33360.1 + """ def __init__( diff --git a/chainladder/development/learning.py b/chainladder/development/learning.py index b46c1f6d..c07a12a4 100644 --- a/chainladder/development/learning.py +++ b/chainladder/development/learning.py @@ -53,10 +53,8 @@ class DevelopmentML(DevelopmentBase): Examples -------- - Wrap a scikit-learn ``Pipeline`` whose first step builds a design matrix - (here via :class:`~chainladder.utils.utility_functions.PatsyFormula`) and - whose last step is a regression with no intercept. The fitted ``ldf_`` - summarizes implied link ratios. + ``fit_incrementals`` toggles whether the pipeline fits on incrementals + versus cumulatives, which shifts the implied ``ldf_``. .. testsetup:: @@ -71,23 +69,61 @@ class DevelopmentML(DevelopmentBase): from chainladder.utils.utility_functions import PatsyFormula tri = cl.load_sample("genins") - est = cl.DevelopmentML( - Pipeline( - steps=[ - ("design_matrix", PatsyFormula("C(development)")), - ("model", LinearRegression(fit_intercept=False)), - ] - ), + pipe = Pipeline( + steps=[ + ("design_matrix", PatsyFormula("C(development)")), + ("model", LinearRegression(fit_intercept=False)), + ] + ) + m_incr = cl.DevelopmentML( + pipe, y_ml=[tri.columns[0]], fit_incrementals=True + ).fit(tri) + m_cum = cl.DevelopmentML( + pipe, y_ml=[tri.columns[0]], fit_incrementals=False + ).fit(tri) + print(float(np.round(m_incr.ldf_.values[0, 0, 0, 0], 4))) + print(float(np.round(m_cum.ldf_.values[0, 0, 0, 0], 4))) + + .. testoutput:: + + 3.508 + 3.515 + + With ``weighted_step='model'``, ``sample_weight`` is forwarded into the + final regressor; squaring the triangle as a crude weight changes the first + LDF versus an unweighted fit. + + .. testcode:: + + import numpy as np + from sklearn.linear_model import LinearRegression + from sklearn.pipeline import Pipeline + + from chainladder.utils.utility_functions import PatsyFormula + + tri = cl.load_sample("genins") + pipe = Pipeline( + steps=[ + ("design_matrix", PatsyFormula("C(development)")), + ("model", LinearRegression(fit_intercept=False)), + ] + ) + m0 = cl.DevelopmentML( + pipe, y_ml=[tri.columns[0]], fit_incrementals=False + ).fit(tri) + m1 = cl.DevelopmentML( + pipe, y_ml=[tri.columns[0]], fit_incrementals=False, - ).fit(tri) - print(est.ldf_.shape) - print(float(np.round(est.ldf_.values[0, 0, 0, 0], 6))) + weighted_step="model", + ).fit(tri, sample_weight=tri * tri) + print(float(np.round(m0.ldf_.values[0, 0, 0, 0], 4))) + print(float(np.round(m1.ldf_.values[0, 0, 0, 0], 4))) .. testoutput:: - (1, 1, 10, 9) - 3.515035 + 3.515 + 3.4459 """ diff --git a/chainladder/methods/mack.py b/chainladder/methods/mack.py index 44e63acc..d42d6306 100644 --- a/chainladder/methods/mack.py +++ b/chainladder/methods/mack.py @@ -79,6 +79,22 @@ class MackChainladder(Chainladder): columns values (Total,) 1424.531543 + + Mack's total error depends on how ``ldf_`` and ``sigma_`` were produced. + Here the same triangle is pre-smoothed with :class:`Development` using + ``average='simple'`` instead of the default volume weights before fitting + ``MackChainladder``, which raises the aggregate Mack standard error. + + .. testcode:: + + tr = cl.load_sample("ukmotor") + tr_simple = cl.Development(average="simple").fit_transform(tr) + print(cl.MackChainladder().fit(tr_simple).total_mack_std_err_) + + .. testoutput:: + + columns values + (Total,) 1591.603339 """ def fit(self, X, y=None, sample_weight=None): @@ -268,6 +284,7 @@ def total_process_risk_(self): -------- .. testsetup:: + import chainladder as cl .. testcode:: @@ -340,9 +357,11 @@ def mack_std_err_(self): error per origin. .. testsetup:: + import chainladder as cl .. testcode:: + tr = cl.load_sample('ukmotor') model = cl.MackChainladder().fit(tr) print(model.mack_std_err_.iloc[..., -3:, -3:])