2.13.8. Other Papers

Miscellaneous short examples from various texts.

2.13.8.2. Wang on the Wang Transform

Source paper: Wang [2000].

2.13.8.2.1. Pricing by Layer

Concepts: Layer expected loss and risk adjusted layer technical premium with Wang and proportional spectral risk measures.

Setup: Ground-up Pareto risk, shape 1.2, scale 2000. Layer and compare Wang(0.1) and PH(0.9245) pricing.

Source Exhibits:

Original paper table.

Thanks: Zach Eisenstein of Aon.

Code:

Build the portfolio.

In [1]: from aggregate import build, qd

In [2]: layers = [0, 50e3, 100e3, 200e3, 300e3, 400e3, 500e3, 1000e3, 2000e3, 5000e3, 10000e3]

In [3]: a1 = build('agg Wang.t1 '
   ...:            '1 claim '
   ...:            '10000e3 xs 0 ' # limit the severity to avoid infinite variance
   ...:            'sev 2000 * pareto 1.2 - 2000 '
   ...:            f'occurrence ceded to tower {layers} '
   ...:            'fixed'
   ...:           )
   ...: 

In [4]: qd(a1)

       E[X] Est E[X]    Err E[X]  CV(X) Est CV(X) Skew(X) Est Skew(X)
X                                                                    
Freq      1                           0                              
Sev  8179.5   8178.5 -0.00012201 11.595    11.596  70.841      70.841
Agg  8179.5   8178.5 -0.00012201 11.595    11.596  70.841      70.841
log2 = 16, bandwidth = 200, validation: n/a, reinsurance.

Expected loss and other ceded statistics by layer.

In [5]: ff = lambda x: x if type(x)==str else (
   ...:         f'{x/1e6:,.1f}M' if x >= 500000 else
   ...:         (f'{x:,.3f}' if x < 100 else f'{x:,.0f}'))
   ...: 

In [6]: fp = lambda x: f'{x:.1%}'

In [7]: qdl = lambda x: qd(x, index=False, line_width=200, formatters={'pct': fp},
   ...:                    float_format=ff, col_space=10)
   ...: 

In [8]: qdl(a1.reinsurance_occ_layer_df .xs('ceded', 1, 1).droplevel(0).reset_index(drop=False))

     limit     attach         ex         cv         en   severity        pct
    50,000      0.000      4,787      1.908      1.000      4,787      58.5%
    50,000     50,000        657      8.058      0.020     32,776       8.0%
   100,000    100,000        582     12.148      0.009     65,143       7.1%
   100,000    200,000        307     17.279      0.004     78,055       3.8%
   100,000    300,000        204     21.484      0.002     83,955       2.5%
   100,000    400,000        150     25.182      0.002     87,349       1.8%
      0.5M       0.5M        428     31.751      0.001    324,046       5.2%
      1.0M       1.0M        373     48.091      0.001       0.6M       4.6%
      3.0M       2.0M        420     76.476      0.000       1.7M       5.1%
      5.0M       5.0M        271        126      0.000       3.2M       3.3%
      infM        gup      8,179     11.596      1.000      8,179     100.0%

ZE provided function to make the exhibit table. The column pct shows the relative loading.

In [9]: def make_table(agg, layers):
   ...:     agg_df = agg.density_df
   ...:     layer_df = agg_df.loc[layers, ['F', 'S', 'lev', 'gS', 'exag']]
   ...:     layer_df['layer_el'] = np.diff(layer_df.lev, prepend = 0)
   ...:     layer_df['premium'] = np.diff(layer_df.exag, prepend = 0)
   ...:     layer_df['pct'] = layer_df['premium'] / layer_df['layer_el'] - 1
   ...:     layer_df = layer_df.rename_axis("exhaust").reset_index()
   ...:     layer_df['attach'] = layer_df['exhaust'].shift(1).fillna(0)
   ...:     qdl(layer_df.loc[1:, ['attach', 'exhaust', 'layer_el', 'premium', 'pct']])
   ...: 

Make the distortions and apply. First, Wang.

In [10]: d1 = build('distortion wang_d1 wang 0.1')

In [11]: a1.apply_distortion(d1)

In [12]: make_table(a1, layers)

    attach    exhaust   layer_el    premium        pct
     0.000     50,000      4,787      5,481      14.5%
    50,000    100,000        657        845      28.6%
   100,000    200,000        582        769      32.2%
   200,000    300,000        307        414      34.9%
   300,000    400,000        204        278      36.6%
   400,000       0.5M        150        207      37.8%
      0.5M       1.0M        428        598      39.9%
      1.0M       2.0M        373        533      43.0%
      2.0M       5.0M        420        616      46.5%
      5.0M      10.0M        271        406      49.9%

Make the distortions and apply. Second, PH.

In [13]: d2 = build('distortion wang_d2 ph 0.9245')

In [14]: a1.apply_distortion(d2)

In [15]: make_table(a1, layers)

    attach    exhaust   layer_el    premium        pct
     0.000     50,000      4,787      5,480      14.5%
    50,000    100,000        657        910      38.4%
   100,000    200,000        582        856      47.2%
   200,000    300,000        307        475      54.7%
   300,000    400,000        204        325      59.6%
   400,000       0.5M        150        246      63.3%
      0.5M       1.0M        428        727      70.1%
      1.0M       2.0M        373        675      81.1%
      2.0M       5.0M        420        818      94.7%
      5.0M      10.0M        271        567     109.5%

It appears the layer 50000 xs 0 is reported incorrectly in the paper.

2.13.8.2.2. Satellite Pricing

Concepts: The cost of a Bernoulli risk with a 5% probability of a total loss of $100m using Wang(0.1) distortion.

Thanks: Zach Eisenstein of Aon.

Code:

Build the portfolio. Illustrates how to set up a Bernoulli.

In [16]: a2 = build('agg wang2 0.05 claims dsev [100] bernoulli')

In [17]: qd(a2)

      E[X] Est E[X]   Err E[X]  CV(X) Est CV(X) Skew(X) Est Skew(X)
X                                                                  
Freq  0.05                     4.3589            4.1295            
Sev    100      100          0      0         0                    
Agg      5        5 8.8818e-16 4.3589    4.3589  4.1295      4.1295
log2 = 8, bandwidth = 1, validation: not unreasonable.

Check the distribution output.

In [18]: qd(a2.density_df.query('p_total > 0')[['p_total', 'F', 'S']])

       p_total    F    S
loss                    
0.0       0.95 0.95 0.05
100.0     0.05    1    0

Build and apply the distortion. Use the price() method. The first argument selects the 100% quantile for pricing, i.e., the limit is fully collateralized. First with the Wang from above. The relative loading reported is the complement of the loss ratio.

In [19]: qd(a2.price(1, d1))

statistic  L      P      M      Q   a      LR       PQ     ROE
line                                                          
wang2      5 6.1191 1.1191 93.881 100 0.81712 0.065179 0.01192

Try with a more severe distortion.

In [20]: d3 = build('distortion wang_d2 wang 0.15')

In [21]: qd(a2.price(1, d2))

statistic  L     P     M      Q   a      LR       PQ      ROE
line                                                         
wang2      5 6.269 1.269 93.731 100 0.79758 0.066883 0.013539

2.13.8.3. Wang on Weather Derivatives

Source paper: Wang [2002].

Concepts: Applying Wang transform to empirical distribution via an Aggregate object.

Source Exhibits:

Original paper table.

Data:

Create a dataframe from the heating degree days (HDD) history laid out in Table 1.

In [22]: d = '''Dec-79
   ....: 972.5
   ....: Dec-87
   ....: 1018.5
   ....: Dec-95
   ....: 1199.5
   ....: Dec-80
   ....: 1147.0
   ....: Dec-88
   ....: 1155.0
   ....: Dec-96
   ....: 1156.0
   ....: Dec-81
   ....: 1244.0
   ....: Dec-89
   ....: 1474.5
   ....: Dec-97
   ....: 1040.0
   ....: Dec-82
   ....: 901.0
   ....: Dec-90
   ....: 1129.5
   ....: Dec-98
   ....: 940.5
   ....: Dec-83
   ....: 1573.0
   ....: Dec-91
   ....: 1077.5
   ....: Dec-99
   ....: 1090.5
   ....: Dec-84
   ....: 1055.0
   ....: Dec-92
   ....: 1129.5
   ....: Dec-00
   ....: 1517.5
   ....: Dec-85
   ....: 1488.0
   ....: Dec-93
   ....: 1090.5
   ....: Dec-86
   ....: 1065.5
   ....: Dec-94
   ....: 938.5'''
   ....: 

In [23]: import pandas as pd

In [24]: d = d.split('\n')

In [25]: df = pd.DataFrame(zip(d[::2], d[1::2]), columns=['month', 'hdd'])

In [26]: df['hdd'] = df.hdd.astype(float)

In [27]: qd(df.head())

    month    hdd
0  Dec-79  972.5
1  Dec-87 1018.5
2  Dec-95 1199.5
3  Dec-80   1147
4  Dec-88   1155

In [28]: qd(df.describe())

         hdd
count     22
mean  1154.7
std   193.37
min      901
25%   1043.8
50%     1110
75%   1188.6
max     1573

Code:

Create an empirical aggregate based on the HDD data.

In [29]: hdd = build(f'agg HDD 1 claim dsev {df.hdd.values} fixed'
   ....:             , bs=1/32, log2=16)
   ....: 

In [30]: qd(hdd)

       E[X] Est E[X]   Err E[X]   CV(X) Est CV(X) Skew(X) Est Skew(X)
X                                                                    
Freq      1                           0                              
Sev  1154.7   1154.7 2.2204e-16 0.16362   0.16362 0.95984     0.95984
Agg  1154.7   1154.7 2.2204e-16 0.16362   0.16362 0.95984     0.95984
log2 = 16, bandwidth = 1/32, validation: not unreasonable.

Build the distortion and apply to call options at different strikes. Reproduces Table 2.

In [31]: from aggregate.extensions import Formatter

In [32]: d1 = build('distortion w25 wang .25')

In [33]: ans = []

In [34]: strikes = np.arange(1250, 1501, 50)

In [35]: bit = hdd.density_df.query('p_total > 0')[['p_total']]

In [36]: for strike in strikes:
   ....:     ser = bit.groupby(by= lambda x: np.maximum(0, x - strike)).p_total.sum()
   ....:     ans.append(d1.price(ser, kind='both'))
   ....: 

In [37]: df = pd.DataFrame(ans, index=strikes,
   ....:                  columns=['bid', 'el', 'ask'])
   ....: 

In [38]: df.index.name = 'strike'

In [39]: df['loading'] = (df.ask - df.el) / df.el

In [40]: qd(df.T, float_format=Formatter(dp=2, w=8))

strike      1250     1300     1350     1400     1450     1500
bid        32.01    25.84    19.67    13.51     7.34     2.44
el         47.86    38.77    29.68    20.59    11.50     4.11
ask        68.21    55.45    42.70    29.94    17.18     6.59
loading    42.5%    43.0%    43.8%    45.4%    49.4%    60.1%

2.13.8.4. Gerber: Stop Loss Premiums

Source papers: Gerber [1982].

Concepts: Stop loss and survival functions for Poisson uniform(1,3) aggregate. Claim count 1, 10, and 100.

Source Exhibits: Matches columns labeled exact in Tables 1-6.

Code:

Expected claim count 1.

In [41]: gerber1 = build('agg Gerber1 1 claim sev 2 * uniform + 1 poisson'
   ....:                 , bs=1/1024)
   ....: 

In [42]: qd(gerber1)

      E[X] Est E[X]    Err E[X]   CV(X) Est CV(X)    Skew(X) Est Skew(X)
X                                                                       
Freq     1                            1                    1            
Sev      2        2           0 0.28868   0.28868 -1.846e-14           0
Agg      2        2 -5.5511e-16  1.0408    1.0408     1.1086      1.1086
log2 = 16, bandwidth = 1/1024, validation: fails sev skew.

In [43]: bit = gerber1.density_df.loc[0:21:2*1024, ['S', 'lev']]

In [44]: bit['stop_loss'] = gerber1.agg_m - bit.lev

In [45]: qd(bit)

                S    lev  stop_loss
loss                               
0.000     0.63212      0          2
2.000     0.44809 1.1723    0.82773
4.000     0.17095 1.7311    0.26891
6.000    0.048993 1.9282   0.071838
8.000    0.012406 1.9837   0.016267
10.000  0.0027232 1.9967  0.0032535
12.000 0.00052133 1.9994 0.00058146
14.000 8.7627e-05 1.9999 9.3461e-05
16.000 1.3371e-05      2 1.3664e-05
18.000 1.8774e-06      2   1.84e-06
20.000 2.4358e-07      2 2.3017e-07

Expected claim count 10.

In [46]: gerber10 = build('agg Gerber10 10 claim sev 2 * uniform + 1 poisson'
   ....:                 , bs=1/128)
   ....: 

In [47]: qd(gerber10)

      E[X] Est E[X]    Err E[X]   CV(X) Est CV(X)    Skew(X) Est Skew(X)
X                                                                       
Freq    10                      0.31623              0.31623            
Sev      2        2           0 0.28868   0.28868 -1.846e-14           0
Agg     20       20 -8.8818e-16 0.32914   0.32914    0.35056     0.35056
log2 = 16, bandwidth = 1/128, validation: fails sev skew.

In [48]: bit = gerber10.density_df.loc[15:61:5*128, ['S', 'lev']]

In [49]: bit['stop_loss'] = gerber10.agg_m - bit.lev

In [50]: qd(bit)

                S    lev  stop_loss
loss                               
15.000    0.76769 14.243     5.7565
20.000    0.47646 17.374     2.6255
25.000    0.21683 19.068    0.93214
30.000   0.072549 19.744    0.25632
35.000   0.018223 19.945   0.055073
40.000  0.0035238 19.991  0.0093834
45.000 0.00053724 19.999  0.0012887
50.000 6.5977e-05     20 0.00014495
55.000 6.6491e-06     20 1.3552e-05
60.000 5.5879e-07     20 1.0673e-06

Expected claim count 100.

In [51]: gerber100 = build('agg Gerber100 100 claim sev 2 * uniform + 1 poisson'
   ....:                 , bs=1/16)
   ....: 

In [52]: qd(gerber100)

      E[X] Est E[X]    Err E[X]   CV(X) Est CV(X)    Skew(X) Est Skew(X)
X                                                                       
Freq   100                          0.1                  0.1            
Sev      2        2           0 0.28868   0.28896 -1.846e-14           0
Agg    200      200 -1.0658e-14 0.10408   0.10409    0.11086     0.11088
log2 = 16, bandwidth = 1/16, validation: fails sev skew.

In [53]: bit = gerber100.density_df.loc[180:301:20*16, ['S', 'lev']]

In [54]: bit['stop_loss'] = gerber100.agg_m - bit.lev

In [55]: qd(bit)

                S    lev  stop_loss
loss                               
180.000   0.83089 178.23     21.774
200.000   0.49203 191.69     8.3051
220.000   0.16763 198.04     1.9595
240.000  0.030257 199.74    0.26481
260.000 0.0028586 199.98   0.019934
280.000  0.000144    200 0.00083749
300.000 3.982e-06    200 1.9972e-05

2.13.8.5. Richardson’s Deferred Approach to the Limit

Source papers: Embrechts et al. [1993], Grübel and Hermesmeier [2000].

Concepts: Given estimators of an unknown quantity \(A^* = A(h) + ch^\alpha + O(h^\beta)\) with \(\alpha < \beta\). Evaluate at \(h\) and \(h/t\). Multiply the estimate at \(h/t\) by \(t^\alpha\), subtract the original estimate, and rearrange to get

\[A^* = \frac{t^\alpha A(h/t) - A(h)}{t^\alpha - 1} + O(h^\beta).\]

The truncation error order of magnitude has decreased. The constant \(c\) need not be known. Applying this approach to estimate the density as pmf divided by bucket size, \(f_{h} / h\), Embrechts et al. [1993] report the following.

Setup: Poisson(20) exponential aggregate.

Source Exhibits: Figure 1.

The variable egp3 is treated as the exact answer. It could also be approximated using the series expansion, but this has been shown already, in REF. Set up basic portfolios evaluated at different bucket sizes.

In [56]: egp1 = build('agg EGP 20 claims sev expon 1 poisson',
   ....:             bs=1/16, log2=10)
   ....: 

In [57]: qd(egp1)

      E[X] Est E[X]    Err E[X]   CV(X) Est CV(X)  Skew(X) Est Skew(X)
X                                                                     
Freq    20                      0.22361            0.22361            
Sev      1  0.99984 -0.00016274       1    1.0005        2       1.998
Agg     20   19.997 -0.00016277 0.31623    0.3163  0.47434     0.47429
log2 = 10, bandwidth = 1/16, validation: fails sev mean, agg mean.

In [58]: egp2 = build('agg EGP 20 claims sev expon 1 poisson',
   ....:             bs=1/32, log2=11)
   ....: 

In [59]: qd(egp2)

      E[X] Est E[X]    Err E[X]   CV(X) Est CV(X)  Skew(X) Est Skew(X)
X                                                                     
Freq    20                      0.22361            0.22361            
Sev      1  0.99996 -4.0689e-05       1    1.0001        2      1.9995
Agg     20   19.999 -4.0713e-05 0.31623   0.31625  0.47434     0.47432
log2 = 11, bandwidth = 1/32, validation: not unreasonable.

In [60]: egp3 = build('agg EGP 20 claims sev expon 1 poisson',
   ....:              log2=16)
   ....: 

In [61]: qd(egp3)

      E[X] Est E[X]    Err E[X]   CV(X) Est CV(X)  Skew(X) Est Skew(X)
X                                                                     
Freq    20                      0.22361            0.22361            
Sev      1        1 -1.5895e-07       1         1        2           2
Agg     20       20 -1.5895e-07 0.31623   0.31623  0.47434     0.47434
log2 = 16, bandwidth = 1/512, validation: not unreasonable.

Concatenate and estimated densities from pmf. Compute errors to egp3. Compute the Richardson extrapolation. It is indistinguishable from egp3. The last table shows cases with the largest errors.

In [62]: import pandas as pd

In [63]: df = pd.concat((egp1.density_df.p_total, egp2.density_df.p_total, egp3.density_df.p_total),
   ....:           axis=1, join='inner', keys=[1, 2, 3])
   ....: 

In [64]: df[1] = df[1] * 16;                   \
   ....: df[2] = df[2] * 32;                   \
   ....: df[3] = df[3] * (1<<10);              \
   ....: df['rich'] = (4 * df[2] - df[1]) / 3; \
   ....: df['diff_1'] = df[1] - df[3];         \
   ....: df['diff_2'] = df[2] - df[3];         \
   ....: m = df.diff_2.max() * .9;             \
   ....: ax = df[['diff_1', 'diff_2']].plot(figsize=(3.5, 2.45));      \
   ....: (df['rich'] - df[3]).plot(ax=ax, lw=.5, label='Richardson');
   ....: 

In [65]: ax.legend(loc='upper right')
Out[65]: <matplotlib.legend.Legend at 0x7f8089354040>

In [66]: qd(df.query(f'diff_2 > {m}').iloc[::10])

Empty DataFrame
Columns: [1, 2, 3, rich, diff_1, diff_2]
Index: []
../../_images/rich.png