diff --git a/chainladder/methods/mack.py b/chainladder/methods/mack.py index 96e4a4081..0af8a48ce 100644 --- a/chainladder/methods/mack.py +++ b/chainladder/methods/mack.py @@ -1,6 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +import numpy as np import pandas as pd from chainladder.methods import Chainladder @@ -39,6 +40,12 @@ class MackChainladder(Chainladder): The total prediction error by origin period total_mack_std_err_: The total prediction error across all origin periods + cdr_std_err_: + The one-year standard error of the claims development result by + origin period (Merz and Wüthrich 2008, ``merz2008``) + total_cdr_std_err_: + The one-year standard error of the claims development result across + all origin periods See Also -------- @@ -465,6 +472,197 @@ def _get_total_mack_std_err_(self, obj): out = obj.sqrt().values[..., 0, -1] return pd.DataFrame(out, index=obj.index, columns=obj.columns) + @property + def cdr_std_err_(self): + """ + One-year standard error of the claims development result by origin + period, Result 3.1 (eq. 3.9) of Merz and Wüthrich (2008, ``merz2008``). + + The claims development result is the change in an origin's estimated + ultimate once the next diagonal is observed. ``mack_std_err_`` is the + counterpart to ultimate. The values follow the ``sigma_interpolation`` + of :class:`Development`, and missing cells above the latest diagonal + give NaN. + + Returns + ------- + Triangle + One value per origin, shaped like ``ultimate_``, NaN for the + fully developed oldest origin. + + Raises + ------ + ValueError + If the triangle is not square, has a tail, or its development + factors are not volume-weighted over every link ratio. + + Examples + -------- + The triangle of Merz and Wüthrich (2008), with Mack's extrapolation + for the last sigma. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('mw2008') + dev = cl.Development(sigma_interpolation='mack').fit_transform(tr) + model = cl.MackChainladder().fit(dev) + print(model.cdr_std_err_) + + .. testoutput:: + + 2261 + 2001 NaN + 2002 566.174395 + 2003 1486.560344 + 2004 3923.098608 + 2005 9722.859763 + 2006 28442.621556 + 2007 20954.286973 + 2008 28119.317963 + 2009 53320.821049 + """ + obj = self.ultimate_.set_backend("numpy") + obj.values = np.sqrt(self._get_cdr_msep()[0])[..., None] + return obj.set_backend(self.X_.array_backend) + + @property + def total_cdr_std_err_(self): + """ + One-year standard error of the claims development result summed over + all origin periods, Result 3.3 (eq. 3.15) of Merz and Wüthrich (2008, + ``merz2008``). It exceeds the quadrature sum of ``cdr_std_err_`` + because all origins share the estimated factors, and it raises + ``ValueError`` where ``cdr_std_err_`` does. + + Returns + ------- + DataFrame + One value per (index, column) combination of the fitted Triangle. + + Examples + -------- + On the paper's triangle the one-year error is about three quarters of + Mack's error to ultimate. + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('mw2008') + dev = cl.Development(sigma_interpolation='mack').fit_transform(tr) + model = cl.MackChainladder().fit(dev) + print(model.total_cdr_std_err_) + print(model.total_mack_std_err_) + + .. testoutput:: + + columns values + (Total,) 81080.546787 + columns values + (Total,) 108401.387451 + """ + obj = self.X_ + out = np.sqrt(self._get_cdr_msep()[1]) + return pd.DataFrame(out, index=obj.index, columns=obj.columns) + + def _get_cdr_msep(self): + """ + Mean squared errors of the one-year claims development result. + + Returns the per-origin values, shape (index, columns, origin), and + the totals over origins, shape (index, columns), from Results 3.1 and + 3.3 of ``merz2008`` with I = J = n - 1. Origins i and development + periods j are 0-based, as in the paper, and the equation numbers in + the comments are the paper's. + """ + from chainladder.development import Development + + X = self.X_.set_backend("numpy") + C = X if X.is_cumulative else X.incr_to_cum() + a, b, n, m = C.shape + msg = "cdr_std_err_ and total_cdr_std_err_ need " + upper = np.add.outer(np.arange(n), np.arange(m)) <= n - 1 + if n != m or not np.array_equal(~np.isnan(X.nan_triangle), upper): + raise ValueError( + msg + "a square triangle valued at its latest diagonal; this " + f"one has {n} origins and {m} development periods." + ) + J = m - 1 + ldf = X.ldf_.set_backend("numpy").values[..., 0, :] + sigma = X.sigma_.set_backend("numpy").values[..., 0, :] + if not ( + np.all(np.nan_to_num(ldf[..., J:], nan=1.0) == 1) + and np.all(np.nan_to_num(sigma[..., J:]) == 0) + ): + raise ValueError( + msg + "a pattern without a tail, as Merz and Wüthrich (2008) " + "assume; fit without a tail estimator or with TailConstant(1.0)." + ) + average = set(np.asarray(X.average_).ravel()) + if average != {"volume"}: + raise ValueError( + msg + "volume-weighted development factors; this pattern uses " + f"{sorted(str(v) for v in average)}." + ) + # A default Development on C gives the paper's factors (2.9), and the + # fitted w_ and ldf_ must match it. + fresh = Development().fit(C) + w, w_fresh = np.broadcast_arrays(np.asarray(X.w_), np.asarray(fresh.w_)) + if not np.array_equal(w, w_fresh, equal_nan=True): + raise ValueError( + msg + "every link ratio of the triangle; this pattern excludes " + "some (drop, drop_high, drop_low, drop_valuation, n_periods)." + ) + f = ldf[..., :J] # f_j^I, (2.9) + if not np.allclose( + f, fresh.ldf_.values[..., 0, :], rtol=1e-8, atol=0, equal_nan=True + ): + raise ValueError( + msg + "development factors estimated from this triangle's " + "own link ratios; ldf_ differs from them, as after " + "Development(fillna=...)." + ) + latest = C.latest_diagonal.values[..., 0] # C_{i, I-i} + diag = latest[..., ::-1][..., :J] # C_{I-j, j} + S1 = C.sum(axis="origin").values[..., 0, :J] # S_j^{I+1}, (2.10) + S = S1 - diag # S_j^I, (2.9) + ult = self.ultimate_.set_backend("numpy").values[..., 0] # C_{i,J}^I, (2.11) + zero = np.zeros((a, b, 1)) + + def later(t): + """Sum of t over the positions after each position, last axis.""" + return np.concatenate((np.cumsum(t[..., ::-1], -1)[..., -2::-1], zero), -1) + + with np.errstate(divide="ignore", invalid="ignore"): + q = sigma[..., :J] ** 2 / f**2 # sigma_j^2 (3.3) over (f_j^I)^2 + i = np.arange(1, n) # origins with a next diagonal + d = J - i # their next development period, I - i + qd, Sd, S1d, Ci = q[..., d], S[..., d], S1[..., d], latest[..., i] + # Sums over j = I-i+1 .. J-1: the summands of Phi (3.5), and those + # of the sum that Delta (3.4) and Lambda (3.13) share. + Phi = later(diag * q / S1**2)[..., d] + shared = later((diag / S1) ** 2 * q / S)[..., d] + Psi = qd / Ci # (3.6) + Delta = qd / Sd + shared # (3.4) + Lam = Ci / S1d * qd / Sd + shared # Lambda, (3.13) + Xi = Phi + qd / S1d # (3.14) + msep = np.full((a, b, n), np.nan) + # (3.9), with Gamma = Phi + Psi (3.7) + msep[..., 1:] = ult[..., i] ** 2 * (Phi + Psi + Delta) + # (3.15): 2 * sum over k > i > 0 of C_{i,J} C_{k,J} (Xi + Lambda) + cross = ult[..., i] * later(ult[..., 1:]) * (Xi + Lam) + total = msep[..., 1:].sum(-1) + 2 * cross.sum(-1) + complete = ~np.isnan(C.values[..., upper]).any(-1) + msep[~complete] = np.nan + total[~complete] = np.nan + return msep, total + @property def summary_(self): """ diff --git a/chainladder/methods/tests/test_mack.py b/chainladder/methods/tests/test_mack.py index 9e9bfb9a0..e9ada0036 100644 --- a/chainladder/methods/tests/test_mack.py +++ b/chainladder/methods/tests/test_mack.py @@ -1,6 +1,63 @@ +""" +Tests for MackChainladder. + +test_mack1994_hardcode and test_mack1999_hardcode check the Mack attributes +against mack1994 and mack1999. The tests from test_cdr_matches_merz2008 on +cover the one-year claims development result (CDR) and cite sources by their +keys in docs/library/references.bib. What these CDR tests check for each +attribute, by source: + +======================== ======== ======== ============ +Attribute merz2008 merz2014 per-triangle +======================== ======== ======== ============ +ldf_, sigma_ Table 2 Table 1 -- +ibnr_ -- Table 2 -- +process_risk_ -- Table 2 -- +parameter_risk_ -- Table 2 -- +mack_std_err_ Table 4 Table 2 -- +total_process_risk_ -- Table 2 -- +total_parameter_risk_ -- Table 2 -- +total_mack_std_err_ Table 4 Table 2 -- +cdr_std_err_ Table 4 -- clrd +total_cdr_std_err_ Table 4 Table 3 clrd +======================== ======== ======== ============ + +merz2008 + The paper prints Tables 2 and 4 for its own triangle, the sample mw2008, + with Mack's extrapolation of the last sigma (sigma_interpolation="mack"). + Table 2 (p. 560) gives the factors to 4 decimals and sigma^2 to 2. Table 4 + (p. 562) gives the one-year and Mack columns per accident year, the + cov^(1/2) row and the totals, in whole units of $1,000. +merz2014 + Table 1 (p. 28) of this paper is the 17-year triangle of the sample + mw2014, fitted with Mack's extrapolation too (p. 25). Page numbers are + those of the version dated July 3, 2015. Table 1 gives the factors to 5 + decimals and sigma^2 to 4. Table 2 (p. 30) gives, per accident year and in + total, the reserve and the square roots of the process variance, the + parameter uncertainty and Mack's msep, in whole units, with the Mack + total to 1 decimal. Table 3 (p. 31), row t = 17, gives the square root of + the one-year uncertainty in total, 1,843. The paper prints no one-year + figures per accident year, so the per-origin CDR is checked on mw2008 only. +per-triangle + A fit on clrd summed by LOB, paid and incurred, gives each of its twelve + triangles the values of a fit on that triangle alone. + +test_cdr_matches_merz2008 and test_cdr_matches_merz2014 state the tolerance +for each printed figure. The mw2008, mw2014 and clrd fixtures run these checks +on the numpy and sparse backends. Other CDR tests check that an incremental +triangle gives the values of its cumulative form, that a triangle with a +missing cell gets NaN, that each unsupported development pattern fits and then +raises ValueError on reading the CDR attributes, and that reading them leaves +the Mack attributes as they were. +""" + +from __future__ import annotations + import numpy as np +import pandas as pd import chainladder as cl +import pytest def test_mack_to_triangle(): @@ -84,3 +141,265 @@ def test_mack1994_hardcode(raa): assert np.allclose(sigma_sq[6:8], sigma_rhs[6:8], atol=0.01) ibnr_rhs = model.summary_.values[0, 0, :, -1] assert np.allclose(ibnr_se, ibnr_rhs[1:], atol=1) + + +# merz2008, Table 2 (p. 560): f_j^I and sigma_j^2 for j = 0 to 7. +TABLE2_LDF = [1.4759, 1.0719, 1.0232, 1.0161, 1.0063, 1.0056, 1.0013, 1.0011] +TABLE2_SIGMA2 = [911.43, 189.82, 97.81, 178.75, 20.64, 3.23, 0.36, 0.04] + +# merz2008, Table 4 (p. 562), accident years 1 to 8 (the sample's origins 2002 +# to 2009): the one-year column, msep^(1/2) of the observable CDR around 0, and +# Mack's column. Then the cov^(1/2) row and the totals of the two columns. +TABLE4_CDR = [567, 1488, 3923, 9723, 28443, 20954, 28119, 53320] +TABLE4_MACK = [567, 1566, 4157, 10536, 30319, 35967, 45090, 69552] +TABLE4_COV = [39746, 50361] +TABLE4_TOTAL = [81080, 108401] + +# fmt: off +# merz2014, Table 1 (p. 28): f_j and sigma_j^2 for j = 0 to 15, the last +# sigma_j^2 by Mack's extrapolation. +MW2014_LDF = [ + 1.51105, 1.05369, 1.02681, 1.01709, 1.01284, 1.01098, 1.00292, 1.01098, + 1.00698, 1.00141, 1.00574, 1.00386, 1.00359, 1.00042, 1.00035, 1.00004, +] +MW2014_SIGMA2 = [ + 29.4993, 16.8327, 2.6478, 5.4444, 3.2477, 11.4238, 0.1882, 2.6132, + 2.0863, 0.0542, 0.9079, 0.5397, 0.7202, 0.0079, 0.0002, 0.0000, +] + +# merz2014, Table 2 (p. 30), accident years 2 to 17 (the sample's origins +# 2002 to 2017; accident year 1 prints 0 throughout): the reserve, then the +# square roots of the process variance, the parameter uncertainty and Mack's +# msep. +MW2014_TABLE2 = { + "ibnr_": [ + 1, 10, 21, 118, 223, 362, 469, 654, + 1009, 1012, 1407, 1493, 1918, 2458, 3384, 9597, + ], + "process_risk_": [ + 0, 2, 15, 140, 186, 238, 262, 358, + 464, 452, 760, 756, 851, 877, 1067, 1256, + ], + "parameter_risk_": [ + 0, 2, 9, 72, 92, 109, 129, 156, + 192, 179, 271, 242, 265, 265, 293, 319, + ], + "mack_std_err_": [ + 0, 3, 17, 157, 207, 262, 292, 391, + 502, 486, 807, 794, 892, 916, 1106, 1296, + ], +} +# fmt: on +# merz2014, Table 2 (p. 30): the totals of the same four columns. +MW2014_TABLE2_TOTAL = [24135, 2467, 2090, 3233.7] + +# merz2014, Table 3 (p. 31), row t = 17: the square root of the one-year +# uncertainty in total. +MW2014_CDR_TOTAL = 1843 + +CDR_UNSUPPORTED = { + "tail": ( + lambda t: cl.TailCurve().fit_transform(cl.Development().fit_transform(t)), + "without a tail", + ), + "n_periods": (lambda t: cl.Development(n_periods=5).fit_transform(t), "every"), + "drop_high": (lambda t: cl.Development(drop_high=1).fit_transform(t), "every"), + "drop": (lambda t: cl.Development(drop=("2003", 12)).fit_transform(t), "every"), + "simple": ( + lambda t: cl.Development(average="simple").fit_transform(t), + "volume-weighted", + ), + "fillna": (lambda t: cl.Development(fillna=1.0).fit_transform(t), "own link"), + "not_square": (lambda t: t[t.origin < "2009"], "square"), +} + + +def mack_sigma_fit(tri): + """MackChainladder with Mack's extrapolation of the last sigma.""" + dev = cl.Development(sigma_interpolation="mack").fit_transform(tri) + return cl.MackChainladder().fit(dev) + + +def by_origin(triangle): + """Values of a per-origin Triangle as an array, origin on the last axis.""" + return triangle.set_backend("numpy").values[..., -1] + + +def assert_within(actual, expected, tol): + """Assert that every entry of actual is less than tol from expected.""" + gap = np.abs(np.asarray(actual) - expected) + np.testing.assert_array_less(gap, np.broadcast_to(tol, gap.shape)) + + +def test_cdr_matches_merz2008(mw2008): + """ + Four copies of mw2008, two index rows by two columns, each give the + figures of merz2008. + + Table 2 prints the factors to 4 decimals and sigma^2 to 2. The factors + round to the printed ones, and the printed sigma^2 are within 0.015 of + the estimates. Table 4 prints whole units of $1,000. Every entry is within + 1 except accident year 2, printed as 1,488 and 1,566 against estimates of + 1,486.6 and 1,563.8, and checked to 2.5. The cov^(1/2) row is the square + root of the total's square less the sum of the per-origin squares. + Accident year 1 has one development period left, so its one-year and Mack + errors are equal (Table 4 prints 567 in both columns). + """ + copy = mw2008.copy() + copy.index = pd.DataFrame({mw2008.key_labels[0]: ["Copy"]}) + tri = cl.concat([mw2008, copy], axis=0) + tri["copy"] = tri["values"] + dev = cl.Development(sigma_interpolation="mack").fit_transform(tri) + model = cl.MackChainladder().fit(dev) + assert model.cdr_std_err_.shape == (2, 2, 9, 1) + ldf = dev.ldf_.set_backend("numpy").values[..., 0, :] + sigma2 = dev.sigma_.set_backend("numpy").values[..., 0, :] ** 2 + assert_within(ldf, TABLE2_LDF, 5e-5) + assert_within(sigma2, TABLE2_SIGMA2, 0.015) + + cdr = by_origin(model.cdr_std_err_) + mack = by_origin(model.mack_std_err_) + assert np.isnan(cdr[..., 0]).all() and np.isnan(mack[..., 0]).all() + tol = np.where(np.arange(1, 9) == 2, 2.5, 1.0) + assert_within(cdr[..., 1:], TABLE4_CDR, tol) + assert_within(mack[..., 1:], TABLE4_MACK, tol) + total = np.stack( + (model.total_cdr_std_err_.values, model.total_mack_std_err_.values), -1 + ) + assert_within(total, TABLE4_TOTAL, 1) + squares = np.stack((np.nansum(cdr**2, -1), np.nansum(mack**2, -1)), -1) + assert_within(np.sqrt(total**2 - squares), TABLE4_COV, 1) + np.testing.assert_allclose(cdr[..., 1], mack[..., 1], rtol=1e-12) + + +def test_cdr_matches_merz2014(mw2014): + """ + mw2014 gives the figures that merz2014 prints for it. + + Every figure is within half a unit in its last printed decimal: the + factors of Table 1 to 5e-6 and sigma^2 to 5e-5; the reserve and the three + errors of Table 2 per accident year, and their totals, to 0.5, except the + Mack total, printed to 1 decimal and checked to 0.05; the one-year total + of Table 3 to 0.5. Table 3 gives the one-year uncertainty for each + accounting year t; the test reads its row t = 17, the latest diagonal, as + total_cdr_std_err_. Table 2 checks process_risk_ and parameter_risk_ as + the two parts of Mack's msep. Accident year 1 has no reserve: the paper + prints 0, and chainladder gives 0 or NaN. + """ + dev = cl.Development(sigma_interpolation="mack").fit_transform(mw2014) + model = cl.MackChainladder().fit(dev) + ldf = dev.ldf_.set_backend("numpy").values[..., 0, :] + sigma2 = dev.sigma_.set_backend("numpy").values[..., 0, :] ** 2 + assert_within(ldf, MW2014_LDF, 5e-6) + assert_within(sigma2, MW2014_SIGMA2, 5e-5) + + for attr, printed in MW2014_TABLE2.items(): + values = by_origin(getattr(model, attr)) + assert (np.nan_to_num(values[..., 0]) == 0).all() + assert_within(values[..., 1:], printed, 0.5) + totals = [ + np.nansum(by_origin(model.ibnr_)), + by_origin(model.total_process_risk_).item(), + by_origin(model.total_parameter_risk_).item(), + model.total_mack_std_err_.values.item(), + ] + assert_within(totals, MW2014_TABLE2_TOTAL, [0.5, 0.5, 0.5, 0.05]) + assert_within(model.total_cdr_std_err_.values, MW2014_CDR_TOTAL, 0.5) + + +def test_cdr_multi_triangle_matches_single(clrd): + """ + A fit on several triangles gives each the values of its own fit. + + clrd summed by LOB, paid and incurred, is twelve complete triangles. Both + attributes of the joint fit match a fit on each triangle alone. + """ + tri = clrd.groupby("LOB").sum()[["CumPaidLoss", "IncurLoss"]] + model = cl.MackChainladder().fit(tri) + per = by_origin(model.cdr_std_err_) + total = model.total_cdr_std_err_.values + assert np.isfinite(total).all() + for i in range(len(tri.index)): + for j in range(len(tri.columns)): + single = cl.MackChainladder().fit(tri.iloc[i, j]) + np.testing.assert_allclose( + per[i, j], by_origin(single.cdr_std_err_)[0, 0], rtol=1e-10 + ) + np.testing.assert_allclose( + total[i, j], single.total_cdr_std_err_.values[0, 0], rtol=1e-10 + ) + + +def test_cdr_incremental_matches_cumulative(mw2008): + """ + An incremental triangle gives the values of its cumulative form. + + The estimators of merz2008 are defined on cumulative claims, and the CDR + attributes accumulate an incremental fit before using it. mw2008 fitted + as cum_to_incr() and as it is gives the same per-origin and total errors. + """ + cumulative = mack_sigma_fit(mw2008) + incremental = mack_sigma_fit(mw2008.cum_to_incr()) + np.testing.assert_allclose( + by_origin(incremental.cdr_std_err_), + by_origin(cumulative.cdr_std_err_), + rtol=1e-12, + ) + np.testing.assert_allclose( + incremental.total_cdr_std_err_.values, + cumulative.total_cdr_std_err_.values, + rtol=1e-12, + ) + + +def test_cdr_missing_cell_gives_nan(mw2008): + """ + A triangle with a missing cell above the latest diagonal gets NaN. + + The estimators of merz2008 use every cell of the upper triangle. Removing + one cell from mw2008 leaves MackChainladder able to fit, and both CDR + attributes are NaN. + """ + tri = mw2008.set_backend("numpy") + tri.values = tri.values.copy() + tri.values[0, 0, 3, 1] = np.nan + model = mack_sigma_fit(tri.set_backend(mw2008.array_backend)) + assert np.isfinite(model.total_mack_std_err_.values).all() + assert np.isnan(by_origin(model.cdr_std_err_)).all() + assert np.isnan(model.total_cdr_std_err_.values).all() + + +@pytest.mark.parametrize("name", list(CDR_UNSUPPORTED)) +def test_cdr_unsupported_pattern_raises_on_read(name, mw2008): + """ + Unsupported development patterns fit, then raise on reading the CDR. + + merz2008 assumes a square triangle, volume-weighted factors from every + link ratio of the triangle and no tail. MackChainladder still fits each + pattern here and gives its Mack standard errors; only the two CDR + attributes raise ValueError. + """ + transform, message = CDR_UNSUPPORTED[name] + model = cl.MackChainladder().fit(transform(mw2008)) + assert np.isfinite(model.total_mack_std_err_.values).all() + with pytest.raises(ValueError, match=message): + _ = model.cdr_std_err_ + with pytest.raises(ValueError, match=message): + _ = model.total_cdr_std_err_ + + +def test_reading_cdr_leaves_mack_unchanged(mw2008): + """ + Reading the CDR attributes leaves the fitted Mack attributes as they were. + + cdr_std_err_ and total_cdr_std_err_ are computed from the fitted model + each time they are read. After reading both, summary_ and + total_mack_std_err_ hold the values they had before. + """ + model = mack_sigma_fit(mw2008) + summary = model.summary_.set_backend("numpy").values.copy() + total = model.total_mack_std_err_.values.copy() + assert np.isfinite(by_origin(model.cdr_std_err_)[..., 1:]).all() + assert np.isfinite(model.total_cdr_std_err_.values).all() + np.testing.assert_array_equal(model.summary_.set_backend("numpy").values, summary) + np.testing.assert_array_equal(model.total_mack_std_err_.values, total) diff --git a/conftest.py b/conftest.py index 7818dadaf..9ad2c7a87 100644 --- a/conftest.py +++ b/conftest.py @@ -49,6 +49,14 @@ "data": "xyz", "runs": ["normal_run", "sparse_only_run"], }, + "mw2008": { + "data": "mw2008", + "runs": ["normal_run", "sparse_only_run"], + }, + "mw2014": { + "data": "mw2014", + "runs": ["normal_run", "sparse_only_run"], + }, } diff --git a/docs/library/references.bib b/docs/library/references.bib index 9d849aefd..4e10e1c81 100644 --- a/docs/library/references.bib +++ b/docs/library/references.bib @@ -103,3 +103,20 @@ @article{boor2006 year = {2006}, url = {https://www.casact.org/sites/default/files/database/forum_06wforum_06w348.pdf} } + +@article{merz2008, + author = {Merz, M. and Wüthrich, M. V.}, + title = {Modelling the {C}laims {D}evelopment {R}esult for {S}olvency {P}urposes}, + journal = {Casualty Actuarial Society E-Forum, Fall 2008}, + pages = {542--568}, + year = {2008}, + url = {https://www.casact.org/abstract/modeling-claims-development-result-solvency-purposes} +} + +@article{merz2014, + author = {Merz, M. and Wüthrich, M. V.}, + title = {Claims {R}un-{O}ff {U}ncertainty: {T}he {F}ull {P}icture}, + journal = {Swiss Finance Institute Research Paper No. 14-69}, + year = {2014}, + url = {https://doi.org/10.2139/ssrn.2524352} +} diff --git a/docs/user_guide/methods.ipynb b/docs/user_guide/methods.ipynb index 7bab41c2c..9cb27c033 100644 --- a/docs/user_guide/methods.ipynb +++ b/docs/user_guide/methods.ipynb @@ -549,6 +549,17 @@ "`Chainladder` should be used.\n", "```\n", "\n", + "### One-year view\n", + "\n", + "`mack_std_err_` is the prediction error of the reserve to ultimate. For a\n", + "one-year view, `cdr_std_err_` and `total_cdr_std_err_` give the standard error\n", + "of the claims development result, the change in the estimated ultimate once\n", + "the next diagonal is observed {cite}`merz2008`. They need a square triangle,\n", + "volume-weighted factors from every link ratio and no tail, and they follow the\n", + "`sigma_interpolation` of `Development`. With `sigma_interpolation=\"mack\"` they\n", + "are within 1.5 of every one-year figure in Table 4 of the paper, whose triangle\n", + "is the sample `mw2008`.\n", + "\n", "### Examples\n", "\n", "::::{grid}\n",