Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion chainladder/methods/capecod.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,20 @@ def predict(self, X, sample_weight=None):
raise ValueError("sample_weight is required.")
X_new = X.copy()
_, X_new.ldf_ = self.intersection(X_new, self.ldf_)
inferred_levels = set(sample_weight.key_labels) - set(self.apriori_.key_labels)
# If model was fit at a higher grain, then need to aggregate predicted aprioris too
if len(set(sample_weight.key_labels) - set(self.apriori_.key_labels)) > 0:
if inferred_levels:
if self.groupby is None:
# The grain was worked out from the data rather than asked for, and the
# apriori comes back at it, so say so instead of regrouping silently.
warnings.warn(
"sample_weight has index levels the fitted apriori does not ("
+ ", ".join(sorted(inferred_levels))
+ "), so the apriori is re-estimated at the fitted grain "
+ str(self.apriori_.key_labels)
+ ". apriori_ is returned at that grain, not the one passed in. "
"Pass groupby to CapeCod to state this explicitly."
)
apriori_, detrended_apriori_ = self._get_capecod_aprioris(
X_new.groupby(self.apriori_.key_labels).sum(),
sample_weight.groupby(self.apriori_.key_labels).sum(),
Expand Down
67 changes: 67 additions & 0 deletions chainladder/methods/tests/test_capecod.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import warnings

import chainladder as cl
import numpy as np
import pytest


def test_struhuss():
Expand Down Expand Up @@ -133,3 +136,67 @@ def test_capecod_predict_one_extra_index_level(clrd):
assert set(sample_weight.key_labels) - set(model.apriori_.key_labels) == {"GRNAME"}
assert np.allclose(pred.apriori_.values, model.apriori_.values)
assert abs(pred.ultimate_.sum().sum() - model.ultimate_.sum().sum()) < 1e-6


def _capecod_grain_warnings(fn):
"""Only the grain warning, so numpy's RuntimeWarnings do not count."""
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
fn()
return [
w
for w in caught
if "the apriori is re-estimated at the fitted grain" in str(w.message)
]


def test_capecod_predict_warns_when_the_grain_is_inferred(clrd):
"""github issue #1274

Fitting on pre-aggregated data and predicting on the granular triangle makes
predict() work the grain out from key_labels. apriori_ then comes back at the
fitted grain rather than the caller's, which used to happen silently.
"""
tri = clrd["CumPaidLoss"]
sample_weight = clrd["EarnedPremDIR"].latest_diagonal

model = cl.CapeCod().fit(
tri.groupby("LOB").sum(), sample_weight=sample_weight.groupby("LOB").sum()
)
with pytest.warns(
UserWarning, match="the apriori is re-estimated at the fitted grain"
):
pred = model.predict(tri, sample_weight=sample_weight)

assert pred.apriori_.shape[0] == model.apriori_.shape[0]
assert pred.apriori_.shape[0] != tri.shape[0]


def test_capecod_predict_does_not_warn_when_groupby_is_explicit(clrd):
"""github issue #1274

groupby reaches the same branch, but the grain was asked for rather than
inferred, so there is nothing to point out.
"""
tri = clrd["CumPaidLoss"]
sample_weight = clrd["EarnedPremDIR"].latest_diagonal

model = cl.CapeCod(groupby="LOB").fit(tri, sample_weight=sample_weight)
assert set(sample_weight.key_labels) - set(model.apriori_.key_labels)

assert (
_capecod_grain_warnings(lambda: model.predict(tri, sample_weight=sample_weight))
== []
)


def test_capecod_predict_does_not_warn_at_a_matching_grain(clrd):
"""github issue #1274"""
tri = clrd["CumPaidLoss"]
sample_weight = clrd["EarnedPremDIR"].latest_diagonal

model = cl.CapeCod().fit(tri, sample_weight=sample_weight)
assert (
_capecod_grain_warnings(lambda: model.predict(tri, sample_weight=sample_weight))
== []
)
Loading