Skip to content

Commit

Permalink
Updated dependences
Browse files Browse the repository at this point in the history
  • Loading branch information
mynl committed Jan 15, 2024
1 parent 6472899 commit 0fd82bd
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 31 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,4 @@ venv.bak/

doc/index - Copy.xrst
cheat-sheets/preamble.fmt
*.png
22 changes: 18 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
-----

aggregate: a powerful Python actuarial modeling library
========================================================
aggregate: a powerful actuarial modeling library
==================================================

Purpose
-----------

``aggregate`` solves insurance, risk management, and actuarial problems using realistic models that reflect
``aggregate`` builds approximations to compound (aggregate) probability distributions quickly and accurately.
It can be used to solve insurance, risk management, and actuarial problems using realistic models that reflect
underlying frequency and severity. It delivers the speed and accuracy of parametric distributions to situations
that usually require simulation, making it as easy to work with an aggregate (compound) probability distribution
as the lognormal. ``aggregate`` includes an expressive language called DecL to describe aggregate distributions
Expand Down Expand Up @@ -47,6 +48,19 @@ Installation
Version History
-----------------


0.21.3
~~~~~~~~

* Risk progression, defaults to linear allocation.
* Added ``g_insurance_statistics`` to ``extensions`` to plot insurance statistics from a distortion ``g``.
* Added ``g_risk_appetite`` to ``extensions`` to plot risk appetite from a distortion ``g`` (value, loss ratio,
return on capital, VaR and TVaR weights).
* Corrected Wang distortion derivative.
* Vectorized ``Distortion.g_prime`` calculation for proportional hazard
* Added ``tvar_weights`` function to ``spectral`` to compute the TVaR weights of a distortion. (Work in progress)
* Updated dependencies in pyproject.toml file.

0.21.2
~~~~~~~~

Expand Down Expand Up @@ -447,4 +461,4 @@ Social media: https://www.reddit.com/r/AggregateDistribution/.

.. |twitter| image:: https://img.shields.io/twitter/follow/mynl.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF
:target: https://twitter.com/SJ2Mi
:alt: Twitter Follow
:alt: Twitter Follow
4 changes: 2 additions & 2 deletions aggregate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
integral_by_doubling, logarithmic_theta, block_iman_conover,
make_var_tvar, test_var_tvar, kaplan_meier, kaplan_meier_np,
more, parse_note, parse_note_ex, introspect, explain_validation)
from .spectral import Distortion, approx_ccoc
from .spectral import Distortion, approx_ccoc, tvar_weights
from .distributions import Frequency, Severity, Aggregate
from .portfolio import Portfolio, make_awkward
from .underwriter import Underwriter, build
Expand Down Expand Up @@ -49,7 +49,7 @@
__email__ = "steve@convexrisk.com"
__status__ = "beta"
# only need to change here, feeds conf.py (docs) and pyproject.toml (build)
__version__ = "0.21.2"
__version__ = "0.21.3"



Expand Down
4 changes: 2 additions & 2 deletions aggregate/bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def __init__(self, distribution_spec):
# hack for beta distribution, you want to force 1 to be in tvar ps, but Fp = 1
# TODO figure out why p_star grinds to a halt if you input b < inf
self.add_one = True
logger.warning('Deprecatation warning. The kind argument is now ignored. Functionality '
'is equivalent to kind="tail", which was the most accurate method.')
# logger.warning('Deprecatation warning. The kind argument is now ignored. Functionality '
# 'is equivalent to kind="tail", which was the most accurate method.')

def __repr__(self):
"""
Expand Down
3 changes: 3 additions & 0 deletions aggregate/distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2769,6 +2769,9 @@ def q(self, p, kind='lower'):

return self._var_tvar_function[kind](p)

# for consistency with scipy
ppf = q

def _make_var_tvar(self, ser):
dict_ans = {}
qf = make_var_tvar(ser)
Expand Down
152 changes: 150 additions & 2 deletions aggregate/extensions/figures.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import matplotlib as mpl
from .. import build, qd
from .. constants import FIG_H, FIG_W, PLOT_FACE_COLOR
from .. spectral import Distortion
from .. spectral import Distortion, tvar_weights


def adjusting_layer_losses():
Expand Down Expand Up @@ -366,4 +366,152 @@ def exact_cdf(x):
['p_total']]/a.bs
df.index = [f'{x: 6.0f}' for x in df.index]
df.index.name = 'x'
qd(df.iloc[:, [1,0,2,3,4, 5]], ff=lambda x: f'{x:11.3e}')
qd(df.iloc[:, [1,0,2,3,4, 5]], ff=lambda x: f'{x:11.3e}')


def g_insurance_statistics(axi, dist, c='C0', ls='-', lw=1, diag=False, grid=False):
"""
Six part plot with EL, premium, loss ratio, profit, layer ROE and P:S for g
axi = axis iterator with 6 axes
dist = distortion
Used to create PIR Figs 11.6 and 11.7
"""
g = dist.g
N = 1000
ps = np.linspace(1 / (2 * N), 1, N, endpoint=False)
gs = g(ps)

dn = dist.name

# FIG: six panel display
for i, ys, key in zip(range(1, 7),
[gs, ps / gs, gs / ps, gs - ps, (gs - ps) / (1 - ps), gs / (1 - gs)],
['Premium', 'Loss Ratio', 'Markup',
'Margin', 'Discount Rate', 'Premium Leverage']):
a = next(axi)
if i == 1:
a.plot(ps, ys, ls=ls, c=c, lw=lw, label=dn)
else:
a.plot(ps, ys, ls=ls, lw=lw, c=c)
if i in [1] and diag:
a.plot(ps, ps, color='k', linewidth=0.25)
a.set(title=key)
if i == 3 or i == 6:
a.axis([0, 1, 0, 5])
a.set(aspect=1 / 5)
elif i == 5:
# discount
a.axis([0, 1, 0, .5])
a.set(aspect=1 / .5)
else:
a.axis([0, 1, 0, 1])
a.set(aspect=1)
if i == 1:
a.legend(loc='lower right')
if grid:
a.grid(lw=0.25)


def g_risk_appetite(axi, dist, c='C0', ls='-', N=1000, lw=1, grid=False, xlabel=True, title=True, add_tvar=False):
"""
Plot to illustrate the risk appetite associated with a distortion g.
Derived from ``g_insurance_statistics``.
Plots premium, loss ratio, margin, return (easier to understand than discount),
VaR wts and optionally TVaR wts
axi = axis iterator with 6 axes
dist = distortion
Used to create PIR Figs 11.6 and 11.7
"""
g = dist.g
ps = np.linspace(1 / (2 * N), 1, N, endpoint=False)
gs = g(ps)

# var weight
gp = dist.g_prime(ps)

# tvar weight
wt_fun = tvar_weights(dist)
ps01 = np.linspace(0, 1, N+1)
tvar = wt_fun(ps01)

# labels etc.
dn = dist.name.replace(' ', '\n').replace('aCC', 'CC')
isccoc = dist.name.lower().find('ccoc') >= 0
istvar = dist.name.lower().find('tvar') >= 0

for i, ys, key in zip(range(6),
[gs,
ps / gs,
gs - ps,
(gs - ps) / (1 - gs),
gp,
tvar],
['Premium',
'Loss ratio',
'Margin',
'Return on capital',
'VaR weight',
'TVaR p weight']):
if i == 5 and not add_tvar:
# skip tvar weights
continue
a = next(axi)
if key == 'VaR weight' and istvar:
ds = 'steps-post'
elif key == 'TVaR p weight' and (istvar or isccoc):
ds = 'steps-mid'
else:
ds = 'default'
kwargs = {'ls': ls, 'c': c, 'lw': lw, 'ds': ds}
if i == 0:
a.plot(ps, ys, label=dn, **kwargs)
a.legend(loc='lower right', fontsize=10)
elif i < 5:
a.plot(ps, ys, **kwargs)
else:
a.plot(ps01, ys, **kwargs)
if title:
a.set(title=key)
if i in (1, 3, 4):
mn, mx = a.get_ylim()
mx = max(1.025, min(5, mx * 1.1))
# a.set_ylim(0)
a.axis([-0.025, 1.025, -0.025, mx])
a.set(aspect=1 / mx)
elif i == 2:
a.axis([-0.025, 1.025, -0.025, .525])
a.set(aspect=2)
elif i in (0,1):
a.axis([-0.025, 1.025, -0.025, 1.025])
a.set(aspect='equal')
elif i == 5:
pass
else:
a.axis([-0.025, 1.025, -0.025, 1.025])
a.set(aspect=1)
if isccoc:
if key == 'Premium':
# add zero point
a.plot(0, 0, 'o', c=c)
if key == 'VaR weight':
# add var mass
mx = 2.2
line, = a.plot(0, mx, '*', c=c) # Point above the plot area
# Allow plotting outside of the plot area
line.set_clip_on(False)
if key == 'Return on capital':
# add var mass
mx = a.get_ylim()[1] * 1.1
line, = a.plot(0, mx, '*', c=c) # Point above the plot area
# Allow plotting outside of the plot area
line.set_clip_on(False)
if grid:
a.grid(lw=0.25)
if xlabel:
if i == 0:
a.set(xlabel='Exceedance probability\n(large losses on left)')
elif i == 5:
a.set(xlabel='p value\n(mean on left, max on right)')
else:
a.set(xlabel='Exceedance probability')
# print(np.sum(gp))
45 changes: 31 additions & 14 deletions aggregate/extensions/risk_progression.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ def make_up_down(ser):
u = np.maximum(0, dy).cumsum()
d = -np.minimum(0, dy).cumsum()
c = u - d
u.name = 'up'
d.name = 'down'
c.name = 'recreated'
u.name = 'Ins'
d.name = 'Fin'
c.name = 'Ins - Fin'
return u, d, c


Expand Down Expand Up @@ -197,13 +197,14 @@ def plot_up_down(self, udd, axs):

# left and middle plots
for unit, ax, recreated_c in zip(self.unit_names, axs.flat, ['C0', 'C1']):
ax = self.density_df[f'exeqa_{unit}'].plot(ax=ax, lw=.5, c='C7', drawstyle='steps-mid')
ax = self.density_df[f'exeqa_{unit}'].plot(ax=ax, lw=.5, c='C7', drawstyle='steps-mid',
label=f'E[{unit} | X]')
(udd.up_functions[unit] - udd.down_functions[unit]
).plot(ax=ax, lw=1.5, ls='-', c=recreated_c, label='recreated', drawstyle='steps-post')
).plot(ax=ax, lw=1.5, ls='-', c=recreated_c, label='Ins - Fin', drawstyle='steps-post')
udd.up_functions[unit].plot(ax=ax, c='C3', drawstyle='steps-post', lw=1, ls='--')
udd.down_functions[unit].plot(ax=ax, c='C5', drawstyle='steps-post', lw=1, ls='-.')
ax.legend()
ax.set(xlabel='loss', ylabel='up or down function')
ax.set(xlabel='X, total', ylabel='Unit or conditional loss')

# plot ud distributions (right hand plot)
ax = axs.flat[-1]
Expand Down Expand Up @@ -241,7 +242,7 @@ def price_work(dn, series, names_ex):
return bit


def price_compare(self, dn, projection_dists, ud_dists):
def price_compare(self, dn, projection_dists, ud_dists, allocation='linear'):
"""
Build out pricing comparison waterfall
Expand All @@ -251,9 +252,9 @@ def price_compare(self, dn, projection_dists, ud_dists):

# linear natural allocation pricing
# KLUDGE
lna_pricea = self.price(self.q(1), dn, view='ask')
lna_pricea = self.price(self.q(1), dn, allocation=allocation, view='ask')
# display(lna_pricea.df)
lna_priceb = self.price(self.q(1), dn, view='bid')
lna_priceb = self.price(self.q(1), dn, allocation=allocation, view='bid')
na_price = lna_pricea.df[['L', 'P']]
na_price.columns = ['el', 'ask']
# if this is nan it gets dropped from stack?!
Expand Down Expand Up @@ -302,7 +303,7 @@ def price_compare(self, dn, projection_dists, ud_dists):
return compare


def full_monty(self, dn, truncate=True, smooth=16, plot=True):
def full_monty(self, dn, truncate=True, smooth=16, plot=True, allocation='linear'):
"""
One-stop shop for a Portfolio self
Unlimited assets
Expand Down Expand Up @@ -342,10 +343,26 @@ def full_monty(self, dn, truncate=True, smooth=16, plot=True):
axs1 = axs[3, :]
plot_up_down(self, ud_dists, axs1)

compare = price_compare(self, dn, projection_dists, ud_dists)
compare = price_compare(self, dn, projection_dists, ud_dists, allocation=allocation)
compare['umd'] = compare['up'] - compare['down']

RiskProgression = namedtuple('RiskProgression', ['compare_df', 'projection_dists', 'ud_dists'])
ans = RiskProgression(compare, projection_dists, ud_dists)
compare2 = compare.copy()
for l in compare2.index.levels[0]:
m = compare2.loc[(l, 'ask')] - compare2.loc[(l, 'el')]
compare2.loc[(l, 'margin'), :] = m
compare2 = compare2.sort_index()
compare2 = compare2.rename(columns={'lna': 'Linear NA' ,
'sa': 'Standalone',
'proj_sa': 'Projection SA',
'up': 'Ins',
'down': 'Fin',
'umd': 'Ins - Fin'})
# or
# {'Linear NA': 'NA(Xi)', 'Standalone': 'ρ(Xi)',
# 'Projection SA': 'ρ(E[Xi | X])',
# 'Ins': 'ρ(Ins)', 'Fin': 'ρ(Fin)', 'Ins - Fin': 'ρ(Ins) - ρ(Fin)'

RiskProgression = namedtuple('RiskProgression',
['compare_df', 'compare_df2', 'projection_dists', 'ud_dists'])
ans = RiskProgression(compare, compare2, projection_dists, ud_dists)
return ans

Loading

0 comments on commit 0fd82bd

Please sign in to comment.