Example 9: two lines, one tail

A capital view is only as good as two things the point estimate never shows: the dependence assumption between the lines and the Monte Carlo error in the numbers. This page runs both through risksim — a two-line portfolio built from lossmodels collective risk models, diversification measured and then stress-tested with Iman–Conover reordering (normal and t scores at the same rank correlation), a two-layer aggregate program priced on the result, and every headline metric reported with the interval its sampling theory supports. Every number on this page is the output of this exact fixed-seed run.

Two lines

A property book and a smaller, heavier-tailed liability line — anything with a .sample method drops into a portfolio item:

import numpy as np
import lossmodels as lm
import risksim as rs
from risksim import metrics, uncertainty
from risksim.dependence import impose_rank_correlation

prop = lm.CollectiveRiskModel(lm.NegativeBinomial(65.3, 0.269),
                                 lm.Lognormal(9.2, 0.95))
liab = lm.CollectiveRiskModel(lm.Poisson(26.0),
                                   lm.Lognormal(10.2, 1.35))

port = rs.Portfolio([rs.PortfolioItem("property", prop),
                     rs.PortfolioItem("liability", liab)])
res = port.simulate(200_000, rng=7)
M = res.component_losses          # the (n_sims, 2) matrix, columns in
                                  # res.component_names order

The diversification you are claiming

Component draws are independent by default, and the standalone-versus- combined comparison quantifies what that assumption is worth:

mean

VaR₉₉

TVaR₉₉

TVaR₉₉.₅

property

2,757,874

3,953,177

4,159,691

4,299,128

liability

1,738,858

4,566,745

5,809,053

6,745,713

combined, independent

4,496,732

7,479,457

8,697,291

9,620,716

The standalone TVaR₉₉.₅ sum to 11,044,841 against the combined 9,620,716 — a 1,424,125 diversification benefit, about 13% of the standalone capital. That is the number the dependence assumption owns.

Impose the dependence

impose_rank_correlation reorders the simulated columns to a target rank correlation without touching any sampler — the marginals are preserved exactly, which is checkable rather than assumed:

corr = np.array([[1.0, 0.5], [0.5, 1.0]])
Mn = impose_rank_correlation(M, corr, rng=11)                      # normal scores
Mt = impose_rank_correlation(M, corr, rng=11, scores="t", df=4)    # t scores

np.allclose(np.sort(M[:, 0]), np.sort(Mn[:, 0]))    # True — same draws, new order

Both hit the target (achieved Spearman 0.481 and 0.494), and the means are identical to the independent run by construction. The tails are not:

dependence

mean

VaR₉₉

TVaR₉₉

TVaR₉₉.₅

benefit at TVaR₉₉.₅

independent

4,496,732

7,479,457

8,697,291

9,620,716

1,424,125

ρ = 0.5, normal scores

4,496,732

8,016,642

9,331,046

10,316,277

728,564

ρ = 0.5, t scores, df = 4

4,496,732

8,003,939

9,272,676

10,219,875

824,966

A rank correlation of 0.5 erases half the diversification benefit — seven hundred thousand of capital relief that existed only inside the independence assumption.

Same rank correlation, different tail

The two score choices above are calibrated to the same ρ, and on every sum-based metric in that table they are within a whisker of each other. So does the copula matter? Ask a joint question instead of a sum question — the probability that both lines blow through their own 1-in-100 in the same year:

qm = metrics.var(M[:, 0], 0.99)
qs = metrics.var(M[:, 1], 0.99)
for X in (M, Mn, Mt):
    print(((X[:, 0] > qm) & (X[:, 1] > qs)).mean())

dependence

joint exceedances (of 200,000)

P(both lines exceed own VaR₉₉)

independent

12

0.00006

ρ = 0.5, normal scores

275

0.001375

ρ = 0.5, t scores, df = 4

397

0.001985

Normal scores raise the joint-exceedance rate 23-fold over independence; t scores raise it another 44% at the identical rank correlation, because normal scores leave joint extremes asymptotically independent while the t copula clusters them. And yet the sum’s TVaR barely moved between the two. Both facts are the lesson: a capital metric on the sum can be nearly blind to tail dependence that a joint trigger — a second-event cover, a combined-ratio covenant, an enterprise-wide stress test — feels at full strength. Choose scores= by the question being asked, which is exactly why the switch exists.

Reinsure it

A two-layer aggregate program prices the dependence decision in treaty currency. limit is the layer width per the coverage semantics, and share scales the second layer’s participation:

program = rs.ContractProgram([
    rs.AggregateLayer(attachment=7_000_000, limit=3_000_000, name="first"),
    rs.AggregateLayer(attachment=10_000_000, limit=5_000_000, share=0.8,
                      name="second"),
])
ceded, retained = rs.apply_contract(Mt.sum(axis=1), program)

world

ceded mean (pure premium)

95% CI

retained TVaR₉₉.₅

independent

17,859

[16,951, 18,766]

7,145,769

ρ = 0.5, normal

29,849

[28,694, 31,003]

7,215,111

ρ = 0.5, t, df = 4

29,475

[28,343, 30,608]

7,186,535

Moving from independence to ρ = 0.5 raises the program’s pure premium 65% — the reinsurer is selling back the diversification the cedent no longer has. Between the two copulas the intervals overlap: this aggregate trigger, like the sum’s TVaR, cannot tell them apart. The dependence level is priced; the tail shape at that level needs a joint trigger to matter.

How much of this is signal

Every number above came from 200,000 simulations, and risksim.uncertainty reports what that is worth — normal theory for the mean, distribution-free order statistics for VaR, percentile bootstrap for TVaR:

uncertainty.summary_with_error(retained, quantiles=(0.99, 0.995), rng=7)

metric

estimate

se

95% CI

mean

4,467,257

2,312

[4,462,726, 4,471,789]

VaR₉₉

7,000,000

[7,000,000, 7,000,000]

TVaR₉₉

7,093,268

12,621

[7,071,722, 7,121,664]

TVaR₉₉.₅

7,186,535

25,040

[7,143,683, 7,242,559]

The retained VaR interval collapsing to a point is not a bug — the treaty attaches at 7,000,000, the retained distribution has an atom there, and the order-statistic interval lands on it from both sides: the contract pins the quantile harder than any amount of simulation could. The ceded side is the opposite story: a pure premium of 29,475 with a 95% interval of [28,343, 30,608] is a ±4% error bar on the price — small enough to quote, big enough that the fourth digit was never real, and visible, which is the point. If the band is too wide, the answer is more simulations, and now you can see it.