From a48331692725ac7ab85820da0d65ecbb6f8ce30b Mon Sep 17 00:00:00 2001 From: MatthewCaseres Date: Thu, 4 Apr 2024 08:19:58 -0500 Subject: [PATCH] some experiments --- Python/basicterm_me_heavylight_numpy.py | 29 +- Python/basicterm_me_recursive_numpy.py | 2 +- Python/benchmark_results.yaml | 41 +- Python/notebook.ipynb | 576 +++++------------------- Python/savings_me_heavylight.py | 473 +++++++++++++++++++ 5 files changed, 637 insertions(+), 484 deletions(-) create mode 100644 Python/savings_me_heavylight.py diff --git a/Python/basicterm_me_heavylight_numpy.py b/Python/basicterm_me_heavylight_numpy.py index 2e041ca..83c715f 100644 --- a/Python/basicterm_me_heavylight_numpy.py +++ b/Python/basicterm_me_heavylight_numpy.py @@ -10,21 +10,21 @@ premium_table = pd.read_excel("BasicTerm_ME/premium_table.xlsx", index_col=[0,1]) class ModelPoints: - def __init__(self, model_point_table: pd.DataFrame, premium_table: pd.DataFrame): + def __init__(self, model_point_table: pd.DataFrame, premium_table: pd.DataFrame, size_multiplier: int = 1): self.table = model_point_table.merge(premium_table, left_on=["age_at_entry", "policy_term"], right_index=True) self.table.sort_values(by="policy_id", inplace=True) self.table["premium_pp"] = np.around(self.table["sum_assured"] * self.table["premium_rate"],2) - self.premium_pp = self.table["premium_pp"].to_numpy() - self.duration_mth = self.table["duration_mth"].to_numpy() - self.age_at_entry = self.table["age_at_entry"].to_numpy() - self.sum_assured = self.table["sum_assured"].to_numpy() - self.policy_count = self.table["policy_count"].to_numpy() - self.policy_term = self.table["policy_term"].to_numpy() + self.premium_pp = np.tile(self.table["premium_pp"].to_numpy(), size_multiplier) + self.duration_mth = np.tile(self.table["duration_mth"].to_numpy(), size_multiplier) + self.age_at_entry = np.tile(self.table["age_at_entry"].to_numpy(), size_multiplier) + self.sum_assured = np.tile(self.table["sum_assured"].to_numpy(), size_multiplier) + self.policy_count = np.tile(self.table["policy_count"].to_numpy(), size_multiplier) + self.policy_term = np.tile(self.table["policy_term"].to_numpy(), size_multiplier) self.max_proj_len: int = np.max(12 * self.policy_term - self.duration_mth) + 1 class Assumptions: def __init__(self, disc_rate_ann: pd.DataFrame, mort_table: pd.DataFrame): - self.disc_rate_ann = disc_rate_ann["zero_spot"].values + self.disc_rate_ann = disc_rate_ann["zero_spot"].to_numpy() self.mort_table = mort_table.to_numpy() def get_mortality(self, age, duration): @@ -32,7 +32,7 @@ def get_mortality(self, age, duration): class TermME(LightModel): def __init__(self, mp: ModelPoints, assume: Assumptions): - super().__init__() + super().__init__(storage_function=lambda x: np.sum(x)) self.mp = mp self.assume = assume @@ -97,6 +97,14 @@ def mort_rate_mth(self, t): def net_cf(self, t): return self.premiums(t) - self.claims(t) - self.expenses(t) - self.commissions(t) + def aggregated_discounted_net_cf(self, t): + return np.sum(self.net_cf(t)) * self.discount(t) + + def accumulated_discounted_net_cf(self, t): + if t < 0: + return 0 + return self.accumulated_discounted_net_cf(t-1) + self.aggregated_discounted_net_cf(t) + def pols_death(self, t): return self.pols_if_at(t, "BEF_DECR") * self.mort_rate_mth(t) @@ -137,8 +145,7 @@ def premiums(self, t): def basicterm_me_heavylight_numpy(): model.ResetCache() - tot = sum(np.sum(model.premiums(t) - model.claims(t) - model.expenses(t) - model.commissions(t)) \ - * model.discount(t) for t in range(model.mp.max_proj_len)) + tot = sum(np.sum(model.net_cf(t)) * model.discount(t) for t in range(model.mp.max_proj_len)) return float(tot) if __name__ == "__main__": diff --git a/Python/basicterm_me_recursive_numpy.py b/Python/basicterm_me_recursive_numpy.py index f307480..b66419d 100644 --- a/Python/basicterm_me_recursive_numpy.py +++ b/Python/basicterm_me_recursive_numpy.py @@ -80,7 +80,7 @@ def disc_rate_mth(): @cash def duration(t): - return duration_mth(t) //12 + return duration_mth(t) // 12 @cash def duration_mth(t): diff --git a/Python/benchmark_results.yaml b/Python/benchmark_results.yaml index c0f7674..3372464 100644 --- a/Python/benchmark_results.yaml +++ b/Python/benchmark_results.yaml @@ -1,34 +1,37 @@ basic_term_benchmark: Python array numpy basic_term_m: - minimum time: 79.0311409999731 milliseconds - result: 14489630.534603368 + minimum time: 95.96395771950483 milliseconds + result: 14489630.534603955 Python array pytorch basic_term_m: - minimum time: 45.24396900001193 milliseconds - result: 14489630.534603368 + minimum time: 86.49199921637774 milliseconds + result: 14489630.534603959 Python lifelib basic_term_m: - minimum time: 614.4032699999684 milliseconds - result: 14489630.534601536 + minimum time: 528.3367084339261 milliseconds + result: 14489630.534602122 Python recursive numpy basic_term_m: - minimum time: 46.281483000029766 milliseconds - result: 14489630.534603368 + minimum time: 52.718209102749825 milliseconds + result: 14489630.534603957 Python recursive pytorch basic_term_m: - minimum time: 72.29064599999901 milliseconds - result: 14489630.53460337 + minimum time: 85.5019586160779 milliseconds + result: 14489630.534603959 basic_term_me_benchmark: Python heavylight numpy basic_term_me: - minimum time: 343.96580999998605 milliseconds - result: 215146132.0684811 + minimum time: 230.15966545790434 milliseconds + result: 215146132.06850433 Python lifelib basic_term_me: - minimum time: 1146.6455289999544 milliseconds - result: 215146132.06848112 + minimum time: 1459.7130427137017 milliseconds + result: 215146132.068504 Python recursive numpy basic_term_me: - minimum time: 320.24258900003133 milliseconds - result: 215146132.0684814 + minimum time: 178.3146671950817 milliseconds + result: 215146132.06850427 mortality: Python PyMort: - minimum time: 9.000889000020607 milliseconds + minimum time: 5.112958140671253 milliseconds result: 1904.4865526636793 savings_benchmark: Python lifelib cashvalue_me_ex4: - minimum time: 585.2152809999893 milliseconds - result: 3507113709040.141 + minimum time: 1246.7584162950516 milliseconds + result: 3507113709040.142 + Python recursive numpy cashvalue_me_ex4: + minimum time: 350.6229165941477 milliseconds + result: 3507113709040.124 diff --git a/Python/notebook.ipynb b/Python/notebook.ipynb index aec79fc..423583d 100644 --- a/Python/notebook.ipynb +++ b/Python/notebook.ipynb @@ -1,530 +1,200 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "UserWarning: Existing model 'CashValue_ME_EX4' renamed to 'CashValue_ME_EX4_BAK1'\n" - ] - }, - { - "data": { - "text/plain": [ - "0.6307517449999978" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "import lifelib\n", - "import timeit\n", - "import pandas as pd\n", + "from basicterm_me_heavylight_numpy import (\n", + " model_point_table,\n", + " premium_table,\n", + " ModelPoints,\n", + " assume,\n", + " model,\n", + " TermME,\n", + ")\n", "import numpy as np\n", - "import modelx as mx\n", - "import openpyxl\n", + "import pandas as pd\n", + "from heavylight import LightModel\n", + "from typing import Callable\n", + "from heavylight.memory_optimized_cache import FunctionCall\n", + "\n", + "\n", + "def calculate_cache_graph_size(model: LightModel):\n", + " cg = model.cache_graph\n", + " return sum(\n", + " np.array(val).nbytes for cache in cg.caches.values() for val in cache.values()\n", + " )\n", "\n", - "ex4 = mx.read_model('CashValue_ME_EX4')\n", - "Projection = ex4.Projection\n", "\n", - "timeit.timeit('ex4.Projection.result_pv()', globals=globals(), number=5)" + "def run_and_check_cache_size(\n", + " model: LightModel,\n", + " proj_len: int,\n", + " should_track_cache_size: Callable[[int], bool] = lambda t: False\n", + "):\n", + " sizes = {}\n", + " for t in range(proj_len + 1):\n", + " max_cache_size = cache_size = 0\n", + " for func in model._single_param_timestep_funcs:\n", + " if (\n", + " FunctionCall(func._func.__name__, (t,), frozenset())\n", + " in model.cache_graph.all_calls\n", + " ):\n", + " continue\n", + " func(t)\n", + " if should_track_cache_size(t):\n", + " cache_size = calculate_cache_graph_size(model)\n", + " max_cache_size = max(max_cache_size, cache_size)\n", + " if should_track_cache_size(t):\n", + " sizes[t] = max_cache_size\n", + " return sizes\n", + "\n", + "\n", + "def get_can_clear(model_miniature: TermME, optimization_proj_len, should_track_cache_size: Callable):\n", + " optimization_cache_sizes = run_and_check_cache_size(\n", + " model_miniature, optimization_proj_len, should_track_cache_size\n", + " )\n", + " model_miniature.OptimizeMemoryAndReset()\n", + " return model_miniature.cache_graph.can_clear, optimization_cache_sizes" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 12, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "model_point(): spec_id age_at_entry sex policy_term policy_count \\\n", - "point_id scen_id \n", - "1 1 A 20 M 10 100 \n", - " 2 A 20 M 10 100 \n", - " 3 A 20 M 10 100 \n", - " 4 A 20 M 10 100 \n", - " 5 A 20 M 10 100 \n", - "... ... ... .. ... ... \n", - "9 996 A 20 M 10 100 \n", - " 997 A 20 M 10 100 \n", - " 998 A 20 M 10 100 \n", - " 999 A 20 M 10 100 \n", - " 1000 A 20 M 10 100 \n", - "\n", - " sum_assured duration_mth premium_pp av_pp_init \\\n", - "point_id scen_id \n", - "1 1 500000 0 500000 0 \n", - " 2 500000 0 500000 0 \n", - " 3 500000 0 500000 0 \n", - " 4 500000 0 500000 0 \n", - " 5 500000 0 500000 0 \n", - "... ... ... ... ... \n", - "9 996 500000 0 300000 0 \n", - " 997 500000 0 300000 0 \n", - " 998 500000 0 300000 0 \n", - " 999 500000 0 300000 0 \n", - " 1000 500000 0 300000 0 \n", - "\n", - " accum_prem_init_pp premium_type has_surr_charge \\\n", - "point_id scen_id \n", - "1 1 0 SINGLE False \n", - " 2 0 SINGLE False \n", - " 3 0 SINGLE False \n", - " 4 0 SINGLE False \n", - " 5 0 SINGLE False \n", - "... ... ... ... \n", - "9 996 0 SINGLE False \n", - " 997 0 SINGLE False \n", - " 998 0 SINGLE False \n", - " 999 0 SINGLE False \n", - " 1000 0 SINGLE False \n", - "\n", - " surr_charge_id load_prem_rate is_wl \n", - "point_id scen_id \n", - "1 1 NaN 0.0 False \n", - " 2 NaN 0.0 False \n", - " 3 NaN 0.0 False \n", - " 4 NaN 0.0 False \n", - " 5 NaN 0.0 False \n", - "... ... ... ... \n", - "9 996 NaN 0.0 False \n", - " 997 NaN 0.0 False \n", - " 998 NaN 0.0 False \n", - " 999 NaN 0.0 False \n", - " 1000 NaN 0.0 False \n", - "\n", - "[9000 rows x 15 columns]\n", - "with indices: MultiIndex([(1, 1),\n", - " (1, 2),\n", - " (1, 3),\n", - " (1, 4),\n", - " (1, 5),\n", - " (1, 6),\n", - " (1, 7),\n", - " (1, 8),\n", - " (1, 9),\n", - " (1, 10),\n", - " ...\n", - " (9, 991),\n", - " (9, 992),\n", - " (9, 993),\n", - " (9, 994),\n", - " (9, 995),\n", - " (9, 996),\n", - " (9, 997),\n", - " (9, 998),\n", - " (9, 999),\n", - " (9, 1000)],\n", - " names=['point_id', 'scen_id'], length=9000)\n" - ] - } - ], + "outputs": [], "source": [ - "# Projection.model_point_table = Projection.model_point_1\n", - "table = Projection.model_point_table\n", - "# print(\"Number of model points: \", len(table))\n", - "# print(\"Model points: \", table)\n", - "# points = Projection.model_point_table_ext()\n", - "# points = Projection.model_point()[\"scen_id\"].values[990:1010]\n", - "points = Projection.model_point()\n", - "print(\"model_point(): \", points)\n", - "print(\"with indices: \", points.index)" + "mp = ModelPoints(model_point_table, premium_table)\n", + "mp_big = ModelPoints(model_point_table, premium_table, size_multiplier=10)\n", + "mp_huge = ModelPoints(model_point_table, premium_table, size_multiplier=100)\n", + "mp_huger = ModelPoints(model_point_table, premium_table, size_multiplier=1000)\n", + "mp_monster = ModelPoints(model_point_table, premium_table, size_multiplier=10000)\n", + "mp_miniature = ModelPoints(model_point_table[:1], premium_table)\n", + "model = TermME(mp, assume)\n", + "model_miniature = TermME(mp_miniature, assume)\n", + "model_big = TermME(mp_big, assume)\n", + "model_huge = TermME(mp_huge, assume)\n", + "model_huger = TermME(mp_huger, assume)\n", + "model_monster = TermME(mp_monster, assume)\n", + "shared_should_log = lambda t: (t % 10 == 0)\n", + "can_clear, optimization_cache_sizes = get_can_clear(model_miniature, 277, shared_should_log) # slow because O(N^2) in timesteps, counting bytes" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(9000,)\n", - "900000.0\n", - "[100. 100. 100. ... 100. 100. 100.]\n" - ] - } - ], + "outputs": [], "source": [ - "pols = ex4.Projection.pols_if_at(12, \"BEF_DECR\")\n", - "print(np.shape(pols))\n", - "print(sum(pols))\n", - "print(pols)" + "def reset_preserve_clearable(model: LightModel, can_clear):\n", + " model.ResetCache()\n", + " model.cache_graph.can_clear = can_clear" ] }, { - "cell_type": "code", - "execution_count": 4, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([100., 100., 100., ..., 100., 100., 100.])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "Projection.pols_if(1)" + "## Memory savings graph" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 16, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "399477611.70743275" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "Projection.result_pv()[\"Net Cashflow\"].groupby(\"point_id\").mean().sum()" + "model.ResetCache()\n", + "cache_sizes_uncleared = run_and_check_cache_size(model, 277, shared_should_log)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "reset_preserve_clearable(model, can_clear)\n", + "cache_sizes_cleared = run_and_check_cache_size(model, 277, shared_should_log)" + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "121\n" - ] - }, - { - "data": { - "text/plain": [ - "array([50000000., 50000000., 50000000., ..., 30000000., 30000000.,\n", - " 30000000.])" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "print(ex4.Projection.max_proj_len())\n", - "ex4.Projection.pv_premiums()" + "import matplotlib.pyplot as plt\n", + "\n", + "results_df = pd.DataFrame({\n", + " 'timestep': list(cache_sizes_uncleared.keys()),\n", + " 'cache_size_uncleared': list(cache_sizes_uncleared.values()),\n", + " 'cache_size_cleared': list(cache_sizes_cleared.values()),\n", + " 'optimization_cache_size': list(optimization_cache_sizes.values())\n", + "})\n", + "results_df[\"Memory Reduction\"] = results_df[\"cache_size_uncleared\"] / results_df[\"cache_size_cleared\"]\n", + "results_df_truncated = results_df[:5]" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([100, 100, 100, ..., 100, 100, 100])" + "Text(0, 0.5, 'cache size ratio, uncleared / cleared')" ] }, - "execution_count": 18, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" - } - ], - "source": [ - "ex4.Projection.pols_new_biz(0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Montlhy investment returns: [ 0.00807793 -0.00048898 -0.00302246 ... -0.00917993 -0.00629737\n", - " -0.00596671]\n", - "with shape: (9000,)\n" - ] - } - ], - "source": [ - "inv = Projection.inv_return_mth(2)\n", - "print(\"Montlhy investment returns: \", inv)\n", - "print(\"with shape: \", np.shape(inv))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + }, { "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
PremiumsDeathSurrenderMaturityExpensesCommissionsInvestment IncomeChange in AVNet Cashflow
point_idscen_id
1150000000.00.00.05.765190e+07975895.9511472500000.01.793864e+071.028674e+07-3.475896e+06
250000000.00.00.04.781116e+07975895.9511472500000.07.638184e+069.827021e+06-3.475896e+06
350000000.00.00.05.184905e+07975895.9511472500000.01.232610e+071.047706e+07-3.475896e+06
450000000.00.00.04.752251e+07975895.9511472500000.07.454824e+069.932312e+06-3.475896e+06
550000000.00.00.05.796074e+07975895.9511472500000.01.852191e+071.056117e+07-3.475896e+06
.................................
999630000000.00.00.04.093654e+07975895.9511471500000.04.256529e+065.753036e+06-1.490894e+07
99730000000.00.00.04.093654e+07975895.9511471500000.07.287750e+066.331561e+06-1.245624e+07
99830000000.00.00.04.093654e+07975895.9511471500000.07.480443e+066.031063e+06-1.196305e+07
99930000000.00.00.04.093654e+07975895.9511471500000.01.098676e+076.345723e+06-8.771397e+06
100030000000.00.00.04.093654e+07975895.9511471500000.08.407759e+066.481302e+06-1.148598e+07
\n", - "

9000 rows × 9 columns

\n", - "
" - ], + "image/png": "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", "text/plain": [ - " Premiums Death Surrender Maturity Expenses \\\n", - "point_id scen_id \n", - "1 1 50000000.0 0.0 0.0 5.765190e+07 975895.951147 \n", - " 2 50000000.0 0.0 0.0 4.781116e+07 975895.951147 \n", - " 3 50000000.0 0.0 0.0 5.184905e+07 975895.951147 \n", - " 4 50000000.0 0.0 0.0 4.752251e+07 975895.951147 \n", - " 5 50000000.0 0.0 0.0 5.796074e+07 975895.951147 \n", - "... ... ... ... ... ... \n", - "9 996 30000000.0 0.0 0.0 4.093654e+07 975895.951147 \n", - " 997 30000000.0 0.0 0.0 4.093654e+07 975895.951147 \n", - " 998 30000000.0 0.0 0.0 4.093654e+07 975895.951147 \n", - " 999 30000000.0 0.0 0.0 4.093654e+07 975895.951147 \n", - " 1000 30000000.0 0.0 0.0 4.093654e+07 975895.951147 \n", - "\n", - " Commissions Investment Income Change in AV Net Cashflow \n", - "point_id scen_id \n", - "1 1 2500000.0 1.793864e+07 1.028674e+07 -3.475896e+06 \n", - " 2 2500000.0 7.638184e+06 9.827021e+06 -3.475896e+06 \n", - " 3 2500000.0 1.232610e+07 1.047706e+07 -3.475896e+06 \n", - " 4 2500000.0 7.454824e+06 9.932312e+06 -3.475896e+06 \n", - " 5 2500000.0 1.852191e+07 1.056117e+07 -3.475896e+06 \n", - "... ... ... ... ... \n", - "9 996 1500000.0 4.256529e+06 5.753036e+06 -1.490894e+07 \n", - " 997 1500000.0 7.287750e+06 6.331561e+06 -1.245624e+07 \n", - " 998 1500000.0 7.480443e+06 6.031063e+06 -1.196305e+07 \n", - " 999 1500000.0 1.098676e+07 6.345723e+06 -8.771397e+06 \n", - " 1000 1500000.0 8.407759e+06 6.481302e+06 -1.148598e+07 \n", - "\n", - "[9000 rows x 9 columns]" + "
" ] }, - "execution_count": 57, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "Projection.result_pv()" + "plt.plot(results_df.timestep, results_df[\"Memory Reduction\"], label='Memory Reduction')\n", + "# title is cache size reduction\n", + "plt.title(\"Cache Size Reduction\")\n", + "plt.xlabel(\"Timestep\")\n", + "plt.ylabel(\"cache size ratio, uncleared / cleared\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "model.mp = mp_big\n", + "model.OptimizeMemoryAndReset()\n", + "cache_sizes_optimized = run_and_check_cache_size(model, 5)" + ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "## working" + ] } ], "metadata": { @@ -543,7 +213,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.10.8" }, "orig_nbformat": 4 }, diff --git a/Python/savings_me_heavylight.py b/Python/savings_me_heavylight.py new file mode 100644 index 0000000..245b712 --- /dev/null +++ b/Python/savings_me_heavylight.py @@ -0,0 +1,473 @@ +from collections import defaultdict +from functools import wraps +import pandas as pd +import numpy as np +from heavylight import LightModel + +class Cash: + def __init__(self): + self.reset() + + def reset(self): + self.caches = defaultdict(dict) + + def __call__(self, func): + @wraps(func) + def wrapper(*args, **kwargs): + key = (args, frozenset(kwargs.items())) + if key not in self.caches[func.__name__]: + self.caches[func.__name__][key] = func(*args, **kwargs) + return self.caches[func.__name__][key] + + return wrapper + +cash = Cash() + +disc_rate_ann = np.array(pd.read_excel("./CashValue_ME_EX4/disc_rate_ann.xlsx")["zero_spot"]) +disc_rate_arr = np.concatenate([[1], np.cumprod((1+np.repeat(disc_rate_ann, 12)) ** (-1/12))]) +mort_table = pd.read_excel("./CashValue_ME_EX4/mort_table.xlsx") +surr_charge_table = pd.read_excel("./CashValue_ME_EX4/surr_charge_table.xlsx") +product_spec_table = pd.read_excel("./CashValue_ME_EX4/product_spec_table.xlsx") +model_point_table = pd.read_csv("./CashValue_ME_EX4/model_point_table_10K.csv") +model_point_table_ext = model_point_table.merge(product_spec_table, on='spec_id') +model_point_moneyness = pd.read_excel("./CashValue_ME_EX4/model_point_moneyness.xlsx") +scen_id = 1 +scen_size = 1 + +@cash +def age(t): + return age_at_entry() + duration(t) + +@cash +def age_at_entry(): + return model_point()["age_at_entry"].values + +@cash +def av_at(t, timing): + if timing == "BEF_MAT": + return av_pp_at(t, "BEF_PREM") * pols_if_at(t, "BEF_MAT") + elif timing == "BEF_NB": + return av_pp_at(t, "BEF_PREM") * pols_if_at(t, "BEF_NB") + elif timing == "BEF_FEE": + return av_pp_at(t, "BEF_FEE") * pols_if_at(t, "BEF_DECR") + else: + raise ValueError("invalid timing") + +@cash +def av_change(t): + return av_at(t+1, 'BEF_MAT') - av_at(t, 'BEF_MAT') + +@cash +def av_pp_at(t, timing): + if timing == "BEF_PREM": + if t == 0: + return av_pp_init() + else: + return av_pp_at(t-1, "BEF_INV") + inv_income_pp(t-1) + elif timing == "BEF_FEE": + return av_pp_at(t, "BEF_PREM") + prem_to_av_pp(t) + elif timing == "BEF_INV": + return av_pp_at(t, "BEF_FEE") - maint_fee_pp(t) - coi_pp(t) + elif timing == "MID_MTH": + return av_pp_at(t, "BEF_INV") + 0.5 * inv_income_pp(t) + else: + raise ValueError("invalid timing") + +@cash +def av_pp_init(): + return model_point()["av_pp_init"].values + +@cash +def claim_net_pp(t, kind): + if kind == "DEATH": + return claim_pp(t, "DEATH") - av_pp_at(t, "MID_MTH") + elif kind == "LAPSE": + return 0 + elif kind == "MATURITY": + return claim_pp(t, "MATURITY") - av_pp_at(t, "BEF_PREM") + else: + raise ValueError("invalid kind") + +@cash +def claim_pp(t, kind): + if kind == "DEATH": + return np.maximum(sum_assured(), av_pp_at(t, "MID_MTH")) + elif kind == "LAPSE": + return av_pp_at(t, "MID_MTH") + elif kind == "MATURITY": + return np.maximum(sum_assured(), av_pp_at(t, "BEF_PREM")) + else: + raise ValueError("invalid kind") + +@cash +def claims(t, kind=None): + if kind == "DEATH": + return claim_pp(t, "DEATH") * pols_death(t) + elif kind == "LAPSE": + return claims_from_av(t, "LAPSE") - surr_charge(t) + elif kind == "MATURITY": + return claim_pp(t, "MATURITY") * pols_maturity(t) + elif kind is None: + return sum(claims(t, k) for k in ["DEATH", "LAPSE", "MATURITY"]) + else: + raise ValueError("invalid kind") + +@cash +def claims_from_av(t, kind): + if kind == "DEATH": + return av_pp_at(t, "MID_MTH") * pols_death(t) + elif kind == "LAPSE": + return av_pp_at(t, "MID_MTH") * pols_lapse(t) + elif kind == "MATURITY": + return av_pp_at(t, "BEF_PREM") * pols_maturity(t) + else: + raise ValueError("invalid kind") + +@cash +def claims_over_av(t, kind): + return claims(t, kind) - claims_from_av(t, kind) + +@cash +def coi(t): + return coi_pp(t) * pols_if_at(t, "BEF_DECR") + +@cash +def coi_pp(t): + return coi_rate(t) * net_amt_at_risk(t) + +@cash +def coi_rate(t): + return 0 #1.1 * mort_rate_mth(t) + +@cash +def commissions(t): + return 0.05 * premiums(t) + +@cash +def disc_factors(): + return disc_rate_arr[:max_proj_len()] + +@cash +def duration(t): + return duration_mth(t) // 12 + +@cash +def duration_mth(t): + if t == 0: + return model_point()['duration_mth'].values + else: + return duration_mth(t-1) + 1 + +@cash +def expense_acq(): + return 5000 + +@cash +def expense_maint(): + return 500 + +@cash +def expenses(t): + return expense_acq() * pols_new_biz(t) \ + + pols_if_at(t, "BEF_DECR") * expense_maint()/12 * inflation_factor(t) + +@cash +def has_surr_charge(): + return model_point()['has_surr_charge'].values + +@cash +def inflation_factor(t): + return (1 + inflation_rate())**(t/12) + +@cash +def inflation_rate(): + return 0.01 + +@cash +def inv_income(t): + return (inv_income_pp(t) * pols_if_at(t+1, "BEF_MAT") + + 0.5 * inv_income_pp(t) * (pols_death(t) + pols_lapse(t))) + +@cash +def inv_income_pp(t): + return inv_return_mth(t) * av_pp_at(t, "BEF_INV") + +@cash +def inv_return_mth(t): + return inv_return_table()[:, t] + +@cash +def inv_return_table(): + mu = 0.02 + sigma = 0.03 + dt = 1/12 + + return np.tile(np.exp( + (mu - 0.5 * sigma**2) * dt + sigma * dt**0.5 * std_norm_rand()) - 1, + (point_size(), 1)) + +@cash +def is_wl(): + return model_point()['is_wl'].values + +@cash +def lapse_rate(t): + return 0 + +@cash +def load_prem_rate(): + return model_point()['load_prem_rate'].values + +@cash +def maint_fee(t): + return maint_fee_pp(t) * pols_if_at(t, "BEF_DECR") + +@cash +def maint_fee_pp(t): + return maint_fee_rate() * av_pp_at(t, "BEF_FEE") + +@cash +def maint_fee_rate(): + return 0 # 0.01 / 12 + +@cash +def margin_expense(t): + return (load_prem_rate()* premium_pp(t) * pols_if_at(t, "BEF_DECR") + + surr_charge(t) + + maint_fee(t) + - commissions(t) + - expenses(t)) + +@cash +def margin_mortality(t): + return coi(t) - claims_over_av(t, 'DEATH') + +@cash +def max_proj_len(): + return max(proj_len()) + +@cash +def model_point(): + mps = model_point_table_ext + + idx = pd.MultiIndex.from_product( + [mps.index, scen_index()], + names = mps.index.names + scen_index().names + ) + + res = pd.DataFrame( + np.repeat(mps.values, len(scen_index()), axis=0), + index=idx, + columns=mps.columns + ) + + return res.astype(mps.dtypes) + +@cash +def mort_rate(t): + return np.zeros(len(model_point().index)) + +@cash +def mort_rate_mth(t): + return 1-(1- mort_rate(t))**(1/12) + +@cash +def mort_table_last_age(): + return 102 # original implementation contained a bug, hard coding for now + +@cash +def mort_table_reindexed(): + result = [] + for col in mort_table.columns: + df = mort_table[[col]] + df = df.assign(Duration=int(col)).set_index('Duration', append=True)[col] + result.append(df) + + return pd.concat(result) + +@cash +def net_amt_at_risk(t): + return np.maximum(sum_assured() - av_pp_at(t, 'BEF_FEE'), 0) + +@cash +def net_cf(t): + return (premiums(t) + + inv_income(t) - claims(t) - expenses(t) - commissions(t) - av_change(t)) + +@cash +def policy_term(): + return (is_wl() * (mort_table_last_age() - age_at_entry()) + + (is_wl() == False) * model_point()["policy_term"].values) + +@cash +def pols_death(t): + return pols_if_at(t, "BEF_DECR") * mort_rate_mth(t) + +@cash +def pols_if(t): + return pols_if_at(t, "BEF_MAT") + +@cash +def pols_if_at(t, timing): + if timing == "BEF_MAT": + if t == 0: + return pols_if_init() + else: + return pols_if_at(t-1, "BEF_DECR") - pols_lapse(t-1) - pols_death(t-1) + elif timing == "BEF_NB": + return pols_if_at(t, "BEF_MAT") - pols_maturity(t) + elif timing == "BEF_DECR": + return pols_if_at(t, "BEF_NB") + pols_new_biz(t) + else: + raise ValueError("invalid timing") + +@cash +def pols_if_init(): + return model_point()["policy_count"].where(duration_mth(0) > 0, other=0).values + +@cash +def pols_lapse(t): + return (pols_if_at(t, "BEF_DECR") - pols_death(t)) * (1-(1 - lapse_rate(t))**(1/12)) + +@cash +def pols_maturity(t): + return (duration_mth(t) == policy_term() * 12) * pols_if_at(t, "BEF_MAT") + +@cash +def pols_new_biz(t): + return model_point()['policy_count'].values * (duration_mth(t) == 0) + +@cash +def prem_to_av(t): + return prem_to_av_pp(t) * pols_if_at(t, "BEF_DECR") + +@cash +def prem_to_av_pp(t): + return (1 - load_prem_rate()) * premium_pp(t) + +@cash +def premium_pp(t): + return model_point()['premium_pp'].values * ((premium_type() == 'SINGLE') & (duration_mth(t) == 0) | + (premium_type() == 'LEVEL') & (duration_mth(t) < 12 * policy_term())) + +@cash +def premium_type(): + return model_point()['premium_type'].values + +@cash +def premiums(t): + return premium_pp(t) * pols_if_at(t, "BEF_DECR") + +@cash +def proj_len(): + return np.maximum(12 * policy_term() - duration_mth(0) + 1, 0) + +@cash +def pv_av_change(): + return sum(av_change(t) * disc_rate_arr[t] for t in range(max_proj_len())) + +@cash +def pv_claims(kind=None): + return sum(claims(t, kind) * disc_rate_arr[t] for t in range(max_proj_len())) + +@cash +def pv_commissions(): + return sum(commissions(t) * disc_rate_arr[t] for t in range(max_proj_len())) + +@cash +def pv_expenses(): + return sum(expenses(t) * disc_rate_arr[t] for t in range(max_proj_len())) + +@cash +def pv_inv_income(): + return sum(inv_income(t) * disc_rate_arr[t] for t in range(max_proj_len())) + +@cash +def pv_pols_if(): + return sum(pols_if_at(t, "BEF_DECR") for t in range(max_proj_len())) + +@cash +def pv_premiums(): + return sum(premiums(t) * disc_rate_arr[t] for t in range(max_proj_len())) + +@cash +def pv_net_cf(): + return (pv_premiums() + + pv_inv_income() + - pv_claims() + - pv_expenses() + - pv_commissions() + - pv_av_change()) + +@cash +def result_pv(): + data = { + "Premiums": pv_premiums(), + "Death": pv_claims("DEATH"), + "Surrender": pv_claims("LAPSE"), + "Maturity": pv_claims("MATURITY"), + "Expenses": pv_expenses(), + "Commissions": pv_commissions(), + "Investment Income": pv_inv_income(), + "Change in AV": pv_av_change(), + "Net Cashflow": pv_net_cf() + } + return pd.DataFrame(data, index=model_point().index) + +@cash +def scen_index(): + return pd.Index(range(1, scen_size + 1), name='scen_id') + +@cash +def sex(): + return model_point()["sex"].values + +@cash +def std_norm_rand(): + if hasattr(np.random, 'default_rng'): + gen = np.random.default_rng(1234) + rnd = gen.standard_normal((scen_size, 242)) + else: + np.random.seed(1234) + rnd = np.random.standard_normal(size=(scen_size, 242)) + return rnd + +@cash +def sum_assured(): + return model_point()['sum_assured'].values + +@cash +def surr_charge(t): + return surr_charge_rate(t) * av_pp_at(t, "MID_MTH") * pols_lapse(t) + +@cash +def surr_charge_id(): + return model_point()['surr_charge_id'].values.astype(str) + +@cash +def surr_charge_max_idx(): + return max(surr_charge_table.index) + +@cash +def surr_charge_rate(t): + ind_row = np.minimum(duration(t), surr_charge_max_idx()) + return surr_charge_table.values.flat[surr_charge_table_column() + ind_row * len(surr_charge_table.columns)] + +@cash +def surr_charge_table_column(): + return surr_charge_table.columns.searchsorted(surr_charge_id(), side='right') - 1 + +@cash +def surr_charge_table_stacked(): + return surr_charge_table.stack().reorder_levels([1, 0]).sort_index() + +@cash +def point_size(): + return len(model_point_table_ext) + +def savings_me_recursive_numpy(): + cash.reset() # Ensure the cache is clear before running calculations + return float(np.sum(pv_net_cf())) + +if __name__ == "__main__": + print(savings_me_recursive_numpy()) \ No newline at end of file