ExperienceAnalysis

Stable Dev Coverage lifecycle

Calculate exposures.

Quickstart

using ExperienceAnalysis
using DataFrames
using Dates

df = DataFrame(
    policy_id = 1:3,
    issue_date = [Date(2020,5,10), Date(2020,4,5), Date(2019, 3, 10)],
    termination_date = [Date(2022, 6, 10), Date(2022, 8, 10), nothing],
    status = ["claim", "lapse", "inforce"]
)

df.policy_year = exposure.(
    ExperienceAnalysis.Anniversary(Year(1)),
    df.issue_date,
    df.termination_date,
    df.status .== "claim"; # continued exposure
    study_start = Date(2020, 1, 1),
    study_end = Date(2022, 12, 31)
)

df = flatten(df, :policy_year)

df.exposure_fraction =
        map(e -> yearfrac(e.from, e.to + Day(1), DayCounts.Thirty360()), df.policy_year) 
# + Day(1) above because DayCounts has Date(2020, 1, 1) to Date(2021, 1, 1) as an exposure of 1.0
# here we end the interval at Date(2020, 12, 31), so we need to add a day to get the correct exposure fraction.
policy_id issue_date termination_date status policy_year exposure_fraction
1 2020-05-10 2022-06-10 claim (from = Date("2020-05-10"), to = Date("2021-05-09"), policy_timestep = 1) 1.0
1 2020-05-10 2022-06-10 claim (from = Date("2021-05-10"), to = Date("2022-05-09"), policy_timestep = 2) 1.0
1 2020-05-10 2022-06-10 claim (from = Date("2022-05-10"), to = Date("2023-05-09"), policy_timestep = 3) 1.0
2 2020-04-05 2022-08-10 lapse (from = Date("2020-04-05"), to = Date("2021-04-04"), policy_timestep = 1) 1.0
2 2020-04-05 2022-08-10 lapse (from = Date("2021-04-05"), to = Date("2022-04-04"), policy_timestep = 2) 1.0
2 2020-04-05 2022-08-10 lapse (from = Date("2022-04-05"), to = Date("2022-08-10"), policy_timestep = 3) 0.35
3 2019-03-10 inforce (from = Date("2020-01-01"), to = Date("2020-03-09"), policy_timestep = 1) 0.191667
3 2019-03-10 inforce (from = Date("2020-03-10"), to = Date("2021-03-09"), policy_timestep = 2) 1.0
3 2019-03-10 inforce (from = Date("2021-03-10"), to = Date("2022-03-09"), policy_timestep = 3) 1.0
3 2019-03-10 inforce (from = Date("2022-03-10"), to = Date("2022-12-31"), policy_timestep = 4) 0.808333

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!

References

API

The exposure function has the following type signature for Anniversary exposures:

function exposure(
    p::Anniversary,
    from::Date,
    to::Union{Date,Nothing},
    continued_exposure::Bool = false;
    study_start::Union{Date,Nothing} = nothing,
    study_end::Date,
    left_partials::Bool = false,
    right_partials::Bool = true,
)::Vector{NamedTuple{(:from, :to, :policy_timestep),Tuple{Date,Date,Int}}}

p, Exposure Basis

Anniversary

ExperienceAnalysis.Anniversary(DatePeriod) will give exposures periods based on the first date. Exposure intervals will fall on annniversaries, start_date + t * dateperiod. DatePeriod is a DatePeriod Type from the Dates standard library.

exposure(
    ExperienceAnalysis.Anniversary(Year(1)), # basis
    Date(2020,5,10),                         # from
    Date(2022, 6, 10);                       # to
    study_start = Date(2020, 1, 1),
    study_end = Date(2022, 12, 31)
)
# returns
# 3-element Vector{NamedTuple{(:from, :to, :policy_timestep), Tuple{Date, Date, Int64}}}:
#  (from = Date("2020-05-10"), to = Date("2021-05-09"), policy_timestep = 1)
#  (from = Date("2021-05-10"), to = Date("2022-05-09"), policy_timestep = 2)
#  (from = Date("2022-05-10"), to = Date("2022-06-10"), policy_timestep = 3)

Calendar

ExperienceAnalysis.Calendar(DatePeriod) will follow calendar periods (e.g. month or year). Quarterly exposures can be created with Month(3), the number of months should divide 12.

exposure(
    ExperienceAnalysis.Calendar(Year(1)), # basis
    Date(2020,5,10),                      # from
    Date(2022, 6, 10);                    # to
    study_start = Date(2020, 1, 1),
    study_end = Date(2022, 12, 31)
)
# returns
# 3-element Vector{NamedTuple{(:from, :to), Tuple{Date, Date}}}:
#  (from = Date("2020-05-10"), to = Date("2020-12-31"))
#  (from = Date("2021-01-01"), to = Date("2021-12-31"))
#  (from = Date("2022-01-01"), to = Date("2022-06-10"))

AnniversaryCalendar

ExperienceAnalysis.AnniversaryCalendar(DatePeriod,DatePeriod) will split into the smaller of the calendar or policy anniversary period. We can ensure that each exposure interval entirely falls within a single calendar year.

exposure(
    ExperienceAnalysis.AnniversaryCalendar(Year(1), Year(1)), # basis
    Date(2020,5,10),                                          # from
    Date(2022, 6, 10);                                        # to
    study_start = Date(2020, 1, 1),
    study_end = Date(2022, 12, 31)
)
# returns
# 5-element Vector{NamedTuple{(:from, :to, :policy_timestep), Tuple{Date, Date, Int64}}}:
#  (from = Date("2020-05-10"), to = Date("2020-12-31"), policy_timestep = 1)
#  (from = Date("2021-01-01"), to = Date("2021-05-09"), policy_timestep = 1)
#  (from = Date("2021-05-10"), to = Date("2021-12-31"), policy_timestep = 2)
#  (from = Date("2022-01-01"), to = Date("2022-05-09"), policy_timestep = 2)
#  (from = Date("2022-05-10"), to = Date("2022-06-10"), policy_timestep = 3)

from, to, study_start, study_end

  • from is the date the policy was issued
  • to is the date the policy was terminated, or nothing if the policy is still in-force
  • study_start is the start of the study period, or nothing if the study period is unbounded on the left
  • study_end is the end of the study period

from and study_end are required to be Date types. to and study_start can be Date or nothing.

continued_exposure

When doing a lapse study, lapsed policies will be given a full year of exposure in the policy year of the lapse. This is accomplished by setting continued_exposure = true. continued_exposure is not a keyword argument so that it can support broadcasting.

The continued exposure may extend beyond the end of the study.

exposure(
    ExperienceAnalysis.AnniversaryCalendar(Year(1), Year(1)), # basis
    Date(2020,5,10),                                          # from
    Date(2022, 6, 10),                                        # to
    true;                                                     # continued_exposure
    study_start = Date(2020, 1, 1),
    study_end = Date(2022, 12, 31)
)
# returns
# 6-element Vector{NamedTuple{(:from, :to, :policy_timestep), Tuple{Date, Date, Int64}}}:
#  (from = Date("2020-05-10"), to = Date("2020-12-31"), policy_timestep = 1)
#  (from = Date("2021-01-01"), to = Date("2021-05-09"), policy_timestep = 1)
#  (from = Date("2021-05-10"), to = Date("2021-12-31"), policy_timestep = 2)
#  (from = Date("2022-01-01"), to = Date("2022-05-09"), policy_timestep = 2)
#  (from = Date("2022-05-10"), to = Date("2022-12-31"), policy_timestep = 3)
#  (from = Date("2023-01-01"), to = Date("2023-05-09"), policy_timestep = 3) # this is the continued exposure

left_partials and right_partials

Assumptions like lapse rates can have uneven distributions within policy years, so we may only want to look at full policy years. This can be accomplished by setting left_partials = false and right_partials = false.

See that by default there are partial exposures at the beginning and end of the study period.

exposure(
    ExperienceAnalysis.Anniversary(Year(1)), # basis
    Date(2019,5,10),                         # from
    Date(2022, 6, 10);                       # to
    study_start = Date(2020, 1, 1),
    study_end = Date(2021, 12, 31)
)

# returns
# 3-element Vector{NamedTuple{(:from, :to, :policy_timestep), Tuple{Date, Date, Int64}}}:
#  (from = Date("2020-01-01"), to = Date("2020-05-09"), policy_timestep = 1)
#  (from = Date("2020-05-10"), to = Date("2021-05-09"), policy_timestep = 2)
#  (from = Date("2021-05-10"), to = Date("2021-12-31"), policy_timestep = 3)

But we can remove these partial exposures by setting left_partials = false and right_partials = false.

exposure(
    ExperienceAnalysis.Anniversary(Year(1)), # basis
    Date(2019,5,10),                         # from
    Date(2022, 6, 10);                       # to
    study_start = Date(2020, 1, 1),
    study_end = Date(2021, 12, 31),
    left_partials = false,
    right_partials = false
)
# returns
# 1-element Vector{NamedTuple{(:from, :to, :policy_timestep), Tuple{Date, Date, Int64}}}:
#  (from = Date("2020-05-10"), to = Date("2021-05-09"), policy_timestep = 2)

Calendar basis does not have left_partials and right_partials because the same effect can always be achieved by setting study_start and study_end.