class IntegerModel: """Integer model for testing.""" a = def_parameter(dtype=int, description="A number A") b = def_parameter(dtype=int, description="A number B") c = def_parameter(dtype=int, description="A number C") d = def_parameter(dtype=int, description="A number D") ret_1 = def_intermediate(dtype=int, description="Results a + b") ret_2 = def_intermediate(dtype=int, description="Results c - d") ret_3 = def_return(dtype=int, description="Result total of step_1 and step_2") @step(uses=["a", "b"], impacts=["ret_1"]) def _step_1(self): """Add a and b together.""" self.ret_1 = self.a + self.b @step(uses=["c", "d"], impacts=["ret_2"]) def _step_2(self): """Subtract d from c""" self.ret_2 = self.c - self.d @step(uses=["ret_1", "ret_2"], impacts=["ret_3"]) def _step_3(self): """Add total of steps 1 and 2.""" self.ret_3 = self.ret_1 + self.ret_2
class TestAttributes: param1 = def_parameter() param2 = def_parameter() sensitivity1 = def_sensitivity(default=1) sensitivity2 = def_sensitivity(default=2) meta1 = def_meta(meta="meta1", dtype=str) meta2 = def_meta(meta="meta2", dtype=str) intermediate1 = def_intermediate() intermediate2 = def_intermediate() return1 = def_return() return2 = def_return() @step(uses=["param1", "param2"], impacts=["return1"]) def _step1(self): pass
class ModelNoSteps: parameter = def_parameter(dtype=int) intermediate = def_intermediate(init_value=0) @step(uses=["parameter"], impacts=["intermediate"]) def _add(self): self.intermediate = self.parameter
class DValSisRPMD(DValBasePMD): """The disabled life reserve (DLR) valuation model for the cost of living adjustment (COLA) policy rider. This model is a child of the `DValBasePMD` with the only changes being the addition of a step `_get_sis_probability` to lookup the probability of the policy holder qualifying for SIS (i.e., sis_probability) and the monthly benefit amount is equal to the (1 - SIS prbability) x benefit amount. """ sis_probability = def_intermediate( dtype=float, description= "The probability of policyholder qualifying for social insurance supplement benefit.", ) coverage_id = def_meta( meta="SIS", dtype=str, description="The coverage id which recognizes base policy vs riders.", ) @step(name="Get SIS Probability", uses=[], impacts=["sis_probability"]) def _get_sis_probability(self): """Get SIS probability.""" self.sis_probability = 0.7 @step( name="Calculate Monthly Benefits", uses=[ "frame", "valuation_dt", "start_pay_dt", "termination_dt", "sis_probability", "benefit_amount", ], impacts=["frame"], ) def _calculate_benefit_cost(self): """Calculate the monthly benefit amount for each duration.""" self.frame = frame_add_exposure( self.frame, begin_date=max(self.valuation_dt, self.start_pay_dt), end_date=self.termination_dt, exposure_name="EXPOSURE", begin_duration_col="DATE_BD", end_duration_col="DATE_ED", ) self.frame["BENEFIT_AMOUNT"] = (self.frame["EXPOSURE"] * self.benefit_amount * (1 - self.sis_probability)).round(2)
class Test: ret = def_return() placeholder = def_intermediate() meta = def_meta(meta="meta") modifier = def_sensitivity(default=1) parameter = def_parameter()
class DisabledLivesValEMD: """Disabled lives deterministic valuation extract model. This model takes an extract of policies and runs them through the respective Policy Models based on the COVERAGE_ID column (i.e., whether it is base policy or rider). """ # parameters extract_base = def_parameter( dtype=pd.DataFrame, description="The disabled lives base extract.") extract_riders = def_parameter( dtype=pd.DataFrame, description="The disabled lives rider extract.") valuation_dt = param_valuation_dt assumption_set = param_assumption_set # sensitivities modifier_ctr = modifier_ctr modifier_interest = modifier_interest # meta model_version = meta_model_version last_commit = meta_last_commit run_date_time = meta_run_date_time # intermediates records = def_intermediate( dtype=dict, description="The extract transformed to records.") # return projected = def_return( dtype=pd.DataFrame, description="The projected reserves for the policyholders.") time_0 = def_return( dtype=pd.DataFrame, description="The time 0 reserve for the policyholders.") errors = def_return(dtype=list, description="Any errors captured.") @step( name="Create Records from Extracts", uses=["extract_base", "extract_riders"], impacts=["records"], ) def _create_records(self): """Turn extract into a list of records for each row in extract.""" frame = self.extract_base.copy() del frame["IDI_MARKET"] del frame["TOBACCO_USAGE"] records = convert_to_records(frame, column_case="lower") rider_data = self.extract_riders.copy() rider_data.columns = [col.lower() for col in rider_data.columns] rider_data = rider_data.pivot( index=["policy_id", "claim_id", "coverage_id"], columns="rider_attribute", values="value", ).to_dict(orient="index") def update_record(record): key = ( record["policy_id"], record["claim_id"], record["coverage_id"], ) kwargs_add = rider_data.get(key, None) if kwargs_add is not None: return {**record, **kwargs_add} return record self.records = [ record if record["coverage_id"] not in ["RES"] else update_record(record) for record in records ] @step( name="Run Records with Policy Models", uses=["records"] + list(FOREACH_PARAMS), impacts=["projected", "errors"], ) def _run_foreach(self): """Foreach record run through respective policy model based on COVERAGE_ID value.""" projected, errors = foreach_model(**get_kws(foreach_model, self)) if isinstance(projected, list): projected = pd.DataFrame( columns=list(DisabledLivesValOutput.columns)) self.projected = projected self.errors = errors @step(name="Get Time0 Values", uses=["projected"], impacts=["time_0"]) def _get_time0(self): """Filter projected reserves frame down to time_0 reserve for each record.""" cols = [ "MODEL_VERSION", "LAST_COMMIT", "RUN_DATE_TIME", "SOURCE", "POLICY_ID", "CLAIM_ID", "COVERAGE_ID", "DATE_DLR", "DLR", ] self.time_0 = self.projected.groupby(cols[4:7], as_index=False).head(1)[cols]
class AValBasePMD(ALRBasePMD): """The active life reserve (ALR) valuation model for the base policy.""" # meta claim_cost_model = def_meta( meta=claim_cost_model, dtype=callable, description="The claim cost model used.", ) coverage_id = def_meta( meta="BASE", dtype=str, description="The coverage id which recognizes base policy vs riders.", ) # intermediate objects age_issued = def_intermediate( dtype=date, description="The calculate age policy was issued." ) lapse_rates = def_intermediate( dtype=pd.DataFrame, description="The placholder for lapse rates." ) mortality_rates = def_intermediate( dtype=pd.DataFrame, description="The placholder for lapse rates." ) incidence_rates = def_intermediate( dtype=pd.DataFrame, description="The placholder for incidence rates." ) modeled_claim_cost = def_intermediate( dtype=dict, description="The placholder for modeled disabled lives." ) # return object frame = def_return(dtype=pd.DataFrame, description="The frame of projected reserves.") ##################################################################################### # Step: Calculate Issue Age ##################################################################################### @step( name="Calculate Issue Age", uses=["birth_dt", "policy_start_dt"], impacts=["age_issued"], ) def _calculate_age_issued(self): """Calculate the age policy issued.""" self.age_issued = calculate_age(self.birth_dt, self.policy_start_dt, method="ALB") ##################################################################################### # Step: Create Projectetd Frame ##################################################################################### @step( name="Create Projectetd Frame", uses=["policy_start_dt", "policy_end_dt"], impacts=["frame"], ) def _create_frame(self): """Create projected benefit frame from policy start date to policy end date by duration year.""" kwargs_frame = { "frequency": "Y", "col_date_nm": "DATE_BD", "duration_year": "DURATION_YEAR", } kwargs_wt = { "as_of_dt": self.valuation_dt, "begin_duration_col": "DATE_BD", "end_duration_col": "DATE_ED", "wt_current_name": "WT_BD", "wt_next_name": "WT_ED", } self.frame = ( create_frame(self.policy_start_dt, self.policy_end_dt, **kwargs_frame) .pipe(_assign_end_date) .pipe(frame_add_weights, **kwargs_wt) .query("DATE_BD <= @self.policy_end_dt") )[["DATE_BD", "DATE_ED", "DURATION_YEAR", "WT_BD", "WT_ED"]] ##################################################################################### # Step: Calculate Age Attained ##################################################################################### @step(name="Calculate Age Attained", uses=["frame", "birth_dt"], impacts=["frame"]) def _calculate_age_attained(self): """Calculate age attained by policy duration on the frame.""" self.frame["AGE_ATTAINED"] = calculate_age( self.birth_dt, self.frame["DATE_BD"], method="ALB" ) ##################################################################################### # Step: Calculate Benefit Term Date ##################################################################################### @step( name="Calculate Benefit Term Date", uses=[ "frame", "idi_benefit_period", "elimination_period", "birth_dt", "policy_end_dt", ], impacts=["frame"], ) def _calculate_termination_dt(self): """Calculate benefit termination date if active individual were to become disabled for each policy duration.""" if self.idi_benefit_period[-1] == "M": # pylint: disable=E1136 months = int(self.idi_benefit_period[:-1]) # pylint: disable=E1136 self.frame["TERMINATION_DT"] = ( self.frame["DATE_BD"] + pd.DateOffset(days=self.elimination_period) + pd.DateOffset(months=months) ) elif self.idi_benefit_period[:2] == "TO": # pylint: disable=E1136 self.frame["TERMINATION_DT"] = self.policy_end_dt elif self.idi_benefit_period == "LIFE": self.frame["TERMINATION_DT"] = pd.to_datetime( date( year=self.birth_dt.year + 120, month=self.birth_dt.month, day=self.birth_dt.day, ) ) ##################################################################################### # Step: Model Claim Cost ##################################################################################### @step( name="Model Claim Cost", uses=["frame", "claim_cost_model",], impacts=["modeled_claim_cost"], ) def _model_claim_cost(self): """Model claim cost for active live if policy holder were to become disabled for each policy duration""" # create records record_cols = ["policy_id", "incurred_dt", "valuation_dt", "termination_dt"] frame = self.frame[["DATE_BD", "TERMINATION_DT"]].assign( policy_id=self.policy_id, incurred_dt=lambda df: df.DATE_BD, valuation_dt=lambda df: df.DATE_BD, termination_dt=lambda df: df.TERMINATION_DT, )[record_cols] records = convert_to_records(frame) # model disabled lives kwargs = { kw: getattr(self, kw) for kw in self.claim_cost_model.constant_params if kw not in record_cols + ["claim_id", "idi_diagnosis_grp"] } success, errors = self.claim_cost_model( records=records, claim_id="NA", idi_diagnosis_grp="AG", **kwargs, ) if len(errors) == 0: self.modeled_claim_cost = {y: val for y, val in enumerate(success, start=1)} else: raise ModelRunError(str(errors)) ##################################################################################### # Step: Get Incidence Rate ##################################################################################### @step( name="Get Incidence Rate", uses=[], # idi_assumptions.uses("incidence_rate", "assumption_set"), impacts=["incidence_rates"], ) def _get_incidence_rates(self): """Get incidence rates and multiply by incidence sensitivity to form final rate.""" assumption_func = idi_assumptions.get(self.assumption_set, "incidence_rates") self.incidence_rates = assumption_func(**get_kws(assumption_func, self)) ##################################################################################### # Step: Get Mortality Rates ##################################################################################### @step( name="Get Mortality Rates", uses=[], # assumptions.uses("mortality_rate", "assumption_set"), impacts=["mortality_rates"], ) def _get_mortality_rates(self): """Get lapse rates and multiply by incidence sensitivity to form final rate.""" assumption_func = idi_assumptions.get(self.assumption_set, "mortality_rates") self.mortality_rates = assumption_func(**get_kws(assumption_func, self)) ##################################################################################### # Step: Get Lapse Rates ##################################################################################### @step( name="Get Lapse Rates", uses=[], # assumptions.uses("lapse_rate", "assumption_set"), impacts=["lapse_rates"], ) def _get_lapse_rates(self): """Get lapse rates and multiply by incidence sensitivity to form final rate.""" assumption_func = idi_assumptions.get(self.assumption_set, "lapse_rates") self.lapse_rates = assumption_func(**get_kws(assumption_func, self)) ##################################################################################### # Step: Calculate Premiums ##################################################################################### @step( name="Calculate Premiums", uses=["frame", "gross_premium", "gross_premium_freq"], impacts=["frame"], ) def _calculate_premiums(self): """Calculate premiums for each duration.""" if self.gross_premium_freq == "MONTH" or self.gross_premium_freq == "M": premium = self.gross_premium * 12 elif self.gross_premium_freq == "QUARTER" or self.gross_premium_freq == "Q": premium = self.gross_premium * 4 elif self.gross_premium_freq == "SEMIANNUAL" or self.gross_premium_freq == "S": premium = self.gross_premium * 2 elif self.gross_premium_freq == "ANNUAL" or self.gross_premium_freq == "A": premium = self.gross_premium self.frame["GROSS_PREMIUM"] = premium ##################################################################################### # Step: Calculate Benefit Cost ##################################################################################### @step( name="Calculate Benefit Cost", uses=["frame", "modeled_claim_cost", "incidence_rates"], impacts=["frame"], ) def _calculate_benefit_cost(self): """Calculate benefit cost by multiplying disabled claim cost by final incidence rate for each duration. """ # merge final incidence rate self.frame = self.frame.merge( self.incidence_rates[["AGE_ATTAINED", "INCIDENCE_RATE"]], how="left", on=["AGE_ATTAINED"], ) # add modeled claim cost (i.e., DLR) self.frame["DLR"] = [df["DLR"].iat[0] for df in self.modeled_claim_cost.values()] # calculate benefit cost self.frame["BENEFIT_COST"] = self.frame["DLR"] * self.frame["INCIDENCE_RATE"] ##################################################################################### # Step: Calculate Lives ##################################################################################### @step(name="Calculate Lives", uses=["frame", "lapse_rates"], impacts=["frame"]) def _calculate_lives(self): """Calculate the beginning, middle, and ending lives for each duration using lapse rates.""" # merge mortality rates self.frame = self.frame.merge( self.mortality_rates[["AGE_ATTAINED", "MORTALITY_RATE"]], how="left", on=["AGE_ATTAINED"], ) # merge lapse rates self.frame = self.frame.merge( self.lapse_rates[["DURATION_YEAR", "LAPSE_RATE"]], how="left", on=["DURATION_YEAR"], ) self.frame["LAPSE_RATE"] = self.frame["LAPSE_RATE"].ffill() # calculate lives lives_ed = calc_continuance( self.frame["MORTALITY_RATE"], self.frame["LAPSE_RATE"] ) lives_bd = lives_ed.shift(1, fill_value=1) lives_md = calc_interpolation( val_0=lives_bd, val_1=lives_ed, wt_0=self.frame["WT_BD"], wt_1=self.frame["WT_ED"], method="log", ) # assign lives to frame self.frame["LIVES_BD"] = lives_bd self.frame["LIVES_MD"] = lives_md self.frame["LIVES_ED"] = lives_ed ##################################################################################### # Step: Calculate Discount Factors ##################################################################################### @step( name="Calculate Discount Factors", uses=["frame"], impacts=["frame"], ) def _calculate_discount(self): """Calculate beginning, middle, and ending discount factors for each duration.""" assumption_func = idi_assumptions.get(self.assumption_set, "interest_rate_al") base_int_rate = assumption_func(**get_kws(assumption_func, self)) self.frame["INTEREST_RATE_BASE"] = base_int_rate self.frame["INTEREST_RATE_MODIFIER"] = self.modifier_interest self.frame["INTEREST_RATE"] = ( self.frame["INTEREST_RATE_BASE"] * self.frame["INTEREST_RATE_MODIFIER"] ) self.frame["DISCOUNT_BD"] = calc_discount(self.frame["INTEREST_RATE"], t_adj=0) self.frame["DISCOUNT_MD"] = calc_discount(self.frame["INTEREST_RATE"], t_adj=0.5) self.frame["DISCOUNT_ED"] = calc_discount(self.frame["INTEREST_RATE"]) ##################################################################################### # Step: Calculate Durational ALR ##################################################################################### @step( name="Calculate Durational ALR", uses=["frame", "net_benefit_method"], impacts=["frame"], ) def _calculate_durational_alr(self): """Calculate active life reserves (ALR) for each duration.""" # calculate present value of future benefits benefit_cols = ["BENEFIT_COST", "LIVES_MD", "DISCOUNT_MD"] pvfb = calc_pv(self.frame[benefit_cols].prod(axis=1)) # calculate present value of future premium premium_cols = ["GROSS_PREMIUM", "LIVES_BD", "DISCOUNT_BD"] pvfp = calc_pv(self.frame[premium_cols].prod(axis=1)) # calculate present value of future net benefifts pvfnb = calc_pvfnb(pvfb, pvfp, net_benefit_method=self.net_benefit_method) # calculate alr at end of duration alr_bd = ( (pvfb - pvfnb) / self.frame["LIVES_BD"] / self.frame["DISCOUNT_BD"] ).clip(lower=0) # assign values to frame self.frame["PVFB"] = pvfb self.frame["PVFP"] = pvfp self.frame["PVFNB"] = pvfnb self.frame["ALR_BD"] = alr_bd self.frame["ALR_ED"] = alr_bd.shift(-1, fill_value=0) ##################################################################################### # Step: Calculate Valuation Date ALR ##################################################################################### @step( name="Calculate Valuation Date ALR", uses=["frame", "valuation_dt"], impacts=["frame"], ) def _calculate_valuation_dt_alr(self): """Calculate active life reserves (ALR) for each duration as of valuation date.""" def alr_date(period): return self.valuation_dt + pd.DateOffset(years=period) # filter frame to valuation_dt starting in duration self.frame = self.frame[self.frame["DATE_ED"] >= self.valuation_dt].copy() # create projected alr date column self.frame["ALR_DATE"] = pd.to_datetime( [alr_date(period) for period in range(0, self.frame.shape[0])] ) # calcualte interpolated alr self.frame["ALR"] = calc_interpolation( val_0=self.frame["ALR_BD"], val_1=self.frame["ALR_ED"], wt_0=self.frame["WT_BD"], wt_1=self.frame["WT_ED"], method="log", ).round(2) ##################################################################################### # Step: Create Output Frame ##################################################################################### @step( name="Create Output Frame", uses=[ "frame", "policy_id", "model_version", "last_commit", "run_date_time", "coverage_id", "benefit_amount", ], impacts=["frame"], ) def _to_output(self): """Reduce output to only needed columns.""" self.frame = self.frame.assign( POLICY_ID=self.policy_id, MODEL_VERSION=self.model_version, LAST_COMMIT=self.last_commit, RUN_DATE_TIME=self.run_date_time, SOURCE=self.__class__.__qualname__, COVERAGE_ID=self.coverage_id, BENEFIT_AMOUNT=self.benefit_amount, # set column order )[list(ActiveLivesValOutput.columns)]