diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 423378a6..749641af 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,3 +6,5 @@ ## Additional Context for Reviewers + +- [ ] I passed tests locally for both code (`uv run pytest`) and documentation changes (`uv run jb build docs --builder=custom --custom-builder=doctest`) diff --git a/chainladder/core/dunders.py b/chainladder/core/dunders.py index e8799703..78226045 100644 --- a/chainladder/core/dunders.py +++ b/chainladder/core/dunders.py @@ -45,7 +45,7 @@ def _validate_arithmetic(self, other: Any) -> tuple: if isinstance(other, np.ndarray) and self.array_backend != 'numpy': obj = self.copy() other = obj.get_array_module().array(other) - elif isinstance(other, sp) and self.array_backend != 'sparse': + elif isinstance(other, sp.COO) and self.array_backend != 'sparse': obj = self.set_backend('sparse') else: obj = self.copy() diff --git a/chainladder/core/slice.py b/chainladder/core/slice.py index 55c29a82..4c5b98e8 100644 --- a/chainladder/core/slice.py +++ b/chainladder/core/slice.py @@ -103,7 +103,7 @@ def _sparse_setitem(self, key, values): (self.obj.values.coords, np.array(key)[:, None]), 1) self.obj.values.data = np.concatenate( (self.obj.values.data, np.array([values])), 0) - self.obj.values = self.obj.get_array_module()( + self.obj.values = self.obj.get_array_module().COO( self.obj.values.coords, self.obj.values.data, prune=True, has_duplicates=False, shape=self.obj.shape, fill_value=self.obj.values.fill_value) @@ -238,8 +238,8 @@ def __setitem__(self, key, value): value.values.coords[1] = i coords = np.concatenate((before.coords, value.values.coords), axis=1) data = np.concatenate((before.data, value.values.data)) - self.values = xp( - coords, data, shape=self.shape, prune=True, fill_value=xp.nan + self.values = xp.COO( + coords, data, shape=self.shape, prune=True, fill_value=xp.COO.nan ) else: if isinstance(value, TriangleSlicer): diff --git a/chainladder/core/triangle.py b/chainladder/core/triangle.py index dd80c21e..a6a33b52 100644 --- a/chainladder/core/triangle.py +++ b/chainladder/core/triangle.py @@ -524,7 +524,7 @@ def __init__( # Construct Sparse multidimensional array. self.values: COO = num_to_nan( - sp( + sp.COO( coords, amts, prune=True, diff --git a/chainladder/methods/benktander.py b/chainladder/methods/benktander.py index 2a7d42f8..6681d5b8 100644 --- a/chainladder/methods/benktander.py +++ b/chainladder/methods/benktander.py @@ -16,6 +16,8 @@ class Benktander(MethodBase): then use 1.0 n_iters: int, optional (default=1) Number of iterations to use in the Benktander model. + When n_iters=1, the result is equivalent to the BornhuetterFerguson method. + When n_iters>>1, the result converges to the traditional Chainladder model. apriori_sigma: float, optional (default=0.0) Standard deviation of the apriori. When used in conjunction with the bootstrap model, the model samples aprioris from a lognormal distribution @@ -49,53 +51,83 @@ class Benktander(MethodBase): .. testcode:: - tr = cl.load_sample('ukmotor') - apriori = cl.Chainladder().fit(tr).ultimate_ * 0 + 14000 + xyz = cl.load_sample("xyz") - With ``n_iters=1`` Benktander reproduces Bornhuetter-Ferguson exactly. + ibnr = cl.Benktander().fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal).ibnr_ + print(ibnr) + + .. testoutput:: + + 2261 + 1998 NaN + 1999 115.472127 + 2000 914.033812 + 2001 2432.394513 + 2002 6037.026677 + 2003 13928.934651 + 2004 33925.451475 + 2005 69724.761575 + 2006 73410.593920 + 2007 52977.560411 + 2008 45873.769490 + + When `n_iters=1`, the model is exactly the same as the BornhuetterFerguson model. .. testcode:: - print( - cl.Benktander(apriori=1.0, n_iters=1).fit( - tr, sample_weight=apriori - ).ultimate_ + xyz = cl.load_sample("xyz") + + bk_ibnr = ( + cl.Benktander(n_iters=1) + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .ibnr_ + ) + bf_ibnr = ( + cl.BornhuetterFerguson() + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .ibnr_ ) + print(bk_ibnr - bf_ibnr) .. testoutput:: - 2261 - 2007 12690.000000 - 2008 13121.098503 - 2009 14028.278620 - 2010 13272.048822 - 2011 13911.968891 - 2012 15614.145287 - 2013 16029.501746 - - Increasing ``n_iters`` pulls the immature origins toward the chainladder - estimate. The 2013 origin shows this most: ``16029`` at ``n_iters=1``, - rising to ``19110`` at ``n_iters=4`` and approaching the chainladder - ultimate of ``20680``. + 2261 + 1998 NaN + 1999 NaN + 2000 NaN + 2001 NaN + 2002 NaN + 2003 NaN + 2004 NaN + 2005 NaN + 2006 NaN + 2007 NaN + 2008 NaN + + When `n_iters>>1`, the model converges to the traditional Chainladder model. .. testcode:: - print( - cl.Benktander(apriori=1.0, n_iters=4).fit( - tr, sample_weight=apriori - ).ultimate_ - ) + 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) .. testoutput:: 2261 - 2007 12690.000000 - 2008 13096.902490 - 2009 14030.535854 - 2010 13138.365841 - 2011 13880.984774 - 2012 16719.527550 - 2013 19110.806503 + 1998 NaN + 1999 NaN + 2000 NaN + 2001 1.455192e-11 + 2002 -7.275958e-12 + 2003 7.275958e-12 + 2004 1.455192e-11 + 2005 -1.455192e-11 + 2006 2.910383e-11 + 2007 -5.820766e-11 + 2008 -7.275958e-11 """ def __init__(self, apriori=1.0, n_iters=1, apriori_sigma=0, random_state=None): @@ -132,13 +164,30 @@ def fit(self, X, y=None, sample_weight=None): .. testcode:: - tr = cl.load_sample('ukmotor') - apriori = cl.Chainladder().fit(tr).ultimate_ * 0 + 14000 - print(cl.Benktander(apriori=1.0, n_iters=2).fit(tr, sample_weight=apriori)) + xyz = cl.load_sample("xyz") + + ultimate = ( + cl.Benktander(apriori=1, n_iters=2) + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .ultimate_ + ) + print(ultimate) .. testoutput:: - Benktander(n_iters=2) + 2261 + 1998 15822.000000 + 1999 24908.397003 + 2000 37547.676656 + 2001 40511.198946 + 2002 49417.354765 + 2003 50042.095135 + 2004 82437.601111 + 2005 95417.171135 + 2006 88485.508416 + 2007 66882.788227 + 2008 50708.755370 + """ if sample_weight is None: raise ValueError("sample_weight is required.") diff --git a/chainladder/methods/bornferg.py b/chainladder/methods/bornferg.py index 07c606cd..695a6f49 100644 --- a/chainladder/methods/bornferg.py +++ b/chainladder/methods/bornferg.py @@ -10,9 +10,14 @@ class BornhuetterFerguson(Benktander): Parameters ---------- apriori: float, optional (default=1.0) - Multiplier for the sample_weight used in the Bornhuetter Ferguson - method. If sample_weight is already an apriori measure of ultimate, - then use 1.0 + Multiplier for the `sample_weight` used in the Bornhuetter Ferguson + method. If `sample_weight` is already an apriori measure of ultimate, + then use 1.0. + The recommended pratice is to seperate the model parameter assumption + and data apart. + For example, if the apriori s 80% of premium, it is recommended to set + the aprior as 0.8 and leave the premium data in `sample_weight` argument + unmodified. apriori_sigma: float, optional (default=0.0) Standard deviation of the apriori. When used in conjunction with the bootstrap model, the model samples aprioris from a lognormal distribution @@ -35,14 +40,10 @@ class BornhuetterFerguson(Benktander): Examples -------- Bornhuetter-Ferguson requires an apriori expected ultimate per origin, - supplied through ``sample_weight``. ``sample_weight`` must be a - chainladder Triangle aligned with ``X``, not a scalar; passing - ``sample_weight=14000`` would raise ``AttributeError`` because the model - accesses ``.shape``. + supplied through ``sample_weight``. A common idiom for building a flat per-origin apriori is to take any - same-shape Triangle, zero it out, and add the desired value. Below uses - the chainladder ultimate as the shape donor. + same-shape Triangle, zero it out, and add the desired value. Here is an example. .. testsetup:: @@ -50,40 +51,49 @@ class BornhuetterFerguson(Benktander): .. testcode:: - tr = cl.load_sample('ukmotor') - cl_ult = cl.Chainladder().fit(tr).ultimate_ - apriori = cl_ult * 0 + float(cl_ult.sum()) / 7 - print(apriori) + 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) .. testoutput:: 2261 - 2007 14903.967562 - 2008 14903.967562 - 2009 14903.967562 - 2010 14903.967562 - 2011 14903.967562 - 2012 14903.967562 - 2013 14903.967562 - - Fit with that apriori. The BF ultimates pull the immature origins toward - the apriori while leaving mature origins close to chainladder. + 1981 NaN + 1982 255.707763 + 1983 717.772687 + 1984 1596.061515 + 1985 2658.738155 + 1986 5239.441491 + 1987 8574.335344 + 1988 12714.889984 + 1989 18585.219714 + 1990 24861.068855 + + One might be tempted to set never set the aprior and modify the sample_weight directly, and they will result in the same answer, but this is not the recommended practice. It not only add confusion, but it alos mixes the model parameter assumption and data together. .. testcode:: - model = cl.BornhuetterFerguson(apriori=1.0).fit(tr, sample_weight=apriori) - print(model.ultimate_) + 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) .. testoutput:: 2261 - 2007 12690.000000 - 2008 13145.318280 - 2009 14095.125641 - 2010 13412.748068 - 2011 14150.549749 - 2012 15999.244850 - 2013 16658.824705 + 1981 NaN + 1982 255.707763 + 1983 717.772687 + 1984 1596.061515 + 1985 2658.738155 + 1986 5239.441491 + 1987 8574.335344 + 1988 12714.889984 + 1989 18585.219714 + 1990 24861.068855 """ def __init__(self, apriori=1.0, apriori_sigma=0.0, random_state=None): diff --git a/chainladder/methods/capecod.py b/chainladder/methods/capecod.py index c6959ffc..0d132d82 100644 --- a/chainladder/methods/capecod.py +++ b/chainladder/methods/capecod.py @@ -12,28 +12,34 @@ class CapeCod(Benktander): Parameters ---------- - trend: float (default=0.0) - The cape cod trend assumption. Any Trend transformer on X will + trend: float, optional (default=0.0) + The cape cod trend assumption. Any Trend transformer on X will override this argument. - decay: float (defaut=1.0) - The cape cod decay assumption + decay: float, optional (default=1.0) + The cape cod decay assumption. This parameter is required by the + Generalized Cape Cod Method, as discussed in [Using Best Practices to + Determine a Best Reserve Estimate](https://www.casact.org/sites/default/files/database/forum_98fforum_struhuss.pdf) + by Struzzieri and Hussian. As the `decay` factor approaches 1 + (the default value), the result approaches the traditional Cape Cod + method. As the `decay` factor approaches 0, the result approaches + the `Chainladder` method. n_iters: int, optional (default=1) Number of iterations to use in the Benktander model. + groupby: str or list, optional (default=None) + An option to group levels of the triangle index together for the + purposes of deriving the apriori measures. If omitted, each level of + the triangle index will receive its own apriori computation. apriori_sigma: float, optional (default=0.0) Standard deviation of the apriori. When used in conjunction with the bootstrap model, the model samples aprioris from a lognormal distribution using this argument as a standard deviation. random_state: int, RandomState instance or None, optional (default=None) - Seed for sampling from the apriori distribution. This is ignored when + Seed for sampling from the apriori distribution. This is ignored when using as a deterministic method. If int, random_state is the seed used by the random number generator; If RandomState instance, random_state is the random number generator; If None, the random number generator is the RandomState instance used by np.random. - groupby: - An option to group levels of the triangle index together for the - purposes of deriving the apriori measures. If omitted, each level of - the triangle index will receive its own apriori computation. Attributes @@ -61,8 +67,27 @@ class CapeCod(Benktander): .. testcode:: - tr = cl.load_sample('ukmotor') - exposure = cl.Chainladder().fit(tr).ultimate_ * 0 + 20000 + xyz = cl.load_sample("xyz") + + ibnr = ( + cl.CapeCod().fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal).ibnr_ + ) + print(ibnr) + + .. testoutput:: + + 2261 + 1998 NaN + 1999 88.211299 + 2000 698.247374 + 2001 1858.151261 + 2002 4611.796594 + 2003 10640.571396 + 2004 25916.281295 + 2005 53264.037933 + 2006 56079.713590 + 2007 40470.540502 + 2008 35043.822927 With default ``decay=1`` and ``trend=0``, every origin receives the same apriori loss ratio: the exposure-weighted mean loss ratio across all @@ -70,19 +95,27 @@ class CapeCod(Benktander): .. testcode:: - model = cl.CapeCod().fit(tr, sample_weight=exposure) - print(model.apriori_) + apriori = ( + cl.CapeCod() + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .apriori_ + ) + print(apriori) .. testoutput:: 2261 - 2007 0.706225 - 2008 0.706225 - 2009 0.706225 - 2010 0.706225 - 2011 0.706225 - 2012 0.706225 - 2013 0.706225 + 1998 0.763919 + 1999 0.763919 + 2000 0.763919 + 2001 0.763919 + 2002 0.763919 + 2003 0.763919 + 2004 0.763919 + 2005 0.763919 + 2006 0.763919 + 2007 0.763919 + 2008 0.763919 Setting ``decay`` below 1 down-weights distant origins when computing each origin's apriori, so each origin receives its own loss-ratio @@ -90,36 +123,52 @@ class CapeCod(Benktander): .. testcode:: - print(cl.CapeCod(decay=0.5).fit(tr, sample_weight=exposure).apriori_) + apriori = cl.CapeCod(decay=0.5).fit( + X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal + ).apriori_ + print(apriori) .. testoutput:: 2261 - 2007 0.653584 - 2008 0.666113 - 2009 0.683132 - 2010 0.689123 - 2011 0.717497 - 2012 0.776364 - 2013 0.836006 + 1998 0.797751 + 1999 0.799990 + 2000 0.804890 + 2001 0.793706 + 2002 0.777420 + 2003 0.748556 + 2004 0.740594 + 2005 0.687204 + 2006 0.705757 + 2007 0.784466 + 2008 0.830368 Setting ``trend`` projects the loss ratio forward over the experience - period. With ``decay=1``, all origins share the trended apriori. + period. .. testcode:: - print(cl.CapeCod(trend=0.05).fit(tr, sample_weight=exposure).apriori_) + apriori = ( + cl.CapeCod(decay=0.5, trend=0.03) + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .apriori_ + ) + print(apriori) .. testoutput:: 2261 - 2007 0.836096 - 2008 0.836096 - 2009 0.836096 - 2010 0.836096 - 2011 0.836096 - 2012 0.836096 - 2013 0.836096 + 1998 1.027105 + 1999 1.014959 + 2000 1.001927 + 2001 0.966406 + 2002 0.924906 + 2003 0.869767 + 2004 0.841331 + 2005 0.765515 + 2006 0.773005 + 2007 0.847345 + 2008 0.890327 """ def __init__( diff --git a/chainladder/methods/expectedloss.py b/chainladder/methods/expectedloss.py index ea09cc99..44444a2e 100644 --- a/chainladder/methods/expectedloss.py +++ b/chainladder/methods/expectedloss.py @@ -5,7 +5,9 @@ class ExpectedLoss(Benktander): - """The deterministic Expected Loss IBNR model + """The deterministic Expected Loss IBNR model, it ignores all data in the + triangle, and only uses the sample_weight modified by the apriori to + calculate the ultimate losses. Parameters ---------- @@ -31,6 +33,67 @@ class ExpectedLoss(Benktander): The ultimate losses per the method ibnr_: Triangle The IBNR per the method + + Examples + -------- + + .. testsetup:: + + import chainladder as cl + + .. testcode:: + + xyz = cl.load_sample("xyz") + + ibnr = ( + cl.ExpectedLoss() + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .ibnr_ + ) + print(ibnr) + + .. testoutput:: + + 2261 + 1998 4178.0 + 1999 6683.0 + 2000 8218.0 + 2001 11481.0 + 2002 16746.0 + 2003 29855.0 + 2004 46511.0 + 2005 98125.0 + 2006 84759.0 + 2007 50573.0 + 2008 44388.0 + + We can specify the apriori as a percentage of the premium. + + .. testcode:: + + xyz = cl.load_sample("xyz") + + ibnr = ( + cl.ExpectedLoss(apriori=0.9) + .fit(X=xyz["Paid"], sample_weight=xyz["Premium"].latest_diagonal) + .ibnr_ + ) + print(ibnr) + + .. testoutput:: + + 2261 + 1998 2178.0 + 1999 3533.0 + 2000 3718.0 + 2001 6481.0 + 2002 10627.7 + 2003 22937.5 + 2004 36578.8 + 2005 84309.9 + 2006 74001.2 + 2007 44329.2 + 2008 39608.3 """ def __init__(self, apriori=1.0, apriori_sigma=0.0, random_state=None): diff --git a/chainladder/utils/sparse.py b/chainladder/utils/sparse.py index 50533d98..2de836b8 100644 --- a/chainladder/utils/sparse.py +++ b/chainladder/utils/sparse.py @@ -2,23 +2,13 @@ # 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 sparse -from sparse import COO as sp +import sparse as sp +from sparse import COO as COO from sparse import elemwise -import pandas as pd -import copy sp.isnan = np.isnan -sp.newaxis = np.newaxis -sp.nan = np.array([1.0, np.nan])[-1] -sp.testing = np.testing -sp.nansum = sparse.nansum -sp.nanmin = sparse.nanmin -sp.nanmax = sparse.nanmax -sp.concatenate = sparse.concatenate -sp.diagonal = sparse.diagonal -sp.zeros = sparse.zeros -sp.testing.assert_array_equal = np.testing.assert_equal +COO.nan = np.array([1.0, np.nan])[-1] +setattr(sp, 'testing', np.testing) sp.sqrt = np.sqrt sp.log = np.log sp.exp = np.exp @@ -31,41 +21,41 @@ def nan_to_num(a): if hasattr(a, "fill_value"): a = a.copy() a.data[np.isnan(a.data)] = 0.0 - return sp(coords=a.coords, data=a.data, fill_value=0.0, shape=a.shape) + return COO(coords=a.coords, data=a.data, fill_value=0.0, shape=a.shape) def ones(*args, **kwargs): - return sp(np.ones(*args, **kwargs), fill_value=sp.nan) + return COO(np.ones(*args, **kwargs), fill_value=COO.nan) def nansum(a, axis=None, keepdims=None, *args, **kwargs): - return sp(data=a.data, coords=a.coords, fill_value=0.0, shape=a.shape).sum( + return COO(data=a.data, coords=a.coords, fill_value=0.0, shape=a.shape).sum( axis=axis, keepdims=keepdims, *args, **kwargs ) -sp.nansum = nansum -def nanmean(a, axis=None, keepdims=None, *args, **kwargs): - n = sp.nansum(a, axis=axis, keepdims=keepdims) - d = sp.nansum(sp.nan_to_num(a) != 0, axis=axis, keepdims=keepdims).astype(n.dtype) - n = sp(data=n.data, coords=n.coords, fill_value=np.nan, shape=n.shape) - d = sp(data=d.data, coords=d.coords, fill_value=np.nan, shape=d.shape) + +def nanmean(a, axis=None, keepdims=None): + n = nansum(a, axis=axis, keepdims=keepdims) + d = nansum(nan_to_num(a) != 0, axis=axis, keepdims=keepdims).astype(n.dtype) + n = COO(data=n.data, coords=n.coords, fill_value=np.nan, shape=n.shape) + d = COO(data=d.data, coords=d.coords, fill_value=np.nan, shape=d.shape) out = n / d - return sp(data=out.data, coords=out.coords, fill_value=0, shape=out.shape) + return COO(data=out.data, coords=out.coords, fill_value=0, shape=out.shape) def array(a, *args, **kwargs): if kwargs.get("fill_value", None) is not None: fill_value = kwargs.pop("fill_value") else: - fill_value = sp.nan - if type(a) == sp: - return sp(a, *args, **kwargs, fill_value=fill_value) + fill_value = COO.nan + if type(a) == sp.COO: + return COO(a, *args, **kwargs, fill_value=fill_value) else: - return sp(np.array(a, *args, **kwargs), fill_value=fill_value) + return COO(np.array(a, *args, **kwargs), fill_value=fill_value) def arange(*args, **kwargs): - return sparse.COO.from_numpy(np.arange(*args, **kwargs)) + return COO.from_numpy(np.arange(*args, **kwargs)) def where(*args, **kwargs): @@ -76,12 +66,12 @@ def cumprod(a, axis=None, dtype=None, out=None): return array(np.cumprod(a.todense(), axis=axis, dtype=dtype, out=out)) -def floor(x, *args, **kwargs): +def floor(x): x.data = np.floor(x.data) return x - +sp.nansum = nansum sp.minimum = np.minimum sp.maximum = np.maximum sp.floor = floor @@ -91,4 +81,6 @@ def floor(x, *args, **kwargs): sp.nan_to_num = nan_to_num sp.ones = ones sp.cumprod = cumprod +COO.cumprod = cumprod sp.nanmean = nanmean +sp.sum = COO.sum diff --git a/chainladder/utils/tests/test_sparse.py b/chainladder/utils/tests/test_sparse.py new file mode 100644 index 00000000..c8a2b211 --- /dev/null +++ b/chainladder/utils/tests/test_sparse.py @@ -0,0 +1,110 @@ +import numpy as np + +from chainladder.utils.sparse import ( + array, + floor, + COO, + where +) + + +def test_array_from_list_default_fill_value() -> None: + """ + Tests chainladder.utils.sparse.array() when no fill value is provided. + Checks whether the default nan is filled. + + Returns + ------- + None + + """ + result: COO = array([1.0, 2.0, 3.0]) + assert isinstance(result, COO) + assert np.isnan(result.fill_value) + + +def test_array_from_list_explicit_fill_value() -> None: + """ + Tests chainladder.utils.sparse.array() when a fill value of 0 is provided. + Checks whether the 0 is filled. + + Returns + ------- + + """ + result: COO = array([1, 2, 3], fill_value=0) + assert isinstance(result, COO) + assert result.fill_value == 0 + + +def test_array_from_coo_default_fill_value() -> None: + """ + Tests chainladder.utils.sparse.array() when initializing from a sparse array with a default fill value. + + Returns + ------- + None + + """ + coo = COO.from_numpy(np.array([1.0, 2.0, 3.0])) + result: COO = array(coo) + assert isinstance(result, COO) + assert np.isnan(result.fill_value) + + +def test_array_from_coo_explicit_fill_value() -> None: + """ + Tests chainladder.utils.sparse.array() when initializing from a sparse array with an explicit fill value. + + Returns + ------- + None + + """ + coo = COO.from_numpy(np.array([1, 2, 3])) + result: COO = array(coo, fill_value=0) + assert isinstance(result, COO) + assert result.fill_value == 0 + + +def test_where_selects_from_two_arrays() -> None: + """ + Tests element-wise where across sparse arrays. Calls np.where on each element triplet + (cond[i], a[i], b[i]) - returning a[i] where the condition is True and b[i] where it's False. + + Returns + ------- + None + """ + a: COO = array([1.0, 2.0, 3.0]) + b: COO = array([10.0, 20.0, 30.0]) + cond: COO = array([True, False, True]) + result: COO = where(cond, a, b) + assert isinstance(result, COO) + np.testing.assert_array_equal(result.todense(), [1.0, 20.0, 3.0]) + + +def test_floor_rounds_down() -> None: + """ + Checks floor function rounding down with positive and negative floats. + + Returns + ------- + None + """ + a: COO = array([1.2, 2.7, -0.3]) + result: COO = floor(a) + np.testing.assert_array_equal(result.todense(), [1.0, 2.0, -1.0]) + + +def test_floor_mutates_in_place() -> None: + """ + Checks in-place mutation of floor function. + + Returns + ------- + None + """ + a = array([1.2, 2.7, -0.3]) + result: COO = floor(a) + assert result is a diff --git a/chainladder/utils/utility_functions.py b/chainladder/utils/utility_functions.py index 37c381f1..e044f9ca 100644 --- a/chainladder/utils/utility_functions.py +++ b/chainladder/utils/utility_functions.py @@ -730,14 +730,14 @@ def num_to_value( arr.coords = arr.coords[:, arr.data != 0] arr.data = arr.data[arr.data != 0] - arr: COO = sp( + arr: COO = sp.COO( coords=arr.coords, data=arr.data, - fill_value=sp.nan, # noqa + fill_value=sp.COO.nan, # noqa shape=arr.shape ) else: - arr: COO = sp( + arr: COO = sp.COO( num_to_nan(np.nan_to_num(arr.todense())), fill_value=value ) diff --git a/chainladder/utils/weighted_regression.py b/chainladder/utils/weighted_regression.py index aec67d22..12a5ef7e 100644 --- a/chainladder/utils/weighted_regression.py +++ b/chainladder/utils/weighted_regression.py @@ -53,7 +53,7 @@ def _fit_OLS(self): y[w == 0] = xp.nan else: w2 = w.copy() - w2 = sp(data=w2.data, coords=w2.coords, fill_value=sp.nan, shape=w2.shape) + w2 = sp.COO(data=w2.data, coords=w2.coords, fill_value=sp.nan, shape=w2.shape) x, y = x * w2, y * w2 with warnings.catch_warnings(): diff --git a/docs/getting_started/install.md b/docs/getting_started/install.md index a53ba6c4..b1374a2d 100644 --- a/docs/getting_started/install.md +++ b/docs/getting_started/install.md @@ -3,15 +3,18 @@ We strongly encourage users to install chainladder in a dedicated virtual environment. ## General Installation -Install the chainladder package using `pip`: +We recommend **uv** for installing `chainladder`, but you can use any of the managers below: [![](https://pepy.tech/badge/chainladder)](https://pepy.tech/project/chainladder) -Installing `chainladder` using `pip`: - -`pip install chainladder` +| Manager | Command | Source | +|:---|:---|:---| +| uv (recommended) | `uv add chainladder` | PyPI | +| pip | `pip install chainladder` | PyPI | +| pixi | `pixi add chainladder` | conda-forge | +| conda | `conda install -c conda-forge chainladder` | conda-forge | Alternatively, if you have git and want to enjoy unreleased features, you can install directly from `Github`: diff --git a/docs/getting_started/tutorials/triangle-tutorial.ipynb b/docs/getting_started/tutorials/triangle-tutorial.ipynb index e2d3a67a..e413faad 100644 --- a/docs/getting_started/tutorials/triangle-tutorial.ipynb +++ b/docs/getting_started/tutorials/triangle-tutorial.ipynb @@ -6085,7 +6085,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`Pandas` has wonderful datetime inference functionality that the `Triangle` heavily uses to infer origin and development granularity. Even still, there are occassions where date format inferences can fail. It is often better to explicitly tell the triangle the date format, and is usually good pratice to explicitly state the date format instead." + "`Pandas` has wonderful datetime inference functionality that the `Triangle` heavily uses to infer origin and development granularity. Even still, there are occasions where date format inferences can fail. It is often better to explicitly tell the triangle the date format, and is usually good practice to explicitly state the date format instead." ] }, { diff --git a/docs/library/api.md b/docs/library/api.md index fd8af1da..d0985fab 100644 --- a/docs/library/api.md +++ b/docs/library/api.md @@ -101,14 +101,15 @@ Classes Chainladder MackChainladder BornhuetterFerguson + ExpectedLoss Benktander CapeCod .. _adjustments_ref: -:mod:`chainladder.workflow`: Adjustments -======================================== -.. automodule:: chainladder.workflow +:mod:`chainladder.adjustments`: Adjustments +=========================================== +.. automodule:: chainladder.adjustments :no-members: :no-inherited-members: diff --git a/docs/library/contributing.md b/docs/library/contributing.md index da85d68d..caab7967 100644 --- a/docs/library/contributing.md +++ b/docs/library/contributing.md @@ -116,10 +116,17 @@ Contributions to documentation are especially helpful for new users. - Follow established naming conventions - Include new unit tests with reasonable coverage -All PRs should be run locally before submission: +All PRs should be run locally before submission. +For codebase tests, run: + +```bash +pytest +``` + +For documentation changes, rebuild the docs locally with: ```bash -pytest chainladder +uv run jb build docs --builder=custom --custom-builder=doctest ``` Large or unfocused PRs may delay merging. Each PR should address a single issue or feature to maintain clarity and quality. diff --git a/docs/library/generated/chainladder.ExpectedLoss.rst b/docs/library/generated/chainladder.ExpectedLoss.rst new file mode 100644 index 00000000..222db062 --- /dev/null +++ b/docs/library/generated/chainladder.ExpectedLoss.rst @@ -0,0 +1,6 @@ +chainladder.ExpectedLoss +======================== + +.. currentmodule:: chainladder + +.. autoclass:: ExpectedLoss \ No newline at end of file diff --git a/docs/library/releases.md b/docs/library/releases.md index 9a4c967a..e2be91e9 100644 --- a/docs/library/releases.md +++ b/docs/library/releases.md @@ -2,6 +2,60 @@ ## Version 0.9 +### Version 0.9.2 + +Release Date: May 11, 2026 + +**What's Changed** +* Bump nbconvert from 7.16.6 to 7.17.0 by @dependabot[bot] in [#672](https://github.com/casact/chainladder-python/pull/672) +* Adding dict support for renaming columns by @henrydingliu in [#671](https://github.com/casact/chainladder-python/pull/671) +* Drop python3.9 by @kennethshsu in [#675](https://github.com/casact/chainladder-python/pull/675) +* Bump pillow from 11.3.0 to 12.1.1 by @dependabot[bot] in [#673](https://github.com/casact/chainladder-python/pull/673) +* Various fix by @henrydingliu in [#676](https://github.com/casact/chainladder-python/pull/676) +* Build(deps): Bump tornado from 6.5.2 to 6.5.5 by @dependabot[bot] in [#677](https://github.com/casact/chainladder-python/pull/677) +* Build(deps): Bump requests from 2.32.5 to 2.33.0 by @dependabot[bot] in [#685](https://github.com/casact/chainladder-python/pull/685) +* Build(deps): Bump pygments from 2.19.2 to 2.20.0 by @dependabot[bot] in [#688](https://github.com/casact/chainladder-python/pull/688) +* Build(deps): Bump pillow from 12.1.1 to 12.2.0 by @dependabot[bot] in [#697](https://github.com/casact/chainladder-python/pull/697) +* Build(deps): Bump pytest from 8.4.2 to 9.0.3 by @dependabot[bot] in [#698](https://github.com/casact/chainladder-python/pull/698) +* [#689](https://github.com/casact/chainladder-python/issues/689) by @kennethshsu in [#690](https://github.com/casact/chainladder-python/pull/690) +* Improve docstring for approximation_grain in ParallelogramOLF by @kennethshsu in [#687](https://github.com/casact/chainladder-python/pull/687) +* Addressed wheel vulnerability by @kennethshsu in [#699](https://github.com/casact/chainladder-python/pull/699) +* Build(deps): Bump nbconvert from 7.17.0 to 7.17.1 by @dependabot[bot] in [#703](https://github.com/casact/chainladder-python/pull/703) +* DOCS: Begin work on examples. by @genedan in [#700](https://github.com/casact/chainladder-python/pull/700) +* Build(deps): Bump lxml from 6.0.2 to 6.1.0 by @dependabot[bot] in [#705](https://github.com/casact/chainladder-python/pull/705) +* nan_triangle 1D logic overhaul by @danielfong-act in [#702](https://github.com/casact/chainladder-python/pull/702) +* Add links to object source code in API Reference section by @genedan in [#710](https://github.com/casact/chainladder-python/pull/710) +* [#707](https://github.com/casact/chainladder-python/issues/707) by @kennethshsu in [#711](https://github.com/casact/chainladder-python/pull/711) +* Pr template by @kennethshsu in [#712](https://github.com/casact/chainladder-python/pull/712) +* DOCS: Expand Triangle constructor examples (#704) by @EKtheSage in [#714](https://github.com/casact/chainladder-python/pull/714) +* FEAT: Initialize triangle from dict. by @genedan in [#706](https://github.com/casact/chainladder-python/pull/706) +* DOCS: Add examples to Triangle methods and properties (#704) by @EKtheSage in [#719](https://github.com/casact/chainladder-python/pull/719) +* DOCS: Add examples to deterministic IBNR methods (#704) by @EKtheSage in [#721](https://github.com/casact/chainladder-python/pull/721) +* Docs/issue 704 tailconstant examples by @priyam0k in [#722](https://github.com/casact/chainladder-python/pull/722) +* Add Friedland datasets to cl.load_sample() by @genedan in [#730](https://github.com/casact/chainladder-python/pull/730) +* CHORE: Update workflows to test docs examples. by @genedan in [#713](https://github.com/casact/chainladder-python/pull/713) +* Added an example and improved docstring for load_sample by @kennethshsu in [#715](https://github.com/casact/chainladder-python/pull/715) +* Missing testoutput by @kennethshsu in [#731](https://github.com/casact/chainladder-python/pull/731) +* Fix test indentation and add print statement by @kennethshsu in [#732](https://github.com/casact/chainladder-python/pull/732) +* Fixed doc site build fails by @kennethshsu in [#733](https://github.com/casact/chainladder-python/pull/733) +* ENH: Continue work on type hinting, add pyright to check type coverage. by @genedan in [#735](https://github.com/casact/chainladder-python/pull/735) +* Rtd branch fix by @henrydingliu in [#741](https://github.com/casact/chainladder-python/pull/741) +* fix(tests): use assert in test_n_periods so failures actually fail by @SaguaroDev in [#744](https://github.com/casact/chainladder-python/pull/744) +* Build(deps): Bump mistune from 3.1.4 to 3.2.1 by @dependabot[bot] in [#748](https://github.com/casact/chainladder-python/pull/748) +* Update pyproject.toml - Add numpy ([#738](https://github.com/casact/chainladder-python/issues/738)) by @wendy-w2029 in [#750](https://github.com/casact/chainladder-python/pull/750) +* Enhance docstrings and examples for improved clarity for many estimators by @kennethshsu in [#747](https://github.com/casact/chainladder-python/pull/747) +* Fix Adjustments API page linking to chainladder.workflow (#757) by @SaguaroDev in [#762](https://github.com/casact/chainladder-python/pull/762) +* Annotate matplotlib dependency as required for TriangleDisplay.heatmap() (#758) by @SaguaroDev in [#761](https://github.com/casact/chainladder-python/pull/761) +* REFACTOR: Reorganize type hierarchy of sparse.py. Move array-level fu… by @genedan in [#739](https://github.com/casact/chainladder-python/pull/739) +* Build(deps): Bump urllib3 from 2.6.3 to 2.7.0 by @dependabot[bot] in [#767](https://github.com/casact/chainladder-python/pull/767) + +**New Contributors** +* @priyam0k made their first contribution in [#722](https://github.com/casact/chainladder-python/pull/722) +* @SaguaroDev made their first contribution in [#744](https://github.com/casact/chainladder-python/pull/744) +* @wendy-w2029 made their first contribution in [#750](https://github.com/casact/chainladder-python/pull/750) + +**Full Changelog**: https://github.com/casact/chainladder-python/compare/v0.9.1...v0.9.2 + ### Version 0.9.1 Release Date: Jan 30, 2026 @@ -143,7 +197,7 @@ Release Date: May 24, 2024 Release Date: Apr 10, 2024 **What's Changed** -* Various bug fixes and improvements +* Fix for [\#509](https://github.com/casact/chainladder-python/issues/509) (triangle / core initialization and packaging metadata). ### Version 0.8.19 @@ -174,7 +228,9 @@ Release Date: Sep 18, 2023 Release Date: Jun 17, 2023 **What's Changed** -* Bump of 0.8.16 +* Relax the `pandas<2.0` upper bound in CI/deps metadata. +* Update `environment-latest.yaml` and `pytest_upstream_nightly.yml` ([#442](https://github.com/casact/chainladder-python/pull/442)). +* Adjust pytest-related dependency pins. ### Version 0.8.16 @@ -185,7 +241,7 @@ Release Date: Jun 17, 2023 * fix for [\#411](https://github.com/casact/chainladder-python/issues/411) * fix for [\#438](https://github.com/casact/chainladder-python/issues/438) -## New Contributors +**New Contributors** * [@MatthewCaseres](https://github.com/MatthewCaseres) made their first contribution. * [@andrejakobsen](https://github.com/andrejakobsen) made their first contribution. @@ -226,7 +282,18 @@ Release Date: Apr 11, 2023 Release Date: Nov 25, 2022 - +**What's Changed** +* Major documentation refresh for Jupyter Book and CAS Annual Meeting materials: user guide and gallery restructure, new exercises and demos, usage/Colab notes, and bibliography updates (including [#311](https://github.com/casact/chainladder-python/pull/311) and related annual-meeting prep PRs [#335](https://github.com/casact/chainladder-python/pull/335)–[#361](https://github.com/casact/chainladder-python/pull/361)). +* Additional Friedland and other sample datasets for `load_sample` ([#347](https://github.com/casact/chainladder-python/pull/347), [#353](https://github.com/casact/chainladder-python/pull/353), [#355](https://github.com/casact/chainladder-python/pull/355), [#357](https://github.com/casact/chainladder-python/pull/357), [#358](https://github.com/casact/chainladder-python/pull/358), [#359](https://github.com/casact/chainladder-python/pull/359), [#362](https://github.com/casact/chainladder-python/pull/362), [#363](https://github.com/casact/chainladder-python/pull/363)). +* `Development` transformer: `std_residuals_` ([#352](https://github.com/casact/chainladder-python/pull/352)). +* Four-dimensional triangles: extend `drop_high` / `drop_low` (including `drop_above` / `drop_below`) ([#375](https://github.com/casact/chainladder-python/pull/375), [#381](https://github.com/casact/chainladder-python/pull/381)). +* Semi-annual key for tail handling ([#384](https://github.com/casact/chainladder-python/pull/384)). +* Triangle `to_frame()` updates and reduced reliance on `origin_as_datetime` inside `to_frame()` ([#360](https://github.com/casact/chainladder-python/pull/360)). +* Bug fixes and hardening for `Development` and triangles ([#373](https://github.com/casact/chainladder-python/pull/373), [#371](https://github.com/casact/chainladder-python/pull/371), [#368](https://github.com/casact/chainladder-python/pull/368), [#366](https://github.com/casact/chainladder-python/pull/366)); follow-ups by @henrydingliu ([#370](https://github.com/casact/chainladder-python/pull/370)). +* `DevelopmentCorrelation` / valuation correlation: annotations, intermediate diagnostics, and `p_critical` validation ([#342](https://github.com/casact/chainladder-python/pull/342)). +* README, GitHub issue templates, and docs environment updates; reduce `to_datetime` deprecation noise. + +**Full Changelog**: https://github.com/casact/chainladder-python/compare/v0.8.13...v0.8.14 ### Version 0.8.13 @@ -250,6 +317,8 @@ Release Date: Jun 27, 2022 Release Date: Mar 8, 2022 +*Note:* There was no `0.8.11` stable release on PyPI; only pre-release tags `v0.8.11-alpha` and `v0.8.11-beta` were published. + **Bug Fixes** - [\#254](https://github.com/casact/chainladder-python/issues/254) Fixed an undesired mutation when using cl.concat diff --git a/pyproject.toml b/pyproject.toml index a81b7460..46a4901d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "chainladder" -version = "0.9.1" +version = "0.9.2" authors = [ {name = "John Bogaardt", email = "jbogaardt@gmail.com"}, ] @@ -30,11 +30,12 @@ classifiers = [ ] keywords = ["actuarial", "reserving", "insurance", "chainladder", "IBNR"] dependencies = [ - "pandas >=2.0, <3.0", + "pandas >=2.3.3, <3.0", "scikit-learn>1.4.2", "numba>0.54", "sparse>=0.9", - "matplotlib", + "numpy", + "matplotlib", # Required for TriangleDisplay.heatmap() "dill", "patsy", ] @@ -101,4 +102,4 @@ include = ["chainladder", "chainladder.*"] chainladder = ["utils/data/*", "py.typed"] [tool.uv] -config-settings = {editable_mode="compat"} \ No newline at end of file +config-settings = {editable_mode="compat"} diff --git a/uv.lock b/uv.lock index f25210e9..6623f826 100644 --- a/uv.lock +++ b/uv.lock @@ -188,12 +188,14 @@ wheels = [ [[package]] name = "chainladder" -version = "0.9.1" +version = "0.9.2" source = { editable = "." } dependencies = [ { name = "dill" }, { name = "matplotlib" }, { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas" }, { name = "patsy" }, { name = "scikit-learn" }, @@ -283,8 +285,9 @@ requires-dist = [ { name = "nbmake", marker = "extra == 'test'" }, { name = "nbsphinx", marker = "extra == 'docs'" }, { name = "numba", specifier = ">0.54" }, + { name = "numpy" }, { name = "numpydoc", marker = "extra == 'docs'" }, - { name = "pandas", specifier = ">=2.0,<3.0" }, + { name = "pandas", specifier = ">=2.3.3,<3.0" }, { name = "parso", marker = "extra == 'docs'", specifier = ">=0.8" }, { name = "patsy" }, { name = "polars", marker = "extra == 'docs'" }, @@ -921,6 +924,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, @@ -931,6 +935,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, @@ -941,6 +946,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -951,6 +957,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -961,6 +968,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -1733,14 +1741,14 @@ wheels = [ [[package]] name = "mistune" -version = "3.1.4" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, ] [[package]] @@ -3580,11 +3588,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]]