Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ chainladder-python/
│ │ ├── slice.py # TriangleSlicer, Location, Ilocation, At, Iat, VirtualColumns
│ │ ├── display.py # TriangleDisplay (__repr__, _repr_html_)
│ │ ├── io.py # TriangleIO, EstimatorIO (pickle, JSON, spreadsheet I/O)
│ │ ├── style.py # Styler (adapts pandas.io.formats.style.Styler)
│ │ ├── typing.py # TriangleProtocol, type aliases (BackendArray, etc.)
│ │ ├── README.md # Per-file summary of this subpackage
│ │ └── tests/
Expand Down
1 change: 1 addition & 0 deletions chainladder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
Triangle,
DevelopmentCorrelation,
ValuationCorrelation,
Styler,
)
from chainladder.development import ( # noqa (API import)
DevelopmentBase,
Expand Down
2 changes: 2 additions & 0 deletions chainladder/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
DevelopmentCorrelation,
ValuationCorrelation,
) # noqa (API import)
from chainladder.core.style import Styler # noqa (API import)

__all__ = [
"Triangle",
"DevelopmentCorrelation",
"ValuationCorrelation",
"Styler",
]
279 changes: 279 additions & 0 deletions chainladder/core/style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
"""
Styler for formatting Triangle output.
"""

# This Source Code Form is subject to the terms of the Mozilla Public
# 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/.
from __future__ import annotations

import copy as copy_module

from functools import partial

import numpy as np
import pandas as pd
from pandas.io.formats.style import Styler as _PandasStyler

from chainladder import options
from chainladder.core.triangle import Triangle

from datetime import (
date,
datetime,
)
from typing import (
Any,
TypeAlias,
)

# A value accepted by pandas.Timestamp's constructor.
ValuationDateLike: TypeAlias = int | float | str | date | datetime | pd.Timestamp

del annotations


class Styler(_PandasStyler):
"""
Styles a Triangle according to the data with HTML and CSS.

This class provides methods for styling and formatting a Triangle. The
styled output can be rendered as HTML or LaTeX, and it supports CSS-based styling, allowing
users to control colors, font styles, and other visual aspects of triangular data. It is particularly
useful for presenting Triangle objects in a Jupyter Notebook environment or when exporting
styled triangles for reports.

Parameters
----------
triangle: Triangle
The Triangle to style.
*args: Any
Additional positional arguments passed to
:class:`pandas.io.formats.style.Styler`.
**kwargs: Any
Additional keyword arguments passed to
:class:`pandas.io.formats.style.Styler`.

Raises
------
TypeError
If ``triangle`` is anything other than a Triangle.
ValueError
If ``triangle`` is multidimensional, i.e. holds more than a single index
and column, or if it is empty, i.e. holds no values to style.

See Also
--------
Triangle.style : Returns a Styler for the Triangle.

Examples
--------

.. testcode::

import chainladder as cl

raa = cl.load_sample("raa")
raa.link_ratio.style.format(precision=1)

Please see: :doc:`Triangle Visualization </user_guide/style>` for more
examples.
"""

def __init__(
self,
triangle: Triangle,
*args: Any,
**kwargs: Any,
) -> None:

if not isinstance(triangle, Triangle):
# The frame case is the likely mistake, so point it somewhere useful.
hint = (
" Use DataFrame.style for a DataFrame."
if isinstance(triangle, (pd.DataFrame, pd.Series))
else ""
)
raise TypeError(
"Styler must be created from a Triangle, not a "
f"{type(triangle).__name__}.{hint}"
)
if triangle._dimensionality in ["multi", "empty"]:
raise ValueError("Styler only supports a single Triangle.")
data = triangle._repr_format(origin_as_datetime=False)
Comment thread
cursor[bot] marked this conversation as resolved.
super().__init__(data, *args, **kwargs)
self._triangle = triangle
self.format(triangle._get_format_str(data=data), na_rep="")

def _copy(
self,
deepcopy: bool = False,
) -> Styler:
"""
Copy the Styler. Overrides the parent Pandas Styler to be able to work on Triangles instead of a DataFrame.

Parameters
----------
deepcopy: bool
If ``True``, deep-copy every attribute carried over from the calling Styler.

Returns
-------
Styler
A new copy of the ``Styler``.
"""
styler = object.__new__(type(self))
_PandasStyler.__init__(styler, self.data)
styler._triangle = self._triangle
for key, value in self.__dict__.items():
if key in (
"data",
"index",
"columns",
"_triangle",
):
continue
setattr(
styler,
key,
copy_module.deepcopy(value) if deepcopy else value,
)
Comment thread
genedan marked this conversation as resolved.
return styler

@staticmethod
def _mask_style(
_data: pd.DataFrame,
mask: np.ndarray,
props: str,
) -> np.ndarray:
"""
Apply ``props`` to the cells ``mask`` selects, and leave every other
cell unstyled.

Parameters
----------
_data: pd.DataFrame
The styled DataFrame, as passed in by ``Styler.apply``. Unused --
``mask`` and ``props`` already carry everything needed to style --
but required by ``apply``'s calling convention.
mask: np.ndarray
A 2-D boolean array, shaped like ``_data``, selecting the cells to
style.
props: str
A CSS properties string applied to the selected cells, e.g.
``"background-color: blue;"``.

Returns
-------
np.ndarray
An array shaped like ``mask``, holding ``props`` at the cells it
selects and ``""`` (unstyled) everywhere else.

Example
-------
>>> mask_array = np.array([[False, True]])
>>> Styler._mask_style(pd.DataFrame(), mask_array, "background-color: red;")
array([['', 'background-color: red;']], dtype='<U22')
"""
return np.where(mask, props, "")

def highlight_lower_triangle(
self,
color: str = "blue",
text_color: str | None = None,
props: str | None = None,
valuation_date: ValuationDateLike | None = None,
) -> Styler:
"""
Highlight the lower triangle -- the cells beyond the Triangle's
latest diagonal -- with a style.

Parameters
----------
color: str
Background color applied to lower-triangle cells. Ignored if
``props`` is given. Defaults to "blue".
props: str | None
A full CSS properties string to apply instead of ``color`` and
``text_color``, e.g. ``"background-color: blue; opacity: 60%;"``.
Optional.
valuation_date: ValuationDateLike | None
The "as of" date used to determine the diagonal beyond which the
cells are highlighted. If ``None``, defaults to the
lastest diagonal.
text_color: str | None
Text color applied to lower-triangle cells. Ignored if ``props``
is given. Left unstyled (i.e. inherited) if not given.

Returns
-------
Styler

Raises
------
ValueError
If the wrapped Triangle is a valuation Triangle.

Examples
--------

.. code-block:: python

import chainladder as cl

cl.load_sample("raa").style.highlight_lower_triangle(color="lightgray")

A softer, higher-contrast pairing than the default:

.. code-block:: python

cl.load_sample("raa").style.highlight_lower_triangle(
color="#BDD7EE", text_color="#1F4E78"
)

A fully-predicted Triangle needs no valuation date, even though its
cells are no longer ``NaN``.

.. code-block:: python

raa = cl.load_sample("raa")
full = cl.Chainladder().fit(raa).full_triangle_
full.style.highlight_lower_triangle(color="lightgray")
"""
if self._triangle.is_val_tri:
raise ValueError(
"highlight_lower_triangle does not support a valuation Triangle."
)

# Find which cells are beyond the valuation date. Does this by filling
# a triangle's cells with their valuation dates and then comparing them to
# the triangle's overall valuation date.
val_array = np.array(self._triangle.valuation).reshape(
self._triangle.shape[-2:],
order="F",
)
if valuation_date is not None:
cutoff = pd.Timestamp(valuation_date)
elif self._triangle.valuation_date >= pd.Timestamp(options.ULT_VAL):
cutoff = pd.Timestamp(val_array[-1, 0])
Comment thread
genedan marked this conversation as resolved.
else:
cutoff = self._triangle.valuation_date
mask = val_array > cutoff
if mask.shape != self.data.shape:
raise ValueError(
"highlight_lower_triangle only supports a single (2-D) Triangle."
)

if props is None:
props = f"background-color: {color};"
if text_color is not None:
props += f" color: {text_color};"

return self.apply( # pyright: ignore[reportReturnType]
partial(
self._mask_style,
mask=mask,
props=props,
),
axis=None,
)
Loading
Loading