"""The sole base Space in the :mod:`~basiclife.BasicTermASL_ME` model.
:mod:`~basiclife.BasicTermASL_ME.Base` is the only base Space defined
in the :mod:`~basiclife.BasicTermASL_ME` model,
and it contains logic and data commonly used in the sub spaces
of :mod:`~basiclife.BasicTermASL_ME.Base`.
.. rubric:: Parameters and References
(In all the sample code below,
the global variable ``Projection`` refers to the
:mod:`~basiclife.BasicTerm_ME.Projection` Space.)
Attributes:
model_point_table: All model points as a DataFrame.
The sample model point data was generated by
*generate_model_points_ASL.ipynb* included in the library.
By default, :func:`model_point` returns this
entire :attr:`model_point_table`.
The DataFrame has an index named ``point_id``,
and has the following columns:
* ``age_at_entry``
* ``sex``
* ``policy_term``
* ``policy_count``
* ``sum_assured``
* ``issue_date``
* ``payment_freq``
* ``payment_term``
Cells with the same names as these columns return
the corresponding columns.
The DataFrame is saved in the Excel file *model_point_table.xlsx*
located in the model folder.
:attr:`model_point_table` is created by
Projection's `new_pandas`_ method,
so that the DataFrame is saved in the separate file.
.. seealso::
* :func:`model_point`
* :func:`age_at_entry`
* :func:`sex`
* :func:`policy_term`
* :func:`pols_if_init`
* :func:`sum_assured`
* :func:`issue_date`
* :func:`payment_freq`
* :func:`payment_term`
disc_rate_ann: Annual discount rates by duration as a pandas Series.
.. code-block::
>>> Projection.disc_rate_ann
year
0 0.00000
1 0.00555
2 0.00684
3 0.00788
4 0.00866
146 0.03025
147 0.03033
148 0.03041
149 0.03049
150 0.03056
Name: disc_rate_ann, Length: 151, dtype: float64
The Series is saved in the Excel file *disc_rate_ann.xlsx*
placed in the model folder.
:attr:`disc_rate_ann` is created by
Projection's `new_pandas`_ method,
so that the Series is saved in the separate file.
.. seealso::
* :func:`disc_rate`
* :func:`disc_factors`
mort_table: Mortality table by age and duration as a DataFrame.
See *basic_term_sample.xlsx* included in this library
for how the sample mortality rates are created.
.. code-block::
>>> Projection.mort_table
0 1 2 3 4 5
Age
18 0.000231 0.000254 0.000280 0.000308 0.000338 0.000372
19 0.000235 0.000259 0.000285 0.000313 0.000345 0.000379
20 0.000240 0.000264 0.000290 0.000319 0.000351 0.000386
21 0.000245 0.000269 0.000296 0.000326 0.000359 0.000394
22 0.000250 0.000275 0.000303 0.000333 0.000367 0.000403
.. ... ... ... ... ... ...
116 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
117 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
118 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
119 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
120 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
[103 rows x 6 columns]
The DataFrame is saved in the Excel file *mort_table.xlsx*
placed in the model folder.
:attr:`mort_table` is created by
Projection's `new_pandas`_ method,
so that the DataFrame is saved in the separate file.
.. seealso::
* :func:`mort_rate`
date_init: The projection start date as a string in the YYYY-MM-DD format.
The start date needs to be an end-of-month date.
By default, '2021-12-31' is assigned.
np: The `numpy`_ module.
pd: The `pandas`_ module.
.. _numpy:
https://numpy.org/
.. _pandas:
https://pandas.pydata.org/
.. _new_pandas:
https://docs.modelx.io/en/latest/reference/space/generated/modelx.core.space.UserSpace.new_pandas.html
"""
from modelx.serialize.jsonvalues import *
_formula = None
_bases = []
_allow_none = None
_spaces = []
# ---------------------------------------------------------------------------
# Cells
[docs]
def age(i):
"""The attained age at :func:`date_(i)<date_>`.
Defined as::
age_at_entry() + duration_y(i)
.. seealso::
* :func:`age_at_entry`
* :func:`duration_y`
"""
return age_at_entry() + duration_y(i)
[docs]
def age_at_entry():
"""The age at entry of the model points
The ``age_at_entry`` column of the DataFrame returned by
:func:`model_point`.
"""
return model_point()["age_at_entry"]
[docs]
def check_pay_count():
"""Check :func:`pay_count`.
Return :obj:`True` if, for all model points, the sum of the
numbers of past and future premium payments
equates to :func:`payment_term` times :func:`payment_freq`.
Th numbers of future payments are calculated
by summing :func:`pay_count` for all ``i`` from 0 to :func:`max_proj_len` - 1.
The numbers of past payments are calculated from
:func:`duration_m(0)<duration_m>`, :func:`payment_freq` and :func:`payment_term`.
.. seealso::
* :func:`pay_count`
* :func:`payment_term`
* :func:`payment_freq`
* :func:`max_proj_len`
* :func:`duration_m`
"""
pays = np.array([pay_count(i) for i in range(max_proj_len())]).transpose()
paid = payment_term() * 12 <= duration_m(0)
not_started = duration_m(0) < 0
past = (duration_m(0) // (12 // payment_freq()) + 1).mask(not_started, 0).mask(paid, payment_term() * payment_freq())
# return not (pays.sum(axis=1) - payment_freq() * payment_term()).any()
return (pays.sum(axis=1) + past == payment_freq() * payment_term()).all()
[docs]
def claim_pp(i):
"""Claim per policy
The claim amount per plicy. Defaults to :func:`sum_assured`.
"""
return sum_assured()
[docs]
def claims(i):
"""Claims
Claims incurred during the period from :func:`date_(i)<date_>` + 1
to :func:`date_(i+1)<date_>` defined as::
claim_pp(i) * pols_death(i)
.. seealso::
* :func:`claim_pp`
* :func:`pols_death`
* :func:`date_`
"""
return claim_pp(i) * pols_death(i)
[docs]
def commissions(i):
"""Commissions
By default, 100% premiums for the first policy year, 0 otherwise.
.. seealso::
* :func:`premiums`
* :func:`duration_y`
"""
return (duration_y(i) == 0) * premiums(i, 'LAST') + (duration_y(i+1) == 0) * premiums(i, 'NEXT')
[docs]
def date_(i):
"""Date at each projection step.
Returns a date representing each projection step as `pandas Timestamp`_.
``date_(0)`` is read from :attr:`date_init`.
For ``i > 0``, ``date_(i)`` is defined recursively as::
date_(i-1) + offset(i-1)
``date_(i)`` must always return an end-of-month date.
The length of each projection step is specified by :func:`offset`.
Items indexed with ``i`` and representing the change of a quantity
in a projection period such as cashflows, represents
the change during the projection period starting from one day
after ``date_(i)`` and ending on ``date_(i+1)``
.. seealso::
* :attr:`date_init`
* :func:`offset`
* `pandas Timestamp`_
.. _pandas Timestamp:
https://pandas.pydata.org/docs/reference/api/pandas.Timestamp.html
"""
if i == 0:
return pd.Timestamp(date_init)
else:
return date_(i-1) + offset(i-1)
[docs]
def disc_factors():
"""Discount factors.
Vector of the discount factors as a Numpy array. Used for calculating
the present values of cashflows.
.. seealso::
* :func:`disc_rate`
* :func:`months_`
* :func:`max_proj_len`
"""
return np.array(list((1 + disc_rate(i))**(-months_(i)/12) for i in range(max_proj_len())))
[docs]
def disc_factors_prem(j):
"""Discount factors for premiums.
Returns a 2D numpy array. The array contains
discount factors for discounting premiums of each model point.
The timings of premium cashflows are
adjusted by :func:`payment_lag`.
Since :func:`payment_lag` differs by model point,
:func:`disc_factors_prem` returns a 2D array by model point
and by time index ``i``.
In each projection step, premium payments before and after
policy anniversary are modeled separately,
so different discount factors are returned depending of the value
of ``j``.
When ``j='LAST'``, the returned discount factors are
for discounting premiums paid before policy anniversary,
while when ``j='NEXT'``, the factors are for premiums
after the anniversary.
For each ``i`` and ``j``, :func:`disc_factors_prem` is defined as::
(1 + disc_rate(i))**(-t)
where ``t`` is defined as for ``j='LAST'``::
(months_(i) + payment_lag(i, j)) / 12
and for ``j='NEXT'`` as::
(months_(i) + last_part(i, j) + payment_lag(i, j)) / 12
Args:
j: 'LAST' or 'NEXT'
.. seealso::
* :func:`disc_rate`
* :func:`max_proj_len`
* :func:`payment_lag`
"""
data = []
for i in range(max_proj_len()):
last = last_part(i) if j == 'NEXT' else 0
t = (months_(i) + last + payment_lag(i, j)) / 12
data.append((1 + disc_rate(i))**(-t))
return np.array(data).transpose()
[docs]
def disc_rate(i):
"""Discount rate for period ``i``
Returns an annual discount rate to be applied for period ``i``,
which starts one day after :func:`date_(i)<date_>` and ends on :func:`date_(i+1)<date_>`.
The discount rate is read from :attr:`disc_rate_ann`.
If the number of years elapsed from :attr:`date_init` changes
during the period, discount rates read from :attr:`disc_rate_ann`
are prorated.
.. seealso::
* :func:`disc_rate_ann`
* :func:`months_`
* :func:`months_in_step`
"""
y0 = months_(i) // 12
y1 = months_(i+1) // 12
if y0 == y1:
return disc_rate_ann[y0]
else:
m0 = y1 * 12 - months_(i)
m1 = months_(i+1) - y1 * 12
m = months_in_step(i)
return disc_rate_ann[y0] * m0 / m + disc_rate_ann[y1] * m1 / m
[docs]
def duration_m(i):
"""Duration of model points at ``i`` in months
Returns durations at period ``i`` in months of all model points
as a Series indexed with model point ID.
:func:`duration_m(i)<duration_m>` is calculated
from :func:`date_(i)<date_>` and :func:`issue_date`.
:func:`duration_m` is 0 in the issue month.
Negative values of :func:`duration_m` indicate future new business
policies. For example, If the :func:`duration_m` is
-15 at time 0, the model point is issued 15 months later.
.. seealso::
* :func:`issue_date`
* :func:`date_`
"""
iss = issue_date()
return date_(i).year * 12 + date_(i).month - iss.dt.year * 12 - iss.dt.month
[docs]
def duration_y(i):
"""Duration of model points at period ``i`` in years
Returns a Series calculated as::
duration_m(i) // 12
.. seealso:: :func:`duration_m`
"""
return duration_m(i) // 12
[docs]
def expense_acq():
"""Acquisition expense per policy
``300`` by default.
"""
return 300
[docs]
def expense_maint():
"""Annual maintenance expense per policy
``60`` by default.
"""
return 60
[docs]
def expenses(i):
"""Expenses
Expenses for Period ``i``.
defined as the sum of acquisition expenses and maintenance expenses.
The acquisition expenses are modeled as :func:`expense_acq`
times :func:`pols_new_biz`.
The maintenance expenses are modeled as :func:`expense_maint`
times :func:`inflation_factor` times :func:`pols_if_avg`.
.. seealso::
* :func:`expense_acq`
* :func:`expense_maint`
* :func:`inflation_factor`
* :func:`pols_new_biz`
* :func:`pols_if_avg`
"""
period = last_part(i).where(is_maturing(i), months_in_step(i))
return (expense_acq() * pols_new_biz(i)
+ expense_maint() * period / 12 * pols_if_avg(i) * inflation_factor(i))
[docs]
def inflation_factor(i):
"""The inflation factor for Period ``i``.
Defined as::
(1 + inflation_rate())**(months_in_step(i)/12)
.. seealso::
* :func:`inflation_rate`
* :func:`months_in_step`
"""
return (1 + inflation_rate())**(months_in_step(i)/12)
[docs]
def inflation_rate():
"""Inflation rate
0.01 by default
"""
return 0.01
[docs]
def is_active(i):
"""Indicates if model points are active.
Returns a Series indexed with model point ID indicating
whether each model point is active at :func:`date_(i)<date_>`
by ``True`` or ``False``.
A model point is active if it has been issued but not expired.
:func:`is_active` is defined as::
(0 <= duration_m(i)) & (duration_m(i) < policy_term() * 12)
.. seealso::
* :func:`duration_m`
* :func:`policy_term`
"""
return (0 <= duration_m(i)) & (duration_m(i) < policy_term() * 12)
[docs]
def is_maturing(i):
"""Indicates if model points are maturing in step ``i``.
Returns a Series indexed with model point ID indicating
whether each model point is maturing during the period
from one day after :func:`date_(i)<date_>` to :func:`date_(i+1)<date_>`
by ``True`` or ``False``.
:func:`is_maturing` is defined as::
(duration_m(i) < policy_term() * 12) & (policy_term() * 12 <= duration_m(i+1))
.. seealso::
* :func:`duration_m`
* :func:`policy_term`
"""
polt = policy_term() * 12
return (duration_m(i) < polt) & (polt <= duration_m(i+1))
[docs]
def is_paying(i):
"""Indicates if premiums are being paid
Returns a Series of booleans indexed with model point ID indicating
whether each model point is paying premiums at :func:`date_(i)<date_>`.
Defined as::
is_active(i) & (duration_m(i) < payment_term() * 12)
.. seealso::
* :func:`is_active`
* :func:`duration_m`
* :func:`policy_term`
"""
return is_active(i) & (duration_m(i) < payment_term() * 12)
[docs]
def issue_date():
"""Issue ages of the model points
The ``issue_age`` column of the DataFrame returned by
:func:`model_point`.
"""
return model_point()['issue_date']
[docs]
def lapse_rate(i):
"""Lapse rate
By default, the lapse rate assumption is defined by duration as::
max(0.1 - 0.02 * duration_y(i), 0.02)
.. seealso::
:func:`duration_y`
"""
return np.maximum(0.1 - 0.02 * duration_y(i), 0.02)
[docs]
def last_part(i, freq_id='ANV'):
"""Length of time till next anniversary in step ``i``
When ``freq_id`` takes its default value 'ANV',
:func:`last_part` returns and a Series indexed with model point ID
that indicates for each model point the length of time in months
until the next policy anniversary in step ``i``.
The decimal fraction of the length, if any, represents
the proportion of the number of days befor anniversary
in the anniversary months.
The measured part corresponds to the remaining
policy year the model point is in at :func:`date_(i)<date_>`.
If no policy anniversary is in Step ``i``,
then :func:`months_in_step(i)<months_in_step>` is returned.
If 'PREM' is given to ``freq_id``,
the lengh of time in months till the next payment date is returned.
.. seealso::
* :func:`next_part`
* :func:`next_anniversary`
* :func:`months_in_step`
"""
anv = next_anniversary(i, freq_id)
diff_m = anv.dt.year * 12 + anv.dt.month - date_(i).year * 12 - date_(i).month
stub_m = (anv.dt.day - 1) / anv.dt.days_in_month
return (diff_m - 1 + stub_m).mask(date_(i+1) < next_anniversary(i, freq_id), months_in_step(i))
[docs]
def loading_prem():
"""Loading per premium
``0.5`` by default.
Used by :func:`premium_pp` in the ``Pricing`` space.
.. seealso::
* :func:`premium_pp`
"""
return 0.5
[docs]
def max_proj_len():
"""The upper bound for the time index ``i``
Defined as::
month_to_step(max(proj_len()))
Note that :func:`proj_len` retuns lengths in months
while this cells retuns an index value converted by :func:`month_to_step`.
"""
return month_to_step(max(proj_len()))
[docs]
def model_point():
"""Target model points
Returns as a DataFrame the model points to be in the scope of calculation.
By default, this Cells returns the entire :attr:`model_point_table`
without change.
To select model points, change this formula so that this
Cells returns a DataFrame that contains only the selected model points.
Examples:
To select only the model point 1::
def model_point():
return model_point_table.loc[1:1]
To select model points whose ages at entry are 40 or greater::
def model_point():
return model_point_table[model_point_table["age_at_entry"] >= 40]
Note that the shape of the returned DataFrame must be the
same as the original DataFrame, i.e. :attr:`model_point_table`.
When selecting only one model point, make sure the
returned object is a DataFrame, not a Series, as seen in the example
above where ``model_point_table.loc[1:1]`` is specified
instead of ``model_point_table.loc[1]``.
Be careful not to accidentally change the original table.
"""
return model_point_table
[docs]
def month_to_step(m):
"""Returns step index for months
For a given number of months elapsed from :func:`date_(0)<date_>`,
returns the minimum step index such that :func:`step_to_month(i)<step_to_month>`
is equal to or greater than ``m``.
.. see also:
* :func:`month_to_step`
"""
if m == 0:
return 0
else:
i = month_to_step(m-1)
while True:
m_i = step_to_month(i)
if m <= m_i:
break
i += 1
return i
[docs]
def months_(i):
"""Number of elapsed months
Returns the number of elapsed months from :func:`date_(0)<date_>`
at :func:`date_(i)<date_>`.
.. see also:
* :func:`date_`
* :func:`months_in_step`
"""
if i == 0:
return 0
else:
return months_(i-1) + months_in_step(i-1)
[docs]
def months_in_step(i):
"""Returns the number of months in step ``i``
Returns the number of month between :func:`date_(i)<date_>`
and :func:`date_(i+1)<date_>`.
.. see also:
* :func:`date_`
"""
return date_(i+1).year * 12 + date_(i+1).month - date_(i).year * 12 - date_(i).month
[docs]
def mort_rate(i):
"""Mortality rate to be applied at time t
Returns a Series of the mortality rates to be applied at time t.
The index of the Series is ``point_id``,
copied from :func:`model_point`.
.. seealso::
* :func:`mort_table_reindexed`
* :func:`model_point`
"""
mi = pd.MultiIndex.from_arrays([age(i), np.minimum(duration_y(i), 5)])
return mort_table_reindexed().reindex(
mi, fill_value=0).set_axis(model_point().index)
[docs]
def mort_table_reindexed():
"""MultiIndexed mortality table
Returns a Series of mortlity rates reshaped from :attr:`mort_table`.
The returned Series is indexed by age and duration capped at 5.
"""
result = []
for col in mort_table.columns:
df = mort_table[[col]]
df = df.assign(Duration=int(col)).set_index('Duration', append=True)[col]
result.append(df)
return pd.concat(result)
[docs]
def net_cf(i):
"""Net cashflow
Net cashflow during step ``i`` defined as::
premiums(i) - claims(i) - expenses(i) - commissions(i)
.. seealso::
* :func:`premiums`
* :func:`claims`
* :func:`expenses`
* :func:`commissions`
"""
return premiums(i) - claims(i) - expenses(i) - commissions(i)
[docs]
def net_premium_rate():
"""Net premium per policy
.. note::
This cells is implmented in the ``Pricing`` subspace.
"""
raise NotImplementedError('net_premium_rate must be implemented in sub spaces')
[docs]
def next_anniversary(i, freq_id='ANV'):
"""Nex anniversary dates
Returns a Series of the next anniversary dates for all the model points.
When the second parameter ``freq_id`` is ommitted or takes the default
value of 'ANV',
the next anniversary date for each model point is calculated
as the first policy anniversary date that comes after :func:`date_(i)<date_>`.
When ``freq_id`` is 'PREM', returns
the next payment date after :func:`date_(i)<date_>`,
based on the premium payment cycle calculated
from :func:`payment_freq` and :func:`issue_date`.
.. seealso::
* :func:`date_`
* :func:`issue_date`
* :func:`payment_freq`
"""
if freq_id == 'ANV':
freq = 1
elif freq_id == 'PREM':
freq = payment_freq()
else:
raise ValueError('invalid freq_id')
iss = issue_date()
iss_y, iss_m = iss.dt.year, iss.dt.month
val_y, val_m = date_(i).year, date_(i).month
diff_m = (val_y - iss_y) * 12 + val_m - iss_m
offset_m = (12 // freq) - (diff_m % (12 // freq))
m = (date_(i).to_period('M') + offset_m).astype('Period[M]')
d = np.minimum(issue_date().dt.day, m.dt.days_in_month)
res = m.dt.to_timestamp().dt.to_period('D') - 1 + d
return res.dt.to_timestamp()
[docs]
def next_part(i):
"""Lentgh of time after next anniversary
Returns the length of time in month after the next anniversary
during the step ``i``. If no anniversary is in step ``i``, then
:func:`months_in_step(i)<months_in_step>` is returned.
Defined as::
months_in_step(i) - last_part(i)
.. seealso::
* :func:`last_part`
* :func:`next_anniversary`
* :func:`months_in_step`
"""
return months_in_step(i) - last_part(i)
[docs]
def offset(i):
"""Time interval in step ``i``
This cells if for controlling the number of months in step ``i``.
Step ``i`` is from one day after :func:`date_(i)<date_>` to
:func:`date_(i+1)<date_>`.
This cells should return an object of a sub calass of pandas `DateOffset`_,
such as `MonthEnd`_ and `YearEnd`_ object.
The returned object must always represent end of month,
and must not be longer than 1 year.
To set the length of step ``i`` to ``N`` monthhs,
offset(i) should return ``pd.offsets.MonthEnd(N)``.
To set the length to 1 year,
offset(i) should return ``pd.offsets.Year(1)``.
By default, the formula is set so that the model
projects monthly for the first 60 months (5 years) then
annually after that.
:func:`offset` is defined as::
if i < 60:
return pd.offsets.MonthEnd(1)
else:
return pd.offsets.YearEnd(1)
.. seealso::
* :func:`date_`
.. _DateOffset:
https://pandas.pydata.org/docs/reference/offset_frequency.html
.. _MonthEnd:
https://pandas.pydata.org/docs/reference/api/pandas.tseries.offsets.MonthEnd.html
.. _YearEnd:
https://pandas.pydata.org/docs/reference/api/pandas.tseries.offsets.YearEnd.html
"""
if i < 60:
return pd.offsets.MonthEnd(1)
else:
return pd.offsets.YearEnd(1)
[docs]
def pay_count(i, j=None):
"""Number of premium payments in step ``i``
The number of premium payments in step ``i``
(from 1 day after :func:`date_(i)<date_>` to :func:`date_(i+1)<date_>`).
If 'LAST' is passed to ``j``, only the payments
in step ``i`` during the last policy period
(the year that the policy is in at :func:`date_(i)<date_>`)
is counted.
If 'NEXT' is passed to ``j``, only the payments
in step ``i`` after the next policy anniversary is counted.
Args:
i: Step index
j(optional): :obj:`None` (default), 'LAST' or 'NEXT'
.. seealso::
* :func:`date_`
* :func:`is_paying`
* :func:`payment_freq`
* :func:`duration_m`
* :func:`duration_y`
"""
if j is None:
return pay_count(i, 'LAST') + pay_count(i, 'NEXT')
else:
paid_next = (duration_m(i+1) % 12) // (12 // payment_freq()) + 1
if j == 'LAST':
paid_last = (duration_m(i) % 12) // (12 // payment_freq()) + 1
paid_next = paid_next.where(duration_y(i) == duration_y(i+1), payment_freq())
return is_paying(i) * (paid_next - paid_last)
elif j == 'NEXT':
return is_paying(i+1) * paid_next.where(duration_y(i) != duration_y(i+1), 0)
[docs]
def payment_freq():
"""Payment frequency
The ``payment_freq`` column of the DataFrame returned by
:func:`model_point`, which indicates the number of payments
in a year of each model point.
"""
return model_point()['payment_freq']
[docs]
def payment_lag(i, j):
"""Average timing of premium payments.
If 'LAST' is passed to ``j``, returns the average time in months from
:func:`date_(i)<date_>` to a payment before the next policy anniversary.
Defined as::
last_part(i, freq_id='PREM') + max(pay_count(i, 'LAST') - 1, 0) * pay_interval / 2
where ``pay_interval`` is defined as ``12 // payment_freq()``.
If 'NEXT' is passed to ``j``, returns the average time in months from
the next anniversary date to a premium payment after the policy anniversary.
Defined as::
max(pay_count(i, 'NEXT') - 1, 0) * pay_interval / 2
.. seealso::
* :func:`pay_count`
* :func:`last_part`
* :func:`payment_freq`
"""
max = np.maximum
pay_interval = 12 // payment_freq()
if j == 'LAST':
return last_part(i, freq_id='PREM') + max(pay_count(i, 'LAST') - 1, 0) * pay_interval / 2
elif j == 'NEXT':
return max(pay_count(i, 'NEXT') - 1, 0) * pay_interval / 2
else:
raise ValueError('invalid j')
[docs]
def payment_term():
"""Premium payment period in years
The ``payment_term`` column of the DataFrame returned by
:func:`model_point`.
"""
return model_point()['payment_term']
[docs]
def policy_term():
"""The policy term of the model points.
The ``policy_term`` column of the DataFrame returned by
:func:`model_point`.
"""
return model_point()["policy_term"]
[docs]
def pols_death(i, j=None):
"""Number of death
The number of death during step ``i``.
"""
if j is None:
return pols_death(i, 'LAST') + pols_death(i, 'NEXT')
elif j == 'LAST':
mort = 1 - (1 - mort_rate(i))**(last_part(i) / 12)
return pols_if_at(i, "BEG_STEP") * mort
elif j == 'NEXT':
mort = 1 - (1 - mort_rate(i+1))**(next_part(i) / 12)
return pols_if_at(i, "AFT_NB") * mort
else:
raise ValueError('invalid j')
[docs]
def pols_if(i):
"""Number of policies in-force
:func:`pols_if(t)<pols_if>` is an alias
for :func:`pols_if_at(t, 'BEG_STEP')<pols_if_at>`.
.. seealso::
* :func:`pols_if_at`
"""
return pols_if_at(i, 'BEG_STEP')
[docs]
def pols_if_at(i, timing):
"""Number of policies in-force
:func:`pols_if_at(t, timing)<pols_if_at>` calculates and returns
the number of policies in-force at various timings in step ``i``.
The second parameter ``timing`` takes a string value to
indicate the timing,
which is either
``'BEG_STEP'``, ``'DECR_LAST'``, ``'AFT_MAT'``,
``'AFT_NB'`` or ``'DECR_NEXT'``.
.. rubric:: BEG_STEP
The number of policies in-force at the beginning of step ``i``.
At time 0, the value is read from :func:`pols_if_init`.
For time > 0, it is equal to ``pols_if_at(i-1, 'DECR_NEXT')``.
.. rubric:: DECR_LAST
The number of policies in-force reflecting decrement by lapse and death
before the next policy anniversary in step ``i``.
Defined as::
pols_if_at(i, 'BEG_STEP') - pols_lapse(i, 'LAST') - pols_death(i, 'LAST')
.. rubric:: AFT_MAT
The number of policies after reflecting maturity decrement.
Defined as::
pols_if_at(i, 'DECR_LAST') - pols_maturity(i)
.. rubric:: AFT_NB
The number of policies after reflecting increase by new business.
Defined as::
pols_if_at(i, 'AFT_MAT') + pols_new_biz(i)
.. rubric:: DECR_NEXT
The number of policies after reflecting decrement by lapse and death
after the next policy anniversary in step ``i``.
Defined as::
pols_if_at(i, 'AFT_NB') - pols_lapse(i, 'NEXT') - pols_death(i, 'NEXT')
.. seealso::
* :func:`pols_if_init`
* :func:`pols_lapse`
* :func:`pols_death`
* :func:`pols_maturity`
* :func:`pols_new_biz`
"""
if timing == 'BEG_STEP':
if i == 0:
return pols_if_init()
else:
return pols_if_at(i-1, 'DECR_NEXT')
elif timing == 'DECR_LAST':
return pols_if_at(i, 'BEG_STEP') - pols_lapse(i, 'LAST') - pols_death(i, 'LAST')
elif timing == 'AFT_MAT':
return pols_if_at(i, 'DECR_LAST') - pols_maturity(i)
elif timing == 'AFT_NB':
return pols_if_at(i, 'AFT_MAT') + pols_new_biz(i)
elif timing == 'DECR_NEXT':
return pols_if_at(i, 'AFT_NB') - pols_lapse(i, 'NEXT') - pols_death(i, 'NEXT')
else:
raise ValueError("invalid timing")
[docs]
def pols_if_avg(i):
"""Average number of policies in-force in step ``i``
For policies existing on :func:`date_(i+1)<date_>`,
defined as the mean of :func:`pols_if_at(i, 'BEG_STEP')<pols_if_at>` and
:func:`pols_if_at(i+1, 'BEG_STEP')<pols_if_at>`.
For policies maturing during step ``i``,
defined as the mean of :func:`pols_if_at(i, 'BEG_STEP')<pols_if_at>` and
:func:`pols_if_at(i, 'DECR_LAST')<pols_if_at>`.
"""
existing = (pols_if_at(i, 'BEG_STEP') + pols_if_at(i+1, 'BEG_STEP')) / 2
maturing = (pols_if_at(i, 'BEG_STEP') + pols_if_at(i, 'DECR_LAST')) / 2
return existing.mask(is_maturing(i), maturing)
[docs]
def pols_if_init():
"""Initial number of policies in-force
Number of in-force policies at time 0 referenced from
:func:`pols_if_at(0, 'BEF_MAT')<pols_if_at>`.
"""
return model_point()["policy_count"].where(duration_m(0) >= 0, other=0)
[docs]
def pols_if_pay(i, j):
"""Number of policies in-force for premium payment
Returns a Series each of whose elements represents
the number of policies in-force adjusted for :func:`payment_lag`.
If 'LAST' is passed to ``j``, returns the number of in-force
policies at :func:`payment_lag(i, 'LAST')<payment_lag>` months
past :func:`date_(i)<date_>`.
If 'NEXT' is passed to ``j``, returns the number of in-force
policies at :func:`payment_lag(i, 'NEXT')<payment_lag>` months
past :func:`next_anniversary(i)<next_anniversary>`.
.. seealso::
* :func:`payment_lag`
* :func:`pols_if_at`
* :func:`mort_rate`
* :func:`lapse_rate`
"""
dt = payment_lag(i, j) / 12
if j == 'LAST':
disc = (1 - mort_rate(i)) * (1 - lapse_rate(i))
return pols_if_at(i, "BEG_STEP") * disc**dt
elif j == 'NEXT':
disc = (1 - mort_rate(i+1)) * (1 - lapse_rate(i+1))
return pols_if_at(i, "AFT_NB") * disc**dt
else:
raise ValueError('invalid j')
[docs]
def pols_lapse(i, j=None):
"""Number of lapse in step ``i``
By default, returns a Series each of whose elements represents
the number of policies lapsed during step ``i`` for each model point.
If 'LAST' is passed to ``j``, only lapse before the next anniversary
in step ``i`` is counted. If 'NEXT' is passed to ``j``,
only lapse after the next anniversary is counted.
Args:
i: Step index
j(optional): :obj:`None`, 'LAST' or 'NEXT'
.. seealso::
* :func:`pols_if_at`
* :func:`pols_death`
* :func:`lapse_rate`
* :func:`last_part`
"""
if j is None:
return pols_lapse(i, 'LAST') + pols_lapse(i, 'NEXT')
elif j == 'LAST':
lapse = 1 - (1 - lapse_rate(i))**(last_part(i) / 12)
return (pols_if_at(i, "BEG_STEP") - pols_death(i, 'LAST')) * lapse
elif j == 'NEXT':
lapse = 1 - (1 - lapse_rate(i+1))**(next_part(i) / 12)
return (pols_if_at(i, "AFT_NB") - pols_death(i, 'NEXT')) * lapse
else:
raise ValueError('invalid j')
[docs]
def pols_maturity(i):
"""Number of maturing policies
Returns a Series each of whose elements represent
the number of policies maturing in step ``i`` for a model point.
Maturity occurs when
:func:`duration_m` equals 12 times :func:`policy_term`.
The amount is equal to :func:`pols_if_at(t, "DECR_LAST")<pols_if_at>`.
Otherwise ``0``.
.. seealso::
* :func:`pols_if_at`
* :func:`duration_m`
* :func:`policy_term`
"""
return is_maturing(i) * pols_if_at(i, "DECR_LAST")
[docs]
def pols_new_biz(i):
"""Number of new business policies
Returns a Series each of whose elements represents
the number of new business policies issued in step ``i``.
Policies are issued when the following condition is met::
(duration_m(i) < 0) & (duration_m(i+1) >= 0)
The numbers of new business policies
are read from the ``policy_count`` column in
:func:`model_point`.
.. seealso::
* :func:`model_point`
* :func:`duration_m`
"""
return model_point()['policy_count'].where(
(duration_m(i) < 0) & (duration_m(i+1) >= 0), other=0)
[docs]
def premium_pp():
"""Premium per policy
This cells is defined in sub spaces.
"""
raise NotImplementedError('premium_pp must be implemented in sub spaces')
[docs]
def premiums(i, j=None):
"""Premium income
By default, returns a Series each of whose elements represent
premium income of a model point in step ``i``.
If 'LAST' is passed to ``j``, only premiums before the next anniversary
in step ``i`` are aggregated.
If 'NEXT' is passed to ``j``, only premiums after the next anniversary
are aggregated.
.. seealso::
* :func:`premium_pp`
* :func:`pols_if_pay`
* :func:`pay_count`
"""
if j is None:
return premiums(i, 'LAST') + premiums(i, 'NEXT')
elif j == 'LAST' or j == 'NEXT':
return premium_pp() * pay_count(i, j) * pols_if_pay(i, j)
else:
raise ValueError('invalid j')
[docs]
def proj_len():
"""Projection length in months
:func:`proj_len` returns how many months the projection
for each model point should be carried out
for all the model point. Defined as::
np.maximum(12 * policy_term() - duration_m(0) + 1, 0)
Since this model carries out projections for all the model points
simultaneously, the projections are actually carried out
from 0 to :attr:`max_proj_len` for all the model points.
.. seealso::
* :func:`policy_term`
* :func:`duration_m`
* :attr:`max_proj_len`
"""
return np.maximum(12 * policy_term() - duration_m(0) + 1, 0)
[docs]
def pv_claims():
"""Present value of claims
Returns a Numpy array of the presenet values of claims.
.. seealso::
* :func:`claims`
* :func:`disc_factors`
"""
cl = np.array(list(claims(t) for t in range(max_proj_len()))).transpose()
return cl @ disc_factors()[:max_proj_len()]
[docs]
def pv_commissions():
"""Present value of commissions
Returns a Numpy array of the presenet values of commissions.
.. seealso::
* :func:`commissions`
* :func:`disc_factors`
"""
result = np.array(list(commissions(t) for t in range(max_proj_len()))).transpose()
return result @ disc_factors()[:max_proj_len()]
[docs]
def pv_expenses():
"""Present value of expenses
Returns a Numpy array of the presenet values of expenses.
.. seealso::
* :func:`expenses`
* :func:`disc_factors`
"""
result = np.array(list(expenses(t) for t in range(max_proj_len()))).transpose()
return result @ disc_factors()[:max_proj_len()]
[docs]
def pv_net_cf():
"""Present value of net cashflows.
Defined as::
pv_premiums() - pv_claims() - pv_expenses() - pv_commissions()
.. seealso::
* :func:`pv_premiums`
* :func:`pv_claims`
* :func:`pv_expenses`
* :func:`pv_commissions`
"""
return pv_premiums() - pv_claims() - pv_expenses() - pv_commissions()
[docs]
def pv_pols_if():
"""Present value of policies in-force
.. note::
This cells is not used by default.
"""
result = np.array(list(pols_if(i) for i in range(max_proj_len()))).transpose()
return result @ disc_factors()[:max_proj_len()]
[docs]
def pv_pols_if_pay():
"""Present value of polices in-force for premium payments
Calculated for 'LAST' and 'NEXT' separately, as
:func:`pay_count` times :func:`pols_if_pay` discounted
by :func:`disc_factors_prem`.
.. seealso::
* :func:`pay_count`
* :func:`pols_if_pay`
* :func:`disc_factors_prem`
"""
def pols(j):
data = []
for i in range(max_proj_len()):
data.append(pay_count(i, j) * pols_if_pay(i, j))
return np.array(data).transpose()
dprems = pols('LAST') * disc_factors_prem('LAST') + pols('NEXT') * disc_factors_prem('NEXT')
return dprems.sum(axis=1)
[docs]
def pv_premiums():
"""Present value of premiums
Returns a Numpy array of the presenet values of premiums.
.. seealso::
* :func:`premiums`
* :func:`disc_factors_prem`
"""
last_y = np.array(list(premiums(i, 'LAST') for i in range(max_proj_len()))).transpose()
next_y = np.array(list(premiums(i, 'NEXT') for i in range(max_proj_len()))).transpose()
total = last_y * disc_factors_prem('LAST') + next_y * disc_factors_prem('NEXT')
return total.sum(axis=1)
[docs]
def result_cells(name, point_id=None, j=None):
"""Output values of a given cells
Returns as a DataFrame the values of a cells specified by ``name`` for
all ``i``. By default, returns the values for all model points.
If ``point_id`` is specified, returns the values only of the model point.
If 'LAST' or 'NEXT' is passed to ``j``, only the values of the 'LAST' or 'NEXT'
part of ``name`` are aggregated.
"""
args = () if j is None else (j,)
res = pd.DataFrame({i: getattr(_space, name)(i, *args) for i in range(max_proj_len())})
return res if point_id is None else res.loc[point_id]
[docs]
def result_cf():
"""Result table of cashflows
.. seealso::
* :func:`premiums`
* :func:`claims`
* :func:`expenses`
* :func:`commissions`
* :func:`net_cf`
"""
t_len = range(max_proj_len())
data = {
"Premiums": [sum(premiums(t)) for t in t_len],
"Claims": [sum(claims(t)) for t in t_len],
"Expenses": [sum(expenses(t)) for t in t_len],
"Commissions": [sum(commissions(t)) for t in t_len],
"Net Cashflow": [sum(net_cf(t)) for t in t_len]
}
return pd.DataFrame(data, index=t_len)
[docs]
def result_pols():
"""Result table of policy decrement
.. seealso::
* :func:`pols_if`
* :func:`pols_maturity`
* :func:`pols_new_biz`
* :func:`pols_death`
* :func:`pols_lapse`
"""
t_len = range(max_proj_len())
data = {
"pols_if": [sum(pols_if(t)) for t in t_len],
"pols_maturity": [sum(pols_maturity(t)) for t in t_len],
"pols_new_biz": [sum(pols_new_biz(t)) for t in t_len],
"pols_death": [sum(pols_death(t)) for t in t_len],
"pols_lapse": [sum(pols_lapse(t)) for t in t_len]
}
return pd.DataFrame(data, index=t_len)
[docs]
def result_pv():
"""Result table of present value of cashflows
.. seealso::
* :func:`pv_premiums`
* :func:`pv_claims`
* :func:`pv_expenses`
* :func:`pv_commissions`
* :func:`pv_net_cf`
"""
data = {
"PV Premiums": pv_premiums(),
"PV Claims": pv_claims(),
"PV Expenses": pv_expenses(),
"PV Commissions": pv_commissions(),
"PV Net Cashflow": pv_net_cf()
}
return pd.DataFrame(data, index=model_point().index)
[docs]
def sex():
"""The sex of the model points
.. note::
This cells is not used by default.
The ``sex`` column of the DataFrame returned by
:func:`model_point`.
"""
return model_point()["sex"]
[docs]
def step_to_month(i):
"""Returns the number of months for step ``i``
Return the number of months from :func:`date_(0)<date_>`
to :func:`date_(i)<date_>`.
.. seealso::
* :func:`months_in_step`
"""
if i == 0:
return 0
else:
return step_to_month(i-1) + months_in_step(i-1)
[docs]
def sum_assured():
"""The sum assured of the model points
The ``sum_assured`` column of the DataFrame returned by
:func:`model_point`.
"""
return model_point()["sum_assured"]
# ---------------------------------------------------------------------------
# References
disc_rate_ann = ("DataSpec", 1596821286624, 1596812327952)
mort_table = ("DataSpec", 1596821308320, 1596814794320)
np = ("Module", "numpy")
pd = ("Module", "pandas")
model_point_table = ("DataSpec", 1596822034176, 1596820957792)
date_init = "2021-12-31"