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
157 changes: 157 additions & 0 deletions chainladder/core/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
del annotations


def _get_starting_dev_lags(tri: Triangle) -> list[str]:
"""
Extract starting development lag intervals for pattern and non-pattern triangles.
"""
if tri.is_pattern:
return [str(x).split("-")[0] for x in tri.development]
return [str(x) for x in tri.development]


class Styler(_PandasStyler):
"""
Styles a Triangle according to the data with HTML and CSS.
Expand Down Expand Up @@ -141,6 +150,154 @@ def _copy(
)
return styler

def _compute(self) -> Styler:
"""
Execute the style functions built up in `self._todo`, or restore pre-computed
styles for concatenated stylers whose shapes were padded.
"""
if getattr(self, "_concatenated_ctx", None) is not None:
self.ctx = copy_module.deepcopy(self._concatenated_ctx)
self.ctx_index = copy_module.deepcopy(self._concatenated_ctx_index)
self.ctx_columns = copy_module.deepcopy(self._concatenated_ctx_columns)
return self
return super()._compute()

def _snapshot_context(self) -> None:
"""
Cache pre-computed style contexts before DataFrame shape modifications.
"""
if getattr(self, "_concatenated_ctx", None) is None:
self._compute()
self._concatenated_ctx = copy_module.deepcopy(self.ctx)
self._concatenated_ctx_index = copy_module.deepcopy(self.ctx_index)
self._concatenated_ctx_columns = copy_module.deepcopy(self.ctx_columns)

def _pad_columns(self, other: Styler) -> None:
"""
Pad column dimensions with NaNs so self and other share identical column labels.
"""
if len(self.data.columns) >= len(other.data.columns):
target_cols = self.data.columns
padded_data = other.data.copy()
orig_len = len(padded_data.columns)
for c in target_cols[orig_len:]:
padded_data[c] = np.nan
padded_data.columns = target_cols
other.data = padded_data
other.columns = target_cols
if orig_len < len(target_cols):
other.format(na_rep="", subset=list(target_cols[orig_len:]))
else:
self._snapshot_context()
new_cols = other.data.columns[len(self.data.columns) :]
for c in new_cols:
self.data[c] = np.nan
self.columns = self.data.columns
self.format(na_rep="", subset=list(new_cols))
for prev in self.concatenated:
if isinstance(prev, Styler):
prev._snapshot_context()
for c in new_cols:
prev.data[c] = np.nan
prev.columns = self.data.columns
prev.format(na_rep="", subset=list(new_cols))
other.data.columns = self.data.columns
other.columns = self.data.columns

def concat(self, other: Any) -> Styler:
"""
Append another Styler to combine the output into a single table.

Parameters
----------
other : Styler
The other Styler object to concatenate. The Styler can wrap
another Triangle, link ratios, or development patterns. If the
columns differ, they must share compatible development periods.

Returns
-------
Styler
``self``, with ``other`` appended to its concatenated list.

Raises
------
TypeError
If ``other`` is not a Styler.
ValueError
If the number of index levels differ, or if the development periods
between Triangles are incompatible.

Examples
--------
.. code-block:: python

import chainladder as cl

raa = cl.load_sample("raa")
dev = cl.Development().fit(raa)

# Concatenate loss triangle with link ratios and selected factors
exhibit = (
raa.style
.highlight_lower_triangle()
.concat(
raa.link_ratio.style.format(
precision=3, na_rep=""
).highlight_diagonal()
)
.concat(dev.ldf_.style.format(precision=3, na_rep=""))
.concat(dev.cdf_.style.format(precision=3, na_rep=""))
)
"""
if isinstance(other, Triangle):
raise TypeError(
"`other` must be of type `Styler`. Use `Triangle.style` for a Triangle."
)
if not isinstance(other, _PandasStyler):
raise TypeError("`other` must be of type `Styler`")
if self.data.index.nlevels != other.data.index.nlevels:
raise ValueError(
"number of index levels must be same in `other` "
"as in `Styler`. See documentation for suggestions."
)

other_copy: Styler = other._copy(deepcopy=True)
other_copy.data = other.data.copy()

if self.data.columns.equals(other.data.columns):
if isinstance(other_copy, Styler):
other_copy._snapshot_context()
super().concat(other_copy)
return self

if not hasattr(other_copy, "_triangle"):
super().concat(other_copy)
return self

if self._triangle.is_val_tri != other_copy._triangle.is_val_tri:
raise ValueError(
"Cannot concatenate a valuation Triangle with a development Triangle."
)
if self._triangle.development_grain != other_copy._triangle.development_grain:
raise ValueError(
"Development grains must match to concatenate stylers: "
f"'{self._triangle.development_grain}' != '{other_copy._triangle.development_grain}'."
)

dev1 = _get_starting_dev_lags(self._triangle)
dev2 = _get_starting_dev_lags(other_copy._triangle)
min_len = min(len(dev1), len(dev2))
if dev1[:min_len] != dev2[:min_len]:
raise ValueError(
"Development periods must be compatible to concatenate stylers."
)

other_copy._snapshot_context()
self._pad_columns(other_copy)
self.concatenated.append(other_copy)
return self

@staticmethod
def _mask_style(
_data: pd.DataFrame,
Expand Down
Loading
Loading