From 0115e8e95437373ebe1cb40f10bc90de2fb8b953 Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer <reinhold@kainhofer.com> Date: Thu, 19 Jan 2023 23:38:50 +0100 Subject: [PATCH] Implement sumInsured calculated from given premium * Add parameter premium, which defines the prescribe premium (either written, written_beforetax or gross) * The sumInsured will be derived from the premium by reversing the formulas (as far as possible) * If both sumInsured and premium are given, the premium is ignored and sumInsured takes precedence * The function calculatePremiums now also returns the sumInsured (in addition to the premiums and the coefficients for the premium calculation) Implements #49, documents #63 --- R/.gitignore | 1 + R/InsuranceContract.R | 1 + R/InsuranceParameters.R | 4 +- R/InsuranceTarif.R | 106 +++++++++++++++++- tests/testthat/test-SumInsured-calculated.R | 46 ++++++++ ...ing-the-lifeinsurancecontracts-package.Rmd | 98 ++++++++++++++++ 6 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 tests/testthat/test-SumInsured-calculated.R diff --git a/R/.gitignore b/R/.gitignore index d392185..f90d1a1 100644 --- a/R/.gitignore +++ b/R/.gitignore @@ -1 +1,2 @@ dummy.R +*.R.bak diff --git a/R/InsuranceContract.R b/R/InsuranceContract.R index 078c910..20bc205 100644 --- a/R/InsuranceContract.R +++ b/R/InsuranceContract.R @@ -630,6 +630,7 @@ InsuranceContract = R6Class( # so we have to extract the coefficients and store them in a separate variable if (recalculatePremiums) { res = private$calculatePremiums(premiumCalculationTime = premiumCalculationTime); + self$Parameters$ContractData$sumInsured = res[["sumInsured"]] self$Values$premiumCoefficients = res[["coefficients"]]; # TODO: Store premiums in a data.frame, including the time they are calculated??? self$Values$premiums = res[["premiums"]] diff --git a/R/InsuranceParameters.R b/R/InsuranceParameters.R index 3774dee..72c8f19 100644 --- a/R/InsuranceParameters.R +++ b/R/InsuranceParameters.R @@ -279,6 +279,7 @@ InsuranceContract.Values = list( #' contracts with multiple parts, e.g. dynamic increases), #' default = "Hauptvertrag"} #' \item{\code{$sumInsured}}{Sum insured, default = 100,000} +#' \item{\code{$premium}}{Premium, given to determine the sumInsured (default: NULL)} #' \item{\code{$initialCapital}}{Reserve/Capital that is already available #' at contract inception, e.g. from a previous contract. No tax #' or acquisition costs are applied to this capital.} @@ -553,7 +554,8 @@ InsuranceContract.Values = list( InsuranceContract.ParameterDefaults = list( ContractData = list( id = "Hauptvertrag", - sumInsured = 100000, + sumInsured = NULL, + premium = NULL, birthDate = NULL, YOB = NULL, age = NULL, diff --git a/R/InsuranceTarif.R b/R/InsuranceTarif.R index 1991083..737dd05 100644 --- a/R/InsuranceTarif.R +++ b/R/InsuranceTarif.R @@ -580,7 +580,13 @@ InsuranceTarif = R6Class( row.names = ages - age ); - cf$additional_capital = pad0(params$ContractData$initialCapital / params$ContractData$sumInsured, cflen) + if (is.null(params$ContractData$sumInsured)) { + # No sumInsured given, determine SI from premium provided + # + cf$additional_capital = pad0(params$ContractData$initialCapital, cflen) + } else { + cf$additional_capital = pad0(params$ContractData$initialCapital / params$ContractData$sumInsured, cflen) + } # Premiums: if (!params$ContractState$premiumWaiver) { premiums = self$getPremiumCF(len = cflen, params = params, values = values) @@ -993,12 +999,95 @@ InsuranceTarif = R6Class( pvCost = values$presentValuesCosts[t,,,] if (pv[["premiums"]] == 0) { - return(list("premiums" = values$premiums, "coefficients" = coefficients)) + return(list("premiums" = values$premiums, "coefficients" = coefficients, "sumInsured" = params$ContractData$sumInsured)) + } + + #======================================================================== = + # Calculate sumInsured from Premium, if needed + # ======================================================================= = + if (is.null(sumInsured)) { + sumInsured = 1 + params$ContractData$sumInsured = 1 # Temporarily set to 1! + + # Premium type can be given using a named array, e.g. premium = c(gross = 1000) + premiumtype = names(params$ContractData$premium) + if (is.null(premiumtype)) premiumtype = "written"; + premium = unname(params$ContractData$premium); + calculating = FALSE; + + + # Calculate unit gross premium (sumInsured=1) + values$premiums["additional_capital"] = values$cashFlows[t, "additional_capital"] + coeff = self$getPremiumCoefficients("gross", pv * 0, pvCost * 0, premiums = values$premiums, params = params, values = values, premiumCalculationTime = premiumCalculationTime) + enumerator = sum(coeff[["SumInsured"]][["benefits"]] * pv) + sum(coeff[["SumInsured"]][["costs"]] * pvCost); + denominator = sum(coeff[["Premium" ]][["benefits"]] * pv) + sum(coeff[["Premium" ]][["costs"]] * pvCost); + values$premiums[["unit.gross"]] = enumerator/denominator * (1 + loadings$ongoingAlphaGrossPremium); + + # Calculate other premium components: + # ATTENTION: This will not work if any of these depend on the absolute values of the premiums, or depend on net or Zillmer premium! + tax = valueOrFunction(loadings$tax, params = params, values = values); + unitCosts = valueOrFunction(loadings$unitcosts, params = params, values = values); + noMedicalExam = valueOrFunction(loadings$noMedicalExam,params = params, values = values); + noMedicalExam.relative = valueOrFunction(loadings$noMedicalExamRelative,params = params, values = values); + extraRebate = valueOrFunction(loadings$extraRebate, params = params, values = values); + sumRebate = valueOrFunction(loadings$sumRebate, params = params, values = values); + premiumRebateRate = valueOrFunction(loadings$premiumRebate,params = params, values = values); + premiumRebate = applyHook(params$Hooks$premiumRebateCalculation, premiumRebateRate, params = params, values = values); + + extraChargeGrossPremium = valueOrFunction(loadings$extraChargeGrossPremium, params = params, values = values); + advanceProfitParticipation = 0; + advanceProfitParticipationUnitCosts = 0; + ppScheme = params$ProfitParticipation$profitParticipationScheme; + if (!is.null(ppScheme)) { + advanceProfitParticipation = ppScheme$getAdvanceProfitParticipation(params = params, values = values) + advanceProfitParticipationUnitCosts = ppScheme$getAdvanceProfitParticipationAfterUnitCosts(params = params, values = values) + } + if (is.null(advanceProfitParticipation)) advanceProfitParticipation = 0; + if (is.null(advanceProfitParticipationUnitCosts)) advanceProfitParticipationUnitCosts = 0; + + partnerRebate = valueOrFunction(loadings$partnerRebate, params = params, values = values); + + # Start from the given premium to derive the sumInsured step-by-step: + # + # Written premium after tax + calculating = calculating | (premiumtype == "written"); + if (calculating) { + premium = premium / (1 + tax); + } + # Written premium before tax + calculating = calculating | (premiumtype == "written_beforetax"); + if (calculating) { + premium = premium / (1 - premiumRebate - advanceProfitParticipationUnitCosts - partnerRebate); + + pv.unitcosts = sum( + pvCost["unitcosts","SumInsured",] * sumInsured + + pvCost["unitcosts","SumPremiums",] * values$unitPremiumSum * values$premiums[["gross"]] + + pvCost["unitcosts","GrossPremium",] * values$premiums[["gross"]] + + pvCost["unitcosts","NetPremium",] * values$premiums[["net"]] + + pvCost["unitcosts","Constant",] + ) + premium.unitcosts = pv.unitcosts / pv[["premiums"]] + valueOrFunction(loadings$unitcosts, params = params, values = values); + if (!params$Features$unitcostsInGross) { + premium = premium - premium.unitcosts; + } + premium = premium / (1 - advanceProfitParticipation) + } + calculating = calculating | (premiumtype == "gross"); + if (calculating) { + sumInsured = premium / + (values$premiums[["unit.gross"]]*(1 + noMedicalExam.relative + extraChargeGrossPremium) + noMedicalExam - sumRebate - extraRebate); + } + params$ContractData$sumInsured = sumInsured } + values$premiums["additional_capital"] = values$cashFlows[t, "additional_capital"] * sumInsured + #======================================================================== = # net, gross and Zillmer premiums are calculated from the present values using the coefficients on each present value as described in the formulas document + # ======================================================================= = + # GROSS Premium + # ----------------------------------------------------------------------- - coeff = self$getPremiumCoefficients("gross", pv * 0, pvCost * 0, premiums = values$premiums, params = params, values = values, premiumCalculationTime = premiumCalculationTime) enumerator = sum(coeff[["SumInsured"]][["benefits"]] * pv) + sum(coeff[["SumInsured"]][["costs"]] * pvCost); denominator = sum(coeff[["Premium" ]][["benefits"]] * pv) + sum(coeff[["Premium" ]][["costs"]] * pvCost); @@ -1006,6 +1095,9 @@ InsuranceTarif = R6Class( values$premiums[["gross"]] = values$premiums[["unit.gross"]] * sumInsured; coefficients[["gross"]] = coeff; + # ======================================================================= = + # NET Premium + # ----------------------------------------------------------------------- - coeff = self$getPremiumCoefficients("net", pv*0, pvCost*0, premiums = values$premiums, params = params, values = values, premiumCalculationTime = premiumCalculationTime) enumerator = sum(coeff[["SumInsured"]][["benefits"]] * pv) + sum(coeff[["SumInsured"]][["costs"]] * pvCost); denominator = sum(coeff[["Premium" ]][["benefits"]] * pv) + sum(coeff[["Premium" ]][["costs"]] * pvCost); @@ -1013,6 +1105,9 @@ InsuranceTarif = R6Class( values$premiums[["net"]] = values$premiums[["unit.net"]] * sumInsured; coefficients[["net"]] = coeff; + # ======================================================================= = + # ZILLMER Premium + # ----------------------------------------------------------------------- - coeff = self$getPremiumCoefficients("Zillmer", pv * 0, pvCost * 0, premiums = values$premiums, params = params, values = values, premiumCalculationTime = premiumCalculationTime); enumerator = sum(coeff[["SumInsured"]][["benefits"]] * pv) + sum(coeff[["SumInsured"]][["costs"]] * pvCost); denominator = sum(coeff[["Premium" ]][["benefits"]] * pv) + sum(coeff[["Premium" ]][["costs"]] * pvCost); @@ -1021,6 +1116,9 @@ InsuranceTarif = R6Class( coefficients[["Zillmer"]] = coeff; + # ======================================================================= = + # Additional premium components (after gross premium) + # ----------------------------------------------------------------------- - # The written premium is the gross premium with additional loadings, rebates, unit costs and taxes tax = valueOrFunction(loadings$tax, params = params, values = values); unitCosts = valueOrFunction(loadings$unitcosts, params = params, values = values); @@ -1061,14 +1159,14 @@ InsuranceTarif = R6Class( premiumBeforeTax = premiumBeforeTax + premium.unitcosts; } premiumBeforeTax = premiumBeforeTax * (1 - premiumRebate - advanceProfitParticipationUnitCosts - partnerRebate); - premiumBeforeTax.y = premiumBeforeTax * (1 + frequencyLoading); + premiumBeforeTax.y = premiumBeforeTax * (1 + frequencyLoading); premiumBeforeTax = premiumBeforeTax.y / params$ContractData$premiumFrequency; values$premiums[["written_yearly"]] = premiumBeforeTax.y * (1 + tax) values$premiums[["written_beforetax"]] = premiumBeforeTax; values$premiums[["tax"]] = premiumBeforeTax * tax; values$premiums[["written"]] = premiumBeforeTax * (1 + tax); - list("premiums" = values$premiums, "coefficients" = coefficients) + list("premiums" = values$premiums, "coefficients" = coefficients, "sumInsured" = params$ContractData$sumInsured) }, #' @description Calculate the reserves of the InsuranceContract given the diff --git a/tests/testthat/test-SumInsured-calculated.R b/tests/testthat/test-SumInsured-calculated.R new file mode 100644 index 0000000..92c8874 --- /dev/null +++ b/tests/testthat/test-SumInsured-calculated.R @@ -0,0 +1,46 @@ +test_that("Calculation of sumInsured from premium", { + library(MortalityTables) + mortalityTables.load("Austria_Census") + + Tarif.EndowmentSI = InsuranceTarif$new( + name = "Example Tariff - Endowment", + type = "endowment", + tarif = "E1-RP", + desc = "An endowment with regular premiums (standard tariff)", + + mortalityTable = mort.AT.census.2011.unisex, + cost = initializeCosts(alpha = 0.04, gamma.contract = 0.0005, unitcosts = 10), + i = 0.03 + ) + Contract.sumInsured = InsuranceContract$new( + tarif = Tarif.EndowmentSI, + age = 40, policyPeriod = 20, + sumInsured = 10000, + contractClosing = as.Date("2020-09-01") + ) + Contract.premium = InsuranceContract$new( + tarif = Tarif.EndowmentSI, + age = 40, policyPeriod = 20, + premium = Contract.sumInsured$Values$premiums[["written"]], + contractClosing = as.Date("2020-09-01") + ) + Contract.premium_beforetax = InsuranceContract$new( + tarif = Tarif.EndowmentSI, + age = 40, policyPeriod = 20, + premium = c(written_beforetax = Contract.sumInsured$Values$premiums[["written_beforetax"]]), + contractClosing = as.Date("2020-09-01") + ) + Contract.premium_gross = InsuranceContract$new( + tarif = Tarif.EndowmentSI, + age = 40, policyPeriod = 20, + premium = c(gross = Contract.sumInsured$Values$premiums[["gross"]]), + contractClosing = as.Date("2020-09-01") + ) + + # All four contracts above should result in the same sumInsured: + expect_equal(Contract.premium$Parameters$ContractData$sumInsured, Contract.sumInsured$Parameters$ContractData$sumInsured) + expect_equal(Contract.premium_beforetax$Parameters$ContractData$sumInsured, Contract.sumInsured$Parameters$ContractData$sumInsured) + expect_equal(Contract.premium_gross$Parameters$ContractData$sumInsured, Contract.sumInsured$Parameters$ContractData$sumInsured) + expect_equal(Contract.sumInsured$Parameters$ContractData$sumInsured, Contract.premium$Parameters$ContractData$sumInsured) + +}) diff --git a/vignettes/using-the-lifeinsurancecontracts-package.Rmd b/vignettes/using-the-lifeinsurancecontracts-package.Rmd index c2fae6e..f293d09 100644 --- a/vignettes/using-the-lifeinsurancecontracts-package.Rmd +++ b/vignettes/using-the-lifeinsurancecontracts-package.Rmd @@ -733,6 +733,104 @@ all_equal(contract.PureEnd.SP1$Values$reserves, contract.PureEnd.SP2$Values$rese ``` +## Determining Sum Insured from the premium + +By default, the insurance contract is created for a given sum insured and the +premiums are calculated accordingly. Sometimes, the reverse is needed: The +premium (either actuarial gross premium, written premium before or after taxes) +is given and the corresponding sum insured should be determined automatically. + +The `InsuranceContract` constructor / parameter set has an additional field +`premium` to indicate the desired premium (written premium after tax by default) +from which the sum insured is then calculated: +```{r PrescribePremium} +# Premium calculated from sumInsured +contract.End = InsuranceContract$new( + Tarif.Endowment, age = 35, policyPeriod = 10, + contractClosing = as.Date("2020-08-18"), + sumInsured = 10000); + +# sumInsured derived from written premium +contract.End.premium = InsuranceContract$new( + Tarif.Endowment, age = 35, policyPeriod = 10, + contractClosing = as.Date("2020-08-18"), + premium = 1139.06); + +contract.End.premiumBeforeTax = InsuranceContract$new( + Tarif.Endowment, age = 35, policyPeriod = 10, + contractClosing = as.Date("2020-08-18"), + premium = c(written_beforetax = 1095.25)); + +contract.End.premiumGross = InsuranceContract$new( + Tarif.Endowment, age = 35, policyPeriod = 10, + contractClosing = as.Date("2020-08-18"), + premium = c(gross = 1085.25)); + +``` +```{r PrescribePremiumOUTPUT,echo = FALSE} +bind_rows( + c(Contract = "contract.End", contract.End$Values$premiums[c("net", "Zillmer", "gross", "written_beforetax", "written")], sumInsured = contract.End$Parameters$ContractData$sumInsured), + c(Contract = "contract.End.premium", contract.End.premium$Values$premiums[c("net", "Zillmer", "gross", "written_beforetax", "written")], sumInsured = contract.End.premium$Parameters$ContractData$sumInsured), + c(Contract = "contract.End.premiumBeforeTax", contract.End.premiumBeforeTax$Values$premiums[c("net", "Zillmer", "gross", "written_beforetax", "written")], sumInsured = contract.End.premiumBeforeTax$Parameters$ContractData$sumInsured), + c(Contract = "contract.End.premiumGross", contract.End.premiumGross$Values$premiums[c("net", "Zillmer", "gross", "written_beforetax", "written")], sumInsured = contract.End.premiumGross$Parameters$ContractData$sumInsured) +) +``` + +The final written premium can be directly passed as the `premium` argument. Other +types of premium must be passed as a named number (i.e. a one-element vector with +name "gross", "written_beforetax" or "written"). + +If a premium is prescribed, a `sumInsured` must not be given. If both are given, +the sumInsured is used and the prescribed premium is silently ignored. + +The are cases, when the sumInsured cannot be derived from a prescribed premium. +One relevant case is when a premium rebate depends on the sum insured, since in +this case we need the sumInsured to calculate the rebate and thus the premium. +All rebates, add-ons and other parameters will temporarily use a sumInsured=1 +during the calculation of the actual sumInsured! + + + +## Providing additional capital at contract inception + +Often, when a contract is changed significantly, this is modelled as a totally +new contract, with the existing reserve provided as additional one-time payment +for the follow-up contract. This is different from a single-premium contract, +because the new contract can have regular premiums, just the existing reserve +is used as the initial reserve of the new contract. + +The package provides the argument `initialCapital` to provide initial capital +that is also included in the calculation of the premium of the contract. + +```{r InitialCapital} +# Contract with initial capital of 5.000 EUR +contract.Endow.initialCapital = InsuranceContract$new( + tarif = Tarif.Endowment, + sumInsured = 10000, + initialCapital = 5000, + age = 40, policyPeriod = 10, + contractClosing = as.Date("2020-09-01") +) +# For comparison: Contract without initial capital of 5.000 EUR +contract.Endow = InsuranceContract$new( + tarif = Tarif.Endowment, + sumInsured = 10000, + age = 40, policyPeriod = 10, + contractClosing = as.Date("2020-09-01") +) +``` +Comparing the reserves, one can clearly see the initial capital used as initial reserve: + +```{r InitialCapitalOUTPUT} +data.frame( + `Premium with initialCapital`= contract.Endow.initialCapital$Values$premiumComposition[,"charged"], + `Premium without initialCapital`= contract.Endow$Values$premiumComposition[,"charged"], + `Res.with initialCapital`= contract.Endow.initialCapital$Values$reserves[,"contractual"], + `Res.without initialCapital`= contract.Endow$Values$reserves[,"contractual"] +) +``` + + ## Premium Waivers After a while, many customers do not want to pay premiums for the contract any -- GitLab