Skip to content
Property-cat reinsurance analytics for software engineers

Analytics Toolkit · Section 4.5

Attachment and exhaustion probability

How often a layer is triggered and how often it is fully consumed, on the aggregate and the occurrence basis, with this site's inequality conventions.

Two metrics compare a contract parameter with the distribution rather than measuring the distribution alone, and the Financial Modelling chapter consumes both: how often a layer is triggered, and how often it is fully consumed.

The figures on this page are quoted at 100% and come from the 20-trial demo tier, where one trial is five percentage points of probability; the basis is stated with each figure. Production trial sets of 10,000 to 1,000,000+ trials resolve these probabilities far more finely, and the demo data’s 1,000-trial tier shows the contrast.

Attachment probability is the probability that a loss exceeds the attachment point of a contract: how often the contract is triggered. The basis matters here more than for any other metric in this chapter, and the aggregate exceedance probability (AEP) basis that the Metrics overview assumes does not settle it: an aggregate contract attaches on the trial total, an occurrence contract on a single occurrence. On trial totals:

Pattach=1N∑j=1N1[Lj>A]P_{\text{attach}} = \frac{1}{N} \sum_{j=1}^{N} \mathbf{1}[L_j > A]

That is the trigger frequency of an aggregate contract. For an occurrence contract — the catastrophe excess of loss (CatXoL), the site’s central type — a trial total above AA says nothing about whether any single occurrence reached it, and one trial can hold several that did. The occurrence basis reads the worst occurrence of each trial, and the frequency form averages the number of attaching occurrences per trial:

Pattachocc=1N∑j=1N1[max⁡(t,e)Lj,t,e>A],nattach=1N∑j=1N∑(t,e)1[Lj,t,e>A]P_{\text{attach}}^{\text{occ}} = \frac{1}{N} \sum_{j=1}^{N} \mathbf{1}\Big[\max_{(t,e)} L_{j,t,e} > A\Big], \qquad n_{\text{attach}} = \frac{1}{N} \sum_{j=1}^{N} \sum_{(t,e)} \mathbf{1}[L_{j,t,e} > A]

In English: for the occurrence basis, ask whether the largest occurrence in the trial cleared the attachment; for the count, tally every occurrence that did. A layer with 30% attachment probability on its own basis pays a claim in roughly 3 trials out of 10. Quoted on the wrong basis, the figure is not off by a little. On SunCoast’s subject loss, 70% of trials total more than $200M and no single occurrence does. Attachment probability is a key input to pricing: it is the frequency component of a layer’s expected loss. The layer’s EL is PattachP_{\text{attach}} times the mean recovery given that the layer attaches, frequency times conditional severity. It is not how often the expected loss is observed. On the aggregate basis, and for Pattach<1P_{\text{attach}} < 1, the subject loss behind the attaching trials averages to the Tail Value at Risk (TVaR) at confidence 1−Pattach1 - P_{\text{attach}}, because with the strict inequality the attaching trials are exactly the top k=N⋅Pattachk = N \cdot P_{\text{attach}} ranks. On SunCoast’s 20-trial tier at A=200MA = 200\text{M}, Pattach=0.70P_{\text{attach}} = 0.70 and the fourteen attaching trials average $339.0M: TVaR0.30\text{TVaR}_{0.30} exactly, at the demo tier’s resolution like every number on this page. On the occurrence basis the identity does not hold: those trials are selected by their worst occurrence, not by their total.

This site’s convention: the inequality is strict, where the exceedance probability (EP) curve counts with ≥\geq: attaching means the layer pays, and a loss that exactly equals AA produces a recovery of zero. The EP curve evaluated at AA answers a different question (how often the loss reaches the attachment), and the two differ by exactly the trials sitting on it, P(L=A)P(L = A). On continuous losses that is nothing. On a trial set it is not. The demo catalog gives an event the same loss each time it recurs for a cedent, so on the occurrence basis the $174.1M Texas hurricane is the worst occurrence in seven of SunCoast’s 20 trials. An occurrence layer attaching exactly there reads 35% off the EP curve (the value at the last of the seven tied ranks) against 0% attachment probability. Transformed streams tie the same way on the aggregate basis. Contract 1’s gross aggregate sits at its $60M cap in two trials, so a retro layer attaching at $60M reaches in 10% of trials and pays in none. Both figures are true; this site’s attachment probability is the paying one. Nothing priced depends on the choice: expected loss is continuous in AA, and the tied trials contribute zero recovery either way.

In code the basis is the curve, not the function: the AEP curve holds each trial’s total and the occurrence exceedance probability (OEP) curve each trial’s worst occurrence, so one function reads the loss column of whichever curve it is handed. Only the count needs the year event loss table (YELT), because an OEP curve keeps one occurrence per trial.

caldera_re/attachment_probability.py Python
from caldera_re.aep import aep
from caldera_re.data import cedents
from caldera_re.oep import oep
from caldera_re.report import table
from caldera_re.utils import OCCURRENCE_KEY, check_curve, trial_count

# The probability functions take an EP curve, not a YELT: aep() for the
# aggregate basis, oep() for the occurrence basis. The curve decides the
# basis; the function is the same. Only the curve's loss column is read.
#
# N is the length of the curve. aep() and oep() give every simulated trial
# a rank, at zero loss if the YELT has no row for it (trident_20.csv holds
# 19 of 20 trials, and a filtered slice can hold fewer). A function that
# reads the YELT directly asks the YELT for N, with trial_count().


def attachment_probability(curve, attachment):
    """Fraction of trials whose curve loss exceeds A.

    On an AEP curve: the trial total exceeds A (an aggregate layer's
    trigger frequency). On an OEP curve: the trial's worst occurrence
    exceeds A (an occurrence layer's).

    Strict '>' -- attaching means the layer pays, and a loss exactly at
    the attachment recovers nothing. The EP curve at A (>=) gives the
    reached-the-attachment figure; the two differ by P(loss == A), which
    is real on a trial set from a mean-only catalog (an event recurs at
    the same loss) and on any stream capped at a limit.
    """
    check_curve(curve)
    return (curve['loss'] > attachment).sum() / len(curve)


def attaching_occurrences(yelt_df, attachment, n_trials=None):
    """Mean number of occurrences per trial that exceed A.

    Not an EP-curve statistic: an OEP curve keeps only each trial's worst
    occurrence, so the count needs the YELT.
    """
    # OCCURRENCE_KEY is (trial_id, timestamp, event_id): one occurrence.
    occurrences = yelt_df.groupby(OCCURRENCE_KEY)['loss'].sum()
    return (occurrences > attachment).sum() / trial_count(yelt_df, n_trials)


if __name__ == '__main__':
    yelt = cedents.suncoast
    aep_curve = aep(yelt)
    oep_curve = oep(yelt)

    rows = []
    for a in [20.0, 60.0, 100.0, 200.0]:
        p_agg = attachment_probability(aep_curve, a)
        p_occ = attachment_probability(oep_curve, a)
        n_occ = attaching_occurrences(yelt, a)
        rows.append((a, p_agg, p_occ, n_occ))
    table(['A', 'aggregate', 'occurrence', 'attaching occ./trial'], rows,
          fmt=['{:.0f}M', '{:.0%}', '{:.0%}', '{:.2f}'])

Attachment probability counts the trials in which the layer pays anything; exhaustion probability counts those in which it pays everything.

Exhaustion probability is the probability that a loss reaches or exceeds the attachment point plus the limit (the contract is fully exhausted):

Pexhaust=1N∑j=1N1[Lj≥A+ℓ]P_{\text{exhaust}} = \frac{1}{N} \sum_{j=1}^{N} \mathbf{1}[L_j \geq A + \ell]

The inequality is ≥\geq where attachment’s is strict, for the same reason: a loss that exactly equals A+ℓA + \ell pays the full limit. Both conditions describe the recovery (positive, and equal to the limit), not the EP curve’s rank convention, and the basis question carries over unchanged: an occurrence layer exhausts on an occurrence, an aggregate layer on the trial total.

The ratio of exhaustion probability to attachment probability measures how close to all-or-nothing a layer is. If a layer almost always exhausts when it attaches, the cedent is buying an all-or-nothing contract: the reinsurer pays the full limit or nothing. This shapes how much capital the reinsurer must hold against the layer.

caldera_re/exhaustion_probability.py Python
from caldera_re.aep import aep
from caldera_re.utils import check_curve
from caldera_re.data import cedents
from caldera_re.oep import oep

# Same contract as attachment_probability: an EP curve in (aep() for the
# aggregate basis, oep() for the occurrence basis), only the loss column
# read, N the length of the curve (aep() and oep() rank every simulated
# trial, so the curve is never short).


def exhaustion_probability(curve, attachment, limit):
    """Fraction of trials whose curve loss reaches or exceeds A + limit.

    On an AEP curve: the trial total exhausts an aggregate layer. On an
    OEP curve: the trial's worst occurrence exhausts an occurrence layer
    at least once.

    The condition is inclusive (>=) because a loss exactly at the top of
    the layer pays the full limit -- the layer is exhausted, not merely
    attached. Mirrors the strict '>' of attachment probability.
    """
    check_curve(curve)
    threshold = attachment + limit
    return (curve['loss'] >= threshold).sum() / len(curve)


if __name__ == '__main__':
    A, limit = 10.0, 30.0

    # C1 covers Florida hurricane only, so both curves are built on that
    # slice. C1 is an occurrence layer: the OEP curve describes it; the AEP
    # curve is shown for the contrast. Three trials have no FL HU occurrence;
    # the slice still knows it represents 20 trials, so oep() and aep() rank
    # them at zero instead of dropping them.
    yelt = cedents.suncoast
    fl_hu = yelt[(yelt['peril'] == 'HU') & (yelt['geography_id'] == 'FL')]
    p_occ = exhaustion_probability(oep(fl_hu), A, limit)
    p_agg = exhaustion_probability(aep(fl_hu), A, limit)

    print(f"Contract C1: Florida HU CatXoL ({limit:.0f}M xs {A:.0f}M), "
          f"threshold {A + limit:.0f}M, FL HU subject loss")
    print(f"  occurrence basis (OEP curve)  P(max occurrence >= {A + limit:.0f}M) = {p_occ:.0%}")
    print(f"  aggregate basis  (AEP curve)  P(trial total    >= {A + limit:.0f}M) = {p_agg:.0%}")

Both probabilities count trials past a boundary and ignore the spread around the mean, which standard deviation measures.