ExperienceAnalysis.jl

Experience Analysis Utility Functions
Author JuliaActuary
Popularity
2 Stars
Updated Last
1 Year Ago
Started In
October 2020

ExperienceAnalysis

Stable Dev Coverage lifecycle

Calculate exposures.

Quickstart

using ExperienceAnalysis
using Dates

issue = Date(2016, 7, 4)
termination = Date(2020, 1, 17)
basis = ExperienceAnalysis.Anniversary(Year(1))
exposure(basis, issue, termination)

This will return an array of tuples with a from and to date:

4-element Array{NamedTuple{(:from, :to),Tuple{Date,Date}},1}:
 (from = Date("2016-07-04"), to = Date("2017-07-04"))
 (from = Date("2017-07-04"), to = Date("2018-07-04"))
 (from = Date("2018-07-04"), to = Date("2019-07-04"))
 (from = Date("2019-07-04"), to = Date("2020-01-17"))

Available Exposure Basis

  • ExperienceAnalysis.Anniversary(period) will give exposures periods based on the first date
  • ExperienceAnalysis.Calendar(period) will follow calendar periods (e.g. month or year)
  • ExperienceAnalysis.AnniversaryCalendar(period,period) will split into the smaller of the calendar or policy anniversary period.

Where period is a Period Type from the Dates standard library.

Calculate exposures with exposures(basis,from,to,continue_exposure).

  • continue_exposures indicates whether the exposure should be extended through the full exposure period rather than terminate at the to date.

Full Example

We'll start with this as our data:

julia> df

3×4 DataFrame
│ Row │ id     │ issue      │ termination │ status  │
│     │ String │ Date       │ Date?       │ String  │
├─────┼────────┼────────────┼─────────────┼─────────┤
│ 112016-07-042020-01-17  │ Claim   │
│ 222016-01-012018-05-04  │ Lapse   │
│ 332016-01-01missing     │ Inforce │

Define the study end:

study_end = Date(2020,12,31)

Next, we do two things by iterating over and creating a new array of dates:

  1. Handle the missing case by letting the to reflect the study_end
  2. Cap the ending date at the study_end. This doesn't come into play in this example, but it's included for demonstration purposes.
to = [ismissing(d) ? study_end : min(study_end,d) for d in df.termination]

Calculate the exposure by broadcasting the exposure function over the three arrays we are passing to it:

df.exposure = exposure.(
    ExperienceAnalysis.Anniversary(Year(1)),   # The basis for our exposures
    df.issue,                             # The `from` date
    to                                    # the `to` date array we created above
    )

In our dataframe, we actually have a column that contains an array of tuples now, so to expand it so that each exposure period gets a row, we flatten the dataframe:

df = flatten(df,:exposure)

So now we have our exposures:

│ id     │ issue      │ termination │ status  │ exposure                                             │
│ String │ Date       │ Date?       │ String  │ NamedTuple{(:from, :to),Tuple{Date,Date}}            │
┼────────┼────────────┼─────────────┼─────────┼──────────────────────────────────────────────────────┼
│ 12016-07-042020-01-17  │ Claim   │ (from = Date("2016-07-04"), to = Date("2017-07-04")) │
│ 12016-07-042020-01-17  │ Claim   │ (from = Date("2017-07-04"), to = Date("2018-07-04")) │
│ 12016-07-042020-01-17  │ Claim   │ (from = Date("2018-07-04"), to = Date("2019-07-04")) │
│ 12016-07-042020-01-17  │ Claim   │ (from = Date("2019-07-04"), to = Date("2020-01-17")) │
│ 22016-01-012018-05-04  │ Lapse   │ (from = Date("2016-01-01"), to = Date("2017-01-01")) │
│ 22016-01-012018-05-04  │ Lapse   │ (from = Date("2017-01-01"), to = Date("2018-01-01")) │
│ 22016-01-012018-05-04  │ Lapse   │ (from = Date("2018-01-01"), to = Date("2018-05-04")) │
│ 32016-01-01missing     │ Inforce │ (from = Date("2016-01-01"), to = Date("2017-01-01")) │
│ 32016-01-01missing     │ Inforce │ (from = Date("2017-01-01"), to = Date("2018-01-01")) │
│ 32016-01-01missing     │ Inforce │ (from = Date("2018-01-01"), to = Date("2019-01-01")) │
│ 32016-01-01missing     │ Inforce │ (from = Date("2019-01-01"), to = Date("2020-01-01")) │
│ 32016-01-01missing     │ Inforce │ (from = Date("2020-01-01"), to = Date("2020-12-31")) │

Exposure Fraction

This can be extended to calculate the decimal fraction of the year under different day count conventions, such as assuming 30/360 or Actual/365, etc. using the DayCounts.jl package.

using DayCounts

df.exposure_fraction = map(e -> yearfrac(e.from,e.to,DayCounts.Actual360()),df.exposure)

So now we have:

│ exposure                                             │ exposure_fraction │
│ NamedTuple{(:from, :to),Tuple{Date,Date}}            │ Float64           │
┼──────────────────────────────────────────────────────┼───────────────────┤
│ (from = Date("2016-07-04"), to = Date("2017-07-04")) │ 1.01389           │
│ (from = Date("2017-07-04"), to = Date("2018-07-04")) │ 1.01389           │
│ (from = Date("2018-07-04"), to = Date("2019-07-04")) │ 1.01389           │
│ (from = Date("2019-07-04"), to = Date("2020-07-04")) │ 0.54722           │
│ (from = Date("2016-01-01"), to = Date("2017-01-01")) │ 1.01667           │
│ (from = Date("2017-01-01"), to = Date("2018-01-01")) │ 1.01389           │
│ (from = Date("2018-01-01"), to = Date("2018-05-04")) │ 0.34167           │
│ (from = Date("2016-01-01"), to = Date("2017-01-01")) │ 1.01667           │
│ (from = Date("2017-01-01"), to = Date("2018-01-01")) │ 1.01389           │
│ (from = Date("2018-01-01"), to = Date("2019-01-01")) │ 1.01389           │
│ (from = Date("2019-01-01"), to = Date("2020-01-01")) │ 1.01389           │
│ (from = Date("2020-01-01"), to = Date("2020-12-31")) │ 1.01389

Continued Exposure

To get the proper exposure for the termination type under consideration, exposure takes an optional fourth argument which will continue the exposure until the end of what would be the period notwithstanding the termination.

Extending the above analysis, we want a full exposure period for any "Claim" in this case:

continue_exposure = df.status .== "Claim"

df.exposure = exposure.(
    ExperienceAnalysis.Anniversary(Year(1)),   # The basis for our exposures
    df.issue,                             # The `from` date
    to,                                   # the `to` date array we created above
    continue_exposure                     # full exposure or not (true/false)
    )

And then the exposures look like the following. Note the difference in the fourth row:

│ exposure                                             │ exposure_fraction │
│ NamedTuple{(:from, :to),Tuple{Date,Date}}            │ Float64           │
┼──────────────────────────────────────────────────────┼───────────────────┤
│ (from = Date("2016-07-04"), to = Date("2017-07-04")) │ 1.01389           │
│ (from = Date("2017-07-04"), to = Date("2018-07-04")) │ 1.01389           │
│ (from = Date("2018-07-04"), to = Date("2019-07-04")) │ 1.01389           │
│ (from = Date("2019-07-04"), to = Date("2020-07-04")) │ 1.01667           │
│ (from = Date("2016-01-01"), to = Date("2017-01-01")) │ 1.01667           │
│ (from = Date("2017-01-01"), to = Date("2018-01-01")) │ 1.01389           │
│ (from = Date("2018-01-01"), to = Date("2018-05-04")) │ 0.341667          │
│ (from = Date("2016-01-01"), to = Date("2017-01-01")) │ 1.01667           │
│ (from = Date("2017-01-01"), to = Date("2018-01-01")) │ 1.01389           │
│ (from = Date("2018-01-01"), to = Date("2019-01-01")) │ 1.01389           │
│ (from = Date("2019-01-01"), to = Date("2020-01-01")) │ 1.01389           │
│ (from = Date("2020-01-01"), to = Date("2020-12-31")) │ 1.01389

Study End/Start dates

The examples above already incorporated the study end date. However, the study start date must be truncated as a post-processing step. This is because the to argument to the exposure(...) function defines the anchor point for the policy anniversary iteration. If the to was the study start date, then the anniversaries would follow that calendar date.

To truncate the exposures, you would need to drop/update exposures. Continuing the example above:

study_start = Date(2017,1,1)

# drop rows where the whole expsoure is before the study_start
df_truncated = filter(row -> row.exposure.to >= study_start,df)

# update the `from` where remaining exposures start before the study_start
df_truncated.exposure = map(e -> (from = max(study_start,e.from),to = e.to), df_truncated.exposure)

And then df_truncated contains:

│ exposure                                             │
│ NamedTuple{(:from, :to),Tuple{Date,Date}}            │
┼──────────────────────────────────────────────────────┼
│ (from = Date("2017-01-01"), to = Date("2017-07-04")) │
│ (from = Date("2017-07-04"), to = Date("2018-07-04")) │
│ (from = Date("2018-07-04"), to = Date("2019-07-04")) │
│ (from = Date("2019-07-04"), to = Date("2020-07-04")) │
│ (from = Date("2017-01-01"), to = Date("2017-01-01")) │
│ (from = Date("2017-01-01"), to = Date("2018-01-01")) │
│ (from = Date("2018-01-01"), to = Date("2018-05-04")) │
│ (from = Date("2017-01-01"), to = Date("2017-01-01")) │
│ (from = Date("2017-01-01"), to = Date("2018-01-01")) │
│ (from = Date("2018-01-01"), to = Date("2019-01-01")) │
│ (from = Date("2019-01-01"), to = Date("2020-01-01")) │
│ (from = Date("2020-01-01"), to = Date("2020-12-31")) │

Last item to note is that you need to recalculate the exposure_fraction as it currently reflects the pre-truncated values. When actually building a process, you could truncate before calculating the fraction to begin with.

Discussion and Questions

If you have other ideas or questions, feel free to also open an issue, or discuss on the community Zulip or Slack #actuary channel. We welcome all actuarial and related disciplines!

Endnotes

References

Related Packages