| Title: | Simulate Synthetic Health Insurance Portfolios and Panels |
|---|---|
| Description: | Generates synthetic South African health insurance data with a known data-generating process, for teaching and testing pricing models. One function simulates a cross-sectional portfolio of principal members with negative binomial claim counts and a lognormal-Pareto severity mixture. Another simulates a multi-year open-cohort panel with ageing, smoking cessation, wearable activity and sleep data joined by a Clayton copula, claims inflation, annual benefit limits, lapses and deaths. Every parameter can be changed, and the true expected frequency and severity are available for checking fitted models. |
| Authors: | Sbusiso Mdingi [aut, cre, cph] (ORCID: <https://orcid.org/0009-0003-4417-968X>) |
| Maintainer: | Sbusiso Mdingi <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-11-14 00:34:38 UTC |
| Source: | https://github.com/cran/healthsim |
The nine South African provinces and the three benefit tiers used by the simulators, in the order that parameter vectors follow.
hs_provinces() hs_plans()hs_provinces() hs_plans()
Essential covers in-hospital treatment; Standard adds out-of-hospital specialists, scans and tests; Comprehensive adds GP visits, prescribed medicines, dental and optical.
A character vector.
hs_provinces() hs_plans()hs_provinces() hs_plans()
Returns the default parameters of simulate_panel(). Claims follow
portfolio_params(), passed as portfolio, plus the panel terms below.
panel_params(..., portfolio = portfolio_params())panel_params(..., portfolio = portfolio_params())
... |
Panel parameters to change, for example |
portfolio |
Claims parameters, from |
A named list.
p <- panel_params(claims_inflation = 0.10, portfolio = portfolio_params(freq_smoker = 0.4))p <- panel_params(claims_inflation = 0.10, portfolio = portfolio_params(freq_smoker = 0.4))
Returns the default data-generating parameters of simulate_portfolio(),
with any named argument replacing its default. Unknown names are an error,
so a typo cannot silently leave a default in place.
portfolio_params(...)portfolio_params(...)
... |
Parameters to change, for example |
Frequency terms are on the log scale of claims per policy year, relative
to a base profile: a 40-year-old woman in Gauteng on the Essential plan,
non-smoker, BMI 25, no children. Severity terms are on the log scale of
the expected cost per claim, in rand. Each claim is the severity scale
times Z, where Z is lognormal with probability 1 - large_prob and a
Pareto large claim otherwise.
The defaults are illustrative assumptions, not estimates from real data.
A named list.
p <- portfolio_params(freq_smoker = 0.4, frailty_var = 0) p$freq_smokerp <- portfolio_params(freq_smoker = 0.4, frailty_var = 0) p$freq_smoker
Converts Sex, Smoker, Province and Plan to factors whose first
level is the base profile, so GLM coefficients line up with the
parameters.
set_levels(df)set_levels(df)
df |
A data frame from |
df with factor columns.
Follows an open cohort of principal members for several calendar years,
one row per member per year. The book starts with n members in force,
and each year new members join, members age, change BMI, quit smoking,
have children, claim, and lapse or die.
simulate_panel( n = 1e+05, years = 5, seed = 123, params = panel_params(), truth = FALSE )simulate_panel( n = 1e+05, years = 5, seed = 123, params = panel_params(), truth = FALSE )
n |
Members in force at the start. |
years |
Number of calendar years. |
seed |
Random seed, or |
params |
Parameters from |
truth |
If |
Claims follow the portfolio model (simulate_portfolio()) with three
additions:
Frailty has a lasting part and a yearly part, so past claims predict future claims.
Daily steps and sleep efficiency lower claim frequency for everyone, but are only recorded for members of the wellness programme. The two are joined by a Clayton copula, so low activity and poor sleep tend to come together. Active members join the programme more often, so the missing values are not missing at random.
Cost per claim grows with claims_inflation, while annual benefit
limits stay fixed in rand, so more members reach the limit each year.
Lapses happen at the end of a year and are more likely in the first policy year and for members who did not claim, so the remaining book gets worse over time. Deaths happen during the year and end exposure early.
A data frame with one row per member per calendar year:
Policy_ID, Calendar_Year, Entry_Year, Duration (policy year,
1 in the year of joining), Age, Sex, BMI, Smoker, Province,
Plan, Children, Wellness, Avg_Daily_Steps and Sleep_Efficiency
(NA outside the wellness programme), Exposure, ClaimCount,
Charges (incurred, rand), Annual_Limit, Charges_Paid (capped at the
limit), Limit_Reached, and Exit ("none", "lapse" or "death").
Members with Exit == "none" in the last year are still in force.
pn <- simulate_panel(2000, years = 3, seed = 1) table(pn$Calendar_Year, pn$Exit) # Faster claims inflation pp <- panel_params(claims_inflation = 0.12) pn2 <- simulate_panel(2000, years = 3, seed = 1, params = pp)pn <- simulate_panel(2000, years = 3, seed = 1) table(pn$Calendar_Year, pn$Exit) # Faster claims inflation pp <- panel_params(claims_inflation = 0.12) pn2 <- simulate_panel(2000, years = 3, seed = 1, params = pp)
Simulates one year of a portfolio of principal members with their
dependent children. Claim counts are Poisson given a gamma frailty, so
negative binomial overall, and every claim has its own cost from a
lognormal-Pareto mixture. A policy's Charges are the sum of its claims.
simulate_portfolio( n = 1e+05, seed = 123, params = portfolio_params(), claims = FALSE, id = FALSE )simulate_portfolio( n = 1e+05, seed = 123, params = portfolio_params(), claims = FALSE, id = FALSE )
n |
Number of policies. |
seed |
Random seed, or |
params |
Parameters from |
claims |
If |
id |
If |
With the defaults, simulate_portfolio(1e5, seed = 123) gives the
Kaggle dataset "Health Insurance Actuarial Playground".
A data frame of policies with columns Age, Sex, BMI,
Smoker, Province, Plan, Children, Exposure, ClaimCount and
Charges (rand), or a list of policies and claims when
claims = TRUE.
df <- simulate_portfolio(1000, seed = 1) head(df) sum(df$ClaimCount) / sum(df$Exposure) # A portfolio where smoking costs more df2 <- simulate_portfolio(1000, seed = 1, portfolio_params(freq_smoker = 0.5))df <- simulate_portfolio(1000, seed = 1) head(df) sum(df$ClaimCount) / sum(df$Exposure) # A portfolio where smoking costs more df2 <- simulate_portfolio(1000, seed = 1, portfolio_params(freq_smoker = 0.5))
The expected claims per policy year and the expected cost per claim under
the data-generating parameters, for comparing with fitted models.
true_frequency() is the mean over the frailty, so it is the target of a
Poisson or negative binomial GLM with log exposure as offset.
true_frequency(df, params = portfolio_params()) true_severity(df, params = portfolio_params())true_frequency(df, params = portfolio_params()) true_severity(df, params = portfolio_params())
df |
A data frame with the portfolio columns. |
params |
Parameters from |
A numeric vector, one value per row of df.
df <- simulate_portfolio(1000, seed = 1) mean(true_frequency(df)) mean(true_severity(df))df <- simulate_portfolio(1000, seed = 1) mean(true_frequency(df)) mean(true_severity(df))
z_mean() is the mean of the multiplier Z, used to scale claims to the
severity means. z_cv2() is the squared coefficient of variation of a
single claim, which is the true dispersion of a Gamma severity GLM.
z_mean(params = portfolio_params()) z_cv2(params = portfolio_params())z_mean(params = portfolio_params()) z_cv2(params = portfolio_params())
params |
Parameters from |
A number.
z_cv2()z_cv2()