Skip to content

Commit 0e831ce

Browse files
author
jsabuco
authored
Merge pull request #5 from EconomicSL/master
Non-prpoportional
2 parents 43a9cde + d3edf59 commit 0e831ce

9 files changed

+274
-11
lines changed

distribution_wrapper_test.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import scipy.stats
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
from distributiontruncated import TruncatedDistWrapper
5+
from distributionreinsurance import ReinsuranceDistWrapper
6+
import pdb
7+
8+
non_truncated_dist = scipy.stats.pareto(b=2, loc=0, scale=0.5)
9+
truncated_dist = TruncatedDistWrapper(lower_bound=0.6, upper_bound=1., dist=non_truncated_dist)
10+
reinsurance_dist = ReinsuranceDistWrapper(lower_bound=0.85, upper_bound=0.95, dist=truncated_dist)
11+
12+
x1 = np.linspace(non_truncated_dist.ppf(0.01), non_truncated_dist.ppf(0.99), 100)
13+
x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.), 100)
14+
x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.), 100)
15+
x_val_1 = reinsurance_dist.lower_bound
16+
x_val_2 = truncated_dist.upper_bound - (reinsurance_dist.upper_bound - reinsurance_dist.lower_bound)
17+
x_val_3 = reinsurance_dist.upper_bound
18+
x_val_4 = truncated_dist.upper_bound
19+
20+
fig, ax = plt.subplots(1, 1)
21+
ax.plot(x1, non_truncated_dist.pdf(x1), 'k-', lw=2, label='non-truncated pdf')
22+
ax.plot(x1, non_truncated_dist.cdf(x1), 'g-', lw=2, label='non-truncated cdf')
23+
ax.plot(x2, truncated_dist.pdf(x2), 'r-', lw=2, label='truncated pdf')
24+
ax.plot(x2, truncated_dist.cdf(x2), 'm-', lw=2, label='truncated cdf')
25+
ax.plot(x3, reinsurance_dist.pdf(x3), 'b-', lw=2, label='reinsurance pdf')
26+
ax.plot(x3, reinsurance_dist.cdf(x3), 'c-', lw=2, label='reinsurance cdf')
27+
ax.set_xlim(0.45, 1.25)
28+
ax.set_ylim(0, 5)
29+
ax.arrow(x_val_1, reinsurance_dist.cdf(x_val_1), x_val_3 - x_val_1, truncated_dist.cdf(x_val_3) - reinsurance_dist.cdf(x_val_1), head_width=0, head_length=0, fc='m', ec='m', ls=':')
30+
ax.arrow(x_val_2, reinsurance_dist.cdf(x_val_2), x_val_4 - x_val_2, truncated_dist.cdf(x_val_4) - reinsurance_dist.cdf(x_val_2), head_width=0, head_length=0, fc='m', ec='m', ls=':')
31+
ax.arrow(x_val_1, reinsurance_dist.pdf(x_val_1+0.00001), x_val_3 - x_val_1, truncated_dist.pdf(x_val_3) - reinsurance_dist.pdf(x_val_1+0.00001), head_width=0, head_length=0, fc='r', ec='r', ls=':')
32+
ax.arrow(x_val_2, reinsurance_dist.pdf(x_val_2), x_val_4 - x_val_2, truncated_dist.pdf(x_val_4) - reinsurance_dist.pdf(x_val_2), head_width=0, head_length=0, fc='r', ec='r', ls=':')
33+
sample = reinsurance_dist.rvs(size=100000)
34+
#sample = sample[sample < scipy.percentile(sample, 90)]
35+
ax.hist(sample, normed=True, histtype='stepfilled', alpha=0.4)
36+
ax.legend(loc='best', frameon=False)
37+
plt.show()
38+
39+
pdb.set_trace()

distributionreinsurance.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import scipy.stats
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
from math import ceil
5+
import scipy
6+
import pdb
7+
8+
class ReinsuranceDistWrapper():
9+
def __init__(self, dist, lower_bound=None, upper_bound=None):
10+
assert lower_bound is not None or upper_bound is not None
11+
self.dist = dist
12+
self.lower_bound = lower_bound
13+
self.upper_bound = upper_bound
14+
if lower_bound is None:
15+
self.lower_bound = -np.inf
16+
elif upper_bound is None:
17+
self.upper_bound = np.inf
18+
assert self.upper_bound > self.lower_bound
19+
self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound)
20+
21+
22+
def pdf(self, x):
23+
x = np.array(x, ndmin=1)
24+
r = map(lambda Y: self.dist.pdf(Y) if Y < self.lower_bound \
25+
else np.inf if Y==self.lower_bound \
26+
else self.dist.pdf(Y + self.upper_bound - self.lower_bound), x)
27+
r = np.array(list(r))
28+
if len(r.flatten()) == 1:
29+
r = float(r)
30+
return r
31+
32+
def cdf(self, x):
33+
x = np.array(x, ndmin=1)
34+
r = map(lambda Y: self.dist.cdf(Y) if Y < self.lower_bound \
35+
else self.dist.cdf(Y + self.upper_bound - self.lower_bound), x)
36+
r = np.array(list(r))
37+
if len(r.flatten()) == 1:
38+
r = float(r)
39+
return r
40+
41+
def ppf(self, x):
42+
x = np.array(x, ndmin=1)
43+
assert (x >= 0).all() and (x <= 1).all()
44+
r = map(lambda Y: self.dist.ppf(Y) if Y <= self.dist.cdf(self.lower_bound) \
45+
else self.dist.ppf(self.dist.cdf(self.lower_bound)) if Y <= self.dist.cdf(self.upper_bound) \
46+
else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, x)
47+
r = np.array(list(r))
48+
if len(r.flatten()) == 1:
49+
r = float(r)
50+
return r
51+
52+
def rvs(self, size=1):
53+
sample = self.dist.rvs(size=size)
54+
sample1 = sample[sample<=self.lower_bound]
55+
sample2 = sample[sample>self.lower_bound]
56+
sample3 = sample2[sample2>=self.upper_bound]
57+
sample2 = sample2[sample2<self.upper_bound]
58+
59+
sample2 = np.ones(len(sample2)) * self.lower_bound
60+
sample3 = sample3 -self.upper_bound + self.lower_bound
61+
62+
sample = np.append(np.append(sample1,sample2),sample3)
63+
return sample[:size]
64+
65+
66+
if __name__ == "__main__":
67+
non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5)
68+
#truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated)
69+
truncated = ReinsuranceDistWrapper(lower_bound=0.9, upper_bound=1.1, dist=non_truncated)
70+
71+
x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100)
72+
x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100)
73+
74+
fig, ax = plt.subplots(1, 1)
75+
ax.plot(x, non_truncated.pdf(x), 'k-', lw=2, label='non-truncated pdf')
76+
ax.plot(x2, truncated.pdf(x2), 'r-', lw=2, label='truncated pdf')
77+
ax.plot(x, non_truncated.cdf(x), 'b-', lw=2, label='non-truncated cdf')
78+
ax.plot(x2, truncated.cdf(x2), 'g-', lw=2, label='truncated cdf')
79+
sample = truncated.rvs(size=1000)
80+
sample = sample[sample < scipy.percentile(sample, 90)]
81+
ax.hist(sample, normed=True, histtype='stepfilled', alpha=0.4)
82+
ax.legend(loc='best', frameon=False)
83+
plt.show()
84+
85+
#pdb.set_trace()

distributiontruncated.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import scipy.stats
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
from math import ceil
5+
import scipy.integrate
6+
7+
class TruncatedDistWrapper():
8+
def __init__(self, dist, lower_bound=0, upper_bound=1):
9+
self.dist = dist
10+
self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound)
11+
self.lower_bound = lower_bound
12+
self.upper_bound = upper_bound
13+
assert self.upper_bound > self.lower_bound
14+
15+
def pdf(self, x):
16+
x = np.array(x, ndmin=1)
17+
r = map(lambda Y: self.dist.pdf(Y) / self.normalizing_factor \
18+
if (Y >= self.lower_bound and Y <= self.upper_bound) else 0, x)
19+
r = np.array(list(r))
20+
if len(r.flatten()) == 1:
21+
r = float(r)
22+
return r
23+
24+
def cdf(self, x):
25+
x = np.array(x, ndmin=1)
26+
r = map(lambda Y: 0 if Y < self.lower_bound else 1 if Y > self.upper_bound \
27+
else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound))/ self.normalizing_factor, x)
28+
r = np.array(list(r))
29+
if len(r.flatten()) == 1:
30+
r = float(r)
31+
return r
32+
33+
def ppf(self, x):
34+
x = np.array(x, ndmin=1)
35+
assert (x >= 0).all() and (x <= 1).all()
36+
return self.dist.ppf(x * self.normalizing_factor + self.dist.cdf(self.lower_bound))
37+
38+
def rvs(self, size=1):
39+
init_sample_size = int(ceil(size / self.normalizing_factor * 1.1))
40+
sample = self.dist.rvs(size=init_sample_size)
41+
sample = sample[sample>=self.lower_bound]
42+
sample = sample[sample<=self.upper_bound]
43+
while len(sample) < size:
44+
sample = np.append(sample, self.rvs(size - len(sample)))
45+
return sample[:size]
46+
47+
def mean(self):
48+
mean_estimate, mean_error = scipy.integrate.quad(lambda Y: Y*self.pdf(Y), self.lower_bound, self.upper_bound)
49+
return mean_estimate
50+
51+
if __name__ == "__main__":
52+
non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5)
53+
truncated = TruncatedDistWrapper(lower_bound=0.55, upper_bound=1., dist=non_truncated)
54+
55+
x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100)
56+
x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100)
57+
58+
fig, ax = plt.subplots(1, 1)
59+
ax.plot(x, non_truncated.pdf(x), 'k-', lw=2, label='non-truncated pdf')
60+
ax.plot(x2, truncated.pdf(x2), 'r-', lw=2, label='truncated pdf')
61+
ax.plot(x, non_truncated.cdf(x), 'b-', lw=2, label='non-truncated cdf')
62+
ax.plot(x2, truncated.cdf(x2), 'g-', lw=2, label='truncated cdf')
63+
sample = truncated.rvs(size=1000)
64+
ax.hist(sample, normed=True, histtype='stepfilled', alpha=0.4)
65+
ax.legend(loc='best', frameon=False)
66+
plt.show()
67+
68+
print(truncated.mean())

insurancecontract.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys, pdb
33

44
class InsuranceContract():
5-
def __init__(self, insurer, properties, time, premium, runtime, payment_period, deductible=0, excess=None, reinsurance=0):
5+
def __init__(self, insurer, properties, time, premium, runtime, payment_period, insurancetype="proportional", deductible=0, excess=None, reinsurance=0):
66
"""Constructor method.
77
Accepts arguments
88
insurer: Type InsuranceFirm.
@@ -12,6 +12,7 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period,
1212
runtime: Type integer.
1313
payment_period: Type integer.
1414
optional:
15+
insurancetype: Type string. The type of this contract, especially "proportional" vs "excess_of_loss"
1516
deductible: Type float (or int)
1617
excess: Type float (or int or None)
1718
reinsurance: Type float (or int). The value that is being reinsured.
@@ -27,8 +28,10 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period,
2728
self.property_holder = properties["owner"]
2829
self.value = properties["value"]
2930
self.contract = properties.get("contract") # will assign None if key does not exist
31+
self.insurancetype = properties.get("insurancetype") if insurancetype is None else insurancetype
3032
self.properties = properties
3133
self.runtime = runtime
34+
self.starttime = time
3235
self.expiration = runtime + time
3336
self.terminating = False
3437

@@ -38,7 +41,7 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period,
3841
# self.deductible = deductible if deductible is not None else 0
3942
self.deductible = deductible
4043

41-
self.excess = excess if excess is not None else self.value
44+
self.excess = excess if excess is not None else 1.
4245

4346
self.reinsurance = reinsurance
4447
self.reinsurer = None
@@ -47,8 +50,11 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period,
4750
#self.is_reinsurancecontract = False
4851

4952
# setup payment schedule
53+
#total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method
54+
total_premium = premium * self.value
55+
self.periodized_premium = total_premium / self.runtime
5056
self.payment_times = [time + i for i in range(runtime) if i % payment_period == 0]
51-
self.payment_values = premium * (np.ones(len(self.payment_times)) / len(self.payment_times))
57+
self.payment_values = total_premium * (np.ones(len(self.payment_times)) / len(self.payment_times))
5258

5359
## Create obligation for premium payment
5460
#self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time)
@@ -62,7 +68,7 @@ def check_payment_due(self, time):
6268
if len(self.payment_times) > 0 and time >= self.payment_times[0]:
6369
# Create obligation for premium payment
6470
#self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time)
65-
self.property_holder.receive_obligation(self.payment_values[0] * (self.excess - self.deductible), self.insurer, time)
71+
self.property_holder.receive_obligation(self.payment_values[0], self.insurer, time)
6672

6773
# Remove current payment from payment schedule
6874
self.payment_times = self.payment_times[1:]

insurancefirm.py

+54-4
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ def init(self, simulation_parameters, agent_parameters):
3131
self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off
3232
self.interest_rate = agent_parameters["interest_rate"]
3333
self.reinsurance_limit = agent_parameters["reinsurance_limit"]
34+
self.simulation_no_risk_categories = simulation_parameters["no_categories"]
35+
self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"]
36+
self.categories_reinsured = [False for i in range(self.simulation_no_risk_categories)]
37+
if self.simulation_reinsurance_type == 'non-proportional':
38+
self.np_reinsurance_deductible = simulation_parameters["default_non-proportional_reinsurance_deductible"]
39+
self.np_reinsurance_excess = simulation_parameters["default_non-proportional_reinsurance_excess"]
3440
self.obligations = []
3541
self.underwritten_contracts = []
3642
#self.reinsurance_contracts = []
3743
self.operational = True
3844
self.is_insurer = True
3945
self.is_reinsurer = False
4046

41-
def iterate(self, time):
47+
def iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods
4248
"""obtain investments yield"""
4349
self.obtain_yield(time)
4450

@@ -71,12 +77,24 @@ def iterate(self, time):
7177
except:
7278
print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format(self.id, contracts_offered, 2*contracts_dissolved))
7379
#print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved)
80+
81+
new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss']
82+
new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None]]
7483

75-
76-
"""make underwriting decisions, category-wise"""
7784
underwritten_risks = [{"excess": contract.value, "category": contract.category, \
7885
"risk_factor": contract.risk_factor, "deductible": contract.deductible, \
86+
"excess": contract.excess, "insurancetype": contract.insurancetype, \
7987
"runtime": contract.runtime} for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0]
88+
89+
"""deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones"""
90+
for risk in new_nonproportional_risks:
91+
#accept = self.riskmodel.evaluate(underwritten_risks, self.cash, risk) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly
92+
#if accept:
93+
# contract = ReinsuranceContract(...)
94+
# self.underwritten_contracts.append(contract)
95+
pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this
96+
97+
"""make underwriting decisions, category-wise"""
8098
# TODO: Enable reinsurance shares other tan 0.0 and 1.0
8199
expected_profit, acceptable_by_category = self.riskmodel.evaluate(underwritten_risks, self.cash)
82100

@@ -171,9 +189,41 @@ def receive(self, amount):
171189
def obtain_yield(self, time):
172190
amount = self.cash * self.interest_rate
173191
self.simulation.receive_obligation(amount, self, time)
192+
193+
def ask_reinsurance(self):
194+
if self.simulation_reinsurance_type == 'proportional':
195+
self.ask_reinsurance_proportional()
196+
elif self.simulation_reinsurance_type == 'non-proportional':
197+
self.ask_reinsurance_non_proportional()
198+
else:
199+
assert False, "Undefined reinsurance type"
200+
201+
def ask_reinsurance_non_proportional(self):
202+
for categ_id in range(len(self.simulation_no_risk_categories)):
203+
# with probability 5% if not reinsured ... # TODO: find a more generic way to decide whether to request reinsurance for category in this period
204+
if (not self.categories_reinsured[categ_id]) and np.random() < 0.05:
205+
total_value = 0
206+
avg_risk_factor = 0
207+
number_risks = 0
208+
periodized_total_premium = 0
209+
for contract in self.underwritten_contracts:
210+
if contract.category == categ_id:
211+
total_value += contract.value
212+
avg_risk_factor += contract.risk_factor
213+
number_risks += 1
214+
periodized_total_premium += contract.periodized_premium
215+
avg_risk_factor /= number_risks
216+
risk = {"value": total_value, "category": categ_id, "owner": self,
217+
#"identifier": uuid.uuid1(),
218+
"insurancetype": 'excess-of-loss', "number_risks": number_risks,
219+
"deductible": self.np_reinsurance_deductible, "excess": np_reinsurance_excess,
220+
"periodized_total_premium": periodized_total_premium, "runtime": 12,
221+
"expiration": time + 12, "risk_factor": avg_risk_factor}
222+
223+
self.simulation.append_reinrisks(risk)
174224

175225
@nb.jit
176-
def ask_reinsurance(self):
226+
def ask_reinsurance_proportional(self):
177227
nonreinsured = []
178228
for contract in self.underwritten_contracts:
179229
if contract.reincontract == None:

insurancesimulation.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from insurancefirm import InsuranceFirm
33
from riskmodel import RiskModel
44
from reinsurancefirm import ReinsuranceFirm
5+
from distributiontruncated import TruncatedDistWrapper
56
import numpy as np
67
import scipy.stats
78
import math
@@ -32,7 +33,14 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters):
3233
self.simulation_parameters = simulation_parameters
3334

3435
# unpack parameters, set up environment (distributions etc.)
35-
self.damage_distribution = scipy.stats.uniform(loc=0, scale=1)
36+
37+
# damage distribution
38+
# TODO: control damage distribution via parameters, not directly
39+
#self.damage_distribution = scipy.stats.uniform(loc=0, scale=1)
40+
non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25)
41+
self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated)
42+
43+
# remaining parameters
3644
self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"])
3745
self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"]
3846
self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"]

isleconfig.py

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
"risk_factor_upper_bound": 0.6,
2222
"initial_acceptance_threshold": 0.5,
2323
"acceptance_threshold_friction": 0.9,
24+
"simulation_reinsurance_type": 'proportional',
25+
"default_non-proportional_reinsurance_deductible": 0.75,
26+
"default_non-proportional_reinsurance_excess": 1.0,
2427
"initial_agent_cash": 10000,
2528
"initial_reinagent_cash": 50000,
2629
"interest_rate": 0,

0 commit comments

Comments
 (0)