# Loss filter

> The term that selects a contract's covered losses: a pointwise predicate that keeps the YELT rows matching a peril, geography, or line of business.

Source: https://reinsurance.dev/financial-modelling/terms/filter/

A property-cat contract covers a defined slice of the cedent's book: a peril, a geography, a [line of business](https://reinsurance.dev/glossary/#line-of-business-lob).[^covered] A **[loss filter](https://reinsurance.dev/glossary/#loss-filter)** is the [term](https://reinsurance.dev/glossary/#term-financial-term) that selects it: a predicate that keeps only the [year event loss table (YELT)](https://reinsurance.dev/toolkit/trials/#the-yelt) rows matching the criteria and drops the rest. It is **pointwise** (each row is evaluated independently, with no state across rows), and it changes which losses are present without transforming their values.

$$
\text{filter}(P): \quad \text{keep row } r \text{ of the YELT iff } P(r) \text{ is true}
$$

The predicate reads a row, not an occurrence. The rows of one occurrence `(trial_id, timestamp, event_id)` are evaluated independently, so a filter can keep part of an occurrence: a geography filter on a hurricane that hit Florida and Texas keeps the Florida rows and drops the Texas rows, and the occurrence excess downstream sees a partial occurrence (the covered share, which is exactly the contract's view of it). What the predicate can express is bounded by the schema: inclusions and exclusions written on a column the YELT carries (peril, geography, line of business) and nothing written on one it does not.

Applied to Trial 9 in isolation, a Florida-hurricane filter, $P = (\text{peril} = \text{HU} \,\wedge\, \text{geography} = \text{FL})$, keeps four of the thirty occurrences and drops the rest. This is the same selection that opens [Contract 1's dissection](https://reinsurance.dev/financial-modelling/dissection/#step-1-filter-to-covered-losses):

*Figure (interactive on the web page): A Florida-hurricane filter on Trial 9: four FL HU occurrences are kept (\$216.8M); the other 26 (earthquakes and non-Florida storms) are dropped.*

| Filter (peril = HU, geography = FL) | Occurrences | Subject |
| --- | ---: | ---: |
| Kept (Florida hurricane) | 4 | \$216.8M |
| Dropped | 26 | \$55.3M |
| **Trial 9 subject** | **30** | **\$272.1M** |

Python (`python/caldera_re/loss_filter.py`, runnable on the web page):

```python
import pandas as pd

from caldera_re.data import cedents
from caldera_re.report import table

def loss_filter(loss: pd.DataFrame, **criteria) -> pd.DataFrame:
    """Keep only YELT rows matching every column = value criterion.

    Pointwise: each row is evaluated independently, with no state across rows.
    Used to restrict a contract to its covered peril, geography, or line of
    business. It changes which losses are present without altering their values.

    Parameters
    ----------
    loss : pd.DataFrame
        YELT.
    **criteria
        Column = value pairs, e.g. peril='HU', geography_id='FL'. A row is kept
        only if it matches every criterion.

    Returns a copy of the matching rows.

    Cost: one boolean mask per criterion, O(R) over the R input rows with no
    sort and no per-trial state. The loss filter is the term whose purpose is
    to reduce the row count and the only one that can be pushed into storage:
    in a columnar store the same predicate becomes partition or row-group
    pruning on the categorical columns, so it runs first and every downstream
    term costs less for it.
    """
    mask = pd.Series(True, index=loss.index)
    for column, value in criteria.items():
        mask &= loss[column] == value
    return loss.loc[mask].copy()

if __name__ == '__main__':
    sc = cedents.suncoast

    # Contract 1's coverage: Florida hurricane.
    fl_hu = loss_filter(sc, peril='HU', geography_id='FL')

    print("Filter: peril=HU, geography=FL (Florida hurricane)")
    print(f"  Rows in:  {len(sc)}")
    print(f"  Rows out: {len(fl_hu)}")
    print()

    N = 20
    rows = []
    for s in range(1, N + 1):
        full = sc[sc['trial_id'] == s]['loss'].sum()
        flhu = fl_hu[fl_hu['trial_id'] == s]['loss'].sum()
        rows.append((s, full, flhu))
    table(['Trial', 'Full subject', 'FL HU subject'], rows,
          fmt=['{}', '{:.1f}M', '{:.1f}M'])

    print(f"\nFull subject EL:  {sc['loss'].sum() / N:.2f}M")
    print(f"FL HU subject EL: {fl_hu['loss'].sum() / N:.2f}M")
```

Across all 20 trials of the [demo tier](https://reinsurance.dev/toolkit/trials/#demo-scale), the filter reduces SunCoast's full subject to its Florida-hurricane component, Contract 1's [covered loss](https://reinsurance.dev/glossary/#covered-loss):

*Figure (interactive on the web page): Loss filter EP curves: SunCoast full subject vs the Florida-hurricane rows only (the covered stream, before any transforming term). The filter removes every non-FL-HU loss; the amber curve is what it drops.*

:::tip[Key Insight]
Order and cost both follow from the loss filter changing which rows exist rather than what they are worth. It commutes with the other pointwise terms (a [scaling](https://reinsurance.dev/financial-modelling/terms/scaling/) or a [contract period](https://reinsurance.dev/financial-modelling/terms/contract-period/) gives the same rows on either side of it) but not with the excess terms. An [aggregate excess](https://reinsurance.dev/financial-modelling/terms/aggregate-excess/) applied before the filter erodes against a trial total that includes rows the contract never covered, and gives a wrong number without an error. An [occurrence excess](https://reinsurance.dev/financial-modelling/terms/occurrence-excess/) applied before a geography filter fails the same way. The term keeps its input rows, so the filter still finds a `geography_id` to read. But the layer has clipped the whole hurricane, and the filter keeps Florida's share of the clipped amount, where the contract clips Florida's share of the hurricane. On Contract 1's \$30M xs \$10M layer the correct order gives a mean occurrence-layer gross of \$18.64M per trial and the reversed order \$18.88M, with no error raised. That is the bug the filter-first order prevents. The same fact sets the cost: the loss filter is the term whose purpose is to reduce the row count, and the only one that can be pushed into storage. In a columnar store, the same predicate becomes partition or row-group pruning on `peril` and `geography_id`, and every downstream term costs less for it.
:::

Because a filter changes only which rows are present, not their values, it is the natural first term in a pipeline, making explicit the coverage a contract names. Contract 1's full composition begins with one (the aggregate cap $2\ell$ [encodes](https://reinsurance.dev/financial-modelling/contracts/catxol/#reinstatements-as-the-aggregate-limit) its single [reinstatement](https://reinsurance.dev/glossary/#reinstatement), and the whole composition is the contract at 100%, because [participation](https://reinsurance.dev/glossary/#participation) applies at [roll-up](https://reinsurance.dev/financial-modelling/portfolios/#the-roll-up-participation-then-sum)):

$$
\text{Contract 1} = \text{agg}_{\text{excess}}(0, 2\ell) \circ \text{occ}_{\text{excess}}(A, \ell) \circ \text{period}(t_0, t_1) \circ \text{filter}(P)
$$

As with every [composition](https://reinsurance.dev/financial-modelling/composition/), the formula reads right to left (the filter applies first) while data flows left to right. The [contract period](https://reinsurance.dev/financial-modelling/terms/contract-period/) follows it, narrowing the kept rows to those inside the inception-to-expiry window. The [catastrophe excess of loss (CatXoL)](https://reinsurance.dev/financial-modelling/contracts/catxol/) page assembles the full pipeline.

[^covered]: Homeowners Choice Property & Casualty Insurance Company, [Property Catastrophe Excess of Loss Reinsurance Contract, 2016](https://www.sec.gov/Archives/edgar/data/1400810/000119312516670071/d190243dex1011.htm), *SEC EDGAR*. The Business Covered article limits the contract to "direct and assumed business classified by the Company as the property perils of Homeowners, Condominium Owners, Renters and Dwelling", the Territory article makes the territorial limits "identical with those of the Company's Policies", and the Exclusions article names what the contract does not cover. The wording calls those four lines of business "property perils", an overload this site does not use. Guy Carpenter, [Glossary of Reinsurance Terms](https://www.guycarp.com/company/news-and-events/glossary.html), defines exclusions as "those risks, perils, or classes of insurance with respect to which the reinsurer will not pay loss or provide reinsurance".
