Skip to content

Claims Module

The Claims module provides tools for managing and analyzing claims data in reinsurance contexts. It includes classes for representing individual claims, collections of claims, and claims development triangles.

Core Classes

Claims Data Structure

The module provides a hierarchical structure for representing claims:

  • ClaimYearType: An enumeration defining different claim year bases (Accident Year, Underwriting Year, etc.)
  • ClaimDevelopmentHistory: Tracks the development of a claim over time, including paid and incurred amounts
  • ClaimsMetaData: Contains metadata about a claim (ID, dates, limits, etc.)
  • Claim: Combines metadata and development history for a single claim
  • Claims: A collection of Claim objects with methods to access and manipulate them

Claims Triangles

The triangles module provides tools for creating and analyzing claims development triangles:

  • Triangle: Represents a claims development triangle with methods for:
  • Converting between cumulative and incremental triangles
  • Calculating age-to-age factors
  • Fitting curves to development patterns
  • IBNERPatternExtractor: Extracts IBNER (Incurred But Not Enough Reported) patterns from triangles

Examples

Creating and Working with Claims

from pyre.claims.claims import Claim, Claims, ClaimsMetaData, ClaimDevelopmentHistory, ClaimYearType
from datetime import date

# Create claim metadata
metadata = ClaimsMetaData(
    claim_id="CL001",
    currency="USD",
    contract_limit=1000000,
    contract_deductible=10000,
    claim_year_basis=ClaimYearType.ACCIDENT_YEAR,
    loss_date=date(2022, 3, 15),
    report_date=date(2022, 4, 1),
    line_of_business="Property"
)

# Create claim development history
development = ClaimDevelopmentHistory(
    development_months=[0, 3, 6, 9, 12],
    cumulative_dev_paid=[0, 15000, 30000, 45000, 50000],
    cumulative_dev_incurred=[80000, 70000, 60000, 55000, 50000]
)

# Create a claim
claim = Claim(metadata, development)

# Access claim properties
print(f"Claim ID: {claim.claims_meta_data.claim_id}")
print(f"Latest paid amount: {claim.uncapped_claim_development_history.latest_paid()}")
print(f"Latest incurred amount: {claim.uncapped_claim_development_history.latest_incurred()}")
print(f"Latest reserved amount: {claim.uncapped_claim_development_history.latest_reserved_amount()}")

# Create a collection of claims
claims_collection = Claims([claim])

# Add another claim
second_claim = Claim(
    ClaimsMetaData("CL002", "EUR", loss_date=date(2022, 5, 10)),
    ClaimDevelopmentHistory(
        [0, 3, 6],
        [0, 5000, 10000],
        [20000, 15000, 12000]
    )
)
claims_collection.append(second_claim)

# Access claims in the collection
for claim in claims_collection:
    print(f"Claim {claim.claims_meta_data.claim_id} - "
          f"Loss date: {claim.claims_meta_data.loss_date}")

# Get unique modelling years
print(f"Modelling years: {claims_collection.modelling_years()}")

Working with Claims Triangles

from pyre.claims.triangles import Triangle, CurveType
from pyre.claims.claims import Claims

# Assuming we have a Claims collection called 'claims_data'

# Create a triangle from claims data
incurred_triangle = Triangle.from_claims(claims_data, value_type="incurred")
paid_triangle = Triangle.from_claims(claims_data, value_type="paid")

# Display the triangle
print(incurred_triangle)

# Convert to incremental triangle
incremental_triangle = incurred_triangle.to_incremental()

# Calculate age-to-age factors
factors = incurred_triangle.calculate_age_to_age_factors()
print("Age-to-age factors:")
for origin_year, factors_dict in factors.items():
    print(f"  Year {origin_year}: {factors_dict}")

# Get average age-to-age factors
avg_factors = incurred_triangle.get_average_age_to_age_factors(method="volume")
print(f"Volume-weighted average factors: {avg_factors}")

# Fit a curve to the development pattern
curve_params = incurred_triangle.fit_curve(CurveType.EXPONENTIAL)
print(f"Curve parameters: {curve_params}")

# Extract IBNER pattern
ibner_extractor = IBNERPatternExtractor(incurred_triangle)
ibner_pattern = ibner_extractor.get_IBNER_pattern()
print(f"IBNER pattern: {ibner_pattern}")

API Reference

Claim

Represents an insurance claim with associated metadata and development history.

This class provides access to the claim's metadata, uncapped and capped development histories, and a string representation for easy inspection. The uncapped and capped development histories are calculated based on the contract deductible and limit specified in the claim's metadata.

Attributes:

Name Type Description
_claims_meta_data ClaimsMetaData

Metadata associated with the claim, such as claim ID, deductible, and limit.

_claim_development_history ClaimDevelopmentHistory

The development history of the claim, including paid and incurred amounts over time.

_uncapped_claim_development_history ClaimDevelopmentHistory

Cached uncapped development history.

_capped_claim_development_history ClaimDevelopmentHistory

Cached capped development history.

Properties

claims_meta_data: Returns the claim's metadata. uncapped_claim_development_history: Returns the claim's development history after applying the deductible, but before applying the contract limit. capped_claim_development_history: Returns the claim's development history after applying both the deductible and the contract limit.

Parameters:

Name Type Description Default
claims_meta_data ClaimsMetaData

Metadata for the claim.

required
claims_development_history ClaimDevelopmentHistory

Development history for the claim.

required
Example

claim = Claim(meta_data, dev_history) print(claim.capped_claim_development_history)

Source code in src\pyre\claims\claims.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Claim:
    """Represents an insurance claim with associated metadata and development history.

    This class provides access to the claim's metadata, uncapped and capped development histories,
    and a string representation for easy inspection. The uncapped and capped development histories
    are calculated based on the contract deductible and limit specified in the claim's metadata.

    Attributes:
        _claims_meta_data (ClaimsMetaData): Metadata associated with the claim, such as claim ID, deductible, and limit.
        _claim_development_history (ClaimDevelopmentHistory): The development history of the claim, including paid and incurred amounts over time.
        _uncapped_claim_development_history (ClaimDevelopmentHistory): Cached uncapped development history.
        _capped_claim_development_history (ClaimDevelopmentHistory): Cached capped development history.

    Properties:
        claims_meta_data: Returns the claim's metadata.
        uncapped_claim_development_history: Returns the claim's development history after applying the deductible, but before applying the contract limit.
        capped_claim_development_history: Returns the claim's development history after applying both the deductible and the contract limit.

    Args:
        claims_meta_data (ClaimsMetaData): Metadata for the claim.
        claims_development_history (ClaimDevelopmentHistory): Development history for the claim.

    Example:
        >>> claim = Claim(meta_data, dev_history)
        >>> print(claim.capped_claim_development_history)
    """
    def __init__(self, claims_meta_data: ClaimsMetaData, claims_development_history: ClaimDevelopmentHistory) -> None:
        self._claims_meta_data = claims_meta_data
        self._claim_development_history = claims_development_history
        self._uncapped_claim_development_history = None
        self._capped_claim_development_history = None

    @property
    def claims_meta_data(self):
        return self._claims_meta_data

    @property
    def uncapped_claim_development_history(self) -> ClaimDevelopmentHistory:
        if self._uncapped_claim_development_history is None:
            if self._claims_meta_data.claim_in_xs_of_deductible:
                uncapped_paid = self._claim_development_history.cumulative_dev_paid
                uncapped_incurred = self._claim_development_history.cumulative_dev_incurred
            else:
                uncapped_paid = [max(paid - self._claims_meta_data.contract_deductible, 0.0) for paid in self._claim_development_history.cumulative_dev_paid]
                uncapped_incurred = [max(incurred - self._claims_meta_data.contract_deductible, 0.0) for incurred in self._claim_development_history.cumulative_dev_incurred]
            self._uncapped_claim_development_history = ClaimDevelopmentHistory(self._claim_development_history.development_months, uncapped_paid, uncapped_incurred)
        return self._uncapped_claim_development_history

    @property
    def capped_claim_development_history(self) -> ClaimDevelopmentHistory:
        if self._capped_claim_development_history is None:
            capped_paid = [min(paid, self._claims_meta_data.contract_limit) for paid in self.uncapped_claim_development_history.cumulative_dev_paid]
            capped_incurred = [min(incurred, self._claims_meta_data.contract_limit) for incurred in self.uncapped_claim_development_history.cumulative_dev_incurred]
            self._capped_claim_development_history = ClaimDevelopmentHistory(self._claim_development_history.development_months, capped_paid, capped_incurred)
        return self._capped_claim_development_history


    def __repr__(self) -> str:
        return (
            f"claim_id={self._claims_meta_data.claim_id},modelling_year={self._claims_meta_data.modelling_year},latest_incurred={self._claim_development_history.latest_incurred},latest_capped_incurred={self.capped_claim_development_history.latest_incurred}"
        )

ClaimDevelopmentHistory

Represents the development history of an insurance claim, tracking cumulative and incremental paid and incurred amounts over development months.

Attributes:

Name Type Description
development_months List[int]

List of development months corresponding to each data point.

cumulative_dev_paid List[float]

Cumulative paid amounts at each development month.

cumulative_dev_incurred List[float]

Cumulative incurred amounts at each development month.

Properties

cumulative_reserved_amount (List[float]): List of reserved amounts (incurred minus paid) at each development month. latest_paid (float): Most recent cumulative paid amount, or 0.0 if no data. latest_incurred (float): Most recent cumulative incurred amount, or 0.0 if no data. latest_reserved_amount (float): Most recent reserved amount (incurred minus paid), or 0.0 if no data. latest_development_month (int): Most recent development month, or 0 if no data. incremental_dev_incurred (List[float]): List of incremental incurred amounts at each development month. incremental_dev_paid (List[float]): List of incremental paid amounts at each development month. mean_payment_duration (Optional[float]): Weighted average development month of payments, or None if no payments.

Methods:

Name Description
incremental_dev

Sequence[float]) -> List[float]: Converts a sequence of cumulative values into incremental values.

Source code in src\pyre\claims\claims.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class ClaimDevelopmentHistory:
    """Represents the development history of an insurance claim, tracking cumulative and incremental paid and incurred amounts over development months.

    Attributes:
        development_months (List[int]): List of development months corresponding to each data point.
        cumulative_dev_paid (List[float]): Cumulative paid amounts at each development month.
        cumulative_dev_incurred (List[float]): Cumulative incurred amounts at each development month.

    Properties:
        cumulative_reserved_amount (List[float]): List of reserved amounts (incurred minus paid) at each development month.
        latest_paid (float): Most recent cumulative paid amount, or 0.0 if no data.
        latest_incurred (float): Most recent cumulative incurred amount, or 0.0 if no data.
        latest_reserved_amount (float): Most recent reserved amount (incurred minus paid), or 0.0 if no data.
        latest_development_month (int): Most recent development month, or 0 if no data.
        incremental_dev_incurred (List[float]): List of incremental incurred amounts at each development month.
        incremental_dev_paid (List[float]): List of incremental paid amounts at each development month.
        mean_payment_duration (Optional[float]): Weighted average development month of payments, or None if no payments.

    Methods:
        incremental_dev(cumulative_dev: Sequence[float]) -> List[float]:
            Converts a sequence of cumulative values into incremental values.
    """
    def __init__(self, development_months=None, cumulative_dev_paid=None, cumulative_dev_incurred=None):
        self._development_months = development_months if development_months is not None else []
        self._cumulative_dev_paid = cumulative_dev_paid if cumulative_dev_paid is not None else []
        self._cumulative_dev_incurred = cumulative_dev_incurred if cumulative_dev_incurred is not None else []

        # Validate that all lists have the same length
        self._validate_list_lengths()

    def _validate_list_lengths(self) -> None:
        """Validates that all development lists have the same length."""
        if len(self._development_months) != len(self._cumulative_dev_paid) or len(self._development_months) != len(self._cumulative_dev_incurred):
            raise ValueError("All development lists (months, paid, incurred) must have the same length.")

    @property
    def development_months(self) -> List[int]:
        return self._development_months

    @development_months.setter
    def development_months(self, value: List[int]) -> None:
        self._development_months = value
        self._validate_list_lengths()

    @property
    def cumulative_dev_paid(self) -> List[float]:
        return self._cumulative_dev_paid

    @cumulative_dev_paid.setter
    def cumulative_dev_paid(self, value: List[float]) -> None:
        self._cumulative_dev_paid = value
        self._validate_list_lengths()

    @property
    def cumulative_dev_incurred(self) -> List[float]:
        return self._cumulative_dev_incurred

    @cumulative_dev_incurred.setter
    def cumulative_dev_incurred(self, value: List[float]) -> None:
        self._cumulative_dev_incurred = value
        self._validate_list_lengths()

    @property
    def cumulative_reserved_amount(self) -> List[float]:
        """Returns a list of reserved amounts (incurred minus paid) at each development month."""
        return [incurred - paid for incurred, paid in zip(self.cumulative_dev_incurred, self.cumulative_dev_paid)]

    @property
    def latest_paid(self) -> float:
        return self.cumulative_dev_paid[-1] if self.cumulative_dev_paid else 0.0

    @property
    def latest_incurred(self) -> float:
        return self.cumulative_dev_incurred[-1] if self.cumulative_dev_incurred else 0.0

    @property
    def latest_reserved_amount(self) -> float:
        return self.cumulative_dev_incurred[-1] - self.cumulative_dev_paid[-1] if self.cumulative_dev_paid else 0.0

    @property
    def latest_development_month(self) -> int:
        return self.development_months[-1] if self.development_months else 0

    @staticmethod
    def incremental_dev(cumulative_dev: Sequence[float]) -> List[float]:
        incremental_dev = [cumulative_dev[0]]
        incremental_dev.extend([cumulative_dev[i] - cumulative_dev[i - 1] for i in range(1, len(cumulative_dev))])
        return incremental_dev
    @property
    def incremental_dev_incurred(self) -> List[float]:
        return self.incremental_dev(self.cumulative_dev_incurred)

    @property
    def incremental_dev_paid(self) -> List[float]:
        return self.incremental_dev(self.cumulative_dev_paid)

    @property
    def mean_payment_duration(self) -> Optional[float]:
        if self.latest_paid > 0:
            time_weighted_payments = sum(month * paid for month, paid in zip(self.development_months, self.incremental_dev_paid))
            return time_weighted_payments / self.latest_paid
        return None

cumulative_reserved_amount property

Returns a list of reserved amounts (incurred minus paid) at each development month.

ClaimYearType

Bases: Enum

Enumeration of claim year types used in insurance data analysis.

Attributes:

Name Type Description
ACCIDENT_YEAR

Represents the year in which the insured event (accident) occurred.

UNDERWRITING_YEAR

Represents the year in which the insurance policy was underwritten or issued.

REPORTED_YEAR

Represents the year in which the claim was reported to the insurer.

Source code in src\pyre\claims\claims.py
 8
 9
10
11
12
13
14
15
16
17
18
class ClaimYearType(Enum):
    """Enumeration of claim year types used in insurance data analysis.

    Attributes:
        ACCIDENT_YEAR: Represents the year in which the insured event (accident) occurred.
        UNDERWRITING_YEAR: Represents the year in which the insurance policy was underwritten or issued.
        REPORTED_YEAR: Represents the year in which the claim was reported to the insurer.
    """
    ACCIDENT_YEAR = auto()
    UNDERWRITING_YEAR = auto()
    REPORTED_YEAR = auto()

Claims

A container class for managing a collection of Claim objects.

This class provides convenient accessors and methods for working with a list of claims, including retrieving modelling years, development periods, and currencies represented in the claims. It also supports list-like behaviors such as indexing, slicing, appending, and iteration.

Attributes:

Name Type Description
claims list[Claim]

The list of Claim objects managed by this container.

Properties

modelling_years (List): Sorted list of unique modelling years across all claims. development_periods (List): Sorted list of unique development periods (in months) across all claims. currencies (Set): Set of unique currencies represented in the claims.

Methods:

Name Description
append

Claim): Appends a Claim object to the collection.

__getitem__

Supports indexing and slicing to access claims.

__iter__

Returns an iterator over the claims.

__len__

Returns the number of claims in the collection.

Source code in src\pyre\claims\claims.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
class Claims:
    """A container class for managing a collection of Claim objects.

    This class provides convenient accessors and methods for working with a list of claims,
    including retrieving modelling years, development periods, and currencies represented in the claims.
    It also supports list-like behaviors such as indexing, slicing, appending, and iteration.

    Attributes:
        claims (list[Claim]): The list of Claim objects managed by this container.

    Properties:
        modelling_years (List): Sorted list of unique modelling years across all claims.
        development_periods (List): Sorted list of unique development periods (in months) across all claims.
        currencies (Set): Set of unique currencies represented in the claims.

    Methods:
        append(claim: Claim): Appends a Claim object to the collection.
        __getitem__(key): Supports indexing and slicing to access claims.
        __iter__(): Returns an iterator over the claims.
        __len__(): Returns the number of claims in the collection.
    """
    def __init__(self, claims: list[Claim]) -> None:
        self._claims = claims

    @property
    def claims(self):
        return self._claims

    @claims.setter
    def claims(self, list_of_claim_classes:list[Claim]):
        self._claims = list_of_claim_classes

    @property
    def modelling_years(self) -> List:
        """
        Returns a list of modelling years for all claims.
        """
        years = {claim.claims_meta_data.modelling_year for claim in self.claims}
        return sorted(years)

    @property
    def development_periods(self) -> List:
        """
        Returns a sorted list of unique development period sequences across all claims.

        Each element in the returned list is a list of development months from a claim.
        """
        dev_periods = {tuple(claim.capped_claim_development_history.development_months) for claim in self.claims}
        return sorted([list(period) for period in dev_periods])

    @property
    def currencies(self) -> Set:
        """
        Returns a list of currencies for all claims.
        """
        return {claim.claims_meta_data.currency for claim in self.claims}

    def append(self, claim: Claim):
        self._claims.append(claim)

    def __getitem__(self, key):
        if isinstance(key,slice):
            cls = type(self)
            return cls(self._claims[key])
        index = operator.index(key)
        return self._claims[index]

    def __iter__(self):
        return iter(self._claims)

    def __len__(self):
        return len(self._claims)

currencies property

Returns a list of currencies for all claims.

development_periods property

Returns a sorted list of unique development period sequences across all claims.

Each element in the returned list is a list of development months from a claim.

modelling_years property

Returns a list of modelling years for all claims.

ClaimsMetaData

Metadata for an insurance claim, including key dates, financial limits, and classification details.

Attributes:

Name Type Description
claim_id str

Unique identifier for the claim.

currency str

Currency code for the claim amounts.

contract_limit float

Maximum limit of the insurance contract. Defaults to 0.0.

contract_deductible float

Deductible amount for the contract. Defaults to 0.0.

claim_in_xs_of_deductible bool

Indicates if the claim is in excess of the deductible. Defaults to False.

claim_year_basis ClaimYearType

Basis for determining the claim year (e.g., accident, underwriting, reported). Defaults to ClaimYearType.ACCIDENT_YEAR.

loss_date date

Date of loss occurrence. Defaults to 1900-01-01.

policy_inception_date date

Policy inception date. Defaults to 1900-01-01.

report_date date

Date the claim was reported. Defaults to 1900-01-01.

line_of_business Optional[str]

Line of business associated with the claim. Defaults to None.

status Optional[str]

Status of the claim (e.g., "Open", "Closed"). Defaults to "Open".

Properties

modelling_year (ClaimsException | int): Returns the modelling year based on the claim_year_basis, or raises ClaimsException if required date is missing.

Source code in src\pyre\claims\claims.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class ClaimsMetaData:
    """Metadata for an insurance claim, including key dates, financial limits, and classification details.

    Attributes:
        claim_id (str): Unique identifier for the claim.
        currency (str): Currency code for the claim amounts.
        contract_limit (float): Maximum limit of the insurance contract. Defaults to 0.0.
        contract_deductible (float): Deductible amount for the contract. Defaults to 0.0.
        claim_in_xs_of_deductible (bool): Indicates if the claim is in excess of the deductible. Defaults to False.
        claim_year_basis (ClaimYearType): Basis for determining the claim year (e.g., accident, underwriting, reported). Defaults to ClaimYearType.ACCIDENT_YEAR.
        loss_date (date): Date of loss occurrence. Defaults to 1900-01-01.
        policy_inception_date (date): Policy inception date. Defaults to 1900-01-01.
        report_date (date): Date the claim was reported. Defaults to 1900-01-01.
        line_of_business (Optional[str]): Line of business associated with the claim. Defaults to None.
        status (Optional[str]): Status of the claim (e.g., "Open", "Closed"). Defaults to "Open".

    Properties:
        modelling_year (ClaimsException | int): Returns the modelling year based on the claim_year_basis, or raises ClaimsException if required date is missing.
    """
    def __init__(
        self,
        claim_id: str,
        currency: str,
        contract_limit: float = 0.0,
        contract_deductible: float = 0.0,
        claim_in_xs_of_deductible: bool = False,
        claim_year_basis: ClaimYearType = ClaimYearType.ACCIDENT_YEAR,
        loss_date: date = date(day=1, month=1, year=1900),
        policy_inception_date: date = date(day=1, month=1, year=1900),
        report_date: date = date(day=1, month=1, year=1900),
        line_of_business: Optional[str] = None,
        status: Optional[str] = "Open"
    ):
        self._claim_id = claim_id
        self._currency = currency
        self._contract_limit = contract_limit
        self._contract_deductible = contract_deductible
        self._claim_in_xs_of_deductible = claim_in_xs_of_deductible
        self._claim_year_basis = claim_year_basis
        self._loss_date = loss_date
        self._policy_inception_date = policy_inception_date
        self._report_date = report_date
        self._line_of_business = line_of_business
        self._status = status

    @property
    def claim_id(self):
        return self._claim_id

    @claim_id.setter
    def claim_id(self, value):
        self._claim_id = value

    @property
    def currency(self):
        return self._currency

    @currency.setter
    def currency(self, value):
        self._currency = value

    @property
    def contract_limit(self):
        return self._contract_limit

    @contract_limit.setter
    def contract_limit(self, value):
        self._contract_limit = value

    @property
    def contract_deductible(self):
        return self._contract_deductible

    @contract_deductible.setter
    def contract_deductible(self, value):
        self._contract_deductible = value

    @property
    def claim_in_xs_of_deductible(self):
        return self._claim_in_xs_of_deductible

    @claim_in_xs_of_deductible.setter
    def claim_in_xs_of_deductible(self, value):
        self._claim_in_xs_of_deductible = value

    @property
    def claim_year_basis(self):
        return self._claim_year_basis

    @claim_year_basis.setter
    def claim_year_basis(self, value):
        self._claim_year_basis = value

    @property
    def loss_date(self):
        return self._loss_date

    @loss_date.setter
    def loss_date(self, value):
        self._loss_date = value

    @property
    def policy_inception_date(self):
        return self._policy_inception_date

    @policy_inception_date.setter
    def policy_inception_date(self, value):
        self._policy_inception_date = value

    @property
    def report_date(self):
        return self._report_date

    @report_date.setter
    def report_date(self, value):
        self._report_date = value

    @property
    def line_of_business(self):
        return self._line_of_business

    @line_of_business.setter
    def line_of_business(self, value):
        self._line_of_business = value

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, value):
        self._status = value

    @property
    def modelling_year(self) -> int:
        """
        Returns the modelling year based on the claim_year_basis.

        Returns:
            int: The year to use for modelling purposes.

        Raises:
            ClaimsException: If the required date for the specified claim_year_basis is missing.
        """
        _modeling_basis_years={
            ClaimYearType.ACCIDENT_YEAR: self.loss_date.year,
            ClaimYearType.UNDERWRITING_YEAR: self.policy_inception_date.year,
            ClaimYearType.REPORTED_YEAR: self.report_date.year
        }
        if self.claim_year_basis in _modeling_basis_years:
            return _modeling_basis_years[self.claim_year_basis]
        else: 
            raise ClaimsException(
                claim_id=self.claim_id, 
                message="Required date missing from data"
                )

modelling_year property

Returns the modelling year based on the claim_year_basis.

Returns:

Name Type Description
int int

The year to use for modelling purposes.

Raises:

Type Description
ClaimsException

If the required date for the specified claim_year_basis is missing.

CurveType

Bases: Enum

Enum representing different types of curves that can be fitted to triangle data.

Source code in src\pyre\claims\triangles.py
11
12
13
14
15
16
17
18
19
class CurveType(Enum):
    """
    Enum representing different types of curves that can be fitted to triangle data.
    """
    EXPONENTIAL = auto()
    POWER = auto()
    WEIBULL = auto()
    INVERSE_POWER = auto()
    OTHER = auto()

IBNERPatternExtractor

Extracts IBNER patterns from either a Claims object or a Triangle object.

Source code in src\pyre\claims\triangles.py
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
class IBNERPatternExtractor:
    """
    Extracts IBNER patterns from either a Claims object or a Triangle object.
    """
    def __init__(self, triangle: Triangle):
        self.triangle = triangle.triangle
        self.origin_years = triangle.origin_years
        self.dev_periods = triangle.dev_periods

        self.N = {oy: {} for oy in self.origin_years}
        self.D = {oy: {} for oy in self.origin_years}
        self._compute_N_and_D()

    def _compute_N_and_D(self):
        """
        Compute the N and D triangles from cumulative data.
        """
        for oy in self.origin_years:
            cumulative = [self.triangle[oy].get(d, None) for d in self.dev_periods]
            for idx, d in enumerate(self.dev_periods):
                if idx >= len(cumulative) or cumulative[idx] is None:
                    self.N[oy][d] = None
                    self.D[oy][d] = None
                    continue

                current = cumulative[idx]
                if idx == 0:
                    self.N[oy][d] = current
                    self.D[oy][d] = None
                else:
                    prev = cumulative[idx - 1]
                    if current is None or prev is None:
                        self.N[oy][d] = None
                        self.D[oy][d] = None
                    else:
                        self.D[oy][d] = prev - current
                        self.N[oy][d] = current - prev + self.D[oy][d]

    def get_N_triangle(self) -> Dict[int, Dict[int, float]]:
        """
        Returns the N triangle (new claims).
        """
        return self.N

    def get_D_triangle(self) -> Dict[int, Dict[int, float]]:
        """
        Returns the D triangle (IBNER development).
        """
        return self.D

    def get_IBNER_pattern(self) -> Dict[int, float]:
        """
        Returns the average D (IBNER) pattern per development year.
        """
        sums = {d: 0.0 for d in self.dev_periods}
        counts = {d: 0 for d in self.dev_periods}

        for oy in self.origin_years:
            for d in self.dev_periods:
                val = self.D.get(oy, {}).get(d)
                if val is not None:
                    sums[d] += val
                    counts[d] += 1

        return {d: (sums[d] / counts[d]) if counts[d] > 0 else None for d in self.dev_periods}

get_D_triangle()

Returns the D triangle (IBNER development).

Source code in src\pyre\claims\triangles.py
491
492
493
494
495
def get_D_triangle(self) -> Dict[int, Dict[int, float]]:
    """
    Returns the D triangle (IBNER development).
    """
    return self.D

get_IBNER_pattern()

Returns the average D (IBNER) pattern per development year.

Source code in src\pyre\claims\triangles.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
def get_IBNER_pattern(self) -> Dict[int, float]:
    """
    Returns the average D (IBNER) pattern per development year.
    """
    sums = {d: 0.0 for d in self.dev_periods}
    counts = {d: 0 for d in self.dev_periods}

    for oy in self.origin_years:
        for d in self.dev_periods:
            val = self.D.get(oy, {}).get(d)
            if val is not None:
                sums[d] += val
                counts[d] += 1

    return {d: (sums[d] / counts[d]) if counts[d] > 0 else None for d in self.dev_periods}

get_N_triangle()

Returns the N triangle (new claims).

Source code in src\pyre\claims\triangles.py
485
486
487
488
489
def get_N_triangle(self) -> Dict[int, Dict[int, float]]:
    """
    Returns the N triangle (new claims).
    """
    return self.N

Triangle

Represents a triangle of claim values (e.g., paid or incurred) by origin (modelling) year and development period.

The triangle is stored as a nested dictionary where: - The outer key is the origin year (int) - The inner key is the development period (int) - The value is the claim amount (float)

Example structure: { 2020: {1: 100.0, 2: 150.0, 3: 175.0}, 2021: {1: 110.0, 2: 165.0}, 2022: {1: 120.0} }

Source code in src\pyre\claims\triangles.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
class Triangle:
    """
    Represents a triangle of claim values (e.g., paid or incurred) by origin (modelling) year and development period.

    The triangle is stored as a nested dictionary where:
    - The outer key is the origin year (int)
    - The inner key is the development period (int)
    - The value is the claim amount (float)

    Example structure:
    {
        2020: {1: 100.0, 2: 150.0, 3: 175.0},
        2021: {1: 110.0, 2: 165.0},
        2022: {1: 120.0}
    }
    """

    def __init__(
        self,
        triangle: Optional[Dict[int, Dict[int, float]]] = None,
        origin_years: Optional[List[int]] = None,
        dev_periods: Optional[List[int]] = None,
    ):
        """
        Initialize a Triangle directly or as an empty structure.

        Args:
            triangle: Dictionary mapping origin years to dictionaries mapping development periods to values
            origin_years: List of origin years in the triangle
            dev_periods: List of development periods in the triangle
        """
        self.triangle = triangle if triangle is not None else {}

        # If origin_years not provided, extract from triangle keys
        if origin_years is None:
            self.origin_years = sorted(self.triangle.keys()) if self.triangle else []
        else:
            self.origin_years = sorted(origin_years)

        # If dev_periods not provided, extract from all inner dictionaries
        if dev_periods is None:
            all_dev_periods = set()
            for year_data in self.triangle.values():
                all_dev_periods.update(year_data.keys())
            self.dev_periods = sorted(all_dev_periods) if all_dev_periods else []
        else:
            self.dev_periods = sorted(dev_periods)

        # Validate the triangle structure
        self._validate_triangle()

    def _validate_triangle(self) -> None:
        """
        Validate the triangle structure to ensure all keys are integers
        and all values are numeric.

        Raises:
            ValueError: If the triangle structure is invalid
        """
        for origin_year, dev_data in self.triangle.items():
            if not isinstance(origin_year, int):
                raise ValueError(f"Origin year must be an integer, got {type(origin_year)}")

            for dev_period, value in dev_data.items():
                if not isinstance(dev_period, int):
                    raise ValueError(f"Development period must be an integer, got {type(dev_period)}")

                if value is not None and not isinstance(value, (int, float)):
                    raise ValueError(f"Triangle values must be numeric or None, got {type(value)}")

    def __repr__(self) -> str:
        """Return a string representation of the Triangle object."""
        return f"Triangle(origin_years={self.origin_years}, dev_periods={self.dev_periods})"

    def __str__(self) -> str:
        """Return a formatted string representation of the triangle."""
        if not self.triangle:
            return "Empty Triangle"

        # Create header row
        header = "Origin Year | " + " | ".join(f"Dev {d}" for d in self.dev_periods)

        # Create rows for each origin year
        rows = []
        for oy in self.origin_years:
            row_values = []
            for dp in self.dev_periods:
                value = self.triangle.get(oy, {}).get(dp)
                row_values.append(f"{value:.2f}" if value is not None else "N/A")
            rows.append(f"{oy} | " + " | ".join(row_values))

        return header + "\n" + "\n".join(rows)

    def __getitem__(self, key: Tuple[int, int]) -> Optional[float]:
        """
        Get a value from the triangle using tuple indexing.

        Args:
            key: Tuple of (origin_year, development_period)

        Returns:
            The value at the specified position or None if not found

        Example:
            value = triangle[2020, 2]  # Gets the value for origin year 2020, development period 2
        """
        origin_year, dev_period = key
        return self.triangle.get(origin_year, {}).get(dev_period)

    def __setitem__(self, key: Tuple[int, int], value: Optional[float]) -> None:
        """
        Set a value in the triangle using tuple indexing.

        Args:
            key: Tuple of (origin_year, development_period)
            value: The value to set

        Example:
            triangle[2020, 2] = 150.0  # Sets the value for origin year 2020, development period 2
        """
        origin_year, dev_period = key

        # Ensure the origin year exists in the triangle
        if origin_year not in self.triangle:
            self.triangle[origin_year] = {}
            if origin_year not in self.origin_years:
                self.origin_years.append(origin_year)
                self.origin_years.sort()

        # Set the value
        self.triangle[origin_year][dev_period] = value

        # Update dev_periods if needed
        if dev_period not in self.dev_periods:
            self.dev_periods.append(dev_period)
            self.dev_periods.sort()

    def get_value(self, origin_year: int, dev_period: int) -> Optional[float]:
        """
        Get a value from the triangle.

        Args:
            origin_year: The origin year
            dev_period: The development period

        Returns:
            The value at the specified position or None if not found
        """
        return self.triangle.get(origin_year, {}).get(dev_period)

    def set_value(self, origin_year: int, dev_period: int, value: Optional[float]) -> None:
        """
        Set a value in the triangle.

        Args:
            origin_year: The origin year
            dev_period: The development period
            value: The value to set
        """
        self[origin_year, dev_period] = value

    def get_latest_diagonal(self) -> Dict[int, float]:
        """
        Get the latest diagonal of the triangle.

        Returns:
            Dictionary mapping origin years to their latest available values
        """
        result = {}
        for oy in self.origin_years:
            # Find the maximum development period with a value for this origin year
            available_devs = [dp for dp in self.dev_periods if self.get_value(oy, dp) is not None]
            if available_devs:
                max_dev = max(available_devs)
                result[oy] = self.get_value(oy, max_dev)
        return result

    def to_incremental(self) -> 'Triangle':
        """
        Convert a cumulative triangle to an incremental triangle.

        Returns:
            A new Triangle with incremental values
        """
        incremental_triangle = {}

        for oy in self.origin_years:
            incremental_triangle[oy] = {}
            prev_value = None

            for dp in self.dev_periods:
                current_value = self.get_value(oy, dp)

                if current_value is None:
                    incremental_triangle[oy][dp] = None
                elif prev_value is None:
                    incremental_triangle[oy][dp] = current_value
                else:
                    incremental_triangle[oy][dp] = current_value - prev_value

                if current_value is not None:
                    prev_value = current_value

        return Triangle(
            triangle=incremental_triangle,
            origin_years=self.origin_years.copy(),
            dev_periods=self.dev_periods.copy()
        )

    def to_cumulative(self) -> 'Triangle':
        """
        Convert an incremental triangle to a cumulative triangle.

        Returns:
            A new Triangle with cumulative values
        """
        cumulative_triangle = {}

        for oy in self.origin_years:
            cumulative_triangle[oy] = {}
            cumulative_value = 0.0

            for dp in self.dev_periods:
                incremental_value = self.get_value(oy, dp)

                if incremental_value is None:
                    cumulative_triangle[oy][dp] = None
                else:
                    cumulative_value += incremental_value
                    cumulative_triangle[oy][dp] = cumulative_value

        return Triangle(
            triangle=cumulative_triangle,
            origin_years=self.origin_years.copy(),
            dev_periods=self.dev_periods.copy()
        )

    @classmethod
    def from_claims(cls, claims: Claims, value_type: str = "incurred") -> "Triangle":
        """
        Construct a Triangle from a Claims object.

        Args:
            claims: Claims object containing claim data
            value_type: Type of values to extract, either "incurred" or "paid"

        Returns:
            A new Triangle object with aggregated claim values

        Raises:
            ValueError: If value_type is not "incurred" or "paid"
        """
        if value_type not in ["incurred", "paid"]:
            raise ValueError(f"value_type must be 'incurred' or 'paid', got '{value_type}'")

        # Collect all unique modelling years and development periods
        origin_years = claims.modelling_years
        dev_periods = claims.development_periods

        # Build the triangle
        triangle = {year: {} for year in origin_years}

        # Aggregate claims by origin year and development period
        for claim in claims:
            origin_year = claim.modelling_year

            # Skip claims with no development history
            if not hasattr(claim, 'capped_claim_development_history'):
                continue

            # Get the appropriate development history based on value_type
            if value_type == "incurred":
                dev_history = claim.capped_claim_development_history.cumulative_dev_incurred
            else:  # value_type == "paid"
                dev_history = claim.capped_claim_development_history.cumulative_dev_paid

            # Add values to the triangle
            for dev_period, value in dev_history.items():
                if dev_period in dev_periods:
                    if dev_period not in triangle[origin_year]:
                        triangle[origin_year][dev_period] = 0.0
                    triangle[origin_year][dev_period] += value

        return cls(triangle=triangle, origin_years=origin_years, dev_periods=dev_periods)

    def calculate_age_to_age_factors(self) -> Dict[int, Dict[int, float]]:
        """
        Calculate age-to-age factors for the triangle.

        Age-to-age factors are calculated as the ratio of the value at development period j+1
        to the value at development period j for each origin year.

        Returns:
            Dict[int, Dict[int, float]]: A dictionary mapping origin years to dictionaries
                mapping development periods to age-to-age factors.
        """
        factors = {}

        for oy in self.origin_years:
            factors[oy] = {}
            for i in range(len(self.dev_periods) - 1):
                current_dev = self.dev_periods[i]
                next_dev = self.dev_periods[i + 1]

                current_value = self.get_value(oy, current_dev)
                next_value = self.get_value(oy, next_dev)

                if current_value is not None and next_value is not None and current_value != 0:
                    factors[oy][current_dev] = next_value / current_value

        return factors

    def get_average_age_to_age_factors(self, method: str = "simple") -> Dict[int, float]:
        """
        Calculate average age-to-age factors across all origin years.

        Args:
            method (str): Method to use for averaging. Options are:
                - "simple": Simple arithmetic mean
                - "volume": Volume-weighted average

        Returns:
            Dict[int, float]: A dictionary mapping development periods to average age-to-age factors.
        """
        factors = self.calculate_age_to_age_factors()
        avg_factors = {}

        for dev_idx in range(len(self.dev_periods) - 1):
            dev = self.dev_periods[dev_idx]

            if method == "simple":
                # Simple average
                dev_factors = [factors[oy].get(dev) for oy in self.origin_years if dev in factors.get(oy, {})]
                dev_factors = [f for f in dev_factors if f is not None]

                if dev_factors:
                    avg_factors[dev] = sum(dev_factors) / len(dev_factors)

            elif method == "volume":
                # Volume-weighted average
                numerator_sum = 0.0
                denominator_sum = 0.0

                for oy in self.origin_years:
                    current_value = self.get_value(oy, dev)
                    next_value = self.get_value(oy, self.dev_periods[dev_idx + 1])

                    if current_value is not None and next_value is not None and current_value != 0:
                        numerator_sum += next_value
                        denominator_sum += current_value

                if denominator_sum != 0:
                    avg_factors[dev] = numerator_sum / denominator_sum

        return avg_factors

    def fit_curve(self, curve_type: CurveType, c_values: List[float] = None) -> Tuple[Dict[str, float], Dict[str, float]]:
        """
        Fit a curve to the average age-to-age factors.

        Args:
            curve_type (CurveType): Type of curve to fit. Options are:
                - CurveType.EXPONENTIAL: Exponential curve
                - CurveType.POWER: Power curve
                - CurveType.WEIBULL: Weibull curve
                - CurveType.INVERSE_POWER: Inverse power curve (Sherman)
            c_values (List[float], optional): List of candidate c values for inverse power fit.
                Defaults to [0.5, 1.0, 1.5, 2.0, 2.5, 3.0].

        Returns:
            Tuple[Dict[str, float], Dict[str, float]]: A tuple containing:
                - Parameters of the fitted curve
                - Metrics assessing the quality of the fit
        """
        if c_values is None:
            c_values = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0]

        # Get average age-to-age factors
        avg_factors = self.get_average_age_to_age_factors()

        # Prepare data for curve fitting
        dev_periods = sorted(avg_factors.keys())
        factors = [avg_factors[dp] for dp in dev_periods]

        # Fit the appropriate curve
        if curve_type == CurveType.EXPONENTIAL:
            a, b = exponential_fit(factors, dev_periods)
            params = {"a": a, "b": b}
            expected = [1 + exp(a + b * dp) for dp in dev_periods]
            num_params = 2

        elif curve_type == CurveType.POWER:
            a, b = power_fit(factors, dev_periods)
            params = {"a": a, "b": b}
            expected = [a * (b ** dp) for dp in dev_periods]
            num_params = 2

        elif curve_type == CurveType.WEIBULL:
            a, b = weibull_fit(factors, dev_periods)
            params = {"a": a, "b": b}
            expected = [1 / (1 - exp(-a * (dp ** b))) for dp in dev_periods]
            num_params = 2

        elif curve_type == CurveType.INVERSE_POWER:
            a, b, c = inverse_power_fit(factors, dev_periods, c_values)
            params = {"a": a, "b": b, "c": c}
            expected = [1 + a * ((dp + c) ** b) for dp in dev_periods]
            num_params = 3

        else:
            raise ValueError(f"Unknown curve type: {curve_type}")

        # Calculate fit metrics
        r_squared_value = r_squared(factors, expected)
        error_metrics = assess_error_assumptions(factors, expected, num_params)

        metrics = {
            "r_squared": r_squared_value,
            **error_metrics
        }

        return params, metrics

__getitem__(key)

Get a value from the triangle using tuple indexing.

Parameters:

Name Type Description Default
key Tuple[int, int]

Tuple of (origin_year, development_period)

required

Returns:

Type Description
Optional[float]

The value at the specified position or None if not found

Example

value = triangle[2020, 2] # Gets the value for origin year 2020, development period 2

Source code in src\pyre\claims\triangles.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def __getitem__(self, key: Tuple[int, int]) -> Optional[float]:
    """
    Get a value from the triangle using tuple indexing.

    Args:
        key: Tuple of (origin_year, development_period)

    Returns:
        The value at the specified position or None if not found

    Example:
        value = triangle[2020, 2]  # Gets the value for origin year 2020, development period 2
    """
    origin_year, dev_period = key
    return self.triangle.get(origin_year, {}).get(dev_period)

__init__(triangle=None, origin_years=None, dev_periods=None)

Initialize a Triangle directly or as an empty structure.

Parameters:

Name Type Description Default
triangle Optional[Dict[int, Dict[int, float]]]

Dictionary mapping origin years to dictionaries mapping development periods to values

None
origin_years Optional[List[int]]

List of origin years in the triangle

None
dev_periods Optional[List[int]]

List of development periods in the triangle

None
Source code in src\pyre\claims\triangles.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def __init__(
    self,
    triangle: Optional[Dict[int, Dict[int, float]]] = None,
    origin_years: Optional[List[int]] = None,
    dev_periods: Optional[List[int]] = None,
):
    """
    Initialize a Triangle directly or as an empty structure.

    Args:
        triangle: Dictionary mapping origin years to dictionaries mapping development periods to values
        origin_years: List of origin years in the triangle
        dev_periods: List of development periods in the triangle
    """
    self.triangle = triangle if triangle is not None else {}

    # If origin_years not provided, extract from triangle keys
    if origin_years is None:
        self.origin_years = sorted(self.triangle.keys()) if self.triangle else []
    else:
        self.origin_years = sorted(origin_years)

    # If dev_periods not provided, extract from all inner dictionaries
    if dev_periods is None:
        all_dev_periods = set()
        for year_data in self.triangle.values():
            all_dev_periods.update(year_data.keys())
        self.dev_periods = sorted(all_dev_periods) if all_dev_periods else []
    else:
        self.dev_periods = sorted(dev_periods)

    # Validate the triangle structure
    self._validate_triangle()

__repr__()

Return a string representation of the Triangle object.

Source code in src\pyre\claims\triangles.py
91
92
93
def __repr__(self) -> str:
    """Return a string representation of the Triangle object."""
    return f"Triangle(origin_years={self.origin_years}, dev_periods={self.dev_periods})"

__setitem__(key, value)

Set a value in the triangle using tuple indexing.

Parameters:

Name Type Description Default
key Tuple[int, int]

Tuple of (origin_year, development_period)

required
value Optional[float]

The value to set

required
Example

triangle[2020, 2] = 150.0 # Sets the value for origin year 2020, development period 2

Source code in src\pyre\claims\triangles.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def __setitem__(self, key: Tuple[int, int], value: Optional[float]) -> None:
    """
    Set a value in the triangle using tuple indexing.

    Args:
        key: Tuple of (origin_year, development_period)
        value: The value to set

    Example:
        triangle[2020, 2] = 150.0  # Sets the value for origin year 2020, development period 2
    """
    origin_year, dev_period = key

    # Ensure the origin year exists in the triangle
    if origin_year not in self.triangle:
        self.triangle[origin_year] = {}
        if origin_year not in self.origin_years:
            self.origin_years.append(origin_year)
            self.origin_years.sort()

    # Set the value
    self.triangle[origin_year][dev_period] = value

    # Update dev_periods if needed
    if dev_period not in self.dev_periods:
        self.dev_periods.append(dev_period)
        self.dev_periods.sort()

__str__()

Return a formatted string representation of the triangle.

Source code in src\pyre\claims\triangles.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __str__(self) -> str:
    """Return a formatted string representation of the triangle."""
    if not self.triangle:
        return "Empty Triangle"

    # Create header row
    header = "Origin Year | " + " | ".join(f"Dev {d}" for d in self.dev_periods)

    # Create rows for each origin year
    rows = []
    for oy in self.origin_years:
        row_values = []
        for dp in self.dev_periods:
            value = self.triangle.get(oy, {}).get(dp)
            row_values.append(f"{value:.2f}" if value is not None else "N/A")
        rows.append(f"{oy} | " + " | ".join(row_values))

    return header + "\n" + "\n".join(rows)

calculate_age_to_age_factors()

Calculate age-to-age factors for the triangle.

Age-to-age factors are calculated as the ratio of the value at development period j+1 to the value at development period j for each origin year.

Returns:

Type Description
Dict[int, Dict[int, float]]

Dict[int, Dict[int, float]]: A dictionary mapping origin years to dictionaries mapping development periods to age-to-age factors.

Source code in src\pyre\claims\triangles.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def calculate_age_to_age_factors(self) -> Dict[int, Dict[int, float]]:
    """
    Calculate age-to-age factors for the triangle.

    Age-to-age factors are calculated as the ratio of the value at development period j+1
    to the value at development period j for each origin year.

    Returns:
        Dict[int, Dict[int, float]]: A dictionary mapping origin years to dictionaries
            mapping development periods to age-to-age factors.
    """
    factors = {}

    for oy in self.origin_years:
        factors[oy] = {}
        for i in range(len(self.dev_periods) - 1):
            current_dev = self.dev_periods[i]
            next_dev = self.dev_periods[i + 1]

            current_value = self.get_value(oy, current_dev)
            next_value = self.get_value(oy, next_dev)

            if current_value is not None and next_value is not None and current_value != 0:
                factors[oy][current_dev] = next_value / current_value

    return factors

fit_curve(curve_type, c_values=None)

Fit a curve to the average age-to-age factors.

Parameters:

Name Type Description Default
curve_type CurveType

Type of curve to fit. Options are: - CurveType.EXPONENTIAL: Exponential curve - CurveType.POWER: Power curve - CurveType.WEIBULL: Weibull curve - CurveType.INVERSE_POWER: Inverse power curve (Sherman)

required
c_values List[float]

List of candidate c values for inverse power fit. Defaults to [0.5, 1.0, 1.5, 2.0, 2.5, 3.0].

None

Returns:

Type Description
Tuple[Dict[str, float], Dict[str, float]]

Tuple[Dict[str, float], Dict[str, float]]: A tuple containing: - Parameters of the fitted curve - Metrics assessing the quality of the fit

Source code in src\pyre\claims\triangles.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
def fit_curve(self, curve_type: CurveType, c_values: List[float] = None) -> Tuple[Dict[str, float], Dict[str, float]]:
    """
    Fit a curve to the average age-to-age factors.

    Args:
        curve_type (CurveType): Type of curve to fit. Options are:
            - CurveType.EXPONENTIAL: Exponential curve
            - CurveType.POWER: Power curve
            - CurveType.WEIBULL: Weibull curve
            - CurveType.INVERSE_POWER: Inverse power curve (Sherman)
        c_values (List[float], optional): List of candidate c values for inverse power fit.
            Defaults to [0.5, 1.0, 1.5, 2.0, 2.5, 3.0].

    Returns:
        Tuple[Dict[str, float], Dict[str, float]]: A tuple containing:
            - Parameters of the fitted curve
            - Metrics assessing the quality of the fit
    """
    if c_values is None:
        c_values = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0]

    # Get average age-to-age factors
    avg_factors = self.get_average_age_to_age_factors()

    # Prepare data for curve fitting
    dev_periods = sorted(avg_factors.keys())
    factors = [avg_factors[dp] for dp in dev_periods]

    # Fit the appropriate curve
    if curve_type == CurveType.EXPONENTIAL:
        a, b = exponential_fit(factors, dev_periods)
        params = {"a": a, "b": b}
        expected = [1 + exp(a + b * dp) for dp in dev_periods]
        num_params = 2

    elif curve_type == CurveType.POWER:
        a, b = power_fit(factors, dev_periods)
        params = {"a": a, "b": b}
        expected = [a * (b ** dp) for dp in dev_periods]
        num_params = 2

    elif curve_type == CurveType.WEIBULL:
        a, b = weibull_fit(factors, dev_periods)
        params = {"a": a, "b": b}
        expected = [1 / (1 - exp(-a * (dp ** b))) for dp in dev_periods]
        num_params = 2

    elif curve_type == CurveType.INVERSE_POWER:
        a, b, c = inverse_power_fit(factors, dev_periods, c_values)
        params = {"a": a, "b": b, "c": c}
        expected = [1 + a * ((dp + c) ** b) for dp in dev_periods]
        num_params = 3

    else:
        raise ValueError(f"Unknown curve type: {curve_type}")

    # Calculate fit metrics
    r_squared_value = r_squared(factors, expected)
    error_metrics = assess_error_assumptions(factors, expected, num_params)

    metrics = {
        "r_squared": r_squared_value,
        **error_metrics
    }

    return params, metrics

from_claims(claims, value_type='incurred') classmethod

Construct a Triangle from a Claims object.

Parameters:

Name Type Description Default
claims Claims

Claims object containing claim data

required
value_type str

Type of values to extract, either "incurred" or "paid"

'incurred'

Returns:

Type Description
Triangle

A new Triangle object with aggregated claim values

Raises:

Type Description
ValueError

If value_type is not "incurred" or "paid"

Source code in src\pyre\claims\triangles.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
@classmethod
def from_claims(cls, claims: Claims, value_type: str = "incurred") -> "Triangle":
    """
    Construct a Triangle from a Claims object.

    Args:
        claims: Claims object containing claim data
        value_type: Type of values to extract, either "incurred" or "paid"

    Returns:
        A new Triangle object with aggregated claim values

    Raises:
        ValueError: If value_type is not "incurred" or "paid"
    """
    if value_type not in ["incurred", "paid"]:
        raise ValueError(f"value_type must be 'incurred' or 'paid', got '{value_type}'")

    # Collect all unique modelling years and development periods
    origin_years = claims.modelling_years
    dev_periods = claims.development_periods

    # Build the triangle
    triangle = {year: {} for year in origin_years}

    # Aggregate claims by origin year and development period
    for claim in claims:
        origin_year = claim.modelling_year

        # Skip claims with no development history
        if not hasattr(claim, 'capped_claim_development_history'):
            continue

        # Get the appropriate development history based on value_type
        if value_type == "incurred":
            dev_history = claim.capped_claim_development_history.cumulative_dev_incurred
        else:  # value_type == "paid"
            dev_history = claim.capped_claim_development_history.cumulative_dev_paid

        # Add values to the triangle
        for dev_period, value in dev_history.items():
            if dev_period in dev_periods:
                if dev_period not in triangle[origin_year]:
                    triangle[origin_year][dev_period] = 0.0
                triangle[origin_year][dev_period] += value

    return cls(triangle=triangle, origin_years=origin_years, dev_periods=dev_periods)

get_average_age_to_age_factors(method='simple')

Calculate average age-to-age factors across all origin years.

Parameters:

Name Type Description Default
method str

Method to use for averaging. Options are: - "simple": Simple arithmetic mean - "volume": Volume-weighted average

'simple'

Returns:

Type Description
Dict[int, float]

Dict[int, float]: A dictionary mapping development periods to average age-to-age factors.

Source code in src\pyre\claims\triangles.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
def get_average_age_to_age_factors(self, method: str = "simple") -> Dict[int, float]:
    """
    Calculate average age-to-age factors across all origin years.

    Args:
        method (str): Method to use for averaging. Options are:
            - "simple": Simple arithmetic mean
            - "volume": Volume-weighted average

    Returns:
        Dict[int, float]: A dictionary mapping development periods to average age-to-age factors.
    """
    factors = self.calculate_age_to_age_factors()
    avg_factors = {}

    for dev_idx in range(len(self.dev_periods) - 1):
        dev = self.dev_periods[dev_idx]

        if method == "simple":
            # Simple average
            dev_factors = [factors[oy].get(dev) for oy in self.origin_years if dev in factors.get(oy, {})]
            dev_factors = [f for f in dev_factors if f is not None]

            if dev_factors:
                avg_factors[dev] = sum(dev_factors) / len(dev_factors)

        elif method == "volume":
            # Volume-weighted average
            numerator_sum = 0.0
            denominator_sum = 0.0

            for oy in self.origin_years:
                current_value = self.get_value(oy, dev)
                next_value = self.get_value(oy, self.dev_periods[dev_idx + 1])

                if current_value is not None and next_value is not None and current_value != 0:
                    numerator_sum += next_value
                    denominator_sum += current_value

            if denominator_sum != 0:
                avg_factors[dev] = numerator_sum / denominator_sum

    return avg_factors

get_latest_diagonal()

Get the latest diagonal of the triangle.

Returns:

Type Description
Dict[int, float]

Dictionary mapping origin years to their latest available values

Source code in src\pyre\claims\triangles.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def get_latest_diagonal(self) -> Dict[int, float]:
    """
    Get the latest diagonal of the triangle.

    Returns:
        Dictionary mapping origin years to their latest available values
    """
    result = {}
    for oy in self.origin_years:
        # Find the maximum development period with a value for this origin year
        available_devs = [dp for dp in self.dev_periods if self.get_value(oy, dp) is not None]
        if available_devs:
            max_dev = max(available_devs)
            result[oy] = self.get_value(oy, max_dev)
    return result

get_value(origin_year, dev_period)

Get a value from the triangle.

Parameters:

Name Type Description Default
origin_year int

The origin year

required
dev_period int

The development period

required

Returns:

Type Description
Optional[float]

The value at the specified position or None if not found

Source code in src\pyre\claims\triangles.py
158
159
160
161
162
163
164
165
166
167
168
169
def get_value(self, origin_year: int, dev_period: int) -> Optional[float]:
    """
    Get a value from the triangle.

    Args:
        origin_year: The origin year
        dev_period: The development period

    Returns:
        The value at the specified position or None if not found
    """
    return self.triangle.get(origin_year, {}).get(dev_period)

set_value(origin_year, dev_period, value)

Set a value in the triangle.

Parameters:

Name Type Description Default
origin_year int

The origin year

required
dev_period int

The development period

required
value Optional[float]

The value to set

required
Source code in src\pyre\claims\triangles.py
171
172
173
174
175
176
177
178
179
180
def set_value(self, origin_year: int, dev_period: int, value: Optional[float]) -> None:
    """
    Set a value in the triangle.

    Args:
        origin_year: The origin year
        dev_period: The development period
        value: The value to set
    """
    self[origin_year, dev_period] = value

to_cumulative()

Convert an incremental triangle to a cumulative triangle.

Returns:

Type Description
Triangle

A new Triangle with cumulative values

Source code in src\pyre\claims\triangles.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def to_cumulative(self) -> 'Triangle':
    """
    Convert an incremental triangle to a cumulative triangle.

    Returns:
        A new Triangle with cumulative values
    """
    cumulative_triangle = {}

    for oy in self.origin_years:
        cumulative_triangle[oy] = {}
        cumulative_value = 0.0

        for dp in self.dev_periods:
            incremental_value = self.get_value(oy, dp)

            if incremental_value is None:
                cumulative_triangle[oy][dp] = None
            else:
                cumulative_value += incremental_value
                cumulative_triangle[oy][dp] = cumulative_value

    return Triangle(
        triangle=cumulative_triangle,
        origin_years=self.origin_years.copy(),
        dev_periods=self.dev_periods.copy()
    )

to_incremental()

Convert a cumulative triangle to an incremental triangle.

Returns:

Type Description
Triangle

A new Triangle with incremental values

Source code in src\pyre\claims\triangles.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def to_incremental(self) -> 'Triangle':
    """
    Convert a cumulative triangle to an incremental triangle.

    Returns:
        A new Triangle with incremental values
    """
    incremental_triangle = {}

    for oy in self.origin_years:
        incremental_triangle[oy] = {}
        prev_value = None

        for dp in self.dev_periods:
            current_value = self.get_value(oy, dp)

            if current_value is None:
                incremental_triangle[oy][dp] = None
            elif prev_value is None:
                incremental_triangle[oy][dp] = current_value
            else:
                incremental_triangle[oy][dp] = current_value - prev_value

            if current_value is not None:
                prev_value = current_value

    return Triangle(
        triangle=incremental_triangle,
        origin_years=self.origin_years.copy(),
        dev_periods=self.dev_periods.copy()
    )