diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 266dcbf4..392cdb6f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -75,5 +75,7 @@ jobs: python-version: "3.12" - name: Install dependencies run: uv sync --extra docs + - name: Generate autosummary stubs + run: uv run --directory docs sphinx-autogen -o library/generated library/api.md - name: Run doctests run: uv run --directory docs jb build . --builder=custom --custom-builder=doctest diff --git a/chainladder/core/common.py b/chainladder/core/common.py index 77f84d51..2d9b1b8f 100644 --- a/chainladder/core/common.py +++ b/chainladder/core/common.py @@ -174,6 +174,17 @@ def pipe(self, func, *args, **kwargs): >>> import chainladder as cl >>> raa = cl.load_sample('raa') >>> raa.pipe(lambda tri: tri.loc[..., 48:]) + 48 60 72 84 96 108 120 + 1981 11805.0 13539.0 16181.0 18009.0 18608.0 18662.0 18834.0 + 1982 10666.0 13782.0 15599.0 15496.0 16169.0 16704.0 NaN + 1983 16141.0 18735.0 22214.0 22863.0 23466.0 NaN NaN + 1984 21266.0 23425.0 26083.0 27067.0 NaN NaN NaN + 1985 22169.0 25955.0 26180.0 NaN NaN NaN NaN + 1986 12935.0 15852.0 NaN NaN NaN NaN NaN + 1987 12314.0 NaN NaN NaN NaN NaN NaN + 1988 NaN NaN NaN NaN NaN NaN NaN + 1989 NaN NaN NaN NaN NaN NaN NaN + 1990 NaN NaN NaN NaN NaN NaN NaN """ return func(self, *args, **kwargs) @@ -269,9 +280,15 @@ def _validate_assumption( axis: Literal[0, 1, 2, 3] ) -> np.ndarray: """ + Used by development estimators to turn user-supplied assumptions into a uniform NumPy array + shaped to broadcast over the triangle. Parameters ---------- + value: str | int | float | list | tuple | set | np.ndarray | dict | Callable + The user-supplied assumption. + axis: Literal[0, 1, 2, 3] + The axis to broadcast over. """ if type(value) in (int, float, str): diff --git a/chainladder/core/tests/test_triangle.py b/chainladder/core/tests/test_triangle.py index 4e47728c..7d81ac3b 100644 --- a/chainladder/core/tests/test_triangle.py +++ b/chainladder/core/tests/test_triangle.py @@ -8,7 +8,9 @@ import pandas as pd import pytest +from chainladder.core.common import Common from chainladder.utils.utility_functions import date_delta_adjustment +from chainladder.utils.sparse import COO from io import StringIO @@ -1434,6 +1436,126 @@ def test_validate_assumption(raa: Triangle) -> None: value=raa, axis=3 # noqa - incorrect type provided on purpose. ) + +@pytest.mark.parametrize("value", [1, 1.5, "volume"]) +def test_validate_assumption_scalar(raa: Triangle, value: int | float | str) -> None: + """ + Scalar int, float, and str values are broadcast across the axis. + + Parameters + ---------- + raa: Triangle + The raa sample data set Triangle. + value: int | float | str + A user-supplied assumption. + + Returns + ------- + None + """ + result = raa._validate_assumption(raa, value, axis=3) + assert result.shape == (1, 1, 1, raa.shape[3]) + assert (result.flat[0] == value) + + +@pytest.mark.parametrize("value", [ + [1] * 10, + (1,) * 10, + np.ones(10), +]) +def test_validate_assumption_sequence(raa: Triangle, value: list | tuple | np.ndarray) -> None: + """ + list, tuple, and ndarray values are wrapped in np.array and reshaped. + + Parameters + ---------- + raa: Triangle + The raa sample data set Triangle. + value: list | tuple | np.ndarray + A user-supplied assumption. + + Returns + ------- + None + """ + result = raa._validate_assumption(raa, value, axis=3) + assert result.shape == (1, 1, 1, raa.shape[3]) + + +def test_validate_assumption_set(raa: Triangle) -> None: + """ + set values reach the sequence branch without raising. + + Parameters + ---------- + raa: Triangle + The raa sample data set Triangle. + + Returns + ------- + None + """ + # np.array(set) produces a 0-d object array in NumPy 2.x, so we only + # assert the call succeeds, not the resulting shape. + raa._validate_assumption(raa, {1, 2, 3}, axis=3) + + +def test_validate_assumption_dict(raa: Triangle) -> None: + """ + Dict values are mapped by axis label. + + Parameters + ---------- + raa: Triangle + The raa sample data set Triangle. + + Returns + ------- + None + """ + dev_periods = raa._get_axis_value(3).tolist() + value = {p: float(i) for i, p in enumerate(dev_periods)} + result = raa._validate_assumption(raa, value, axis=3) + assert result.shape == (1, 1, 1, raa.shape[3]) + np.testing.assert_array_equal(result.flat[:], list(value.values())) + + +def test_validate_assumption_callable(raa: Triangle) -> None: + """ + Callable values are applied to each axis label. + + Parameters + ---------- + raa: Triangle + The raa sample data set Triangle. + + Returns + ------- + None + """ + result = raa._validate_assumption(raa, lambda x: x * 2, axis=3) + assert result.shape == (1, 1, 1, raa.shape[3]) + expected = np.array([p * 2 for p in raa._get_axis_value(3).tolist()]) + np.testing.assert_array_equal(result.flatten(), expected) + + +def test_validate_assumption_axis2(raa: Triangle) -> None: + """ + axis=2 produces shape (1, 1, n_origin, 1). + + Parameters + ---------- + raa: Triangle + The raa sample data set Triangle. + + Returns + ------- + None + """ + result = raa._validate_assumption(raa, 1, axis=2) + assert result.shape == (1, 1, raa.shape[2], 1) + + def test_xs(clrd): # when slicing with .loc on the first term in the index, Triangle will drop the term assert clrd.xs('Adriatic Ins Co') == clrd.loc['Adriatic Ins Co'] @@ -1636,3 +1758,219 @@ def test_to_datetime_uninferrable_format_raises() -> None: columns='value', cumulative=True ) + + +def test_set_backend_via_ldf(raa: Triangle) -> None: + """ + Call set_backend on a fitted estimator. The estimator has no array_backend attribute + of its own, so set_backend resolves the old backend through ldf_. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + dev = cl.Development().fit(raa) + dev.set_backend("sparse", inplace=True, deep=True) + assert dev.ldf_.array_backend == "sparse" + + +def test_set_backend_no_array_backend_raises() -> None: + """ + Call set_backend on an unfitted estimator. The estimator has neither array_backend + nor ldf_, so set_backend raises ValueError. + + Returns + ------- + None + """ + with pytest.raises(ValueError, match="Unable to determine array backend"): + cl.Development().set_backend("sparse", inplace=True) + + +def test_set_backend_inplace_updates_array_backend_attr(raa: Triangle) -> None: + """ + set_backend(inplace=True) updates self.array_backend when the attribute exists. + When called on an object without array_backend, the attribute is not added. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + # Triangle has array_backend, updated in-place. + tri = raa.set_backend("numpy") + tri.set_backend("sparse", inplace=True) + assert tri.array_backend == "sparse" + + # Estimator has no array_backend, attribute is not added. + dev = cl.Development().fit(raa.set_backend("numpy")) + dev.set_backend("sparse", inplace=True) + assert not hasattr(dev, "array_backend") + + +def test_set_backend_deep_propagates_to_nested_common(raa: Triangle) -> None: + """ + set_backend with deep=True iterates vars(self) and recursively converts every + nested Common instance. Without deep=True, the nested + attributes keep their original backend; with deep=True all are converted. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + dev = cl.Development().fit(raa.set_backend("numpy")) + common_attrs = [k for k, v in vars(dev).items() if isinstance(v, Common)] + + # deep=False: nested Triangle attributes keep numpy + dev.set_backend("sparse", inplace=True, deep=False) + assert all(getattr(dev, k).array_backend == "numpy" for k in common_attrs) + + # deep=True: every nested Common attribute is converted to sparse + dev.set_backend("sparse", inplace=True, deep=True) + assert all(getattr(dev, k).array_backend == "sparse" for k in common_attrs) + + +def test_set_backend_inplace_mutates_values(raa: Triangle) -> None: + """ + set_backend(inplace=True) reassigns self.values. + Verify the in-place mutation produces the correct array type: numpy to sparse yields a COO, + sparse to numpy yields an ndarray. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + numpy_tri = raa.set_backend("numpy") + numpy_tri.set_backend("sparse", inplace=True) + assert isinstance(numpy_tri.values, COO) + + numpy_tri.set_backend("numpy", inplace=True) + assert isinstance(numpy_tri.values, np.ndarray) + + +def test_set_backend_invalid_raises(raa: Triangle) -> None: + """ + Pass an unsupported backend name to set_backend. Should raise AttributeError. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + with pytest.raises(AttributeError): + raa.set_backend("invalid_backend", inplace=True) + + +def test_set_backend_roundtrip(raa: Triangle) -> None: + """ + Convert numpy to sparse and back to numpy, and verify values are preserved. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + sparse_raa = raa.set_backend("sparse") + assert sparse_raa.array_backend == "sparse" + restored = sparse_raa.set_backend("numpy") + assert restored.array_backend == "numpy" + assert restored == raa + + +def test_has_zeta_true(raa: Triangle) -> None: + """ + has_zeta returns True after fitting IncrementalAdditive, which sets zeta_. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + fitted = cl.IncrementalAdditive().fit(raa, sample_weight=raa.latest_diagonal) + assert fitted.has_zeta is True + + +def test_has_zeta_false(raa: Triangle) -> None: + """ + has_zeta returns False for an estimator that does not set zeta_. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + assert cl.Development().fit(raa).has_zeta is False + + +def test_cum_zeta_raises_when_no_zeta(raa: Triangle) -> None: + """ + cum_zeta_ raises AttributeError when the estimator has no zeta_. + + Parameters + ---------- + raa : Triangle + The raa sample data set. + + Returns + ------- + None + """ + with pytest.raises(AttributeError): + _ = cl.Development().fit(raa).cum_zeta_ + + +def test_cum_zeta_returns_incr_to_cum(atol) -> None: + """ + cum_zeta_ returns zeta_.incr_to_cum() when zeta_ is present. + + Parameters + ---------- + atol : float + Absolute tolerance fixture. + + Returns + ------- + None + """ + ia = cl.load_sample("ia_sample") + fitted = cl.IncrementalAdditive().fit(ia["loss"], sample_weight=ia["exposure"].latest_diagonal) + np.testing.assert_allclose( + fitted.cum_zeta_.values.flatten(), + [0.888447, 0.645235, 0.423275, 0.269296, 0.127443, 0.036770], + atol=atol, + ) diff --git a/chainladder/development/clark.py b/chainladder/development/clark.py index 6a39e3ad..413e5420 100644 --- a/chainladder/development/clark.py +++ b/chainladder/development/clark.py @@ -134,29 +134,29 @@ class ClarkLDF(DevelopmentBase): clrd = cl.load_sample("clrd")[["CumPaidLoss"]] print(len(clrd.index)) m = cl.ClarkLDF(groupby="LOB").fit(clrd) - print(m.omega_.round(3)) - print(m.theta_.round(3)) + print(m.omega_.round(2)) + print(m.theta_.round(2)) .. testoutput:: :options: +NORMALIZE_WHITESPACE 775 CumPaidLoss - LOB - comauto 1.082 - medmal 1.889 - othliab 1.468 - ppauto 1.149 - prodliab 1.441 - wkcomp 1.107 + LOB + comauto 1.08 + medmal 1.89 + othliab 1.47 + ppauto 1.15 + prodliab 1.44 + wkcomp 1.11 CumPaidLoss - LOB - comauto 20.481 - medmal 35.128 - othliab 37.745 - ppauto 10.023 - prodliab 64.352 - wkcomp 20.111 + LOB + comauto 20.48 + medmal 35.13 + othliab 37.75 + ppauto 10.02 + prodliab 64.35 + wkcomp 20.11 """ diff --git a/chainladder/development/tests/test_development.py b/chainladder/development/tests/test_development.py index d8bd8c72..430687e3 100644 --- a/chainladder/development/tests/test_development.py +++ b/chainladder/development/tests/test_development.py @@ -814,46 +814,4 @@ def test_pipeline(clrd): dev2 = pipe.fit(X=clrd) assert np.array_equal( dev1.w_v2_.values, dev2.named_steps.drop_hilo.w_v2_.values, True - ) - -def test_sigma1(atol): - data = { - "valuation": [ - 1981,1982,1983,1984,1985, - 1982,1983,1984,1985, - 1983,1984,1985, - 1984,1985, - 1985, - ], - "origin": [ - 1981,1981,1981,1981,1981, - 1982,1982,1982,1982, - 1983,1983,1983, - 1984,1984, - 1985, - ], - "values": [ - 1000,1385,1700,1905,2000, - 1000,1395,1700,1895, - 1000,1405,1700, - 1000,1415, - 1000, - ], - } - tri = cl.Triangle( - data, - origin="origin", - development="valuation", - columns=["values"], - cumulative=True, - ) - assert np.allclose( - cl.Development(average='simple').fit(tri).sigma_.values, - cl.Development(average='volume').fit(tri).sigma_.values, - atol = atol - ) - assert np.allclose( - cl.Development(average='simple').fit(tri).sigma_.values, - cl.Development(average='regression').fit(tri).sigma_.values, - atol = atol - ) + ) \ No newline at end of file diff --git a/chainladder/methods/benktander.py b/chainladder/methods/benktander.py index a83536dd..bcf84390 100644 --- a/chainladder/methods/benktander.py +++ b/chainladder/methods/benktander.py @@ -52,7 +52,6 @@ class Benktander(MethodBase): .. testcode:: xyz = cl.load_sample("xyz") - ibnr = cl.Benktander().fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal).ibnr_ print(ibnr) @@ -76,7 +75,6 @@ class Benktander(MethodBase): .. testcode:: xyz = cl.load_sample("xyz") - bk_ibnr = ( cl.Benktander(n_iters=1) .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) @@ -109,7 +107,6 @@ class Benktander(MethodBase): .. testcode:: xyz = cl.load_sample("xyz") - bk_ibnr = cl.Benktander(n_iters=1000).fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal).ibnr_ cl_ibnr = cl.Chainladder().fit(xyz["Paid"]).ibnr_ print(bk_ibnr - cl_ibnr) @@ -165,7 +162,6 @@ def fit(self, X, y=None, sample_weight=None): .. testcode:: xyz = cl.load_sample("xyz") - ultimate = ( cl.Benktander(apriori=1, n_iters=2) .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) @@ -218,6 +214,7 @@ def predict(self, X, sample_weight=None): current Triangle and a refreshed apriori. .. testsetup:: + import chainladder as cl .. testcode:: @@ -229,8 +226,8 @@ def predict(self, X, sample_weight=None): model = cl.Benktander(apriori=1.0, n_iters=2).fit( tr_prior, sample_weight=apriori_prior ) - - print(model.predict(tr, sample_weight=apriori).ultimate_) + ultimate = model.predict(tr, sample_weight=apriori).ultimate_ + print(ultimate) .. testoutput:: diff --git a/chainladder/methods/bornferg.py b/chainladder/methods/bornferg.py index 695a6f49..ee968b2e 100644 --- a/chainladder/methods/bornferg.py +++ b/chainladder/methods/bornferg.py @@ -53,7 +53,6 @@ class BornhuetterFerguson(Benktander): raa = cl.load_sample("raa") premium = raa.latest_diagonal * 0 + 40_000 # zero out and add 40,000 to each origin - ibnr = cl.BornhuetterFerguson(apriori=0.7).fit(X=raa, sample_weight=premium).ibnr_ print(ibnr) @@ -77,7 +76,6 @@ class BornhuetterFerguson(Benktander): raa = cl.load_sample("raa") premium = raa.latest_diagonal * 0 + 40_000 * 0.7 # premium is modified by 70% - ibnr = cl.BornhuetterFerguson().fit(X=raa, sample_weight=premium).ibnr_ print(ibnr) @@ -123,13 +121,15 @@ def fit(self, X, y=None, sample_weight=None): Fit returns the estimator itself, with ``ultimate_`` populated. .. testsetup:: + import chainladder as cl .. testcode:: tr = cl.load_sample('ukmotor') apriori = cl.Chainladder().fit(tr).ultimate_ * 0 + 14000 - print(cl.BornhuetterFerguson(apriori=1.0).fit(tr, sample_weight=apriori)) + model = cl.BornhuetterFerguson(apriori=1.0).fit(tr, sample_weight=apriori) + print(model) .. testoutput:: @@ -160,6 +160,7 @@ def predict(self, X, sample_weight=None): current Triangle and a refreshed apriori. .. testsetup:: + import chainladder as cl .. testcode:: @@ -171,10 +172,10 @@ def predict(self, X, sample_weight=None): model = cl.BornhuetterFerguson(apriori=1.0).fit( tr_prior, sample_weight=apriori_prior ) + ultimate = model.predict(tr, sample_weight=apriori).ultimate_ + print(ultimate) - print(model.predict(tr, sample_weight=apriori).ultimate_) - - .. testoutput + .. testoutput:: 2261 2007 12690.000000 diff --git a/chainladder/methods/capecod.py b/chainladder/methods/capecod.py index 4175e2ae..1470104d 100644 --- a/chainladder/methods/capecod.py +++ b/chainladder/methods/capecod.py @@ -218,7 +218,7 @@ def fit(self, X, y=None, sample_weight=None): exposure = cl.Chainladder().fit(tr).ultimate_ * 0 + 20000 print(cl.CapeCod(trend=0.05).fit(tr, sample_weight=exposure)) - .. testoutput: + .. testoutput:: CapeCod(trend=0.05) """ diff --git a/chainladder/methods/chainladder.py b/chainladder/methods/chainladder.py index 893933f3..2ab4ca61 100644 --- a/chainladder/methods/chainladder.py +++ b/chainladder/methods/chainladder.py @@ -124,12 +124,13 @@ def fit(self, X, y=None, sample_weight=None): attribute access. .. testsetup:: + import chainladder as cl .. testcode:: tr = cl.load_sample('ukmotor') - cl.Chainladder().fit(tr) + print(cl.Chainladder().fit(tr)) .. testoutput:: diff --git a/chainladder/methods/tests/test_mack.py b/chainladder/methods/tests/test_mack.py index ac9bf6c5..fb63e695 100644 --- a/chainladder/methods/tests/test_mack.py +++ b/chainladder/methods/tests/test_mack.py @@ -27,4 +27,46 @@ def test_multi_triangle_mack(clrd,atol): mack = cl.MackChainladder().fit(tri) for i in range(len(tri.index)): for j in range(len(tri.columns)): - assert np.all(abs(mack.full_std_err_.iloc[i,j].values-cl.MackChainladder().fit(tri.iloc[i,j]).full_std_err_.values) < atol) \ No newline at end of file + assert np.all(abs(mack.full_std_err_.iloc[i,j].values-cl.MackChainladder().fit(tri.iloc[i,j]).full_std_err_.values) < atol) + +def test_mack1997_hardcode(): + """ + Reconciles key MackChainladder statistics to values provided in the paper + """ + #sourced from Table 1, p365 of Mack(1997) + ldf_se = [2.24,.517,.122,.051,.042,.023,.015,.012] + sigma = [1337,988.5,440.1,207.0,164.2,74.6,35.49,16.89] + #sourced from Table 2, p366 of Mack(1997) + ibnr_se = [0,61,140,319,596,1038,1298,1806,2182] + + tri = cl.load_sample("mortgage") + dev = cl.Development(sigma_interpolation = 'mack').fit_transform(tri) + model = cl.MackChainladder().fit(dev) + ldf_rhs = dev.std_err_.values.flatten() + assert np.allclose(ldf_se[0],ldf_rhs[0],atol=0.01) + assert np.allclose(ldf_se[1:],ldf_rhs[1:],atol=0.001) + sigma_rhs = dev.sigma_.values.flatten() + assert np.allclose(sigma[0],sigma_rhs[0],atol=1) + assert np.allclose(sigma[1:],sigma_rhs[1:],atol=0.1) + ibnr_rhs = model.summary_.values[0,0,:,-1]/1000 + assert np.allclose(ibnr_se,np.nan_to_num(ibnr_rhs,nan=0),atol=1,equal_nan=True) + +def test_mack1994_hardcode(raa): + """ + Reconciles key MackChainladder statistics to values provided in the paper + """ + #sourced from top table on p130 of Mack(1994) + sigma_sq = [27883,1109,691,61.2,119,40.8,1.34,7.88] + #sourced from bottom table on p130 of Mack(1994) + ibnr_se = [206,623,747,1469,2002,2209,5358,6333,24566] + + dev = cl.Development(sigma_interpolation = 'mack').fit_transform(raa) + model = cl.MackChainladder().fit(dev) + sigma_rhs = dev.sigma_.values.flatten() ** 2 + assert np.allclose(sigma_sq[:3],sigma_rhs[:3],atol=1) + assert np.allclose(sigma_sq[3:4],sigma_rhs[3:4],atol=0.1) + assert np.allclose(sigma_sq[4:5],sigma_rhs[4:5],atol=1) + assert np.allclose(sigma_sq[5:6],sigma_rhs[5:6],atol=0.1) + 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) diff --git a/chainladder/utils/weighted_regression.py b/chainladder/utils/weighted_regression.py index 1497587a..5add4f24 100644 --- a/chainladder/utils/weighted_regression.py +++ b/chainladder/utils/weighted_regression.py @@ -1,25 +1,61 @@ # 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 from chainladder.utils.sparse import sp from sklearn.base import BaseEstimator import warnings +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from types import ModuleType + from typing import Literal + from chainladder.core.typing import BackendArray class WeightedRegression(BaseEstimator): - """Helper class that fits a system of regression equations + """ + Helper class that fits a system of regression equations as a closed-form solution. This greatly speeds up the implementation of the Mack stochastic properties. + + Parameters + ---------- + axis: integer (default = 2) + the axis along with the perform the regression; + axis of 2 is along the origin periods; + axis of 3 is along the development periods; + thru_orig: bool (default = False) + whether the regression is forced to go through the origin + xp: ModuleType (default = numpy) + array module for calculations + + Attributes + ---------- + slope_: Triangle + coefficients of the regression + sigma_: Triangle + standard deviation of the Triangle values according to Mack (1997), not be confused with sigma from the regression + std_err_: DatetimeIndex + standard error of estimator``slope_`` """ - def __init__(self, axis=None, thru_orig=False, xp=None, average=None): + def __init__( + self, + axis: int = 2, + thru_orig: bool = False, + xp: ModuleType = np, + ): self.axis = axis self.thru_orig = thru_orig self.xp = xp - self.average = average def infer_x_w(self): + """ + Creates dummy X and/or w for the regression when none are given + """ xp = self.xp if self.w is None: self.w = xp.ones(self.y.shape) @@ -27,7 +63,32 @@ def infer_x_w(self): self.x = xp.cumsum(xp.ones(self.y.shape), self.axis) return self - def fit(self, X, y=None, sample_weight=None, average=None): + def fit( + self, + X:BackendArray, + y:BackendArray|None=None, + sample_weight:BackendArray|None=None, + average: Literal["volume", "simple", "regression", "geometric"] | None = None + ): + """ + Fit the model with X. + + Parameters + ---------- + X : Array + independent variable for the regression + y : Array or None (default = None) + dependent variable for the regression + sample_weight : Array or None (default = None) + which (x,y) pairs should be used in the regression + average: literal (or list of literals), or None, optional (default = None) + type of averaging to use; dictates the weights used in the regression + + Returns + ------- + self : object + Returns the instance itself. + """ self.x = X self.y = y self.w = sample_weight @@ -44,6 +105,10 @@ def fit(self, X, y=None, sample_weight=None, average=None): return self def _fit_OLS_thru_orig(self): + """ + Given a set of w, x, y, and an axis, this Function returns OLS slope + and other statistics, while forcing an intercept of 0 + """ from chainladder.utils.utility_functions import num_to_nan x = self.x @@ -106,19 +171,14 @@ def _fit_OLS_thru_orig(self): mse_denom = xp.nansum((y * 0 + 1) * (xp.nan_to_num(w) != 0), axis) - 1 mse_denom = num_to_nan(mse_denom) mse = wss_residual / mse_denom - + sigma = xp.sqrt(mse) std_err = xp.sqrt(mse / denominator) - sigma = std_err * xp.sqrt(mse_denom + 1) - - coef = coef[..., None] - sigma = sigma[..., None] - std_err = std_err[..., None] self._w_reg = w - self.slope_ = coef - self.sigma_ = sigma - self.std_err_ = std_err + self.slope_ = coef[..., None] + self.sigma_ = sigma[..., None] + self.std_err_ = std_err[..., None] return self