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
4 changes: 4 additions & 0 deletions chainladder/_config/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class Options:
when comparing or concatenating them.
ULT_VAL: str
The default ultimate valuation datetime, precision set to default of Pandas installation.
ULT_LABEL: str | None
Label displayed in place of the ultimate development period. ``None`` shows the
underlying ``ULT_VAL``/9999 sentinel. Overridden per-Triangle by ``Triangle.ult_label``.

"""

Expand All @@ -65,6 +68,7 @@ def __init__(self):
self.ULT_VAL = str(
pd.Timestamp("2262-01-01") - pd.Timedelta(1, unit=__dt64_unit__) # noqa
)
self.ULT_LABEL = None
# Store initial values as defaults.
self._defaults = copy.deepcopy({
k: v for k, v in vars(self).items() if not k.startswith("_")
Expand Down
2 changes: 1 addition & 1 deletion chainladder/core/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _repr_format(self, origin_as_datetime: bool = False) -> DataFrame:
.replace("Q3", "H2")
)
origin = origin_formatted
development = self.development.copy()
development = self._display_development().copy()
development.name = None
return pd.DataFrame(out, index=origin, columns=development)

Expand Down
2 changes: 1 addition & 1 deletion chainladder/core/pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def to_frame(
0: self.kdims,
1: self.vdims,
2: self.origin,
3: self.development,
3: self._display_development(),
}

# Set the index to be key dimension if the key dimension is greater than length 1.
Expand Down
24 changes: 24 additions & 0 deletions chainladder/core/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,30 @@ def development(self):
]
return pd.Series(list(ddims), name="development")

@property
def ult_label(self):
"""Label shown in place of the ultimate development period.

Overrides ``chainladder.options.ULT_LABEL`` for this Triangle only.
``None`` falls back to the global option.
"""
return getattr(self, "_ult_label", None)

@ult_label.setter
def ult_label(self, value):
self._ult_label = value

def _display_development(self):
"""``development`` with the ultimate period relabelled for display."""
development = self.development
label = self.ult_label
if label is None:
label = options.ULT_LABEL
if label is None or not self.is_ultimate:
return development
# the sentinel is always the last development period
return pd.Series(list(development[:-1]) + [label], name=development.name)

@development.setter
def development(self, value):
self._len_check(self.development, value)
Expand Down
44 changes: 44 additions & 0 deletions chainladder/utils/tests/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@ def test_options_defaults() -> None:
assert options.AUTO_SPARSE
assert options.ARRAY_PRIORITY == ["dask", "sparse", "cupy", "numpy"]
assert isinstance(options.ULT_VAL, str)
assert options.ULT_LABEL is None


def test_get_option() -> None:
Expand All @@ -717,6 +718,7 @@ def test_get_option() -> None:
assert cl.options.get_option("AUTO_SPARSE") == cl.options.AUTO_SPARSE
assert cl.options.get_option("ARRAY_PRIORITY") == cl.options.ARRAY_PRIORITY
assert cl.options.get_option("ULT_VAL") == cl.options.ULT_VAL
assert cl.options.get_option("ULT_LABEL") == cl.options.ULT_LABEL


def test_set_option_consistency() -> None:
Expand Down Expand Up @@ -1306,3 +1308,45 @@ def test_triangleweight_full_triangle(raa: Triangle) -> None:
tw = cl.TriangleWeight(n_periods=4).fit(raa)
tw_full = cl.TriangleWeight(n_periods=4).fit(ult.full_triangle_)
assert tw.w_.iloc[:, :, :, 0] == tw_full.w_.iloc[:, :, :, 0]


def test_ult_label() -> None:
"""
ULT_LABEL should relabel the ultimate period for display only, with a
Triangle-level setting taking precedence over the global option.

Returns
-------
None

"""
triangle = cl.load_sample("raa")
model = cl.Chainladder().fit(triangle)
ultimate = model.ultimate_
full_expectation = model.full_expectation_

assert ultimate.development.tolist() == ["2261"]
assert full_expectation.development.tolist()[-1] == 9999

try:
cl.options.set_option("ULT_LABEL", "Ultimate")
assert list(ultimate.to_frame(origin_as_datetime=False).columns) == ["Ultimate"]
assert (
list(full_expectation.to_frame(origin_as_datetime=False).columns)[-1]
== "Ultimate"
)

# a Triangle-level label wins, and doesn't leak to other Triangles
override = ultimate.copy()
override.ult_label = "ULT"
assert list(override.to_frame(origin_as_datetime=False).columns) == ["ULT"]
assert list(ultimate.to_frame(origin_as_datetime=False).columns) == ["Ultimate"]
assert override.copy().ult_label == "ULT"

# the sentinel itself is untouched, so calculations are unaffected
assert ultimate.development.tolist() == ["2261"]

# a Triangle without an ultimate period keeps its labels
assert list(triangle.to_frame(origin_as_datetime=False).columns)[-1] == 120
finally:
cl.options.set_option("ULT_LABEL", None)
Loading