Source code for basiclife.BasicTermASL_ME.Base

"""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"