From 8293febb4b7ec46afdfa6b8deedff57042b5b8a9 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 2 May 2026 17:23:54 -0500 Subject: [PATCH 1/8] ENH: Continue work on type hinting, add pyright to check type coverage. --- chainladder/core/common.py | 46 ++++++++++++-- chainladder/core/triangle.py | 43 +++++++++---- chainladder/development/base.py | 45 +++++++++++-- chainladder/development/development.py | 88 ++++++++++++++++++-------- chainladder/py.typed | 0 pyproject.toml | 12 +++- pyrightconfig.json | 9 +++ uv.lock | 25 ++++++++ 8 files changed, 215 insertions(+), 53 deletions(-) create mode 100644 chainladder/py.typed create mode 100644 pyrightconfig.json diff --git a/chainladder/core/common.py b/chainladder/core/common.py index d600f06d..62d4c46a 100644 --- a/chainladder/core/common.py +++ b/chainladder/core/common.py @@ -1,13 +1,27 @@ # 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/. +from __future__ import annotations + +import numpy as np import pandas as pd + +from chainladder import options from chainladder.utils.cupy import cp -from chainladder.utils.sparse import sp from chainladder.utils.dask import dp -import numpy as np +from chainladder.utils.sparse import sp from chainladder.utils.utility_functions import concat -from chainladder import options + +from typing import ( + Callable, + Literal, + TYPE_CHECKING +) + +if TYPE_CHECKING: + from numpy.typing import ArrayLike + from chainladder.core.typing import TriangleLike + def _get_full_expectation(cdf_, ultimate_, is_cumulative=True): @@ -201,15 +215,33 @@ def set_backend( obj = self.copy() return obj.set_backend(backend=backend, inplace=True, deep=deep, **kwargs) - def _validate_assumption(self, triangle, value, axis): + @staticmethod + def _validate_assumption( + triangle: TriangleLike, + value: str | int | float | list | tuple | set | np.ndarray | dict | Callable, + axis: Literal[1, 2, 3, 4] + ) -> np.ndarray: + """ + + Parameters + ---------- + + """ if type(value) in (int, float, str): arr = np.repeat(value, triangle.shape[axis]) - if type(value) in (list, tuple, set, np.ndarray): + elif type(value) in (list, tuple, set, np.ndarray): arr = np.array(value) - if type(value) is dict: + elif type(value) is dict: arr = np.array([value[a] for a in triangle._get_axis_value(axis)]) - if callable(value): + elif callable(value): arr = np.array([value(a) for a in triangle._get_axis_value(axis)]) + else: + raise TypeError( + """ + Invalid type provided to value parameter. + Accepted types are str, int, float, list, tuple, set, ndarray, dict, or Callable. + """ + ) if axis == 3: arr = arr[None, None, None] if axis == 2: diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index 826bb959..a7caf1c9 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -919,8 +919,18 @@ def link_ratio(self) -> Triangle: Examples -------- - >>> tr = cl.load_sample('ukmotor') - >>> tr.link_ratio + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.link_ratio) + + .. testoutput:: + 12-24 24-36 36-48 48-60 60-72 72-84 2007 1.915694 1.336902 1.190391 1.098935 1.049902 1.02753 2008 1.925269 1.295729 1.118225 1.085655 1.051911 NaN @@ -1254,16 +1264,25 @@ def val_to_dev(self, inplace=False): development triangle through valuation form and back returns the original layout. - >>> tr = cl.load_sample('ukmotor') - >>> tr.dev_to_val().val_to_dev() - 12 24 36 48 60 72 84 - 2007 3511.0 6726.0 8992.0 10704.0 11763.0 12350.0 12690.0 - 2008 4001.0 7703.0 9981.0 11161.0 12117.0 12746.0 NaN - 2009 4355.0 8287.0 10233.0 11755.0 12993.0 NaN NaN - 2010 4295.0 7750.0 9773.0 11093.0 NaN NaN NaN - 2011 4150.0 7897.0 10217.0 NaN NaN NaN NaN - 2012 5102.0 9650.0 NaN NaN NaN NaN NaN - 2013 6283.0 NaN NaN NaN NaN NaN NaN + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + tr.dev_to_val().val_to_dev() + + .. testoutput:: + + 12 24 36 48 60 72 84 + 2007 3511.0 6726.0 8992.0 10704.0 11763.0 12350.0 12690.0 + 2008 4001.0 7703.0 9981.0 11161.0 12117.0 12746.0 NaN + 2009 4355.0 8287.0 10233.0 11755.0 12993.0 NaN NaN + 2010 4295.0 7750.0 9773.0 11093.0 NaN NaN NaN + 2011 4150.0 7897.0 10217.0 NaN NaN NaN NaN + 2012 5102.0 9650.0 NaN NaN NaN NaN NaN + 2013 6283.0 NaN NaN NaN NaN NaN NaN """ if not self.is_val_tri: if inplace: diff --git a/chainladder/development/base.py b/chainladder/development/base.py index 6a8b7198..0488f80c 100644 --- a/chainladder/development/base.py +++ b/chainladder/development/base.py @@ -1,17 +1,34 @@ # 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/. +from __future__ import annotations + import numpy as np import pandas as pd import warnings -from sklearn.base import BaseEstimator, TransformerMixin + +from sklearn.base import ( + BaseEstimator, + TransformerMixin +) + from chainladder.utils import WeightedRegression from chainladder.utils.utility_functions import num_to_nan from chainladder.core.io import EstimatorIO from chainladder.core.common import Common +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder.core.typing import TriangleLike -class DevelopmentBase(BaseEstimator, TransformerMixin, EstimatorIO, Common): + +class DevelopmentBase( + BaseEstimator, + TransformerMixin, + EstimatorIO, + Common +): def fit(self, X, y=None, sample_weight=None): average_ = self._validate_assumption(y, self.average, axis=3) self.average_ = average_.flatten() @@ -25,8 +42,22 @@ def fit(self, X, y=None, sample_weight=None): ) return self - def _set_fit_groups(self, X): - """Used for assigning group_index in fit""" + def _set_fit_groups( + self, + X: TriangleLike + ) -> TriangleLike: + """ + Used for assigning group_index in fit. + + Parameters + ---------- + X: TriangleLike + + Returns + ------- + TriangleLike, after performing the groupby on it. + + """ backend = "numpy" if X.array_backend in ["sparse", "numpy"] else "cupy" if self.groupby is None: return X.set_backend(backend) @@ -354,7 +385,11 @@ def _param_array_helper(self, size, param, default_value): param_array = param_array.astype(type(default_value)) return param_array.to_numpy() - def _set_weight_func(self, factor, secondary_rank=None): + def _set_weight_func( + self, + factor: TriangleLike, + secondary_rank: TriangleLike = None + ): w = (~np.isnan(factor.values)).astype(float) w = w * self._assign_n_periods_weight_func(factor) if self.drop is not None: diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 975bad33..6ab5dd16 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -1,15 +1,34 @@ # 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/. +from __future__ import annotations + import numpy as np import pandas as pd import warnings -from chainladder.utils import WeightedRegression + from chainladder.development.base import DevelopmentBase +from chainladder.utils import WeightedRegression +from chainladder.utils.utility_functions import num_to_nan + +from typing import ( + Callable, + Literal, + Optional, + Self, + TYPE_CHECKING +) + +if TYPE_CHECKING: + from chainladder.core.typing import TriangleLike + from numpy.typing import ArrayLike + from pandas import Series + from types import ModuleType class Development(DevelopmentBase): - """A Transformer that allows for basic loss development pattern selection. + """ + A Transformer that allows for basic loss development pattern selection. Parameters ---------- @@ -49,7 +68,7 @@ class Development(DevelopmentBase): excluded from the ``ldf_`` calculation. For the specific case of 'volume' averaging in a deterministic method, this may be reasonable. For all other averages and stochastic methods, this assumption should be avoided. - groupby: + groupby: Callable, list, str, Series (default = None) An option to group levels of the triangle index together for the purposes of estimating patterns. If omitted, each level of the triangle index will receive its own patterns. @@ -73,18 +92,18 @@ class Development(DevelopmentBase): def __init__( self, - n_periods=-1, - average="volume", - sigma_interpolation="log-linear", - drop=None, - drop_high=None, - drop_low=None, - preserve=1, - drop_valuation=None, - drop_above=np.inf, - drop_below=0.00, - fillna=None, - groupby=None, + n_periods: int = -1, + average: str = "volume", + sigma_interpolation: Literal['log-linear', 'mack'] = "log-linear", + drop: tuple | list[tuple] | None = None, + drop_high: bool | int | list[bool] | list[int] | None = None, + drop_low: bool | int | list[bool] | list[int] | None = None, + preserve: int = 1, + drop_valuation: str | list[str] = None, + drop_above: float = np.inf, + drop_below: float = 0.00, + fillna: float | None = None, + groupby: Callable | list | str | Series = None, ): self.n_periods = n_periods self.average = average @@ -99,12 +118,19 @@ def __init__( self.fillna = fillna self.groupby = groupby - def fit(self, X, y=None, sample_weight=None): + # Undeclared until fitted attributes - scikit-learn convention. + self.average_: np.ndarray + + def fit( + self, X: TriangleLike, + y: None = None, + sample_weight: None = None + ) -> Self: """Fit the model with X. Parameters ---------- - X : Triangle-like + X : TriangleLike Set of LDFs to which the munich adjustment will be applied. y : None Ignored @@ -116,30 +142,34 @@ def fit(self, X, y=None, sample_weight=None): self : object Returns the instance itself. """ - from chainladder.utils.utility_functions import num_to_nan - # Triangle must be cumulative and in "development" mode - obj = self._set_fit_groups(X).incr_to_cum().val_to_dev().copy() - xp = obj.get_array_module() + # Triangle must be cumulative and in "development" mode. + obj: TriangleLike = self._set_fit_groups(X).incr_to_cum().val_to_dev().copy() + xp: ModuleType = obj.get_array_module() if self.fillna: - tri_array = num_to_nan((obj + self.fillna).values) + tri_array: ArrayLike = num_to_nan((obj + self.fillna).values) else: - tri_array = num_to_nan(obj.values.copy()) + tri_array: ArrayLike = num_to_nan(obj.values.copy()) - average_ = self._validate_assumption(X, self.average, axis=3)[ + average_: np.ndarray = self._validate_assumption(X, self.average, axis=3)[ ..., : X.shape[3] - 1 ] - self.average_ = average_.flatten() - n_periods_ = self._validate_assumption(X, self.n_periods, axis=3)[ + + # noinspection PyAttributeOutsideInit + self.average_: np.ndarray = average_.flatten() + n_periods_: np.ndarray = self._validate_assumption(X, self.n_periods, axis=3)[ ..., : X.shape[3] - 1 ] + + x: ArrayLike + y: ArrayLike x, y = tri_array[..., :-1], tri_array[..., 1:] - exponent = xp.array( + exponent: ArrayLike = xp.array( [{"regression": 0, "volume": 1, "simple": 2}[x] for x in average_[0, 0, 0]] ) exponent = xp.nan_to_num(exponent * (y * 0 + 1)) - link_ratio = y / x + link_ratio: ArrayLike = y / x if hasattr(X, "w_v2_"): self.w_v2_ = self._set_weight_func( @@ -188,6 +218,8 @@ def fit(self, X, y=None, sample_weight=None): return self + + def transform(self, X): """If X and self are of different shapes, align self to X, else return self. diff --git a/chainladder/py.typed b/chainladder/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index 36f050c9..4d060dce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ dev = [ "jinja2", "ipython", "ipykernel", + "pyright", ] docs = [ "sphinx", @@ -86,6 +87,12 @@ all = [ "chainladder[dev,docs,test]", ] +# Exclude if TYPE_CHECKING block at the beginning of modules. +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:" +] + [tool.pytest.ini_options] testpaths = ["chainladder/*", "docs/_ext"] pythonpath = ["docs/_ext"] @@ -97,4 +104,7 @@ include-package-data = true include = ["chainladder", "chainladder.*"] [tool.setuptools.package-data] -chainladder = ["utils/data/*"] +chainladder = ["utils/data/*", "py.typed"] + +[tool.uv] +config-settings = {editable_mode="compat"} \ No newline at end of file diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..74bff3fc --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,9 @@ +{ + "include": ["chainladder"], + "exclude": [".venv"], + "venvPath": ".", + "extraPaths": ["."], + "venv": ".venv", + "pythonVersion": "3.14", + "typeCheckingMode": "basic" +} \ No newline at end of file diff --git a/uv.lock b/uv.lock index e4612f6c..f25210e9 100644 --- a/uv.lock +++ b/uv.lock @@ -214,6 +214,7 @@ all = [ { name = "numpydoc" }, { name = "parso" }, { name = "polars" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, @@ -228,6 +229,7 @@ dev = [ { name = "ipython", version = "9.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jinja2" }, { name = "lxml" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, @@ -286,6 +288,7 @@ requires-dist = [ { name = "parso", marker = "extra == 'docs'", specifier = ">=0.8" }, { name = "patsy" }, { name = "polars", marker = "extra == 'docs'" }, + { name = "pyright", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, @@ -1876,6 +1879,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numba" version = "0.63.1" @@ -2459,6 +2471,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] +[[package]] +name = "pyright" +version = "1.1.409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, +] + [[package]] name = "pytest" version = "9.0.3" From f667a5ad8d8fa5cae6bdf770a0c28a07bbc4458a Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 2 May 2026 20:50:57 -0500 Subject: [PATCH 2/8] FIX: Fix glob capturing py.typed. --- chainladder/development/development.py | 5 ++--- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 6ab5dd16..5d2a962d 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -14,8 +14,7 @@ from typing import ( Callable, Literal, - Optional, - Self, + # Self, # Make use of this once Python 3.10 is deprecated. TYPE_CHECKING ) @@ -125,7 +124,7 @@ def fit( self, X: TriangleLike, y: None = None, sample_weight: None = None - ) -> Self: + ): """Fit the model with X. Parameters diff --git a/pyproject.toml b/pyproject.toml index 4d060dce..c644d32b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ exclude_also = [ ] [tool.pytest.ini_options] -testpaths = ["chainladder/*", "docs/_ext"] +testpaths = ["chainladder", "docs/_ext"] pythonpath = ["docs/_ext"] [tool.setuptools] From c5305b8c37056ed6b48b8e7e9a244f8c1d8d7d84 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 2 May 2026 21:27:07 -0500 Subject: [PATCH 3/8] FIX: Fix incorrect literal. Fix exclusion of `if TYPE_CHECKING:` block. --- .coveragerc | 2 ++ chainladder/core/common.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 79f03f1f..f786e10c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,8 @@ omit = *tests* [report] show_missing = True +exclude_also = + if TYPE_CHECKING: # Regexes for lines to exclude from consideration exclude_lines = diff --git a/chainladder/core/common.py b/chainladder/core/common.py index 62d4c46a..7ad6e1f5 100644 --- a/chainladder/core/common.py +++ b/chainladder/core/common.py @@ -219,7 +219,7 @@ def set_backend( def _validate_assumption( triangle: TriangleLike, value: str | int | float | list | tuple | set | np.ndarray | dict | Callable, - axis: Literal[1, 2, 3, 4] + axis: Literal[0, 1, 2, 3] ) -> np.ndarray: """ From 1012136121561904673581e82c86857c35f414e0 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 2 May 2026 21:43:01 -0500 Subject: [PATCH 4/8] FIX: Apply bugbot fixes. --- pyproject.toml | 6 ------ pyrightconfig.json | 1 - 2 files changed, 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c644d32b..a81b7460 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,12 +87,6 @@ all = [ "chainladder[dev,docs,test]", ] -# Exclude if TYPE_CHECKING block at the beginning of modules. -[tool.coverage.report] -exclude_also = [ - "if TYPE_CHECKING:" -] - [tool.pytest.ini_options] testpaths = ["chainladder", "docs/_ext"] pythonpath = ["docs/_ext"] diff --git a/pyrightconfig.json b/pyrightconfig.json index 74bff3fc..2efc03ab 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -4,6 +4,5 @@ "venvPath": ".", "extraPaths": ["."], "venv": ".venv", - "pythonVersion": "3.14", "typeCheckingMode": "basic" } \ No newline at end of file From af2b680cb7479b6b0fbecf6c8f1c0e1f8267c79f Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 2 May 2026 21:55:37 -0500 Subject: [PATCH 5/8] TEST: Add unit test for incorrect type provided to Common._validate_assumption. --- chainladder/core/tests/test_triangle.py | 29 ++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index f9acf21c..dc1a7c4c 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -1,13 +1,19 @@ +from __future__ import annotations + import chainladder as cl -import pandas as pd +import io import numpy as np +import pandas as pd import pytest -import io -from datetime import datetime + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from chainladder import Triangle try: from IPython.core.display import HTML -except: +except ImportError: HTML = None @@ -1004,4 +1010,17 @@ def test_triangle_init_from_dict() -> None: cumulative=True ) - assert tri_from_df == tri_from_dict \ No newline at end of file + assert tri_from_df == tri_from_dict + + +def test_validate_assumption(raa: Triangle) -> None: + """ + Tests Common._validate_assumption. + """ + + # Check incorrect type provided to value argument. + with pytest.raises(TypeError): + raa._validate_assumption( + triangle=raa, + value=raa, axis=3 # noqa - incorrect type provided on purpose. + ) From 5b4221684e9b50e0f7ea1fd6364b34028e0d3d2e Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 3 May 2026 08:18:35 -0500 Subject: [PATCH 6/8] FIX: Missing print function. --- chainladder/core/triangle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index a7caf1c9..e7c04038 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -1271,7 +1271,7 @@ def val_to_dev(self, inplace=False): .. testcode:: tr = cl.load_sample('ukmotor') - tr.dev_to_val().val_to_dev() + print(tr.dev_to_val().val_to_dev()) .. testoutput:: From 08176c1ae58bfaec8113a0fcc337a1bef34922e7 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 3 May 2026 09:04:27 -0500 Subject: [PATCH 7/8] FIX: Reformat docstring tests. --- chainladder/core/triangle.py | 765 +++++++++++++++++++++++------------ 1 file changed, 512 insertions(+), 253 deletions(-) diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index e7c04038..dd80c21e 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -643,14 +643,28 @@ def origin(self): -------- Annual-origin Triangle. - >>> tr = cl.load_sample('ukmotor') - >>> tr.origin.year.tolist() - [2007, 2008, 2009, 2010, 2011, 2012, 2013] + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.origin.year.tolist()) + + .. testoutput:: + + [2007, 2008, 2009, 2010, 2011, 2012, 2013] The number of origin periods matches ``Triangle.shape[-2]``. - >>> len(tr.origin) == tr.shape[-2] - True + .. testcode:: + + print(len(tr.origin) == tr.shape[-2]) + + .. testoutput:: + + True """ if self.is_pattern and len(self.odims) == 1: return pd.Series(["(All)"]) @@ -699,19 +713,38 @@ def development(self): -------- Annual-grain development on a loss Triangle is reported as month lags. - >>> tr = cl.load_sample('ukmotor') - >>> tr.development.tolist() - [12, 24, 36, 48, 60, 72, 84] + .. testsetup: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.development.tolist()) + + .. testoutput:: + + [12, 24, 36, 48, 60, 72, 84] On a valuation Triangle the labels become calendar periods. - >>> tr.dev_to_val().development.tolist() - ['2007', '2008', '2009', '2010', '2011', '2012', '2013'] + .. testcode:: + + print(tr.dev_to_val().development.tolist()) + + .. testoutput:: + + ['2007', '2008', '2009', '2010', '2011', '2012', '2013'] On a link-ratio (pattern) Triangle the labels span the from/to lags. - >>> tr.link_ratio.development.tolist() - ['12-24', '24-36', '36-48', '48-60', '60-72', '72-84'] + .. testcode:: + + print(tr.link_ratio.development.tolist()) + + .. testoutput:: + + ['12-24', '24-36', '36-48', '48-60', '60-72', '72-84'] """ ddims = self.ddims.copy() if self.is_val_tri: @@ -763,15 +796,29 @@ def is_val_tri(self): -------- A development-lag Triangle has integer lags on the development axis. - >>> tr = cl.load_sample('ukmotor') - >>> tr.is_val_tri - False + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.is_val_tri) + + .. testoutput:: + + False Calling ``dev_to_val`` reshapes the development axis into calendar valuation periods. - >>> tr.dev_to_val().is_val_tri - True + .. testcode:: + + print(tr.dev_to_val().is_val_tri) + + .. testoutput:: + + True """ return type(self.ddims) == pd.DatetimeIndex @@ -790,16 +837,30 @@ def is_full(self) -> bool: A loaded sample loss Triangle is upper-triangular: future cells below the latest diagonal are NaN, so ``is_full`` is ``False``. - >>> tr = cl.load_sample('ukmotor') - >>> bool(tr.is_full) - False + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(bool(tr.is_full)) + + .. testoutput:: + + False A ``cdf_`` Triangle from a fitted development model has every cell populated, so it is full. - >>> cdf = cl.Development().fit(tr).cdf_ - >>> bool(cdf.is_full) - True + .. testcode:: + + cdf = cl.Development().fit(tr).cdf_ + print(bool(cdf.is_full)) + + .. testoutput:: + + True """ return self.nan_triangle.sum().sum() == np.prod(self.shape[-2:]) @@ -826,15 +887,28 @@ def is_pattern(self) -> bool: -------- A loss Triangle is not a pattern. - >>> tr = cl.load_sample('ukmotor') - >>> tr.is_pattern - False + .. testsetup:: + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.is_pattern) + + .. testoutput: + + False Calling ``link_ratio`` returns a Triangle of age-to-age factors, which is flagged as a pattern. - >>> tr.link_ratio.is_pattern - True + .. testcode:: + + print(tr.link_ratio.is_pattern) + + .. testoutput:: + + True """ return self._pattern @@ -861,16 +935,30 @@ def is_ultimate(self) -> np.bool: -------- A loaded sample triangle has no ultimate column. - >>> tr = cl.load_sample('ukmotor') - >>> bool(tr.is_ultimate) - False + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(bool(tr.is_ultimate)) + + .. testoutput:: + + False Fitting a chainladder model produces an ``ultimate_`` Triangle whose single development column is the ultimate valuation. - >>> ult = cl.Chainladder().fit(tr).ultimate_ - >>> bool(ult.is_ultimate) - True + .. testcode:: + + ult = cl.Chainladder().fit(tr).ultimate_ + print(bool(ult.is_ultimate)) + + .. testoutput:: + + True """ return sum(self.valuation >= options.ULT_VAL[:4]) > 0 @@ -892,16 +980,26 @@ def latest_diagonal(self) -> Triangle: Examples -------- - >>> tr = cl.load_sample('ukmotor') - >>> tr.latest_diagonal - 2013 - 2007 12690.0 - 2008 12746.0 - 2009 12993.0 - 2010 11093.0 - 2011 10217.0 - 2012 9650.0 - 2013 6283.0 + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.latest_diagonal) + + .. testoutput:: + + 2013 + 2007 12690.0 + 2008 12746.0 + 2009 12993.0 + 2010 11093.0 + 2011 10217.0 + 2012 9650.0 + 2013 6283.0 """ return self[self.valuation == self.valuation_date].sum(axis="development") @@ -973,15 +1071,25 @@ def age_to_age(self): Examples -------- - >>> tr = cl.load_sample('ukmotor') - >>> tr.age_to_age - 12-24 24-36 36-48 48-60 60-72 72-84 - 2007 1.915694 1.336902 1.190391 1.098935 1.049902 1.02753 - 2008 1.925269 1.295729 1.118225 1.085655 1.051911 NaN - 2009 1.902870 1.234826 1.148734 1.105317 NaN NaN - 2010 1.804424 1.261032 1.135066 NaN NaN NaN - 2011 1.902892 1.293782 NaN NaN NaN NaN - 2012 1.891415 NaN NaN NaN NaN NaN + + .. testsetup:: + + import chainladder cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.age_to_age) + + .. testoutput:: + + 12-24 24-36 36-48 48-60 60-72 72-84 + 2007 1.915694 1.336902 1.190391 1.098935 1.049902 1.02753 + 2008 1.925269 1.295729 1.118225 1.085655 1.051911 NaN + 2009 1.902870 1.234826 1.148734 1.105317 NaN NaN + 2010 1.804424 1.261032 1.135066 NaN NaN NaN + 2011 1.902892 1.293782 NaN NaN NaN NaN + 2012 1.891415 NaN NaN NaN NaN NaN """ return self.link_ratio @@ -1001,42 +1109,67 @@ def incr_to_cum(self, inplace=False): -------- Construct an incremental triangle and accumulate it along the development axis. - >>> df = pd.DataFrame( - ... data={ - ... 'origin': [1981, 1981, 1981, 1981, 1982, 1982, 1982, 1983, 1983, 1984], - ... 'development': [1981, 1982, 1983, 1984, 1982, 1983, 1984, 1983, 1984, 1984], - ... 'reported': [5012, 3257, 2638, 898, 106, 4179, 1111, 3410, 5582, 5655], - ... } - ... ) - >>> tr = cl.Triangle( - ... data=df, - ... origin='origin', - ... development='development', - ... columns=['reported'], - ... cumulative=False, - ... ) - >>> tr - 12 24 36 48 - 1981 5012.0 3257.0 2638.0 898.0 - 1982 106.0 4179.0 1111.0 NaN - 1983 3410.0 5582.0 NaN NaN - 1984 5655.0 NaN NaN NaN + .. testsetup:: - >>> tr.incr_to_cum() - 12 24 36 48 - 1981 5012.0 8269.0 10907.0 11805.0 - 1982 106.0 4285.0 5396.0 NaN - 1983 3410.0 8992.0 NaN NaN - 1984 5655.0 NaN NaN NaN + import chainladder as cl + + .. testcode:: + + df = pd.DataFrame( + data={ + 'origin': [1981, 1981, 1981, 1981, 1982, 1982, 1982, 1983, 1983, 1984], + 'development': [1981, 1982, 1983, 1984, 1982, 1983, 1984, 1983, 1984, 1984], + 'reported': [5012, 3257, 2638, 898, 106, 4179, 1111, 3410, 5582, 5655], + } + ) + tr = cl.Triangle( + data=df, + origin='origin', + development='development', + columns=['reported'], + cumulative=False, + ) + print(tr) + + .. testoutput:: + + 12 24 36 48 + 1981 5012.0 3257.0 2638.0 898.0 + 1982 106.0 4179.0 1111.0 NaN + 1983 3410.0 5582.0 NaN NaN + 1984 5655.0 NaN NaN NaN + + .. testcode:: + + print(tr.incr_to_cum()) + + .. testoutput:: + + 12 24 36 48 + 1981 5012.0 8269.0 10907.0 11805.0 + 1982 106.0 4285.0 5396.0 NaN + 1983 3410.0 8992.0 NaN NaN + 1984 5655.0 NaN NaN NaN By default ``incr_to_cum`` returns a new Triangle. Pass ``inplace=True`` to mutate the calling Triangle instead. - >>> tr.is_cumulative - False - >>> _ = tr.incr_to_cum(inplace=True) - >>> tr.is_cumulative - True + .. testcode:: + + print(tr.is_cumulative) + + .. testoutput:: + + False + + .. testcode:: + + tr.incr_to_cum(inplace=True) + print(tr.is_cumulative) + + .. testoutput:: + + True """ if inplace: xp = self.get_array_module() @@ -1099,16 +1232,25 @@ def cum_to_incr(self, inplace=False): differences each cell against the prior development period, returning per-period increments. - >>> tr = cl.load_sample('ukmotor') - >>> tr.cum_to_incr() - 12 24 36 48 60 72 84 - 2007 3511.0 3215.0 2266.0 1712.0 1059.0 587.0 340.0 - 2008 4001.0 3702.0 2278.0 1180.0 956.0 629.0 NaN - 2009 4355.0 3932.0 1946.0 1522.0 1238.0 NaN NaN - 2010 4295.0 3455.0 2023.0 1320.0 NaN NaN NaN - 2011 4150.0 3747.0 2320.0 NaN NaN NaN NaN - 2012 5102.0 4548.0 NaN NaN NaN NaN NaN - 2013 6283.0 NaN NaN NaN NaN NaN NaN + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr.cum_to_incr()) + + .. testoutput:: + + 12 24 36 48 60 72 84 + 2007 3511.0 3215.0 2266.0 1712.0 1059.0 587.0 340.0 + 2008 4001.0 3702.0 2278.0 1180.0 956.0 629.0 NaN + 2009 4355.0 3932.0 1946.0 1522.0 1238.0 NaN NaN + 2010 4295.0 3455.0 2023.0 1320.0 NaN NaN NaN + 2011 4150.0 3747.0 2320.0 NaN NaN NaN NaN + 2012 5102.0 4548.0 NaN NaN NaN NaN NaN + 2013 6283.0 NaN NaN NaN NaN NaN NaN """ if inplace: v = self.valuation_date @@ -1193,29 +1335,43 @@ def dev_to_val(self, inplace=False): ``cl.load_sample('ukmotor')`` is a 7x7 cumulative Triangle in development form. Each column represents months of development from the origin year. - >>> tr = cl.load_sample('ukmotor') - >>> tr - 12 24 36 48 60 72 84 - 2007 3511.0 6726.0 8992.0 10704.0 11763.0 12350.0 12690.0 - 2008 4001.0 7703.0 9981.0 11161.0 12117.0 12746.0 NaN - 2009 4355.0 8287.0 10233.0 11755.0 12993.0 NaN NaN - 2010 4295.0 7750.0 9773.0 11093.0 NaN NaN NaN - 2011 4150.0 7897.0 10217.0 NaN NaN NaN NaN - 2012 5102.0 9650.0 NaN NaN NaN NaN NaN - 2013 6283.0 NaN NaN NaN NaN NaN NaN + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('ukmotor') + print(tr) + + .. testoutput:: + + 12 24 36 48 60 72 84 + 2007 3511.0 6726.0 8992.0 10704.0 11763.0 12350.0 12690.0 + 2008 4001.0 7703.0 9981.0 11161.0 12117.0 12746.0 NaN + 2009 4355.0 8287.0 10233.0 11755.0 12993.0 NaN NaN + 2010 4295.0 7750.0 9773.0 11093.0 NaN NaN NaN + 2011 4150.0 7897.0 10217.0 NaN NaN NaN NaN + 2012 5102.0 9650.0 NaN NaN NaN NaN NaN + 2013 6283.0 NaN NaN NaN NaN NaN NaN Calling ``dev_to_val`` reshapes the columns from development lags to valuation periods, so each column corresponds to a calendar year. - >>> tr.dev_to_val() - 2007 2008 2009 2010 2011 2012 2013 - 2007 3511.0 6726.0 8992.0 10704.0 11763.0 12350.0 12690.0 - 2008 NaN 4001.0 7703.0 9981.0 11161.0 12117.0 12746.0 - 2009 NaN NaN 4355.0 8287.0 10233.0 11755.0 12993.0 - 2010 NaN NaN NaN 4295.0 7750.0 9773.0 11093.0 - 2011 NaN NaN NaN NaN 4150.0 7897.0 10217.0 - 2012 NaN NaN NaN NaN NaN 5102.0 9650.0 - 2013 NaN NaN NaN NaN NaN NaN 6283.0 + .. testcode:: + + print(tr.dev_to_val()) + + .. testoutput:: + + 2007 2008 2009 2010 2011 2012 2013 + 2007 3511.0 6726.0 8992.0 10704.0 11763.0 12350.0 12690.0 + 2008 NaN 4001.0 7703.0 9981.0 11161.0 12117.0 12746.0 + 2009 NaN NaN 4355.0 8287.0 10233.0 11755.0 12993.0 + 2010 NaN NaN NaN 4295.0 7750.0 9773.0 11093.0 + 2011 NaN NaN NaN NaN 4150.0 7897.0 10217.0 + 2012 NaN NaN NaN NaN NaN 5102.0 9650.0 + 2013 NaN NaN NaN NaN NaN NaN 6283.0 """ if self.is_val_tri: if inplace: @@ -1334,72 +1490,93 @@ def grain(self, grain="", trailing=False, inplace=False): -------- Build a quarterly origin / quarterly development Triangle (OQDQ). - >>> df = pd.DataFrame( - ... data={ - ... 'origin': [ - ... '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', - ... '2022Q2', '2022Q2', '2022Q2', '2022Q2', '2022Q2', '2022Q2', '2022Q2', - ... '2022Q3', '2022Q3', '2022Q3', '2022Q3', '2022Q3', '2022Q3', - ... '2022Q4', '2022Q4', '2022Q4', '2022Q4', '2022Q4', - ... '2023Q1', '2023Q1', '2023Q1', '2023Q1', - ... '2023Q2', '2023Q2', '2023Q2', - ... '2023Q3', '2023Q3', - ... '2023Q4', - ... ], - ... 'development': [ - ... '2022Q1', '2022Q2', '2022Q3', '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', - ... '2022Q2', '2022Q3', '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', - ... '2022Q3', '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', - ... '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', - ... '2023Q1', '2023Q2', '2023Q3', '2023Q4', - ... '2023Q2', '2023Q3', '2023Q4', - ... '2023Q3', '2023Q4', - ... '2023Q4', - ... ], - ... 'reported': [ - ... 100, 200, 300, 400, 480, 540, 580, 600, - ... 110, 220, 320, 420, 500, 560, 600, - ... 120, 240, 350, 450, 520, 580, - ... 130, 250, 370, 470, 540, - ... 140, 260, 380, 480, - ... 150, 270, 390, - ... 160, 280, - ... 170, - ... ], - ... } - ... ) - >>> tr = cl.Triangle( - ... data=df, - ... origin='origin', - ... development='development', - ... columns=['reported'], - ... cumulative=True, - ... ) - >>> tr - 3 6 9 12 15 18 21 24 - 2022Q1 100.0 200.0 300.0 400.0 480.0 540.0 580.0 600.0 - 2022Q2 110.0 220.0 320.0 420.0 500.0 560.0 600.0 NaN - 2022Q3 120.0 240.0 350.0 450.0 520.0 580.0 NaN NaN - 2022Q4 130.0 250.0 370.0 470.0 540.0 NaN NaN NaN - 2023Q1 140.0 260.0 380.0 480.0 NaN NaN NaN NaN - 2023Q2 150.0 270.0 390.0 NaN NaN NaN NaN NaN - 2023Q3 160.0 280.0 NaN NaN NaN NaN NaN NaN - 2023Q4 170.0 NaN NaN NaN NaN NaN NaN NaN + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import pandas as pd + + df = pd.DataFrame( + data={ + 'origin': [ + '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', '2022Q1', + '2022Q2', '2022Q2', '2022Q2', '2022Q2', '2022Q2', '2022Q2', '2022Q2', + '2022Q3', '2022Q3', '2022Q3', '2022Q3', '2022Q3', '2022Q3', + '2022Q4', '2022Q4', '2022Q4', '2022Q4', '2022Q4', + '2023Q1', '2023Q1', '2023Q1', '2023Q1', + '2023Q2', '2023Q2', '2023Q2', + '2023Q3', '2023Q3', + '2023Q4', + ], + 'development': [ + '2022Q1', '2022Q2', '2022Q3', '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', + '2022Q2', '2022Q3', '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', + '2022Q3', '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', + '2022Q4', '2023Q1', '2023Q2', '2023Q3', '2023Q4', + '2023Q1', '2023Q2', '2023Q3', '2023Q4', + '2023Q2', '2023Q3', '2023Q4', + '2023Q3', '2023Q4', + '2023Q4', + ], + 'reported': [ + 100, 200, 300, 400, 480, 540, 580, 600, + 110, 220, 320, 420, 500, 560, 600, + 120, 240, 350, 450, 520, 580, + 130, 250, 370, 470, 540, + 140, 260, 380, 480, + 150, 270, 390, + 160, 280, + 170, + ], + } + ) + tr = cl.Triangle( + data=df, + origin='origin', + development='development', + columns=['reported'], + cumulative=True, + ) + print(tr) + + .. testoutput:: + + 3 6 9 12 15 18 21 24 + 2022Q1 100.0 200.0 300.0 400.0 480.0 540.0 580.0 600.0 + 2022Q2 110.0 220.0 320.0 420.0 500.0 560.0 600.0 NaN + 2022Q3 120.0 240.0 350.0 450.0 520.0 580.0 NaN NaN + 2022Q4 130.0 250.0 370.0 470.0 540.0 NaN NaN NaN + 2023Q1 140.0 260.0 380.0 480.0 NaN NaN NaN NaN + 2023Q2 150.0 270.0 390.0 NaN NaN NaN NaN NaN + 2023Q3 160.0 280.0 NaN NaN NaN NaN NaN NaN + 2023Q4 170.0 NaN NaN NaN NaN NaN NaN NaN Convert to annual origin / annual development. Origins are summed within each calendar year and development periods are aggregated to year-end. - >>> tr.grain('OYDY') - 12 24 - 2022 1090.0 2320.0 - 2023 1320.0 NaN + .. testcode:: + + print(tr.grain('OYDY')) + + .. testoutput:: + + 12 24 + 2022 1090.0 2320.0 + 2023 1320.0 NaN Convert origin to annual but keep development quarterly (``OYDQ``). - >>> tr.grain('OYDQ') - 3 6 9 12 15 18 21 24 - 2022 100.0 310.0 640.0 1090.0 1500.0 1860.0 2130.0 2320.0 - 2023 140.0 410.0 810.0 1320.0 NaN NaN NaN NaN + .. testcode:: + + print(tr.grain('OYDQ')) + + .. testoutput:: + + 3 6 9 12 15 18 21 24 + 2022 100.0 310.0 640.0 1090.0 1500.0 1860.0 2130.0 2320.0 + 2023 140.0 410.0 810.0 1320.0 NaN NaN NaN NaN """ ograin_old, ograin_new = self.origin_grain, grain[1:2] dgrain_old, dgrain_new = self.development_grain, grain[-1] @@ -1536,39 +1713,66 @@ def trend( Examples -------- - >>> df = pd.DataFrame( - ... data={ - ... 'origin': [2020, 2020, 2020, 2021, 2021, 2022], - ... 'development': [2020, 2021, 2022, 2021, 2022, 2022], - ... 'reported': [100, 200, 300, 110, 220, 120], - ... } - ... ) - >>> tr = cl.Triangle( - ... data=df, - ... origin='origin', - ... development='development', - ... columns=['reported'], - ... cumulative=True, - ... ) + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import pandas as pd + + df = pd.DataFrame( + data={ + 'origin': [2020, 2020, 2020, 2021, 2021, 2022], + 'development': [2020, 2021, 2022, 2021, 2022, 2022], + 'reported': [100, 200, 300, 110, 220, 120], + } + ) + tr = cl.Triangle( + data=df, + origin='origin', + development='development', + columns=['reported'], + cumulative=True, + ) + print(tr) + + .. testoutput:: + + 12 24 36 + 2020 100.0 200.0 300.0 + 2021 110.0 220.0 NaN + 2022 120.0 NaN NaN Apply a 10% annual trend along the origin axis. The latest origin year (2022) is unchanged; older origins are scaled up by ``1.10`` per year of distance from the latest origin. - >>> tr.trend(0.10, axis='origin') - 12 24 36 - 2020 121.0 242.0 363.0 - 2021 121.0 242.0 NaN - 2022 120.0 NaN NaN + .. testcode:: + + print(tr.trend(0.10, axis='origin')) + + .. testoutput:: + + 12 24 36 + 2020 121.0 242.0 363.0 + 2021 121.0 242.0 NaN + 2022 120.0 NaN NaN Apply a 10% annual trend along the valuation axis instead. The latest diagonal is unchanged and earlier diagonals are scaled up. - >>> tr.trend(0.10, axis='valuation') - 12 24 36 - 2020 121.0 220.0 300.0 - 2021 121.0 220.0 NaN - 2022 120.0 NaN NaN + .. testcode:: + + print(tr.trend(0.10, axis='valuation')) + + .. testoutput:: + + 12 24 36 + 2020 121.0 220.0 300.0 + 2021 121.0 220.0 NaN + 2022 120.0 NaN NaN """ if axis not in ["origin", "valuation", 2, -2]: raise ValueError( @@ -1638,10 +1842,20 @@ def development_correlation(self, p_critical=0.5): Examples -------- - >>> tr = cl.load_sample('raa') - >>> dc = tr.development_correlation() - >>> bool(dc.t_critical.iloc[0, 0]) - False + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('raa') + dc = tr.development_correlation() + print(bool(dc.t_critical.iloc[0, 0])) + + .. testoutput:: + + False ``t_critical`` reports whether the calculated rank correlation falls outside the no-correlation confidence interval. ``False`` indicates the @@ -1671,11 +1885,21 @@ def valuation_correlation(self, p_critical=0.1, total=False): Examples -------- - >>> tr = cl.load_sample('raa') - >>> vc = tr.valuation_correlation() - >>> vc.z_critical - 1982 1983 1984 1985 1986 1987 1988 1989 1990 - 1981 False False False False False False False False False + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + tr = cl.load_sample('raa') + vc = tr.valuation_correlation() + print(vc.z_critical) + + .. testoutput:: + + 1982 1983 1984 1985 1986 1987 1988 1989 1990 + 1981 False False False False False False False False False Each cell of ``z_critical`` flags whether the calendar-period z-statistic for that valuation falls outside the no-effect confidence interval. @@ -1704,43 +1928,64 @@ def shift(self, periods=-1, axis=3): Examples -------- - >>> df = pd.DataFrame( - ... data={ - ... 'origin': [2020, 2020, 2020, 2021, 2021, 2022], - ... 'development': [2020, 2021, 2022, 2021, 2022, 2022], - ... 'reported': [100, 200, 300, 110, 220, 120], - ... } - ... ) - >>> tr = cl.Triangle( - ... data=df, - ... origin='origin', - ... development='development', - ... columns=['reported'], - ... cumulative=True, - ... ) - >>> tr - 12 24 36 - 2020 100.0 200.0 300.0 - 2021 110.0 220.0 NaN - 2022 120.0 NaN NaN + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + import pandas as pd + df = pd.DataFrame( + data={ + 'origin': [2020, 2020, 2020, 2021, 2021, 2022], + 'development': [2020, 2021, 2022, 2021, 2022, 2022], + 'reported': [100, 200, 300, 110, 220, 120], + } + ) + tr = cl.Triangle( + data=df, + origin='origin', + development='development', + columns=['reported'], + cumulative=True, + ) + print(tr) + + .. testoutput:: + + 12 24 36 + 2020 100.0 200.0 300.0 + 2021 110.0 220.0 NaN + 2022 120.0 NaN NaN Shift one period along the development axis (the default). Values move right by one column and the leading column is filled with zeros. - >>> tr.shift() - 12 24 36 - 2020 0.0 100.0 200.0 - 2021 0.0 110.0 220.0 - 2022 0.0 120.0 NaN + .. testcode:: + + print(tr.shift()) + + .. testoutput:: + + 12 24 36 + 2020 0.0 100.0 200.0 + 2021 0.0 110.0 220.0 + 2022 0.0 120.0 NaN Shift one period along the origin axis. Each origin row's data moves down by one and the first origin row is zeroed out. - >>> tr.shift(periods=-1, axis='origin') - 12 24 36 - 2020 0.0 0.0 0.0 - 2021 100.0 200.0 300.0 - 2022 110.0 220.0 NaN + .. testcode:: + + print(tr.shift(periods=-1, axis='origin')) + + .. testoutput:: + + 12 24 36 + 2020 0.0 0.0 0.0 + 2021 100.0 200.0 300.0 + 2022 110.0 220.0 NaN """ axis = self._get_axis(axis) if axis < 2: @@ -1807,29 +2052,43 @@ def sort_axis(self, axis): -------- Build a Triangle with two columns supplied in non-alphabetical order. - >>> df = pd.DataFrame( - ... data={ - ... 'origin': [2020, 2020, 2021, 2021], - ... 'development': [2020, 2021, 2021, 2021], - ... 'reported': [100, 200, 110, 110], - ... 'paid': [50, 100, 60, 60], - ... } - ... ) - >>> tr = cl.Triangle( - ... data=df, - ... origin='origin', - ... development='development', - ... columns=['reported', 'paid'], - ... cumulative=True, - ... ) - >>> list(tr.columns) - ['reported', 'paid'] + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + df = pd.DataFrame( + data={ + 'origin': [2020, 2020, 2021, 2021], + 'development': [2020, 2021, 2021, 2021], + 'reported': [100, 200, 110, 110], + 'paid': [50, 100, 60, 60], + } + ) + tr = cl.Triangle( + data=df, + origin='origin', + development='development', + columns=['reported', 'paid'], + cumulative=True, + ) + print(list(tr.columns)) + + .. testoutput:: + + ['reported', 'paid'] Sorting on the columns axis returns a new Triangle with columns in alphabetical order. - >>> list(tr.sort_axis('columns').columns) - ['paid', 'reported'] + .. testcode:: + + print(list(tr.sort_axis('columns').columns)) + + .. testoutput:: + + ['paid', 'reported'] """ axis = self._get_axis(axis) From 9358eddb7295faf8a726500755ef0d17e4694b41 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Tue, 5 May 2026 06:11:37 -0500 Subject: [PATCH 8/8] FIX: Correct typing for average: to Literal. --- chainladder/development/development.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chainladder/development/development.py b/chainladder/development/development.py index 5d2a962d..07fc86c7 100644 --- a/chainladder/development/development.py +++ b/chainladder/development/development.py @@ -92,7 +92,7 @@ class Development(DevelopmentBase): def __init__( self, n_periods: int = -1, - average: str = "volume", + average: Literal['volume', 'simple', 'regression'] = "volume", sigma_interpolation: Literal['log-linear', 'mack'] = "log-linear", drop: tuple | list[tuple] | None = None, drop_high: bool | int | list[bool] | list[int] | None = None, @@ -130,7 +130,7 @@ def fit( Parameters ---------- X : TriangleLike - Set of LDFs to which the munich adjustment will be applied. + Set of LDFs to which the Munich adjustment will be applied. y : None Ignored sample_weight : None