utils functions

Many of these methods are available on triangles directly, such as summarize, aggregate and merge, among others. However, the core functionality depends on the following independent functions.

adjust

bermuda.utils.adjust.paid_bs_adjustment(triangle: Triangle, triangle_ult_claim_counts: Triangle) Triangle

Adjust paid losses in triangle using the “paid” Berquist-Sherman adjustment for changes in claim settlement rate. The adjustment assumes that the latest diagonal of a triangle will reflect current claim settlement rates, so paid losses in other cells are adjusted using linear interpolation. If the claim disposal rate for a given cell is less than the selected disposal rate for that development lag, losses are adjusted upwards (i.e. using the next cell for the same experience period); if the claim disposal rate for a given cell is greater than the selected disposal rate for that development lag, losses are adjusted downwards (i.e. using the previous cell in the same experience period).

For more details on Berquist-Sherman, see Chapter 13 of this text: https://www.casact.org/sites/default/files/database/studynotes_friedland_estimating.pdf

Parameters:
  • triangle – The Triangle that will be adjusted. It must include cwp_claims and paid_loss as fields.

  • triangle_ult_claim_counts – A right edge Triangle that has the ultimate reported_claims corresponding to each period in tri. The mean of reported_claims is used in disposal rate calculations, so its value can be either an array or a scalar.

Returns:

A Triangle with adjusted paid_loss values. Note that closed claim counts remain unchanged from the original triangle.

bermuda.utils.adjust.reported_bs_adjustment(triangle: Triangle, annual_severity_trend: float = 0.0, sev_trend_method: str | None = None)

Adjust reported losses in triangle using “reported” Berquist-Sherman adjustment for changes in case reserve adequacy. This method detrends average case outstanding values from the latest diagonal using a severity trend.

NOTE: the current implementation has the option to calculate a severity trend from the data. This solution is deterministic and non-ideal. Instead, severity trends should be modelled and provided to this method.

Parameters:
  • triangle – The Triangle for which reported_loss should be adjusted. Note that this triangle must include reported_loss, paid_loss, open_claims, and cwp_claims.

  • annual_severity_trend – The annualized severity trend that will be used to adjust reported_loss provided as a decimal (e.g. 0.15 is 15% severity trend). Ignored if sev_trend_method is not None.

  • sev_trend_method – Optional method used to calculate severity trend. Options are “latest” or “all”. If “all”, the mean of calculate severity trend for all cells will be used as the severity trend; if “latest”, instead the mean will be taken only of the latest diagonal of triangle.

Returns:

A triangle with adjusted reported_loss values.

bermuda.utils.adjust.weight_geometric_decay(triangle: Triangle, annual_decay_factor: float, basis: str = 'evaluation', tri_fields: str | list[str] | None = None, weight_as_field: bool = True) Triangle

Implements loss experience period geometric decay weighting. Suppose a triangle’s loss experience periods are indexed by t=1:T, with T being the latest experience period in the triangle. Then, each loss experience period is weighted by a geometric progression (r)^{(T-t)*frequency}, where r is the geometric factor and frequency is defined as (resolution of tri) / 12.

Note that if weight_as_field = False, the processed triangle returned from this function will have fields that are meaningless. The usage of this function should be limited towards the preprocessing of triangle data used when fitting loss development models if weight_as_field is False.

Parameters:
  • triangle – triangle input data.

  • annual_decay_factor – the geometric factor to apply on an annual basis. Annual rates are

  • 1 (automatically converted to equal the implied rates that match the resolution of tri. Note that a factor of)

  • periods ((and technically 0) will apply equal weighting to all experience)

  • Must (i.e. the triangle will not be modified.)

  • 1. (be > 0 and <=)

  • basis – what basis to weight the triangle on? Available options are evaluation (weighting is applied diagonally, i.e. by calendar year)

  • experience (and)

  • tri_fields – loss fields in tri to geometrically weight. Default is None, in which case

  • accordingly. (all fields in the triangle are weighted)

  • weight_as_field – should the weights be returned as an additional field (True) or should the data be weighted in the triangle directly (False)?

Returns:

A triangle with a field called geometric_weight (if weight_as_field is True), or a triangle with directly weighted fields.

aggregate

bermuda.utils.aggregate.aggregate(triangle: Triangle[Cell], period_resolution: tuple[int, str] | None = None, eval_resolution: tuple[int, str] | None = None, period_origin: date = datetime.date(1999, 12, 31), eval_origin: date = datetime.date(1999, 12, 31), summarize_premium: bool = True) Triangle[Cell]

Change the period and/or evaluation resolution of a triangle.

Period resolution is defined as the length of each experience period in a triangle. Similarly, evaluation resolution is defined as the interval between each successive evaluation date in a triangle. Period resolution and evaluation are commonly equal to each other, but this is not a hard requirement.

This aggregation implementation will not try to interpolate, apportion, or otherwise estimate triangle cells when data is insufficient to determine an exact answer. This means that if a triangle has two experience periods, 2020-01-01:2020-09-30 and 2020-10-01:2021-06-30, aggregation to annual data is not possible.

Aggregation over experience periods is equivalent to summing all values from all cells that are being merged into a new cell. No warnings are issued if not all cells have the same set of attributes, or if the new experience period isn’t completely covered by merged cells. Depending on the semantics of your source data and intended application, this may not be advisable.

Aggregation over evaluation dates is equivalent to filtering out evaluation dates. No effort is made to “carry forward” cells with gaps in their evaluation date history. The aggregation strategy is to sum fields over cells. This makes sense for fields such as paid loss, earned premium, and claim counts, but aggregation-by-summation doesn’t make sense for fields such as loss ratio or average claim severity. This function does not attempt to infer an appropriate aggregation strategy based on the name of each field.

Parameters:
  • triangle – Triangle to aggregate.

  • period_resolution – If supplied, the length of each experience perid. Format is a tuple with an integer number of units and a string specifying the unit. E.g., (7, “day”), (3, “months”), (1, “year”). If missing, no experience period aggregation will be performed.

  • eval_resolution – If supplied, the interval between each evaluation date. Format is the same as for period_resolution. If missing, no evaluation date aggregation will be performed.

  • period_origin – A date that will be guaranteed to be a period start date (if there is any contemporaneous data). This argument is used for offseting experience periods temporally: e.g., for annual experience periods that start on July 1 of each year, supply period_origin=datetime.date(2000, 7, 1) or similar.

  • eval_origin – A date that will be guaranteed to be an evaluation date (if there is any contemporaneous data). See period_origin for details of semantics.

  • summarize_premium – Whether or not to sum up premiums over aggregated cells. Defaults to True, set to False when summarizing over loss details over which premiums should not be summed.

basis

bermuda.utils.basis.to_cumulative(triangle: Triangle[IncrementalCell]) Triangle[CumulativeCell]

Convert a Triangle from incremental to cumulative basis.

bermuda.utils.basis.to_incremental(triangle: Triangle[CumulativeCell]) Triangle[IncrementalCell]

Convert a Triangle from cumulative to incremental basis.

bootstrap

bermuda.utils.bootstrap.bootstrap(triangle: Triangle, n: int, seed: int, field: str | list[str] | None = None) list[Triangle]

Sample bootstrap replicates from a triangle.

Traditional bootstrapping would sample triangle cells with replacement, but this is inappropriate because cells are not iid realizations. This function implements two different types of bootstrapping that preserve structure between cells:

  • If a triangle has multiple development lags and experience periods, the ‘ata’ method is implemented, which resamples volume-weighted age-to-age factors between experience periods.

  • If the triangle has a single experience period, diagonal, or development lag, then maximum entropy bootstrapping is used. Maximum entropy bootstrapping (Vinod, 2006) is a bootstrapping method for time series data that does not require stationarity, detrending, or block bootstrapping.

The function returns a multi-slice triangle with metadata details key-value pair (‘bootstrap’, i), where i is the i-th replicate.

Parameters:
  • triangle – The triangle to re-sample.

  • n – The number of boostrap replicates to sample.

  • seed – The RNG seed.

  • field – The field, or list of fields, to resample in the triangle. Defaults to None, which selects all fields.

Returns:

A list of (potentially multi-slice) triangles, one for each bootstrap replicate.

bermuda.utils.bootstrap.maximum_entropy_ensemble(x: list[float], U: list[float], L: tuple[float, float] | None = None) ndarray

Applies the maximum entropy bootstrap algorithm of Vinod (2006).

See the following links for Python and R implementations that this function is heavily based on. However, note that the Python implementations are wrong in that they don’t appear to sort the final quantiles:

  • Vinod, H.D. 2006. Maximum entropy ensembles for time series inference

    in economics. Journal of Asian Economics. 17 (6), 955-978.

currency

bermuda.utils.currency.convert_currency(triangle: Triangle, target_currency: str, exchange_rates: dict[str, float]) Triangle

Convert all currency-denominated fields in a Triangle to a target currency.

Non-currency fields (e.g., claim counts, policy counts) will not be altered.

Parameters:
  • triangle – A triangle to convert to a single currency.

  • target_currency – The currency that all values will be expressed in, e.g. USD or EUR.

  • exchange_rates – A dictionary with exchange rates from other currencies to the target currency. For example, to convert a triangle with GBP-denominated losses to USD with an assumed exchange rate of 1.0 GBP = 1.4 USD, then exchange_rates would be {“GBP”: 1.4}.

Returns:

A triangle with all cells denominated in the target currency.

bermuda.utils.currency.convert_to_dollars(triangle: Triangle, exchange_rates: dict[str, float] | None = None) Triangle

Convert all currency-denominated fields in a Triangle to USD.

See convert_currency for more details on the conversion process.

Parameters:
  • triangle – A triangle to convert to dollars.

  • exchange_rates – A dictionary that maps currencies to currency-to-dollar exchange rates.

Returns:

A triangle with all cells denominated in dollars.

disaggregate

bermuda.utils.disaggregate.disaggregate(triangle: Triangle, resolution_exp_months: int = 3, resolution_dev_months: int = 3, fields: list[str] | None = None, period_weights: list[float] | dict[date, list[float]] | None = None, interpolation_method: str = 'linear', extrapolate_first_period: bool = True, **interpolation_kwargs) Triangle
Disaggregate a triangle to a finer resolution of development and experience periods.

First it will attempt to disaggregate the triangle across experience periods putting different weights to field values over each subperiod. Next it will try to disaggregate across development axis applying interpolation.

Implement this function with caution. It should not be used to refine transaction program data. Instead, it can be applied to triangles used for comparison or similar purposes during transaction analysis.

Parameters:
  • triangle – The triangle to disaggregate.

  • resolution_exp_months – The resolution of experience periods in the output triangle.

  • resolution_dev_months – The resolution of evaluation dates in the output triangle.

  • fields – The list of fields in the triangle to disaggregate.

  • period_weights – Weights for field values applied during disaggregation of one experience period. Weights must be from [0, 1], and should sum to 1. If set as a dictionary, each experience period will use a separate list of weights. If set as a list, the same weights are applied to all experience periods. If not supplied, weights are equally distributed across each experience period.

  • interpolation_method – The interpolation method to use. See scipy.interpolate.interp1d to see the list of available interpolation methods.

  • extrapolate_first_period – Boolean flag. If true, extrapolation will be applied to get field values for development periods prior to the first evaluation date.

  • interpolation_kwargs – Keyword arguments passed to the interpolation method.

Returns:

A triangle with disaggregated experience periods and evaluation dates

at the desired resolutions.

bermuda.utils.disaggregate.disaggregate_development(triangle: Triangle, resolution_months: int, fields: list[str] | None = None, interpolation_method: str = 'linear', extrapolate_first_period: bool = True, **interpolation_kwargs) Triangle
Disaggregate a triangle to a finer resolution of development periods

using a selected interpolation method.

Parameters:
  • triangle – The triangle to disaggregate across development periods.

  • resolution_months – The resolution of evaluation dates in the output triangle.

  • fields – The list of fields in the triangle to interpolate.

  • interpolation_method – The interpolation method to use. See scipy.interpolate.interp1d to see the list of available interpolation methods.

  • extrapolate_first_period – Boolean flag. If true, extrapolation will be applied to get field values for development periods prior to the first evaluation date.

  • interpolation_kwargs – Keyword arguments passed to the interpolation method.

Returns:

A triangle with interpolated evaluation dates at the desired resolution.

bermuda.utils.disaggregate.disaggregate_experience(triangle: Triangle, resolution_months: int, period_weights: list[float] | dict[date, list[float]] | None = None, fields: list[str] | None = None) Triangle
Disaggregate a triangle to a finer resolution across experience periods.

The process allows to put different weights to each subinterval within one experience period.

Parameters:
  • triangle – The triangle to disaggregate across experience periods.

  • resolution_months – The resolution of experience periods in the output triangle.

  • period_weights – Weights for field values applied during disaggregation of one experience period. Weights must be from [0, 1], and should sum to 1. If set as a dictionary, each experience period will use a separate list of weights. If set as a list, the same weights are applied to all experience periods. If not supplied, weights are equally distributed across each experience period.

  • fields – The list of fields in the triangle to disaggregate.

Returns:

A triangle with disaggregated field values at the desired resolution.

extend

bermuda.utils.extend.make_right_diagonal(triangle, evaluation_dates, include_historic=False) Triangle

Create a new Triangle with one or more diagonals to the right of existing data.

bermuda.utils.extend.make_right_triangle(triangle: Triangle[Cell], dev_lags=None, dev_lag_unit='month') Triangle

Create the lower-right complement of a triangle.

fields

bermuda.utils.fields.add_statics(triangle: Triangle, source: Triangle, statics: list[str] = ['earned_premium', 'earned_exposure']) Triangle

Add “static” fields to a Triangle. Some fields (for example, accident-period earned premium) can be considered as fixed with respect to development lag. This method takes static fields from one triangle and adds them to another triangle. For example, if we have a triangle of predicted paid_loss and another disjoint triangle with observed paid_loss, we can fill in earned_premium values in the former triangle with values from the latter triangle. :param source: A Triangle that will be used to fill in static fields. :param statics: A list of field names to fill in.

Returns:

A Triangle with static fields filled in from source.

bermuda.utils.fields.array_from_field(triangle: Triangle, field: str) ndarray

Return an array with cell-level values from the triangle.

Parameters:
  • tri – Triangle to pull the cell values from.

  • field – Name of the field to turn into an array.

Returns:

An array of dimension (N, K) where N is the number of cells in the source triangle

and K is the array_size of the field.

bermuda.utils.fields.array_size(triangle: Triangle) int

Get the consistent non-scalar array size for field values in the Triangle. If there is more than one distinct array size, raise an error.

bermuda.utils.fields.array_sizes(triangle: Triangle) list[int]

Get the ordered set of all distinct non-scalar array sizes in field values in the Triangle.

join

bermuda.utils.join.join(tri1: Triangle, tri2: Triangle, join_type: str = 'full', on: list[str] | None = None) list[Tuple[Cell | None, Cell | None]]

Create pairs of cells that share the same coordinates and metadata from two triangles.

Join is a lower-level operation that returns a list of pairs of cells. Many common use cases are better served by its higher-order cousin, merge.

Parameters:
  • tri1 – The left triangle in the join.

  • tri2 – The right triangle in the join.

  • join_type

    The type of relational join to perform. Available join types are: * inner: Return only cell coordinates that are present in both triangles. * left: Return all cell coordinates in the left triangle, along with any

    available matches on the right triangle.

    • right: Return all cell coordinates in the right triangle, along with any

      available matches on the left triangle.

    • full: Return all cell coordinates in both triangles, regardless of

      whether there is a match on the other side.

    • left_anti: Return only those cells in the left triangle that do not have

      matching coordinates in the right triangle.

    • right_anti: Return only those cells in the right triangle that do not

      have matching coordinates in the left triangle.

  • on – which metadata attributes and detail keys to join on when merging cell values. Default behavior is to consider all metadata attributes when merging cells.

Returns:

A list of 2-tuples of Cell`s. Each tuple represents a pair of cells with the same coordinates and metadata. The first and second elements of the tuple are the cell from the left and right triangles, respectively. Depending on the type of join, either of the cells in the tuple may be `None, but by construction, every tuple will have at least one non-None cell.

merge

bermuda.utils.merge.coalesce(triangles: list[Triangle]) Triangle

Coalesce triangle cells from a list of Triangles such that the resulting triangle is a union of the cells from each triangle. Matching cells in the earlier triangles in the triangle list take precedence over cells from later triangles.

coalesce is different from merge in that it only keeps the fields and values from the cell with precedence, wheras merge will return the union of fields across the matching cells.

Parameters:

triangles – A list of Triangle whose cells will be coalesced.

Returns:

A Triangle with Cells at all coordinates of the triangles in the triangle list.

bermuda.utils.merge.loose_period_merge(tri1: Triangle[Cell], tri2: Triangle[Cell], suffix: str | None = None) Triangle[Cell]

Add period-level values of tri2 to tri1 cells. tri2 must have a single cell per period index.

All cells from tri1 are preserved by this merge. If there is a matching cell in tri2 by index (period_start, period_end, metadata), then values from tri2 take precedence. There must be only one cell in tri2 per index.

Note that this is different from period_merge in that it will allow partial matches on metadata details. For examples, if tri1 has a cell with metadata details {“a”: 1, “b”: 2} and tri2 has a cell with metadata details {“a”: 1}, then the cell from tri2 will be merged with the cell from tri1. This is useful for merging triangles with different levels of granularity, particularly in BFing. Tri1 must be more granular (or as granular) as tri2.

Parameters:
  • tri1 – First triangle in the merge.

  • tri2 – Second triangle in the merge.

  • suffix – If present, then every field in tri2 will have the suffix appended to the name of the field after the merge to avoid name conflicts. If None, then no suffix will be appended and the values in tri2 will overwrite the values in tri1.

bermuda.utils.merge.merge(tri1: Triangle, tri2: Triangle, join_type: str = 'full', on: list[str] | None = None) Triangle

Merge cells of two triangles, if coordinates overlap then prioritize right cell values.

All cells from tri1 and tri2 are preserved by this merge. If two cells overlap by index, (period_start, period_end, evaluation_date, metadata), then values from tri2 take precedence.

Merge is a higher-level operation that returns a Triangle of merged cells. For a lower-level implementation that returns a list of pairs of matched cells, see join.

merge is different from coalesce in that it combines the union of fields in cells at the same coordinates, where coalesce only keeps the fields and values from the cell with precedence.

Parameters:
  • tri1 – The lower-precedence, left triangle to merge.

  • tri2 – The higher-precedence, right triangle to merge.

  • join_type – The type of relational join to perform. For a list of all join types and their descriptions, see the documentation for join.

  • on – which metadata attributes and detail keys to join on when merging cell values. Default behavior is to consider all metadata attributes when merging cells.

Returns:

A triangle with the merged cells from the two triangles.

bermuda.utils.merge.period_merge(tri1: Triangle[Cell], tri2: Triangle[Cell], suffix: str | None = None) Triangle[Cell]

Add period-level values of tri2 to tri1 cells. tri2 must have a single cell per period index.

All cells from tri1 are preserved by this merge. If there is a matching cell in tri2 by index (period_start, period_end, metadata), then values from tri2 take precedence. There must be only one cell in tri2 per index.

Parameters:
  • tri1 – First triangle in the merge.

  • tri2 – Second triangle in the merge.

  • suffix – If present, then every field in tri2 will have the suffix appended to the name of the field after the merge to avoid name conflicts. If None, then no suffix will be appended and the values in tri2 will overwrite the values in tri1.

method_moments

bermuda.utils.method_moments.moment_match(triangle: Triangle, field_names: list[str], distribution: Literal['normal', 'lognormal', 'gamma']) Triangle

Use method of moments matching to convert samples in a triangle from one distribution to another.

Parameters:
  • triangle – bermuda.triangle.Triangle with fields that need converted.

  • field_names – List of field names to apply conversion to. Typical use cases include “reported_loss”, “paid_loss”,

  • "incurred_loss"

  • etc.

  • distribution – Distribution to convert sample to. The empirical sample mean and variance are matched to moments of

  • distribution

  • from. (which is then sampled)

Raises:

KeyError – if supplied field_names are not all contained in the input triangle.

Returns:

bermuda.triangle.Triangle with field_names samples in each cell replaced with samples from distribution. If a field contains only scalar values in each cell, the scalar values are returned (no moment matching is done). Otherwise, if the field contains arrays of samples, moment matched samples will be returned (all with the same rank order as the original samples).

Return type:

Triangle

shift_origin

bermuda.utils.shift_origin.shift_origin(triangle: Triangle, origin_match_triangle: Triangle)

Shift the origin of a triangle to match the origin of another triangle.

Parameters:
  • triangle – The triangle to shift.

  • origin_match_triangle – The triangle to match the origin of.

Returns:

The shifted triangle.

Return type:

Triangle

slice

summarize

bermuda.utils.summarize.blend(triangles: list[Triangle], weights: list[float] | dict[str, ndarray] | None = None, method: Literal['mixture', 'linear'] = 'mixture', seed: int | None = None) Triangle

Return a weighted blend of triangles by their values.

There are two distinct methods of blending. Mixture blending samples cell values in proportion to their weight. Linear blending is a linear weighted average of the cell values. Mixture blending only applies to distributions of cell values with the same dimensions, but linear blending can be applied between distributions and scalars alike.

Parameters:
  • triangles – The list of triangles to blend.

  • weights – The optional list or dictionary of weights. Cell-wise weights are passed as a dictionary.

  • method – The blending method to use. Can be one of [‘mixture’, ‘linear’]. Defaults to mixture.

  • seed – The seed to use for mixture blending.

Returns:

A blended triangle.

bermuda.utils.summarize.split(triangle: Triangle, detail_keys: list[str]) dict[Any, Triangle]

Turn a Triangle into a dictionary of Triangles grouped by specified detail keys.

bermuda.utils.summarize.summarize(triangle: Triangle, summary_fns: dict[str, Callable] | None = None, summarize_premium: bool = True) Triangle

Aggregate a Triangle across metadata.

For example, given a triangle with state and coverage-level detail, this would generate a triangle where all cells with the same coordinates are merged, and any metadata that varies over the triangle would be dropped.

Parameters:
  • triangle – The single triangle to summarize.

  • summary_fns – A dictionary of functions to use to summarize each field in the triangle. If None, a default set of functions is supplied. If specified, the dictionary is added to the default set of summary functions, which makes it possible to easily add functions for custom fields without affecting defaults.

  • summarize_premium – Whether to summarize the premium field. If False, the premium field will not be summed accross cells, but will be copied from the first matching cell in the triangle.

Returns:

A summarized triangle with a single cell for each unique combination of coordinates.

bermuda.utils.summarize.summarize_cell_values(cells: list[Cell], agg_fns: dict[str, Callable] | None = None, summarize_premium: bool = True) dict[str, float | int | ndarray | float64 | int64 | None]

Combine all of the values in cells into a single dictionary.

The semantics of combining each individual field in values is controlled by the agg_fns argument. Each summarization operation is passed a dictionary of all cell values so that fields whose aggregation depends on multiple values (i.e., loss ratio) can work correctly.

If a key is present in some, but not all cells, default behavior is to pass in explicit None s for the missing keys into the aggregation function. Each aggregation function has the flexibility to decide whether to return a value in the presence of None s.