GrowthRate#
GrowthRate models benefit revaluation schedules for life
annuities and insurances. It supports:
Geometric (compound) growth: \(F(t) = \prod_{j=0}^{t-1}(1+g_j)\)
Arithmetic (linear additive) growth: \(F(t) = 1 + \sum_{j=0}^{t-1} g_j\)
where \(F(0) = 1\) under the standard actuarial convention
(apply_from_first=False).
Like InterestRate, growth rates can be constant,
piecewise (segment rates with durations), or multi-scenario.
shifted() generates time-shifted copies for
prospective reserve calculations.
See also
Growth Rates — Full guide to growth rate modeling.
Growth Rate Conventions — Anniversary convention used by the annuity engine.
- class lactuca.GrowthRate(rate: float | dict | None = None, growth_type: Literal['g', 'a'] = 'g', rates: Sequence[float] | NDArray[float64] | None = None, terms: Sequence[int] | None = None, apply_from_first: object = False, description: str | None = None)#
Bases:
objectGrowth rate curve for actuarial annuity and insurance revaluation.
Analogous to
InterestRate, this class encapsulates constant, piecewise, or multi-scenario growth rate schedules for revaluation of payments in life annuities and insurances.Growth can be geometric (compound) or arithmetic (linear additive):
Geometric (
growth_type='g'): \(F(t) = \prod_{j=0}^{t-1}(1+g_j)\)Arithmetic (
growth_type='a'): \(F(t) = 1 + \sum_{j=0}^{t-1} g_j\)
where \(F(0) = 1\) when
apply_from_first=False(standard convention).- Parameters:
rate (float or dict or None, optional) –
scalar float: constant growth rate for all periods.
dict: multi-scenario container. Each value must be a
float(converted toGrowthRate) or aGrowthRateinstance. Nested multi-scenario instances are not allowed.None: userates+termsto construct a piecewise curve.
growth_type ({'g', 'a'}, optional) – Growth type:
'g'for geometric (default),'a'for arithmetic.rates (sequence of float or None, optional) – Growth rates per segment for piecewise construction. Length must equal
len(terms) + 1; the last rate applies indefinitely.terms (sequence of int or None, optional) – Segment durations (strictly positive integers) for piecewise construction. Consistent with
InterestRateconvention.terms=[3, 2]means segment 0 covers periods 0–2, segment 1 covers 3–4, beyond that the last rate ofratesapplies.apply_from_first (bool, optional) – If
False(default): standard actuarial convention — \(F(0) = 1\), growth applies from the second payment onward. IfTrue: \(F(0) = 1 + g\), growth applies from the first payment.description (str or None, optional) – Human-readable label (stored, not used in computations).
Notes
Period indices
tpassed tofactor()are non-negative integers (\(t \geq 0\)), base-0.For the formula of the provision/reserve: \(V_{ts} = R \cdot \texttt{gr.factor}(ts) \cdot \texttt{ax}(x+ts,\; ts=ts,\; gr=\texttt{gr})\).
Negative growth rates are permitted as long as all cumulative factors remain strictly positive across the payment schedule.
See Growth Rate Conventions for the full reference of the anniversary convention and fractional-
tspolicy used by the annuity and insurance dispatchers.
Examples
Constant geometric growth:
>>> from lactuca import GrowthRate >>> import numpy as np >>> gr = GrowthRate(0.03) >>> gr.factor(2) # (1.03)^2 1.0609
Constant arithmetic growth:
>>> gr_a = GrowthRate(0.02, 'a') >>> gr_a.factor(3) # 1 + 0.02*3 1.06
Piecewise geometric growth (1% for the first year, 2% thereafter):
terms=[1]means the first segment (rate 1%) covers periodj=0only; from periodj=1onward the second rate (2%) applies indefinitely.factor(t)accumulates rates for periodsj = 0, 1, ..., t-1:>>> gr_p = GrowthRate(rates=[0.01, 0.02], terms=[1]) >>> gr_p.factor(0) # no periods accumulated 1.0 >>> gr_p.factor(1) # j=0 only → 1 × 1.01 1.01 >>> gr_p.factor(2) # j=0, j=1 → 1.01 × 1.02 1.0302 >>> gr_p.factor(3) # j=0..2 → 1.01 × 1.02² 1.050804
Effect of
apply_from_first=True(growth applied already att=0):>>> gr_f = GrowthRate(0.03, apply_from_first=True) >>> gr_f.factor(0) # t=0 → (1.03)^1 (growth from first payment) 1.03 >>> gr_f.factor(1) # t=1 → (1.03)^2 1.0609 >>> gr_f.factor(2) # t=2 → (1.03)^3 1.092727
Compare with
apply_from_first=False(default):>>> gr_d = GrowthRate(0.03, apply_from_first=False) >>> gr_d.factor(0) # t=0 → (1.03)^0 (reference payment, no growth yet) 1.0 >>> gr_d.factor(1) # t=1 → (1.03)^1 1.03 >>> gr_d.factor(2) # t=2 → (1.03)^2 1.0609
Multi-scenario (constant geometric, piecewise geometric, constant arithmetic, piecewise arithmetic — four named scenarios in one container):
>>> gr_s = GrowthRate({ ... 'base': GrowthRate(0.02), ... 'stress': GrowthRate(0.04), ... 'piecewise': GrowthRate(rates=[0.01, 0.03], terms=[2]), ... 'arithmetic': GrowthRate(0.02, 'a'), ... 'pw_arith': GrowthRate(rates=[0.01, 0.03], terms=[2], growth_type='a'), ... }) >>> gr_s.active_scenario 'base' >>> gr_s.factor(2) # base: (1.02)^2 1.0404 >>> gr_s.active_scenario = 'stress' >>> gr_s.factor(2) # stress: (1.04)^2 1.0816 >>> gr_s.active_scenario = 'piecewise' >>> gr_s.factor(1) # j=0 → x1.01 1.01 >>> gr_s.factor(2) # j=0 + j=1 → 1.01 x 1.01 1.0201 >>> gr_s.factor(3) # j=0..2 → 1.01^2 x 1.03 1.050703 >>> gr_s.active_scenario = 'arithmetic' >>> gr_s.factor(3) # arithmetic: 1 + 0.02*3 1.06 >>> gr_s.active_scenario = 'pw_arith' >>> gr_s.factor(1) # j=0 → 1 + 0.01 1.01 >>> gr_s.factor(3) # j=0..2 → 1 + 0.01 + 0.01 + 0.03 1.05
See also
GrowthRate.factorCompute cumulative growth factor for given period indices.
GrowthRate.shiftedReturn a new GrowthRate with the first
tsyears consumed.GrowthRate.copyReturn an independent deep copy of this instance.
InterestRateAnalogous class for interest rate modeling.
- add_scenario(name: str, scenario: GrowthRate) None#
Add a named scenario to this
GrowthRateinstance.- Parameters:
name (str) – Unique scenario name.
scenario (GrowthRate) – A non-nested
GrowthRateinstance (constant or piecewise).
- Raises:
TypeError – If
scenariois not aGrowthRateinstance.ValueError – If
scenarioitself contains scenarios (nesting not allowed).
Notes
If no active scenario is set, the new scenario becomes active automatically.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate({'base': 0.02}) >>> gr.add_scenario('stress', GrowthRate(0.04)) >>> gr.active_scenario = 'stress'
- amounts(times: NDArray[float64] | Sequence[float] | float, start: object, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365]) NDArray[float64]#
Generate cashflow amounts for each payment time using this growth rate.
Computes the amount for the \(k\)-th payment (0-based) as:
\[A_k = R \cdot F\!\left(\left\lfloor \frac{k}{m} \right\rfloor\right)\]where \(R\) is the initial amount (
start), \(m\) is the number of payments per year, and \(F\) is the cumulative growth factor fromfactor(). The anniversary index \(\lfloor k/m \rfloor\) matches the convention used internally by the calculation engine whencashflow_amountsis passed toax().- Parameters:
times (array-like of float) – Payment times in years, typically from
payment_times(). Used only to determine the total number of payments. Must not beNoneand must not contain non-finite values.start (float) – Cashflow amount for the first payment (before any growth). Must be a finite numeric scalar (boolean values raise
TypeError).m (PaymentFrequencyLiteral) – Number of payments per year in
times. Groups payments by anniversary year: payments \(k = 0, 1, \ldots, m-1\) share anniversary 0 (factor \(F(0) = 1\)), payments \(k = m, \ldots, 2m-1\) share anniversary 1, etc. Must be one of the allowed payment frequencies (1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365).
- Returns:
Array of shape
(len(times),)and dtypefloat64with the escalated cashflow amount for each payment.- Return type:
NDArray[np.float64]
- Raises:
TypeError – If
timesisNoneorstartis boolean.ValueError – If this instance has multiple scenarios, if
timescontains non-finite values, ifstartis not finite, ifmis not one of the allowed payment frequencies (1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365), or if constant arithmetic growth would produce a non-positive factor for any payment anniversary.
Notes
When
timesis produced bypayment_times()withselected_periods, passm = len(selected_periods)(not the original frequency) so that anniversary boundaries are correctly aligned.Examples
Monthly step-up pension: 2 % geometric annual growth, 12 payments per year.
>>> from lactuca import GrowthRate, payment_times >>> import numpy as np >>> gr = GrowthRate(0.02) >>> times = payment_times(n=3, m=12) >>> amounts = gr.amounts(times, start=1000.0, m=12) >>> amounts[0].item() # first year: no growth 1000.0 >>> np.testing.assert_allclose(amounts[12], 1020.0) # second year: ×1.02
Annual arithmetic growth: +5 % per year.
>>> gr_a = GrowthRate(0.05, growth_type='a') >>> times_a = payment_times(n=3, m=1) >>> gr_a.amounts(times_a, start=1000.0, m=1) array([1000., 1050., 1100.])
- copy() GrowthRate#
Return a deep copy of this instance.
- Returns:
Independent copy with all state duplicated.
- Return type:
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(0.02) >>> gr2 = gr.copy()
- curve_analysis() dict#
Return a quantitative analytical summary of the growth rate curve.
Computes curve shape properties, monotonicity flags, and cumulative growth factors at key actuarial time horizons.
- Returns:
Analytical summary with the following keys:
'type':'constant','piecewise', or'scenarios''growth_type':'geometric'or'arithmetic''rate_range': dict with'min'and'max'(float)'curve_properties': dict with boolean flags:'is_flat': all segment rates are equal'is_monotone_inc': rates non-decreasing'is_monotone_dec': rates non-increasing'has_zero_rates': any rate equals zero'has_negative_rates': any rate is negative
'key_factors': dict with't1','t5','t10','t20'— cumulative growth factors at those horizons (float)'segment_count':int— piecewise only'total_defined_term':int— piecewise only'active_scenario':str— scenario container only'scenarios': dict of per-scenario results — scenario container only
- Return type:
dict
Notes
For multi-scenario containers,
rate_range,curve_properties, andkey_factorsat the top level mirror the active scenario; per-scenario detail remains inscenarios.See also
GrowthRate.validateStructural validation with regulatory warnings.
InterestRate.curve_analysisAnalogous method on the interest rate class.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(0.03) >>> ca = gr.curve_analysis() >>> ca['curve_properties']['is_flat'] True >>> round(ca['key_factors']['t1'], 2) 1.03
- export(format: str = 'dict') dict | str#
Export the
GrowthRateconfiguration as a dict or JSON string.- Parameters:
format ({'dict', 'json', 'regulatory'}, optional) –
Output format. Default is
'dict'.'dict': plain Python dict.'json': JSON-encoded string (2-space indented).'regulatory': enriched JSON string with audit-trail metadata (export_timestamp,format_version,library) suitable for Solvency II / IFRS 17 regulatory compliance.
- Returns:
Configuration dictionary when
format='dict', or a JSON string forformat='json'orformat='regulatory'.- Return type:
dict or str
- Raises:
ValueError – If
formatis not one of'dict','json', or'regulatory'.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(0.03) >>> result = gr.export() >>> result['is_constant'], result['growth_type'], result['rate'] (True, 'g', 0.03) >>> import json; d = json.loads(gr.export(format='regulatory')) >>> d['format_version'] '1.0'
- factor(t: float | int | Sequence[float] | NDArray[float64]) float | NDArray[float64]#
Compute the cumulative growth factor for each period index in
t.The factor depends on
apply_from_first:apply_from_first=False(default, standard actuarial convention):\[F(0) = 1, \quad F(t) = \prod_{j=0}^{t-1}(1+g_j) \;\text{(geometric)}\]Equivalent: \(F(t) = 1 + \sum_{j=0}^{t-1} g_j\) for arithmetic.
apply_from_first=True:\[F(t) = \prod_{j=0}^{t}(1+g_j) \;\text{(geometric)}\]Equivalent: \(F(t) = 1 + \sum_{j=0}^{t} g_j\) for arithmetic.
Used in the reserve formula:
\[V_{ts} = R \cdot \texttt{gr.factor}(ts) \cdot \texttt{ax}(x+ts,\; ts=ts,\; gr=gr)\]- Parameters:
t (scalar or array-like) – Non-negative integer-valued period index (or indices). Scalars return
float; arrays returnNDArray[np.float64].- Returns:
Cumulative growth factor(s). Same shape as input.
- Return type:
float or NDArray[np.float64]
- Raises:
TypeError – If
tisNone.ValueError – If any element of
tis negative or non-integer-valued, or if constant arithmetic growth would produce a non-positive cumulative factor at any requestedt.
Examples
>>> from lactuca import GrowthRate >>> import numpy as np >>> gr = GrowthRate(0.03) >>> gr.factor(0) 1.0 >>> gr.factor(2) 1.0609 >>> gr.factor(np.array([0, 1, 2, 3])) array([1. , 1.03 , 1.0609, 1.092727])
Piecewise:
rates=[0.01, 0.02], terms=[1]— periodj=0uses 1%, periodj >= 1uses 2%.factor(t)accumulatesj = 0..t-1:>>> gr_p = GrowthRate(rates=[0.01, 0.02], terms=[1]) >>> gr_p.factor(1) # j=0 → ×1.01 1.01 >>> gr_p.factor(2) # j=0 + j=1 → 1.01 × 1.02 1.0302
With
apply_from_first=True,factor(t)accumulatesj = 0..t(one extra period), so growth is already present att=0:>>> gr_first = GrowthRate(0.03, apply_from_first=True) >>> gr_first.factor(0) # j=0 included → 1 × 1.03 1.03 >>> gr_first.factor(1) # j=0..1 → 1.03² 1.0609
- get_rate(t: int | float | Sequence[float] | NDArray[float64]) float | NDArray[float64]#
Retrieve the growth rate of the segment applying at period
t.For piecewise schedules, returns the rate of the segment containing period
t(0-based anniversary index). The tail rate extends indefinitely beyond the last defined segment.- Parameters:
t (int, float, or array-like) – Non-negative period index (or indices). Scalar input returns
float; array-like input returnsNDArray[np.float64].- Returns:
Rate(s) applicable at period
t. Shape matches the input.- Return type:
float or NDArray[np.float64]
- Raises:
ValueError – If any value in
tis negative, non-finite, or non-integer-valued.
Notes
For constant
GrowthRate, returns the same rate for allt.tmust be non-negative integer-valued (same rule asfactor()).See also
GrowthRate.get_segment_infoFull segment metadata at period
t.InterestRate.get_rateAnalogous method on the interest rate class.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(0.03) >>> gr.get_rate(5) 0.03 >>> gr_pw = GrowthRate(rates=[0.01, 0.02], terms=[3]) >>> gr_pw.get_rate(0) # period 0 is in segment 0 (rate 1%) 0.01 >>> gr_pw.get_rate(3) # period 3 is the tail (rate 2%) 0.02
- get_segment_info(t: int | float) dict#
Return detailed information about the growth segment at period
t.- Parameters:
t (int or float) – Non-negative period index (scalar). Fractional values are rounded to the nearest integer.
- Returns:
A dict with the following keys:
'type':'constant'or'piecewise''rate':float— growth rate of the applicable segment'growth_type':'geometric'or'arithmetic''apply_from_first':bool'segment_index':int— 0-based index of the segment'segment_start':int— first period of segment (inclusive)'segment_end':intorNone— first period of the next segment (exclusive), orNonefor the open-ended tail'is_terminal':bool—Trueif segment extends indefinitely
- Return type:
dict
- Raises:
TypeError – If
tis not a finite scalar numeric (includingbool).ValueError – If
tis negative.
See also
GrowthRate.get_rateReturn only the rate at period
t.InterestRate.get_segment_infoAnalogous method on the interest rate class.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(0.03) >>> info = gr.get_segment_info(0) >>> info['type'], info['is_terminal'] ('constant', True) >>> gr_pw = GrowthRate(rates=[0.01, 0.02], terms=[3]) >>> gr_pw.get_segment_info(2)['segment_end'] # segment 0 ends at period 3 3 >>> gr_pw.get_segment_info(3)['is_terminal'] # tail segment True
- shifted(ts: object) GrowthRate#
Return a new
GrowthRatewith the firsttsyears consumed.Analogous to
InterestRate.shifted(), this method produces a growth schedule starting from the momenttsyears into the original contract, so that period index 0 in the shifted schedule corresponds to the correct contractual growth factor.Always uses
ts_int = int(ts)(truncation toward zero), since anniversary indices are integers by construction. Ifts <= 0, returns an independent copy (no temporal change), matchingInterestRate.shifted().- Parameters:
ts (float or int) – Number of years already elapsed. Only the integer part is used.
- Returns:
Shifted growth schedule. Returns an independent copy when
ts <= 0. For constant schedules withts > 0, returns the same instance unchanged because the growth factor series is period-relative.- Return type:
- Raises:
TypeError – If
tsis boolean or not a numeric type.ValueError – If
tsis not finite.
Notes
For multi-scenario containers, the call is forwarded to the active scenario and a shifted copy of that scenario is returned.
For constant
GrowthRatewithts > 0, the original instance is returned unchanged — the growth factor series is period-relative and unaffected by time displacement.For piecewise schedules, the first segment is truncated by
ts_intyears. If a segment is exactly consumed, it is dropped.Preserves
growth_typeandapply_from_first.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(rates=[0.02, 0.03, 0.04], terms=[3, 2]) >>> gr.shifted(2) GrowthRate(rates=[0.02, 0.03, 0.04], terms=[1, 2], growth_type='g') >>> gr.shifted(3) GrowthRate(rates=[0.03, 0.04], terms=[2], growth_type='g') >>> gr.shifted(6) GrowthRate(0.04, growth_type='g') # geometric, constant
- summary() str#
Alias for
__str__for compatibility.- Returns:
Enhanced human-readable representation of the growth rate curve.
- Return type:
str
- validate() dict#
Return a structural validation report for this growth rate curve.
Checks actuarial invariants and rate properties, returning a structured diagnostic dict suitable for compliance reporting.
- Returns:
Validation report with the following keys:
'type':'constant','piecewise', or'scenarios''growth_type':'geometric'or'arithmetic''apply_from_first':bool'status':'ok'if no warnings, else'warnings''warnings':listofstr— informational messages'rate_statistics': dict with'min','max','mean','zero_count','negative_count'(omitted for scenario containers)'segment_count':int— piecewise only'total_defined_term':int— piecewise only'active_scenario':str— scenario container only'scenarios': dict of per-scenario results — scenario container only
- Return type:
dict
Notes
Warnings do not indicate invalid configurations; they flag conditions that warrant review in regulatory contexts (IFRS 17, Solvency II). A zero growth rate produces flat factors; a negative rate produces shrinkage, which may be intentional (e.g. benefit reduction clauses).
See also
GrowthRate.curve_analysisQuantitative analytical summary of curve shape.
InterestRate.validateAnalogous method on the interest rate class.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate(0.0) >>> result = gr.validate() >>> result['status'] 'warnings' >>> gr2 = GrowthRate(0.02) >>> gr2.validate()['status'] 'ok'
- property active_scenario: str | None#
Get or set the name of the active scenario.
- Returns:
Name of the currently active scenario, or
Noneif no scenario has been activated yet.- Return type:
str or None
- Raises:
ValueError – When setting, if
nameis not found in the scenarios dict.
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate({'base': 0.02, 'stress': 0.04}) >>> gr.active_scenario 'base' >>> gr.active_scenario = 'stress' >>> gr.active_scenario 'stress'
- property apply_from_first: bool#
Return
Trueif growth applies from the first period.When
False(default), \(F(0) = 1\) and growth starts from period 1.- Return type:
bool
- property growth_type: str#
'g'for geometric,'a'for arithmetic.- Return type:
str
- Type:
Return the growth type
- property is_constant: bool#
Return
Trueif the growth rate is constant (scalar).- Return type:
bool
- property rates: NDArray[float64]#
Return a copy of the growth rates array.
- Returns:
A copy of the internal rates array (size
1for constant,n_segments + 1for piecewise including the tail rate).- Return type:
NDArray[np.float64]
- property scenario_names: list#
Return a list of all scenario names.
- Returns:
Scenario names in insertion order. Empty list if no scenarios are defined.
- Return type:
list of str
Examples
>>> from lactuca import GrowthRate >>> gr = GrowthRate({'base': 0.02, 'stress': 0.04}) >>> gr.scenario_names ['base', 'stress']
- property terms: NDArray[int64]#
Return a copy of the segment duration array (integer periods).
- Returns:
Empty array for constant
GrowthRate. Lengthn_segmentsfor piecewise (each element is the duration of the corresponding segment in anniversary years).- Return type:
NDArray[np.int64]