Source code for ratingmodels.trend
r"""Trend: bringing historical experience to the rating-period cost level.
The trend factor compounds an annual rate over the gap between the midpoint of
the experience period and the midpoint of the rating period:
.. math::
\text{factor} = (1 + t)^{\Delta}, \qquad
\Delta = \frac{m_{\text{rate}} - m_{\text{exp}}}{365.25}\ \text{years}.
A total trend is often decomposed into utilization and unit-cost components,
which combine multiplicatively: :math:`(1+t) = (1+t_u)(1+t_c)`.
"""
from __future__ import annotations
from datetime import date, datetime
from typing import Union
DateLike = Union[str, date, datetime]
def _to_date(d: DateLike) -> date:
if isinstance(d, datetime):
return d.date()
if isinstance(d, date):
return d
return datetime.fromisoformat(str(d)).date()
[docs]
def years_between(start: DateLike, end: DateLike) -> float:
"""Fractional years between two dates using a 365.25-day year."""
s, e = _to_date(start), _to_date(end)
return (e - s).days / 365.25
[docs]
def period_midpoint(start: DateLike, end: DateLike) -> date:
"""Midpoint date of a period ``[start, end]`` (inclusive endpoints)."""
s, e = _to_date(start), _to_date(end)
if e < s:
raise ValueError("end must not precede start")
return s + (e - s) / 2
[docs]
def trend_factor(annual_trend: float, years: float) -> float:
r""":math:`(1 + \text{annual\_trend})^{\text{years}}`."""
base = 1.0 + annual_trend
if base <= 0:
raise ValueError("annual_trend must exceed -1")
return float(base**years)
[docs]
def trend_factor_between(
annual_trend: float,
experience_period: tuple[DateLike, DateLike],
rating_period: tuple[DateLike, DateLike],
) -> float:
"""Midpoint-to-midpoint trend factor from two date ranges."""
m_exp = period_midpoint(*experience_period)
m_rate = period_midpoint(*rating_period)
return trend_factor(annual_trend, years_between(m_exp, m_rate))
[docs]
def apply_trend(value: float, annual_trend: float, years: float) -> float:
"""Trend a value forward (or back, for negative ``years``)."""
return float(value) * trend_factor(annual_trend, years)
[docs]
def combine_trend(util_trend: float, cost_trend: float) -> float:
r"""Combine utilization and unit-cost trends: :math:`(1+t_u)(1+t_c)-1`."""
return float((1 + util_trend) * (1 + cost_trend) - 1)
[docs]
def split_total_trend(total_trend: float, util_trend: float) -> float:
"""Back out the unit-cost trend implied by a total and a utilization trend."""
return float((1 + total_trend) / (1 + util_trend) - 1)