Skip to content

Conversation

@danielfong-act
Copy link

There is a small grain error in learning.py that affects semiannual grain triangles. As it stands, passing a semiannual triangle to learning.py (via some estimator) causes a size mismatch error during triangle arithmetic.

here's an example largely ripped from an earlier grain issue (#609)
import chainladder as cl, pandas as pd
# Create semi-annual triangle data
data = {
    'origin': ["2007-01-01", "2007-01-01", "2007-01-01", "2007-01-01", "2007-01-01", "2007-01-01", "2007-01-01",
               "2007-07-01", "2007-07-01", "2007-07-01", "2007-07-01", "2007-07-01", "2007-07-01",
               "2008-01-01", "2008-01-01", "2008-01-01", "2008-01-01", "2008-01-01",
               "2008-07-01", "2008-07-01", "2008-07-01", "2008-07-01",
               "2009-01-01", "2009-01-01", "2009-01-01",
               "2009-07-01", "2009-07-01",
               "2010-01-01"],
    'development': ["2007-01-01", "2007-07-01", "2008-01-01", "2008-07-01", "2009-01-01", "2009-07-01", "2010-01-01",
                    "2007-07-01", "2008-01-01", "2008-07-01", "2009-01-01", "2009-07-01", "2010-01-01",
                    "2008-01-01", "2008-07-01", "2009-01-01", "2009-07-01", "2010-01-01",
                    "2008-07-01", "2009-01-01", "2009-07-01", "2010-01-01",
                    "2009-01-01", "2009-07-01", "2010-01-01",
                    "2009-07-01", "2010-01-01",
                    "2010-01-01"],
    'loss': [100, 200, 300, 400, 500, 600, 700, 
             150, 300, 450, 500, 550, 600, 
             200, 250, 350, 400, 450, 
             50, 100, 150, 200,
             100, 200, 300,
             50, 150, 
             100]
}

df = pd.DataFrame(data)

tri = cl.Triangle(
    df, 
    origin='origin',
    development='development',
    columns='loss',
    cumulative=True
)
# ODP fit for example
BZsemi = cl.BarnettZehnwirth(formula = 'C(development)+C(origin)').fit(tri)
ValueError                                Traceback (most recent call last)
Cell In[1], line 36
     27 df = pd.DataFrame(data)
     29 tri = cl.Triangle(
     30     df,
     31     origin='origin',
   (...)     34     cumulative=True
     35 )
---> 36 BZsemi = cl.BarnettZehnwirth(formula = 'C(development)+C(origin)').fit(tri)

File ~\chainladder-python\chainladder\development\barnzehn.py:54, in BarnettZehnwirth.fit(self, X, y, sample_weight)
     49     warnings.warn("Using more than one linear predictor with BarnettZehnwirth may lead to issues with multicollinearity.")
     50 self.model_ = DevelopmentML(Pipeline(steps=[
     51     ('design_matrix', PatsyFormula(self.formula)),
     52     ('model', LinearRegression(fit_intercept=False))]),
     53             y_ml=response, fit_incrementals=False).fit(tri)
---> 54 resid = tri - self.model_.triangle_ml_[
     55     self.model_.triangle_ml_.valuation <= tri.valuation_date]
     56 self.mse_resid_ = (resid**2).sum(0).sum(1).sum(2).sum() / (
     57     np.nansum(tri.nan_triangle) -
     58     len(self.model_.estimator_ml.named_steps.model.coef_))
     59 self.std_residuals_ = (resid / np.sqrt(self.mse_resid_))

File ~\chainladder-python\chainladder\core\dunders.py:270, in TriangleDunders.__sub__(self, other)
    269 def __sub__(self, other):
--> 270     obj, other = self._validate_arithmetic(other)
    271     if isinstance(obj, TriangleGroupBy):
    272         def f(k, self, obj, other):

File ~\chainladder-python\chainladder\core\dunders.py:35, in TriangleDunders._validate_arithmetic(self, other)
     33 """ Common functionality BEFORE arithmetic operations """
     34 if isinstance(other, TriangleDunders):
---> 35     obj, other = self._compatibility_check(self, other)
     36     if obj.is_pattern != other.is_pattern:
     37         obj.is_pattern = other.is_pattern = False

File ~\chainladder-python\chainladder\core\dunders.py:69, in TriangleDunders._compatibility_check(self, x, y)
     63 x, y = set_common_backend([x, y])
     64 if (
     65     x.origin_grain != y.origin_grain
     66     or (x.development_grain != y.development_grain and
     68 ):
---> 69     raise ValueError(
     70         "Triangle arithmetic requires both triangles to be the same grain."
     71     )
     72 return x, y

ValueError: Triangle arithmetic requires both triangles to be the same grain.

This is caused by the wrong number of periods per year being given for semiannual triangles: there are 2 semis in an annual, not 6. This leads to self.model_.triangle_ml_ being constructed with a finer granularity than it ought to have. I fixed the incorrect values and reordered the grain dictionaries to make things more obvious. The above code runs as expected with this change.

@codecov
Copy link

codecov bot commented Dec 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.44%. Comparing base (358c89c) to head (f2d61bf).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #633   +/-   ##
=======================================
  Coverage   84.44%   84.44%           
=======================================
  Files          84       84           
  Lines        4823     4823           
  Branches      610      610           
=======================================
  Hits         4073     4073           
  Misses        538      538           
  Partials      212      212           
Flag Coverage Δ
unittests 84.44% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kennethshsu
Copy link
Collaborator

Good eye, can you add some tests too? :)

@kennethshsu kennethshsu merged commit d4b2c42 into casact:master Dec 22, 2025
11 checks passed
@danielfong-act
Copy link
Author

@kennethshsu sure, were you looking for grain tests just for learning.py?

@kennethshsu
Copy link
Collaborator

Yes, I think that'll be good if you are up for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants