Concepts

The core of the tool is a projection kernel which implements a generic (Markov) multi state evolution process. Projections are based on one of the built-in (or a self-provided) state models, a set of compatible state transition providers, a product definition and a portfolio of insureds. Model runs can be specified in code or using yaml configuration files.

State Models

When invoking the projection engine of PyProtolinc the state model to be used is a key parameter which is fixed throughout a run. This implies that portfolios of insurance policies which would required distinct state models must be broken up accordingly and run separately from one another.

Concept

In principle a state model is given by a (finite) number of ordered states, which represent different status a policyholder can be in at a given point in time, as well as the possible transitions between them. We can imagine this as a graph as in the following example:

../_images/state_annuity.png

There are two states and the (one-sided) arrow indicates that a transition from state Annuitant to state Dead is possible. This could represent a state model that can be used for a portfolio of annuities in the payment phase (which could be the results of a single initial premium payment made by the policyholder to the insurer). In this scenario the insurer would make annual or monthly payments to the insured person as long as the person is alive.

Let’s have a look at another example which could be used for a mortality term insurance. In this case the policy has a certain duration (e.g. 10 years) and the insurer is obliged to pay the insured amount upon death of the insured.

../_images/state_term.png

At the beginning of the lifetime of the policy the insured is in the state Active (and would pay recurring insurance premiums). The arrows represent the following state changes:

  • to Dead if the insured dies before the end of the term, in this case the insurer would pay the insured amount.

  • to Lapsed if the policy holder surrenders. This means no death benefit will be paid and no premiums are due anymore but there might be surrenders benefits (depending on product terms and general regulation)

  • to Matured if the term ends before the insured has died.

It is important to note that the statements about payments made from either side are only given for illustration, they are not linked to the state model but rather a part of the product definition discussed later (which itself is, of course, linked to a state model again).

The final example discussed here is the the following:

../_images/state_di_annuity.png

There are seven states and various transitions between them. The insurance product one should think of is an income protection cover with a given term (or end age) which pays an annuity as long as the insured is disabled (the exact meaning of this needs to be defined in the policy terms) and has not reached the end of the term. While actives can become disabled it is also possible that disbaled become actives again, something often referred to as recovery. In the graph above there are therefore two arrows between the states Active and Disabled indicating the possible transitions in both ways. Note furthermore that there are two Dead states in the example. In most cases it is probably a modelling decision if these states should be separated or not, the same applies to the Matured states.

Remark: Sometimes modelling purposes may imply the need to break a logical state (e.g., one that is naturally motivated by the product) up into more granular model states. An example could be that the modeller want to use different recovery probabilities depending on how long the policy holder is in the disabled state and for this purpose the disabled state could be broken up into the states as follows:

  • Disabled (1st year)

  • Disabled (2nd to 5th year)

  • Disabled (6th year and later)

These additional states would then be complemented by a number of additional transitions.

Implementation

In PyProtolinc the states in a states model are essentially represented by a Python IntEnum which is implictly inherited through the abstract base class AbstractStateModel. As an example recall the first example above and then let’s have a look at the following class from the module pyprotolinc.models.state_models which looks as follows:

from pyprotolinc.models.state_models import AbstractStateModel

@unique
class AnnuityRunoffStates(AbstractStateModel):
    """ A state model consisting of two states:
        - DIS1 (=0) representing the annuity phase
        - DEATH (=1)
    """
    DIS1 = 0    # the "annuitant state"
    DEATH = 1   # the death state

    @classmethod
    def to_std_outputs(cls):
        return {
            ProbabilityVolumeResults.VOL_DIS1: cls.DIS1,
            ProbabilityVolumeResults.VOL_DEATH: cls.DEATH,
            ProbabilityVolumeResults.MV_DIS1_DEATH: (cls.DIS1, cls.DEATH),
        }

Note that there are two member DIS1 and DEATH representing the two states (albeit with different names). Besides that there is a classmethod which provides a mapping of the standard output model to this model (but this discussion will need to take place elsewhere).

While the IntEnum above declares the states the transitions between them will be modelled by a corresponding matrix of transition providers, a topic which will be discussed further in the Section about assumptions.

Portfolios

The key input needed to run pyprotolinc is the portfolio data, i.e. a table containing seriatim record information. The most convenient way is to import a portfolio from an Excel file using a pyprotolinc.portfolio.PortfolioLoader object:

import pyprotolinc.portfolio as ptf

portfolio_path = "../portfolio_small3.xlsx"
ptf_loader = ptf.PortfolioLoader(portfolio_path)

Portfolio files have to look like this (where the order of the columns does not matter):

../_images/portfolio_file.png

The columns have the obvious meanings:

  • DATE_PORTFOLIO the date at which the snapshot is valid, must be equal for all records.

  • ID a unique indentifier

  • DATE_OF_BIRTH

  • DATE_START_OF_COVER

  • SUM_INSURED sum insured (or yearly annuity amount)

  • CURRENT_STATUS corresponds with the state in the state model the record is in at DATE_PORTFOLIO

  • SEX gender (m/f)

  • PRODUCT is a string that references a product

  • PRODUCT_PARAMETERS additional product parameters

  • SMOKERSTATUS

  • RESERVING_RATE the interest rate that should be used for the reserve calculations

  • DATE_OF_DISABLEMENT

When importing using the loader certain validations will be performed on the fly and to be able to validate the the status a corresponding state model class must be passed in to load the portfolio:

portfolio = ptf_loader.load(selected_state_model)

Moreover, to save some time a cacheing of the portfolios is implemented when instatiating with a second path argument which has to be a folder:

ptf.PortfolioLoader(portfolio_path, cache_path)

In this case pickled versions of the portfolios read will be stored under the path and loading portfolios will be attempted under this path first.

Assumptions

Transition Provider Matrices

(todo)

Products

todo

Configuration and Runs

To start a run of pyprotolinc we have to combine various relevant information for the projection kernel in a configuration object.

Configuration

The main configuration object is pyprotolinc.RunConfig and its __init__ method has the following signature:

def __init__(self,
             state_model_name: str,
             working_directory: Path = ".",
             model_name: str = "GenericMultiState",
             years_to_simulate: int = 120,
             steps_per_month: int = 1,
             portfolio_path: Optional[str] = None,
             assumptions_path: Optional[str] = None,
             outfile: str = "ncf_out_generic.csv",
             portfolio_cache: Optional[str] = None,
             profile_out_dir: Optional[str] = None,
             portfolio_chunk_size: int = 20000,
             use_multicore: bool = False,
             kernel_engine: str = "PY",
             max_age: int = 120
             ) -> None:

Most of the parameters have sensible defaults but one has to provide a state_model_name. Besides that the following applies:

  • if portfolio_path is not provided a portfolio must be injected into the runner directly

  • if assumptions_path is not provided an AssumptionSetWrapper must be injected into the runner directly

A convenient way to create an RunConfig object is by reading it from a yaml file, e.g. like this:

run_cfg2 = pyprotolinc.get_config_from_file("../config.yml")

The structure of the yaml file closely mimics the structur of the object with some additional grouping. This is a valid example:

io:
    portfolio_cache: "portfolio/portfolio_cache"
    profile_out_dir: "."

# kernel engine is "C" or "PY"
kernel:
    engine: "C"  # "PY" / "C"
    max_age: 119

model:
    # Type of Model to be run, currently only "GenericMultiState" is supported
    type: "GenericMultiState"

# generic settings
years_to_simulate: 119
steps_per_month: 1
use_multicore: true  # true / false

run_type_specs:

    GenericMultiState:

        state_model: "DeferredAnnuityStates"
        assumptions_spec:  "di_assumptions.yml"
        outfile: "results/ncf_out_generic.csv"
        portfolio_path: "portfolio_small3.xlsx"
        portfolio_chunk_size: 8192

In this case the configuration object can be created as follows:

run_cfg2 = pyprotolinc.get_config_from_file("../config.yml")

Runs

Calculation runs can be triggered by two methods in the module pyprotolinc.main: either project_cashflows or project_cashflows_cli.

If using the latter all data must be delivered via the config file path or object. The former allows to inject some objects programmatically.

Here is an example: Assume that a portfolio file has been read into a pandas.DataFrame df and an AssumptionSetWrapper asw has been constructed. Then the following will trigger a run using these objects:

project_cashflows(run_cfg, df, asw, export_to_file=False)

Alternatively, if a configuration file exists then:

project_cashflows_cli(path_to_config)

will trigger a run using the specification in the configuration file. Alternatively a (complete) RunConfig object can be provided to either of the methods.