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 diff --git a/chainladder/development/barnzehn.py b/chainladder/development/barnzehn.py index 20ae0b79..d4253ab3 100644 --- a/chainladder/development/barnzehn.py +++ b/chainladder/development/barnzehn.py @@ -33,6 +33,35 @@ class BarnettZehnwirth(TweedieGLM): gamma: list of int iota: list of int + Examples + -------- + 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:: + + import chainladder as cl + + .. testcode:: + + import numpy as np + + tri = cl.load_sample("abc") + 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:: + + 11.837 + 12.151 + """ 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..e4ff9177 100644 --- a/chainladder/development/clark.py +++ b/chainladder/development/clark.py @@ -54,6 +54,58 @@ class ClarkLDF(DevelopmentBase): norm_resid_: Triangle The "Normalized" Residuals of the model according to Clark. + Examples + -------- + ``growth`` selects the incremental curve; the first LDF cell moves slightly + between ``loglogistic`` (default) and ``weibull``. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import numpy as np + + tri = cl.load_sample("ukmotor") + 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 + 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) + """ def __init__( diff --git a/chainladder/development/glm.py b/chainladder/development/glm.py index c44bb0d5..a3d50f56 100644 --- a/chainladder/development/glm.py +++ b/chainladder/development/glm.py @@ -76,6 +76,48 @@ class TweedieGLM(DevelopmentBase): ---------- model_: sklearn.Pipeline A scikit-learn Pipeline of the GLM + + Examples + -------- + ``design_matrix`` controls which patsy terms enter the GLM; dropping + ``C(origin)`` changes the first fitted LDF. + + .. testsetup:: + + import chainladder as cl + + .. 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") + m = cl.TweedieGLM(power=0, link="identity").fit(tri) + print(float(np.round(m.ldf_.values[0, 0, 0, 0], 2))) + + .. testoutput:: + + 2.31 + """ def __init__(self, design_matrix='C(development) + C(origin)', diff --git a/chainladder/development/incremental.py b/chainladder/development/incremental.py index fb0537e0..82266857 100644 --- a/chainladder/development/incremental.py +++ b/chainladder/development/incremental.py @@ -69,6 +69,47 @@ class IncrementalAdditive(DevelopmentBase): A triangle of full incremental values. + Examples + -------- + Basic fit on ``ia_sample`` with exposure on the latest diagonal. + + .. 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) + + ``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 041a9abc..c07a12a4 100644 --- a/chainladder/development/learning.py +++ b/chainladder/development/learning.py @@ -50,6 +50,81 @@ class DevelopmentML(DevelopmentBase): The estimated loss development patterns. cdf_: Triangle The estimated cumulative development patterns. + + Examples + -------- + ``fit_incrementals`` toggles whether the pipeline fits on incrementals + versus cumulatives, which shifts the implied ``ldf_``. + + .. 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") + 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, + 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:: + + 3.515 + 3.4459 + """ 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..d42d6306 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,14 +71,30 @@ 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 + + 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): @@ -217,7 +233,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 @@ -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:])