Quick Start

Triangles

The core of Bermuda’s functionality is housed in the Triangle class. Triangle objects contain Cell objects, ordered by experience period and evaluation date. Here’s a quick example of a triangle with one cell for an experience period starting January 1st, 2025 and ending January 31st, 2025, evaluated at the end of January:

import datetime

from bermuda import Cell, Triangle

cell = Cell(
    period_start=datetime.date(2025, 1, 1),
    period_end=datetime.date(2025, 1, 31),
    evaluation_date=datetime.date(2025, 1, 31),
    values={"paid_loss": 1e3, "earned_premium": 10e3},
)
triangle = Triangle([cell])

The Cell object has the required fields period_start, period_end, evaluation_date, and values.

The Triangle class also provides an easy to read __repr__ method when calling/printing the object:

>>> triangle

               Cumulative Triangle

     Number of slices:  1
     Number of cells:  1
     Triangle category:  Regular
     Experience range:  2025-01-01/2025-01-31
     Experience resolution:  1
     Evaluation range:  2025-01-31/2025-01-31
     Evaluation resolution:  None
     Dev Lag range:  0.0 - 0.0 months
     Fields:
       earned_premium
       paid_loss
     Common Metadata:
       risk_basis   Accident

The term ‘slices’ at the top of the __repr__ output is Bermuda’s name for distinct types of metadata in the triangle. In this case, there is only one cell and, therefore, only one type of metadata and one slice. You can see this in more detail by running triangle.slices, which returns a dictionary of (Metadata, Triangle) key-value pairs and is useful for iterating over multi-slice triangles (see below).

Most of the attributes of triangles can be accessed directly through properties and methods, for instance:

>>> triangle.periods
[(datetime.date(2025, 1, 1), datetime.date(2025, 1, 31))]
>>> triangle.evaluation_dates
[datetime.date(2025, 1, 31)]
>>> triangle.dev_lags()
[0.0]
>>> triangle.fields
['earned_premium', 'paid_loss']

Note, Bermuda defines development lags as time since period end, not time since period start, and by default uses months as the time unit. In the above example, there has been exactly 0.0 months since period end. We could specify days as the time unit using triangle.dev_lags(unit="day").

You can also return individual cell field values via Python’s typical __get_item__ square-bracket syntax:

>>> cell["paid_loss"]
1000.0

And you can return individual triangle cells via indexing:

>>> triangle[0]["paid_loss"]
1000.0

Triangles can be concatenated. For instance, here’s another cell for January evaluated as of February 2025:

february_cell = Cell(
    period_start=datetime.date(2025, 1, 1),
    period_end=datetime.date(2025, 1, 31),
    evaluation_date=datetime.date(2025, 2, 28),
    values={"paid_loss": 3e3, "earned_premium": 10e3},
)
triangle += Triangle([february_cell])

This example uses Python’s += in-place operator.

A useful property on triangles is right_edge, which returns the cells for the latest evaluation date for each period in the triangle, which in this example is the February-evaluated cell:

>>> triangle.right_edge

               Cumulative Triangle

     Number of slices:  1
     Number of cells:  1
     Triangle category:  Regular
     Experience range:  2025-01-01/2025-01-31
     Experience resolution:  1
     Evaluation range:  2025-02-28/2025-02-28
     Evaluation resolution:  None
     Dev Lag range:  1.0 - 1.0 months
     Fields:
       earned_premium
       paid_loss
     Common Metadata:
       risk_basis   Accident

Triangle Metadata

The Metadata class identifies distinct triangle slices. This is useful when working with multi-program triangles, for example, or when specific details are useful to house in the triangle data structure. By default, empty Metadata have the following structure:

>>> from bermuda import Metadata
>>> Metadata()
Metadata(
    risk_basis='Accident',
    country=None,
    currency=None,
    reinsurance_basis=None,
    loss_definition=None,
    per_occurrence_limit=None,
    details={},
    loss_details={}
)

The details dictionary can take arbitrary key-value pairs (e.g. company name), and the loss_details dictionary is useful for distinguishing detail fields that should only be taken into account when operating on losses, not premium. The latter is used during certain grouping functions, such as bermuda.utils.aggregate and bermuda.utils.summarize, to exclude premium fields from grouping function operations. Here’s a quick worked example of handling triangles with multiple slices.

import datetime
from bermuda import Metadata, Cell, Triangle

metadata_a = Metadata(details=dict(company="A"))
metadata_b = Metadata(details=dict(company="B"))

cell = Cell(
    period_start=datetime.date(2025, 1, 1),
    period_end=datetime.date(2025, 12, 31),
    evaluation_date=datetime.date(2025, 12, 31),
    values={"reported_loss": 1e3, "written_premium": 1e4},
    metadata=metadata_a,
)

triangle_a = Triangle([cell])

# Use .replace to use the same cell with different metadata
triangle_b = triangle_a.replace(metadata=metadata_b)

combined = triangle_a + triangle_b
assert combined.slices == {metadata_a: triangle_a, metadata_b: triangle_b}

# bermuda.summarize combines cells with the same
# (period_start, period_end, evalution_date) coordinates
summarized = combined.summarize()
assert len(summarized) == 1
assert summarized[0]["reported_loss"] == 2e3
assert summarized[0]["written_premium"] == 2e4

# Only summarize the loss fields
summarized_losses = combined.summarize(summarize_premium=False)
assert summarized_losses[0]["reported_loss"] == 2e3
assert summarized_losses[0]["written_premium"] == 1e4

Above, we introduced a triangle method called summarize from combining multi-slice triangles. This exists as its own function in bermuda.utils.summarize, as well as a method on the Triangle class, i.e. triangle.summarize().

Loading and saving triangles

Bermuda can load and save triangles into a number of formats, namely long and wide data frames from CSV files, JSON files, and an internal triangle binary format known as trib. The io module includes relevant triangle_from_* and *_to_triangle* formats, e.g.

>>> from bermuda import meyers_tri, Triangle

>>> meyers_tri.to_json("meyers_triangle.json")
>>> meyers_tri = Triangle.from_json("meyers_triangle.json")
>>> meyers_tri

           Cumulative Triangle

Number of slices:  1
Number of cells:  100
Triangle category:  Regular
Experience range:  1988-01-01/1997-12-31
Experience resolution:  12
Evaluation range:  1988-12-31/2006-12-31
Evaluation resolution:  12
Dev Lag range:  0.0 - 108.0 months
Fields:
  earned_premium
  paid_loss
  reported_loss
Common Metadata:
  currency   USD
  country   US
  risk_basis   Accident
  reinsurance_basis   Net
  loss_definition   Loss+DCC

The .trib file stands for [tri]angle[b]inary, and is often quicker to work with for larger triangles than other formats.

Bermuda also integrates with triangles from the Python ChainLadder package using the bermuda.io.chain_ladder_to_triangle and bermuda.io.triangle_to_chain_ladder functions. For example, to convert the sample data in the ChainLadder package to a Bermuda triangle:

import chainladder as cl
from bermuda import (
    chain_ladder_to_triangle,
    triangle_to_chain_ladder,
)

chain_ladder_tri = cl.load_sample("clrd")
bermuda_tri = chain_ladder_to_triangle(chain_ladder_tri)
chain_ladder_from_bermuda = triangle_to_chain_ladder(bermuda_tri)