4.4. Distributions

The computational core. aggregate.distributions is a thin re-export façade: the classes live in kind-split submodules but are imported and used through the stable public names (aggregate.Aggregate / aggregate.distributions.Aggregate both resolve to the same class).

4.4.1. Class hierarchy

Frequency          base class for frequency distributions   (aggregate._frequency)
  └── Aggregate    compound distribution (frequency x sev)   (aggregate._aggregate)
        └── Portfolio   collection of Aggregate units        (aggregate._portfolio)

Severity           scipy.stats wrapper + layers/limits/splicing  (aggregate._severity)

Severity is a standalone wrapper around scipy.stats continuous RVs and discrete empirical distributions, with support for layers, limits, and spliced forms. One named severity is the library’s own rather than scipy’s: sev cantor is the generalized Cantor law, singular continuous with no density, in aggregate.cantor. Aggregate combines a Frequency and one or more Severity objects by FFT convolution.

4.4.2. Where the code lives

Since 1.0.0a90 to a95 the former monolith is split behind the façade:

  • aggregate._frequency, aggregate._severity, aggregate._aggregate hold the Frequency / Severity / Aggregate classes (kind subclasses use the Base<Kind> registry dispatch);

  • aggregate._fits holds the method-of-moments severity fits (lognorm_fit, sln_fit, sgamma_fit, …) re-exported here;

  • the concern modules hold the orchestration that used to sit on Aggregate: reinsurance (aggregate._reinsurance), bucket/window sizing (aggregate._bucket_window), validation (aggregate._validation), single-distribution pricing (aggregate._pricing), and the FFT convolution kernel (aggregate._aggregate_compute). The Aggregate methods are thin delegators to these. See Internal Architecture.

PnL is re-exported through this façade for historical reasons; it is documented on its own Profit and Loss page.

Frequency([freq_name])

Manages Frequency distributions: creates moment function and MGF.

Severity([sev_name, exp_attachment, ...])

Aggregate(name[, exp_el, exp_premium, ...])

Compound (aggregate) probability distribution.

lognorm_fit(m, cv)

Lognormal (mu, sigma) parameters from mean m and cv cv.

sln_fit(m, cv, skew)

method of moments shifted lognormal fit matching given mean, cv and skewness

sgamma_fit(m, cv, skew)

method of moments shifted gamma fit matching given mean, cv and skewness

gamma_fit(m, cv)

gamma parameters from mean and cv.

beta_fit(m, cv)

alpha and beta parameters from mean and cv.

invgamma_fit(cv)

Inverse gamma shape parameter from cv.

invgauss_fit(cv)

Inverse Gaussian shape parameter from cv.

lognorm_lev(mu, sigma, n, limit)

return E(min(X, limit)^n) for lognormal using exact calculation currently only for n=1, 2

lognorm_approx(ser)

Lognormal approximation to series, index = loss values, values = density.

approximate_from_mcvsk(m, cv, skew, name, ...)

Dispatch (m, cv, skew) to a method-of-moments fit, in the requested form.

4.4.3. Frequency class

class aggregate.distributions.Frequency(freq_name=None, *args, **kwargs)[source]

Bases: HelpMixin

Manages Frequency distributions: creates moment function and MGF.

  • freq_moms(n): returns EN, EN^2 and EN^3 when EN=n

  • freq_pgf(n, z): returns the moment generating function applied to z when EN=n

Frequency distributions are either non-mixture types or mixture types.

Non-Mixture Frequency Types

  • fixed: no parameters

  • bernoulli: exp_en interpreted as a probability, must be < 1

  • binomial: Binomial(n/p, p) where p = freq_a, and n = exp_en

  • poisson: Poisson(n)

  • geometric: geometric(1/(n + 1)), supported on 0, 1, 2, … (the number-of-failures convention). The number-of-trials variant on 1, 2, … with p = 1/n is geometric zt, the same law by memorylessness.

  • logarithmci: logarithmic(theta), supported on 1, 2, …; theta solved numerically

  • neymana: Po(n/freq_a) stopped sum of Po(freq_a) freq_a = “eggs per cluster”

  • negbin: freq_a is the variance multiplier, ratio of variance to mean

  • pascal:

  • pascal: (generalized) pascal-poisson distribution, a poisson stopped sum of negative binomial; exp_en gives the overall claim count. freq_a is the CV of the frequency distribution and freq_b is the number of claimants per claim (or claims per occurrence). Hence, the Poisson component has mean exp_en / freq_b and the number of claims per occurrence has mean freq_b. This parameterization may not be ideal(!).

Mixture Frequency Types

These distributions are G-mixed Poisson, so N | G ~ Poisson(n G). They are labelled by the name of the mixing distribution or the common name for the resulting frequency distribution. See Panjer and Willmot or JKK.

In all cases freq_a is the CV of the mixing distribution which corresponds to the asympototic CV of the frequency distribution and of any aggregate when the severity has a variance.

  • gamma: negative binomial, freq_a = cv of gamma distribution

  • delaporte: shifted gamma, freq_a = cv of mixing disitribution, freq_b = proportion of certain claims = shift. freq_b must be between 0 and 1.

  • ig: inverse gaussian, freq_a = cv of mixing distribution

  • sig: shifted inverse gaussian, freq_a = cv of mixing disitribution, freq_b = proportion of certain claims = shift. freq_b must be between 0 and 1.

  • sichel: generalized inverse gaussian mixing distribution, freq_a = cv of mixing distribution and freq_b = lambda value. The beta and mu parameters solved to match moments. Note lambda = -0.5 corresponds to inverse gaussian and 0.5 to reciprocal inverse gauusian. Other special cases are available.

  • sichel.gamma: generalized inverse gaussian mixture where the parameters match the moments of a delaporte distribution with given freq_a and freq_b

  • sichel.ig: generalized inverse gaussian mixture where the parameters match the moments of a shifted inverse gaussian distribution with given freq_a and freq_b. This parameterization has poor numerical stability and may fail.

  • beta: beta mixing with freq_a = Cv where beta is supported on the interval [0, freq_b]. This method should be used carefully. It has poor numerical stability and can produce bizzare aggregates when the alpha or beta parameters are < 1 (so there is a mode at 0 or freq_b).

Code proof for Neyman A:

from aggregate import build, qd
mean = 10
eggs_per_cluster = 4
neya = build(f'agg Neya {mean} claims dsev[1] neymana {eggs_per_cluster}')
qd(neya)

po = build(f'agg Po4 {eggs_per_cluster} claims dsev[1] poisson')
po_pmf = po.density_df.query('p_total > 1e-13').p_total

byhand = build(f'agg ByHand {mean / eggs_per_cluster} claims dsev {list(po_pmf.index)} {po_pmf.values} poisson')
qd(byhand)

df = pd.concat((neya.density_df.p_total, byhand.density_df.p_total), axis=1)
df.columns = ['neya', 'byhand']
df['err'] = df.neya - df.byhand
assert df.err.abs().max() < 1e-5
df.head(40)

Code proof for Pascal:

from aggregate import build, qd
mean = 10
claims_per_occ =1.24
overall_cv = 1.255
pascal = build(f'agg PascalEg {mean} claims dsev[1] pascal {overall_cv} {claims_per_occ}', log2=16)
qd(pascal)

c = (mean * overall_cv**2 - 1 - claims_per_occ) / claims_per_occ
th = claims_per_occ * c
a = 1 / c
# from form of nb pgf identify r = a and beta = theta, mean is rb, var is rb(1+b)
nb = build(f'agg NB {claims_per_occ} claims dsev[1] negbin {th + 1}', log2=16)
nb_pmf = nb.density_df.query('p_total > 1e-13').p_total
qd(nb)

byhand = build(f'agg ByHand {mean / claims_per_occ} claims dsev {list(nb_pmf.index)} {nb_pmf.values} poisson', log2=16)
qd(byhand)

df = pd.concat((pascal.density_df.p_total, byhand.density_df.p_total), axis=1)
df.columns = ['pascal', 'byhand']
df['err'] = df.pascal - df.byhand
assert df.err.abs().max() < 1e-5
df.head(40)
Parameters:
  • freq_name – name of the frequency distribution, poisson, geometric, etc.

  • freq_a

  • freq_b

supports_zm = False
carries_own_count = False
modify_mean(base_mean=None)[source]

Forward map: the realized E[N] for an un-modified (base) mean.

The zero-modified construction holds the base distribution fixed and reweights it, so the realized mean is an output: \(E[N^M] = c\,E[N]\) with c from _zm_weight(). Closed form, defined for every p0M in [0, 1) – no solver, and no infeasible region.

Parameters:

base_mean (float, optional) – The un-modified mean. Defaults to base_mean, the value in force for this object.

Returns:

float – The realized E[N]. Returns base_mean unchanged when the frequency is not zero modified, so callers need not special-case.

solve_base_mean(target_mean)[source]

Inverse map: the base mean whose realized E[N] is target_mean.

The counterpart of modify_mean(), and the DecL default: a bare poisson zm 0.5 states the realized mean and comes here. The ! marker (poisson zm 0.5 !) opts out and takes modify_mean() instead. This is the one place a numerical solve survives in the zero-modification path, and since a325 it is on the common path, so its infeasible branch is reachable from ordinary programs.

Notes

modify_mean() increases in the base mean from a finite infimum \(L = (1 - p_0^M)\lim_{m \to 0} m / (1 - p_0(m))\), so a target at or below L is genuinely unattainable – for a zero-truncated Poisson L = 1, which is why a ZT count can never average fewer than one claim. The bracket is located by evaluating at a small positive floor and doubling upwards, so it stays family-agnostic.

Raises:

ValueError – If target_mean lies at or below the attainable infimum.

apply_deductible(survival)[source]

Loss Models §8.6: the payment count N^P implied by this loss count.

A deductible thins each loss independently with retention probability v = S(d). For a frequency whose pgf takes the form

\[P_{N^L}(z; \theta, \alpha) = \alpha + (1 - \alpha) \frac{\phi[\theta(1 - z)] - \phi(\theta)}{1 - \phi(\theta)}\]

the payment count is the same family with \(\theta \to v\theta\) and a fresh zero mass \(\alpha^* = P_{N^L}(1 - v)\) (Klugman, Panjer and Willmot 2012, eq. 8.3 ff.). A zero-truncated loss count therefore yields a zero-modified payment count: periods with no payment become possible again.

Parameters:

survival (float) – \(v = S(d)\), the probability that a loss exceeds the deductible.

Returns:

Frequency – A new frequency of the same kind with base_mean and en set, describing N^P.

Raises:
  • ValueError – If survival is outside (0, 1], or base_mean is unset.

  • NotImplementedError – For families whose base parameter does not scale linearly under thinning (currently logarithmic).

freq_pgf_prime(n, z)[source]

Derivative of the pgf, \(P_N'(z)\), elementwise in z, when E[N] = n.

Same contract as freq_pgf(): n is the un-modified (base) mean and the evaluation is elementwise in the transformed argument z. The derivative is the Fourier-side ingredient of the Palm conditional-mean identity (aggregate._aggregate_compute.palm_conditional_mean()): for two compounds sharing one event process, the kernel of \(\mathsf{P}[S_c \mid S_n = s]\) is \(\mathrm{iFFT}(P_N'(\varphi_n))\) with \(\varphi_n\) the transform of the conditioning image severity.

Parameters:
  • n (float) – The un-modified (base) expected claim count, exactly as for freq_pgf() (pass Aggregate.base_mean, never the realized n, under zero modification).

  • z (scalar or ndarray, real or complex) – Evaluation points (e.g. an rfft vector).

Returns:

ndarray – \(P_N'(z)\), elementwise.

Raises:

NotImplementedError – For families outside the supported set (sig, beta, sichel and variants, neymana, pascal, logarithmic). Callers catch this and degrade (the P&L ladder stays marginal).

Notes

Two routes. (a, b, 0) families (Poisson, binomial, negbin, geometric, flagged _panjer_ab0) use the Panjer recursion k p_k = (a k + b) p_(k-1): multiplying by z^(k-1) and summing gives P'(z) = a z P'(z) + (a + b) P(z), so

\[P_N'(z) = \frac{(a + b)\, P_N(z)}{1 - a z}.\]

For Poisson (a = 0, b = n) this is the size-bias fact P' = n P. The identity holds for the base pgf, so the base implementation reads the class-level freq_pgf, never an instance-level zero-modified wrapper; _install_zm_wrappers layers c G' on top. Closed-form overrides cover fixed, bernoulli, empirical (and renewal through it), and the common mixed Poissons (gamma, delaporte, ig) via \(P_N'(z) = n M_G'(n(z-1))\) for mixing mgf \(M_G\).

A universal fallback, deliberately not built (author’s ruling, 2026-09-29): P_N'(z) = sum_k k p_k z^(k-1) off the freq_pmf FFT inversion, evaluated with evaluate_pgf_polynomial, would cover every family at the cost of materializing the count pmf.

property supports_pgf_prime

True when the family can evaluate the pgf derivative.

The capability the kappa chart and the Palm scenario ladder key on: callers that would otherwise catch NotImplementedError from freq_pgf_prime() read this instead, so chart availability and the emitter’s route switch agree without a try/except at predicate time.

Returns:

bool – True for the (a, b, 0) families (the _panjer_ab0 flag) and for any subclass carrying a closed-form freq_pgf_prime() override (fixed, bernoulli, empirical/renewal, and the gamma, delaporte and ig mixed Poissons); False for the rest (logarithmic, sig, beta, sichel and variants, neymana, pascal).

Notes

Reads the override off the class, never the instance: _install_zm_wrappers layers a c G' wrapper onto the instance, and a zero-modified member of an unsupported family must still answer False (the wrapper wraps a route that raises).

__init__(freq_name, freq_a, freq_b, freq_zm, freq_p0)[source]

Creates the freq_pgf and moment function:

  • moment function(n) returns EN, EN^2, EN^3 when EN=n.

  • freq_pgf(n, z) is the freq_pgf evaluated at log(z) when EN=n

Parameters:
  • freq_name – name of the frequency distribution, poisson, geometric, etc.

  • freq_a

  • freq_b

  • freq_zm – freq_zm True if zero modified, default False

  • freq_p0 – modified p0, probability of zero claims

freq_name = ''
property info

Fixed-layout multi-line summary string (terse).

Every row is always present, in the same order, for every Frequency; a value that does not apply – or that needs the exposure the owning Aggregate supplies (the moment rows before en is stamped) – renders as n/a. Shares the label/value convention (aggregate.constants.info_row()) with Aggregate / Portfolio / Severity. The row catalogue is documented in dev/info-strings.rst.

property name: str

Object name – the frequency family ('poisson', 'negbin', …).

Read-only alias of freq_name, so every first-class class answers to name. A frequency has no user-settable identity of its own: it is its family plus its parameters.

property tail_description: str

this frequency family’s right-tail class (count layer).

E.g. poisson frequency, super-exponential count. The count support depends on the exposure (the aggregate’s n), so a standalone frequency reports only its family class; the full count support appears in the aggregate’s tail_behavior_df. The verbose form is tail_explanation.

Type:

One line

property tail_explanation: str

family, dispersion, tail class.

The Frequency twin of tail_explanation: the family and its parameters, the zero-modification if any, how the count tail classifies (and hence whether the aggregate tail can be set by the frequency rather than the severity), and – once the owning Aggregate has stamped en – the mean, SD and variance-to-mean ratio that says over- or under-dispersed relative to Poisson.

Type:

Verbose prose over the count law

property prob_eq_0

P(N = 0) – the probability of no claims.

The frequency-level member of the prob_eq_0 family: the same question one level down from prob_eq_0 (P(X = 0)), under the same name and the same zero-argument shape.

Evaluated at base_mean, the un-modified mean the owning Aggregate stamps at construction; a standalone frequency raises until it is set. For an unmodified frequency base_mean equals en, so this is simply P(N = 0) at the expected claim count.

Under zm / zt the answer is freq_p0 by construction – that is what zero modification means – not _prob_eq_0 evaluated at any mean. Reading it back is the cheapest check that a zero modification took.

Returns:

float – P(N = 0) at the current mean.

Raises:

ValueError – If this kind has no closed-form zero probability (the class-level _prob_eq_0 default is None; only the zero-modifiable kinds override it – see supports_zm), or if the mean is not set.

Notes

Renamed from prn_eq_0(n) at 1.0.0a151. The parametrized worker survives as the private _prob_eq_0(n): Aggregate calls it per mixture component at that component’s mean, and the ZM machinery (_zm_weight()) evaluates it at the base mean, so the argument form is genuinely needed internally. This mirrors the a149 tail_df (no-argument property) / tail_periods_df(periods=) (parametrized worker) split.

property freq_df[source]

Count pmf vs mean-matched Poisson comparison table (on demand).

Index n (count), columns p (this frequency’s pmf) and po_p (the Poisson pmf with the same mean) – an eyeball diagnostic for how far the count law sits from Poisson (over/under-dispersion, cluster fatness, renewal regularity). Computed on first access and cached.

A parametric frequency is a family until the exposure fixes its mean, so the pmf uses self.base_mean – the un-modified mean the owning Aggregate stamps at construction, which equals en unless zm / zt is in force (a standalone frequency raises until one of them is set). The pmf inverts freq_pgf on a power-of-two FFT grid sized by the frequency’s own moments (mean + 10 sd); trailing rows where both columns are below 1e-15 are trimmed. The Poisson comparison column is matched to the realized mean, so a zero-modified row reads against the Poisson an observer would fit to the same data. The empirical family (dfreq / renewal) overrides with its exact materialized pmf and intrinsic mean. Guard: mean <= 1000 (a toy, small-count diagnostic).

4.4.4. Severity class

class aggregate.distributions.Severity(sev_name=None, exp_attachment=None, exp_limit=inf, sev_mean=0, sev_cv=0, sev_a=nan, sev_b=0, sev_loc=0, sev_scale=0, sev_xs=None, *args, **kwargs)[source]

Bases: HelpMixin, LabeledMixin, ProgramMixin, rv_continuous

sev_kind: str = ''
reference_id: str = ''

The dotted id as written ('agg.SL'), for messages and reports.

reference_support_max: float = None

The referenced source’s own THEORETICAL upper support end (np.inf when unbounded), which is not the largest materialized atom. Reporting only: tail_behavior_df and the tail narrative read it so an unbounded inner is described as unbounded, while every number – bucket selection, moments, the FFT – rides the finite atoms. See dev/done/plan-agg-port-as-sev.md section 4.6.

reference_bs: float = None

The referenced source’s own bucket size, known exactly on the resolution path (never inferred from np.diff of the atoms). Read by the commensurability step at the end of bucket selection.

__init__(sev_name, exp_attachment=None, exp_limit=inf, sev_mean=0, sev_cv=0, sev_a=nan, sev_b=0, sev_loc=0, sev_scale=0, sev_xs=None, sev_ps=None, sev_wt=1, sev_lb=0, sev_ub=inf, sev_conditional=True, sev_signed=False, sev_reflect=False, name='', note='', hints='', tags=(), label=None, label_map=None, exp_retention_end=inf)[source]

Continuous random variable adding layer/attachment to ss.rv_continuous.

Construction is delegated to a registered subclass — __new__ classifies the inputs via _classify_sev() and dispatches to SeverityScipy, SeverityDHistogram, SeverityCHistogram, SeverityFixed, SeverityMeta, or SeverityCopy. The chosen subclass’s _build populates self.fz from the stored spec inputs; the post-build helpers below then apply truncation decorators, compute attachment/detachment probabilities, and validate the achieved mean/CV against the targets.

Parameters:
  • sev_name (str | Severity | Aggregate | Portfolio) – scipy.stats distribution name (e.g. lognorm), special form dhistogram / chistogram / fixed, or an existing Severity/Aggregate/Portfolio instance for the copy / meta paths.

  • exp_attachment (float | None) – Layer attachment point. None means “no layer clause” and conditions on X >= 0; 0 means “conditional on X > 0” (this distinction matters for distributions with mass at zero).

  • exp_limit (float) – Layer width (the “y” in y xs a).

  • sev_mean (float) – Target mean / coefficient of variation for distributions parameterised by moments (lognorm, gamma, beta, …).

  • sev_cv (float) – Target mean / coefficient of variation for distributions parameterised by moments (lognorm, gamma, beta, …).

  • sev_a (float) – scipy shape parameters where applicable.

  • sev_b (float) – scipy shape parameters where applicable.

  • sev_loc (float) – scipy location / scale.

  • sev_scale (float) – scipy location / scale.

  • sev_xs (array-like) – Support points and probabilities for the histogram kinds.

  • sev_ps (array-like) – Support points and probabilities for the histogram kinds.

  • sev_wt (float) – Mixture weight (not used internally; passed through for callers).

  • sev_lb (float) – Optional support bounds; if not the trivial [0, inf] the scipy methods are wrapped with conditional-truncation decorators.

  • sev_ub (float) – Optional support bounds; if not the trivial [0, inf] the scipy methods are wrapped with conditional-truncation decorators.

  • sev_conditional (bool) – Whether layered moments / functions divide out P(X > attachment).

  • name (str) – Identifier (e.g. set by sev SOMENAME … in DecL).

  • note (str) – Free-text annotation (from a note{...} clause).

  • hints (str) – Raw hints{...} build-settings string; retained as annotation (a standalone Severity has no build settings to apply).

  • exp_retention_end (float) – Ground-up loss d2 at which the retention reaches zero ([Franchise-Deductible]). inf (default) is the ordinary deductible; d2 == exp_attachment is the franchise, which pays the whole loss, up to the limit, once it exceeds the attachment. A finite value needs a layer clause and d2 >= attachment. The limit is the largest payment in every form; detachment is the loss at which the payment first reaches it. See _retention_payment() and _retention_detachment().

Warning

Layer/attachment parameters (attachment, limit, detachment, pattach, pdetach, conditional) and splice parameters (sev_lb, sev_ub) are captured in closure by _apply_lb_ub() and _apply_layer_attachment() at the end of __init__. Mutating these attributes on an existing instance produces stale results because the wrapped methods on self.fz retain the original values. If the policy changes, build a new Severity rather than reassigning fields on an existing one.

Raises:

ValueError – If sev_lb / sev_ub describe a splice window with zero probability mass under the underlying distribution (fz.cdf(sev_ub) - fz.cdf(sev_lb) <= 1e-15); conditioning on a measure-zero set is mathematically undefined.

tags

Tag slugs from the DecL tags{...} trailer (‘()’ when none).

property info

Fixed-layout multi-line summary string (terse).

Every row is always present, in the same order, for every Severity; a value that does not apply renders as n/a. Shares the label/value convention (aggregate.constants.info_row()) with Aggregate / Portfolio / Frequency. The row catalogue is documented in dev/info-strings.rst.

property actual_m

Severity mean E[X(a, d)] (analytic, post-layer, post-splice).

property actual_var

Severity variance (analytic).

property actual_sd

Severity standard deviation (analytic).

property actual_cv

Severity coefficient of variation (analytic).

The achieved CV. Compare sev_cv, the CV requested in the declaration – they differ whenever a layer, splice or shift is applied (and their agreement on an unlimited severity is exactly what _validate_moments checks at construction).

property actual_skew

Severity skewness (analytic).

property tail_class

This severity’s TailClass rung.

Deterministic family lookup (param-aware): structural-bounded test first (so bounded is spec-only), then scipy family. See aggregate.tail.classify_severity().

property bounded: bool

Whether the (post-layer, post-splice) severity has bounded support.

Derived view: True iff tail_class is BOUNDED — i.e. fixed / dhistogram / chistogram (finite by construction), a scipy family in _BOUNDED_SCIPY_SEVS, a finite layer exp_limit or splice sev_ub, or a meta / copy wrapping a bounded object.

property tail_description: str

family, declared support, claim-space support, tail class.

E.g. lognorm, [0, inf), subexponential right tail for an unlimited severity, or lognorm, [0, 500], bounded; 500 xs 250 once a layer is declared and dhistogram, [1, 6], bounded; atoms [1 2 3 4 5 6] for a dsev. Derived from the same severity_tail_row() as the aggregate’s tail_behavior_df comp rows, with the declared layer / atom phrase appended when it says something the interval does not (a149: this absorbed the old support_description). The verbose form is tail_explanation.

Type:

One line

property tail_explanation: str

Verbose prose over this severity’s support and tail behavior.

The Severity twin of tail_explanation: the family and its claim-space support and per-side classes, then the declared layer / atom support, then what the tail class means for the moments (the power-law tail index and the first moment that fails to exist, or the guarantee that every moment is finite on a bounded support).

moms()[source]

First three moments of the layered severity X(a, d) = min(d, (X-a)+).

Notes

Three paths in order:

  1. Histogram fast-path. If sev1 was precomputed during _build (histograms and meta-severities populate sev1 / sev2 / sev3 directly from the support) and the layer is the trivial [0, inf], return those values immediately.

  2. Analytic shortcut. For lognorm, pareto, gamma, and expon (with no shift/truncation), delegate to _moms_analytic(), which computes layered moments in closed form via partial expected values. Defensive np.inf overrides are applied when the underlying moment does not formally exist (e.g. pareto shape <= 1).

  3. Numerical fallback _numerical_moms() integrates the survival quantile function over the layer interval and applies the attachment/detachment/conditional adjustments.

Returns:

  • (E[X(a,d)], E[X(a,d)^2], E[X(a,d)^3]) (tuple of float) – np.nan signals an unreliable numerical result; np.inf means the moment is genuinely undefined.

  • Mathematical background

  • -----------------------

  • With :math:`X(a, d) = min(d, (X-a)_+)` and using the

  • quantile-space change of variables :math:`x = q(p)`, \(f(x)dx = dp\),

  • math:: –

    E[X(a, d)^n] = int_{F(a)}^{F(d)} (q(p) - a)^n, dp
    • (d - a)^n S(d).

  • The numerical path integrates :math:int q(p)^n dp`` and then

  • applies the binomial expansion to recover :math:`E[(X-a)^n].`

plot(n=None, log=False, full_range=False, reflect=False, return_period=False, invert=False)[source]

Plot the severity: its density, and its quantile (Lee) diagram.

Parameters:
  • n (int, optional) – Grid points. The grid is quantile spaced, so this buys resolution in probability rather than in loss.

  • log (bool) – Read both axes of the density panel on log, which is the log density panel this plot used to draw as a third picture.

  • full_range (bool) – Show the whole computed grid, out to the 1-in-100,000 loss, rather than the crop at the 0.1% exceedance.

  • reflect (bool) – Read the Lee panel against the exceeding probability rather than the non-exceeding one, so the curve drawn is the survival function. With invert it is S(x) the usual way round, and that axis offers a log reading where the non-exceeding one does not.

  • return_period (bool) – Read the Lee panel against return period rather than non-exceedance probability.

  • invert (bool) – Exchange the Lee panel’s axes, which draws the distribution function: a quantile function and a cdf are inverses, so it is the same pairs read the other way round.

Returns:

matplotlib.figure.Figure – Also stashed on self.figure.

Notes

Draws the document charts.chart_severity emits. Four panels became two, and neither collapse loses anything. The density and the log density were one quantity read two ways, so log is a declared reading of the one panel. The distribution and the Lee diagram are inverses, the same curve with its axes exchanged, so only one of them carries information the other does not; the Lee orientation is kept because it is the one that pairs with a return-period reading.

A severity is the one genuinely continuous law in this library, so its series says support='continuous' and the renderer draws a line rather than steps. A discrete severity has no density at all, so it is drawn as probability mass and is atomic.

4.4.5. Aggregate class

class aggregate.distributions.Aggregate(name, exp_el=0.0, exp_premium=0.0, exp_lr=0.0, exp_en=0.0, exp_attachment=None, exp_limit=inf, sev_name='', sev_a=nan, sev_b=0.0, sev_mean=0.0, sev_cv=0.0, sev_loc=0.0, sev_scale=0.0, sev_xs=None, sev_ps=None, sev_wt=1.0, sev_lb=0.0, sev_ub=inf, sev_conditional=True, sev_signed=False, sev_reflect=False, sev_pick_attachments=None, sev_pick_losses=None, occ_reins=None, occ_kind='', occ_reins_label=None, freq_name='', freq_a=0.0, freq_b=0.0, freq_zm=False, freq_p0=nan, freq_pin_mean=True, exp_years=0.0, exp_rate=0.0, wait_name='', wait_a=nan, wait_b=0.0, wait_mean=0.0, wait_cv=0.0, wait_loc=0.0, wait_scale=0.0, wait_xs=None, wait_ps=None, wait_wt=1.0, wait_lb=0.0, wait_ub=inf, wait_conditional=True, wait_attachment=None, wait_limit=inf, agg_reins=None, agg_kind='', agg_reins_label=None, reins_bucket=None, dsev_bucket=None, value_type='loss', approximate='exact', label=None, label_map=None, note='', hints='', tags=(), _tweedie=None, exp_retention_end=None)[source]

Bases: HelpMixin, LabeledMixin, ProgramMixin

Compound (aggregate) probability distribution.

Implements the FFT-based algorithm of Mildenhall (2024): discretize severity, FFT, apply frequency PGF, inverse FFT. See _freq_sev_convolution for the five-line core; update_work for the orchestration (severity prep → occurrence reinsurance → convolution → aggregate reinsurance → audit). Validation by theoretical-vs-empirical moment comparison (paper §4.7) lives in the empirical and error columns of stats_df written at the end of update_work.

Construction is via the __init__ arguments below, or — more usually — via build() parsing DecL.

Public surface that Portfolio and Bounds depend on. Three stats surfaces — info for text, summary_df for the daily risk view, and stats_df for everything else — plus the compute and risk-measure surface:

Stats / display
  • info: one-screen textual summary (frequency, severity, layer, grid, validation flag). Not stats.

  • summary_df: 3-row Freq / Sev / Agg at-a-glance moments + key percentiles. The daily-driver headline. tail_df is the companion return-period table; validation_df the moment-error QA frame.

  • stats_df: MultiIndex (component, measure) × per-component / mixed / independent / empirical / error. Single source of truth for Aggregate moments — see the property for the row / column reference.

  • stats_df: MultiIndex (component, measure) × per-component / mixed / independent / empirical / error. Single source of truth for Aggregate moments — see the property for the row / column reference.

Data attributes
  • agg_density: empirical PMF on the bucket grid (set by update_work; consumed by Portfolio).

  • ftagg_density: FT of the aggregate density (consumed by Portfolio’s copula combine).

  • density_df: per-bucket density / CDF / risk-measure frame.

  • n: total frequency.

  • name, program, note: spec metadata.

  • bs, log2, xs: discretization grid.

Methods
  • update, update_work: trigger / drive the compute.

  • q, q_sev, tvar, tvar_sev, cdf, sf, pdf, pmf, var_dict: risk-measure surface.

  • sample: draw from the discretised aggregate.

  • price: distortion-based pricing.

  • approximate, entropy_fit: parametric fits to the FFT output.

  • apply_distortion, pollaczeck_khinchine: distortion / ruin.

  • plot: single plotting entry point.

  • snap, picks, unwrap, aggregate_error_analysis, severity_error_analysis: utilities.

Methods / attributes with a leading underscore are internal — _init_stats_df, _record_component, _freq_sev_convolution, _apply_reins_work, _limits, _html_info_blob, _grid_distribution, … . The legacy audit_df / report_df / report_ser / statistics / statistics_df / statistics_total_df surface has been removed; consult stats_df instead.

property spec

Get the dictionary specification, but treat as a read only property

Returns:

property spec_ex

All relevant info.

Returns:

property tail_class

The (freq, sev, agg) TailClass triple.

The authoritative tail-thickness computation: frequency and severity rungs by deterministic family lookup, the aggregate rung by the max combine rule. .bounded and the tail text are derived from this. Returns a TailClasses namedtuple with .freq / .sev / .agg fields.

property bounded: bool

Whether the aggregate has bounded support.

Derived view: True iff the aggregate tail class is BOUNDED, i.e. the frequency and every severity component is bounded. Frequencies in _BOUNDED_FREQS (fixed, bernoulli, binomial, empirical) are bounded; mixed Poisson / negbin / etc. are not. A severity is bounded when it is a histogram, a fixed atom, a bounded scipy family, or carries a finite layer exp_limit or splice sev_ub. Conservative: False whenever boundedness cannot be proved from the spec. Set self.bounded = True to certify (e.g. a fat-tailed scipy severity with a large layer cap the heuristic misses).

property tail_description: str

Three aligned lines summarising the frequency, severity, and aggregate tails.

Short narrative over the layered tail_behavior_df report – per-layer support and per-side tail class, plus the aggregate concentration. E.g.:

frequency tail           poisson, count [0, inf), super-exponential right tail
severity tail            lognorm, [0, inf), subexponential right tail
aggregate tail           [0, inf), subexponential right tail; not concentrated (cv=1.5)

The verbose form is tail_explanation; the lines are also appended to info().

property tail_explanation: str

Verbose prose over the layered tail report (the per-component story).

Walks the book bottom-up – frequency, the severity components and their blend, the aggregate – naming the single-big-jump mechanism (or the frequency driver) for a thick right tail, any power-law moment failure, and the concentration. Derived from tail_behavior_df.

property bs_window_df: DataFrame

Curated, read-only view of the bucket/window decision ([bs-reporting]).

One row per sizing method that ran (moment, exact_discrete, bounded_small, windowed, sbj) plus the realized used grid, indexed by method, culled to the user-facing columns: whether the method applies, whether it was selected, the method window (x_min / x_max) and its width W, the grid (bs / log2), the log2 the window log2_need``s at that ``bs, what fraction of the distribution the window holds (coverage), any estimated clipped far-tail mass, and a one-line note.

Returns None before update(). See bs_description / bs_explanation for the narrative.

Notes

``W`` and ``coverage`` are the point of the frame, and were missing from it until 1.0.0a254. The leaf exists to answer “is this grid big enough”, and the width and the coverage are the answer: without them a reader has a list of candidate windows and no way to compare them. They were curated out as expert material, which was the wrong call about which columns carry the meaning.

coverage is a string ('1-1e-12', 'E[N]-adj 1-1e-12'), carrying precision a float cannot and saying which of two things was held to that precision.

Every column carries its own dtype, which it did not until 1.0.0a275: applies / selected bool, the window and grid columns float, log2 / log2_need nullable Int64 (an exponent reads as an integer, and a method that never ran records none), coverage / note string. The frame used to be built by transposing a method-per-column block, which typed the lot object and cost a served table its right alignment and its raw values.

property bs_description: str

One-line summary of the chosen bucket grid ([bs-reporting]).

The winning method and the realized (bs, log2, x_min) with the grid top, plus a clip note when the far tail is truncated. The verbose form is bs_explanation; the ANSI-coloured variant is aggregate.distributions.bs_describe(agg, color=True).

property bs_explanation: str

Verbose prose explaining the bucket-grid choice ([bs-reporting]).

What the book is (the aggregate tail one-liner), which methods applied and which won and why, the realized grid, and any far-tail clip with how to widen it. The ANSI-coloured variant is aggregate.distributions.bs_explain(agg, color=True).

sharpen(bs=None, log2=None, *, log2_cap=20, bs_limit=16, power=2, good_enough=0.5, execute=True)[source]

Probe the grid neighbourhood and move to a better (bs, log2).

Delegated to sharpen(), where the score, the probe geometry and the selection rule are documented in full.

update() chooses a grid from the analytic moments before any FFT runs; this audits that choice afterwards. A row per log2, and within each a line search out from the current bucket: bs is doubled until validation_score stops improving, then halved likewise, capped at bs_limit each way. It then takes the best score among the cells that do not grow log2, growing by one only when nothing at the current size or smaller reaches good_enough. A discrete severity whose atoms are a whole number of buckets is already exact, so the bucket is pinned and only log2 is probed.

Populates sharpen_df, sharpen_description and sharpen_explanation. Returns self, so the call chains; the object is moved in place, so a = a.sharpen() rebinds the same object rather than producing a second one.

It also pins the outcome onto the object’s own text. program, note and hints are rewritten together so the three agree and build(a.program) reproduces the sharpened object: a moved grid becomes hints{log2=...; bs=...}, a confirmed one a note{sharpen: ...} and no hints. Probing twice replaces the verdict rather than stacking a second one. See aggregate._program.pin_sharpen().

Examples

a = build('agg X 100 claims sev lognorm 100 cv 2 poisson')
a.sharpen()
print(a.sharpen_description)
print(a.hints)          # the grid it settled on
build(a.program)        # rebuilds on that grid
property sharpen_df: DataFrame

The last sharpen() probe, one tidy row per grid cell.

Columns: the integer offsets d_bs / d_log2 from the probe centre, the realized bs / log2 / extent / x_min, the score and its six normalized terms (u_sev_mean through u_agg_skew), the aliasing ratio, the validation verdict, a warnings count, seconds, the selected winner, and a note carrying the exception text for any cell that failed.

Indexed by (d_bs, d_log2), so the picture is one unstack away:

a.sharpen_df.score.unstack('d_log2')

The line search makes the rows ragged, so cells never visited come back nan. None before sharpen() runs.

property sharpen_description: str

One-line summary of the last sharpen() probe.

What was scored, what won, and whether the grid moved. The verbose form is sharpen_explanation.

property sharpen_explanation: str

Verbose prose explaining the last sharpen() probe.

What the score means, how to read the probe as extent-limited versus resolution-limited, why log2 was or was not grown, and what changed. The short form is sharpen_description.

property sharpen_program: str

The program that rebuilds this aggregate on the sharpened grid.

The fourth thing a probe produces, beside sharpen_df, sharpen_description and sharpen_explanation. sharpen() pins its outcome onto program as it finishes, so this is that program rendered to read: the spread layout with the trailer left in, since here the trailer is the payload. hints{log2=...; bs=...} when the grid moved, a note{sharpen: ...} and deliberately no hints when it did not. '' before sharpen() runs.

Which of the four to reach for: this one for text to read or share, program for the one-line stamp, hints for the settings alone, note for the verdict alone.

Delegated to aggregate._program.sharpen_program(); the three outcomes are documented on aggregate._program.pin_sharpen().

pnl_program(loss_ratio=0.7, expense_ratio=0.25, *, net_combined_ratio=None, occ_combined_ratio=None, agg_combined_ratio=None, premium_style='deposit')[source]

The program that wraps this aggregate in a P&L.

Returns pnl NAME_PnL <premium> less <engine> less <expense>, with this object’s body inlined as the engine, so the text is self-contained and builds anywhere. The premium is inherit premium when the exposure states one, and otherwise expected loss over loss_ratio; expense_ratio=0 omits the expense clause.

net_combined_ratio additionally prices the reinsurance, which is what silences the ZeroPremiumCessionWarning an unpriced cession raises: the net book and each cover are priced separately and added, then grossed up once for expenses.

Delegated to aggregate._program.pnl_program(), where the premium rule and the trailer’s move up to the wrapping pnl are documented in full, and to aggregate._program._pnl_technical() for the ladder’s algebra.

Parameters:
  • loss_ratio (float, default 0.70) – Sizes the premium, and only when there is none to inherit and net_combined_ratio is None.

  • expense_ratio (float, default 0.25) – Gross expense as a fraction of premium.

  • net_combined_ratio (float, optional) – Expected net loss over net technical premium. None, the default, leaves the function exactly as it was and any cession unpriced; a number engages the ladder.

  • occ_combined_ratio (float or sequence, optional) – Each tier’s combined ratio, defaulting to net_combined_ratio. A sequence gives one value per layer, in declaration order.

  • agg_combined_ratio (float or sequence, optional) – Each tier’s combined ratio, defaulting to net_combined_ratio. A sequence gives one value per layer, in declaration order.

  • premium_style ({'deposit', 'rate'}, default 'deposit') – How each ladder-priced layer premium is written: a currency deposit, or a rate of the P&L’s stated gross premium, the identical premium in the quote-sheet spelling. Moot with no ladder, and a clause the author wrote is kept verbatim.

Returns:

str – DecL for the wrapping P&L.

Return type:

str

Examples

a = build('agg X 100 claims sev lognorm 100 cv 2 poisson')
build(a.pnl_program(loss_ratio=0.65))

r = build('agg Y 10 claims sev lognorm 100 cv 2 '
          'occurrence net of 100 xs 100 poisson')
build(r.pnl_program(net_combined_ratio=0.9))
reins_program(cession)[source]

The program that rebuilds this aggregate with cession added.

The clause lands in its correct slot, which is the point: an occurrence cession sits before the frequency clause and an aggregate cession after it, so neither can simply be appended to the program text. The result is self-contained, with this object’s body inlined, so it builds in any session with nothing registered first.

Delegated to aggregate._program.reins_program().

Parameters:

cession (str or iterable of str) – One cession clause per tier, each opening with occurrence or aggregate. The clause is authoritative for its own tier and leaves the other alone.

Returns:

str – DecL for the reinsured aggregate.

Return type:

str

Examples

a = build('agg X 100 claims sev lognorm 100 cv 2 poisson')
build(a.reins_program('occurrence net of 500 xs 500'))
property tail_behavior_df: DataFrame

The layered tail report – support and tail class per layer – as a DataFrame.

One row per layer, bottom-up – frequency; one per severity mix component (comp0 …); the combined effective severity (only when there is more than one component); and the aggregate. Columns: min / max structural support (-inf / inf at an unbounded end), left_tail / right_tail (the per-side tail class – bounded at a finite end, else the family decay rung), bounded (the support finite both ends), and (aggregate row only) the conservative concentrated flag and cv = sd / mean. A note carries power-law alpha / infinite-moment and capped-base annotations.

Spec-only – built from the family classifier, the structural support, and the pre-computed moments, so it is valid before update(). The numeric grid reach lives in the bucket report (bs_window_df), not here. See aggregate.tail and dev/bucket-selection.rst.

Returns:

pandas.DataFrame – Indexed by component.

property reins_bucket: str

Rebucketing scheme for reinsurance net/ceded distributions.

'linear' (default) splits each off-grid net/ceded value’s mass across its two bracketing grid buckets, preserving the first moment exactly; 'nearest' rounds to the closest bucket (≤ bs/2 positional bias). Mirrors Portfolio.allocation_method().

Reinsurance is baked in during update(), so a change after build requires a re-update() to take effect. The setter clears the cached reinsurance frames so they rebuild on next access.

property dsev_bucket: str

Scheme for placing discrete-severity atoms onto the model grid.

Applies to dsev / dhistogram / fixed (point-mass) severities; continuous severities are already exact via the cdf-difference and ignore this. Sibling of reins_bucket.

'linear' (default) splits each off-grid atom’s mass across its two bracketing grid buckets k, k+1 with weights 1-f, f (f = x/bs - k), so the discretized first moment equals Σ xₖ pₖ exactly; 'nearest' snaps each atom to its closest bucket (the historical behaviour, with up to bs/2 positional bias). On-grid atoms (f == 0, e.g. integer atoms with bs == 1) give identical results under both schemes.

Discretization happens in update(), so a change after build requires a re-update() to take effect. The setter clears the cached density frames so they rebuild on next access.

Notes

Phase 1 covers unlayered discrete severities (the empirical-sample use case). A layered discrete severity (a xs b dsev ...) discretizes via the standard cdf-difference and so behaves as 'nearest' regardless of this setting.

property density

The “live” part of density_df — rows with positive total mass.

Returns density_df.query('p_total > 0'): the actual support of the aggregate, dropping the leading and trailing zero-probability buckets of the FFT grid. This is usually what you want to see. It is recomputed on each access (a plain property, not cached) because the underlying frame can be rebuilt by update.

property density_df

Per-bucket density / distribution / risk-measure frame.

Built lazily on first access after update. Stored in self._density_df; treat as read-only.

Columns (in construction order):

Column

Set from

Read by

loss

self.xs (also the index)

plot, q, bounds

p_total

self.agg_density

Portfolio (when Aggregate

is in a port), bounds, plot, user code

p

alias of p_total

Portfolio API compat

log_p

np.log(p)

plot log scale

F

p.cumsum()

q, var, tvar

S

1 - p_total.cumsum()

q, tvar, plot

lev

cumsum(loss·p) + loss·S

pricing

exa

alias of lev

Portfolio API compat

exlea

cumsum(loss·p) / F

pricing

e

self.est_m (constant column)

exgta

exgta

(e - cumsum(loss·p)) / S

pricing

exeqa

loss (since E[X|X=a] = a)

Portfolio API compat

lev, exlea and exgta are direct sums that carry the window origin x0 (numerics-2): on a windowed or signed grid E[X ∧ a] = Σ_{x≤a} x·p + a·S(a), never cumsum(S)·bs (which silently assumes the grid starts at 0). Ratio denominators are guarded: exlea (exgta) is NaN where F (S) is at or below the validation noise floor. The epd column was removed at numerics-2 (no consumers; max(0, e - lev)/e if needed).

Duplicated columns (p == p_total, exa == lev, exeqa == loss) are intentional: Portfolio’s filter(regex='p_<name>') / exeqa_* / exa_* patterns require these names exist on the unit’s frame so the unit can be inlined into a portfolio.

Returns:

DataFrame indexed by loss, columns as tabulated above.

property sev_density_df

Per-bucket severity density / distribution on the severity grid.

The severity lives on its own grid xs_sev (physical 0 at index i0), which equals the aggregate grid only on the default 0-based case. On a windowed / signed aggregate the severity (near 0) and the aggregate (windowed, possibly far from 0) genuinely occupy different grids, so the severity reporting columns live here – indexed by the severity’s own loss – rather than in density_df. This frame is always correct regardless of the aggregate output window.

4.4. Columns

loss : severity grid xs_sev (also the index). p_sev : self.sev_density. log_p_sev : log(p_sev) (-inf at zero buckets). F_sev : p_sev.cumsum(). S_sev : 1 - p_sev.cumsum().

returns:

DataFrame indexed by severity ``loss`.`

property reins_density_df

Per-bucket gross / ceded / net densities under reinsurance.

One row per model-grid bucket (loss = k * bs); the empirical, FFT-ready rebucketed densities. Columns are always present regardless of which stages are configured; a missing stage contributes the no-cession values (ceded mass at 0, net = subject).

4.4. Columns

lossfloat

The model grid (also the index).

p_sev_gross, p_sev_ceded, p_sev_netoccurrence-level severity

views. With no occurrence cover p_sev_ceded is a point mass at 0 and p_sev_net == p_sev_gross.

p_agg_gross, p_agg_ceded_occ, p_agg_net_occaggregate of each

occurrence severity view. p_agg_gross is the true gross aggregate (_fft_aggregate of the gross severity).

p_agg_subject, p_agg_ceded, p_agg_netaggregate-cover views.

p_agg_subject is the aggregate input to the aggregate cover (= aggregate of the requested occurrence output); it equals p_agg_gross only when there is no occurrence cover.

Notes

All aggregate columns route through _fft_aggregate so the zero-risk / fixed-1 shortcuts apply consistently. Renamed from the legacy reinsurance_df: p_agg_gross_occ -> p_agg_gross and the old p_agg_gross (the agg-cover input) -> p_agg_subject. Returns None when no reinsurance is configured.

property reins_views

The distributions this aggregate’s cession makes available for pricing.

The accepted values of the reins_view= keyword on calibrate_distortions() and evaluate(), and the answer to “what can I price this on”: [] when nothing cedes, ['gross', 'ceded', 'net'] with one stage, and those plus 'ceded occ' / 'net occ' when a program has both. Mirrors available_charts() and available_exhibits(), which answer the same shape of question about the same object.

ceded and net are end to end, so on an occurrence-only program they are the occurrence stage’s cession and retention, not the empty aggregate-stage columns of reins_density_df.

Returns:

list of str

See also

aggregate._reinsurance.reins_view_columns

the mapping onto columns.

reins_price_df(distortion=None, *, p=None, a=None, views=None)[source]

What a stated risk measure says this cession is worth.

The fourth reins_* frame, and the one that crosses over into pricing: reins_density_df computes the cession, price() prices any pmf, and this walks one through the other. The DecL ceded premium clause records a price someone agreed; this is the price a distortion implies, and the gap between them is the reading worth having.

See aggregate._reinsurance.reins_price_df() for the full contract. The usual call is after calibrate_distortions(), with no distortion argument, which prices every view with every calibrated family.

Parameters:
  • distortion (Distortion, str, dict, or None) – A distortion, a name in distortions, a mapping, or None for the whole calibrated set.

  • p (float, optional) – Asset probability; each view resolves its own a = q(p).

  • a (float, optional) – A common asset level. At most one of p or a; with neither the price is unlimited.

  • views (sequence of str, optional) – Defaults to all of reins_views.

Returns:

pandas.DataFrame – (distortion, view) rows by a / el / bid / ask / margin.

Examples

a = build('agg Re 100 claims sev lognorm 50 cv 2 '
          'occurrence net of 100 xs 100 poisson')
a.calibrate_distortions(0.10, p=0.999)
a.reins_price_df().loc[(slice(None), 'ceded'), :]
reins_occ_plot(log=False, full_range=False, reflect=False, return_period=False, invert=False)[source]

Plot the occurrence program: per claim, and in total.

Parameters:
  • log (bool) – Read the aggregate panel’s axes on log. The occurrence panel is read on log either way, and offers no other reading.

  • full_range (bool) – Show the whole grid rather than the cropped windows.

  • reflect (bool) – Read the aggregate panel against the exceeding probability, so the three curves are survival functions rather than quantile functions of non-exceedance.

  • return_period (bool) – Read the aggregate panel against return period rather than non-exceedance probability.

  • invert (bool) – Exchange the aggregate panel’s axes, which draws the distribution function the Lee diagram inverts to.

Returns:

matplotlib.figure.Figure – Also stashed on self.figure.

Notes

Draws the document charts.chart_reins emits. Two panels, and they share nothing, not even a loss axis: a per-claim loss and an annual aggregate are different quantities, and one window across both would say they were the same. The left is the gross, ceded and net severity as the treaty sees each claim; the right is the same three for the year, as a Lee diagram.

The three curves are separate distributions and not a decomposition: they no more satisfy gross = net (+) ceded than a portfolio’s marginals do.

occ_bivariate(views=('net', 'ceded'), bs=None, log2_x=None, log2_y=None, total_log2=None, store_dir=None, row_chunk=512, col_chunk=512, keep_transform=False)[source]

Joint law of two of the occurrence {gross, ceded, net} aggregates via 2D FFT.

Computes the joint distribution of two aggregate occurrence views – e.g. ceded C = sum c(X_i) and net N = sum n(X_i) – where each gross claim X_i is split deterministically by the occurrence cession map. The two are not deterministic functions of one another – the random claim count decouples them – so the joint law carries genuine information (their correlation, co-moments, reinsurer-vs-cedent dependency) beyond the two univariate margins already in reins_density_df. The three views satisfy ceded + net = gross, so any two determine the third; the pick of which two is views.

Parameters:
  • views ((str, str), default :py:class:`(`’net’, ``'ceded')``) – The (x, y) axis view pair, each one of 'gross' / 'ceded' / 'net'. Axis 0 (x) is views[0], axis 1 (y) is views[1]. The default ('net', 'ceded') matches the DecL netceded form; ('gross', 'ceded') matches grossceded and ('gross', 'net') matches grossnet.

  • bs (float, optional) – Bucket-size override (a single common bs for both axes). Default: the exact common lattice when the budget affords it, so every per-claim point of the comonotone curve is a cell center and the scatter never fires; otherwise one common bs sized from the budget (coarser than the gross bucket, since the gross grid is far finer than a 2-D grid affords). Which one was taken reads off bs_explanation.

  • log2_x (int, optional) – Axis-0 / axis-1 log2 grid lengths (grid has 1 << log2 points). Default: measured from the two views’ occurrence aggregate margins via balanced_window(), each axis independently, since the two windows of a cession are structurally asymmetric (ceded is capped by the cover, gross is not).

  • log2_y (int, optional) – Axis-0 / axis-1 log2 grid lengths (grid has 1 << log2 points). Default: measured from the two views’ occurrence aggregate margins via balanced_window(), each axis independently, since the two windows of a cession are structurally asymmetric (ceded is capped by the cover, gross is not).

  • total_log2 (int, optional) – Total 2-D cell budget, 2**total_log2 cells. None uses the BivariateSettings.total_log2 default (20). Raise it for a fine joint, and pair it with store_dir past the point where the joint fits in memory: the two are coupled, since without the first no grid is large enough to want the second.

  • store_dir (str, optional) – Backing directory for a massive (disk-backed) build: the joint streams to density.zarr there and never materializes in RAM. Requires the massive extra.

  • row_chunk (int, optional) – Massive-path band and tile sizes (ignored in core).

  • col_chunk (int, optional) – Massive-path band and tile sizes (ignored in core).

  • keep_transform (bool, optional) – Massive path: keep the staging stores after the build.

Returns:

BivariateAggregate – A first-class joint object in netceded mode, with the joint density, the two axis grids (axis_xs), marginals / moments (E[X^i Y^j]) / corr / summary_df / stats_df / info and a two-panel plot (comonotone per-claim severity and joint aggregate). Equivalent to the DecL netceded / grossceded / grossnet <agg> prefix forms.

Raises:

ValueError – If the object carries no occurrence reinsurance, has not been updated (no severity densities present), or carries a pinned grid that cannot be honored inside total_log2. A pinned pair that overflows used to clip the wider axis, which is not a tail loss: occ_bivariate(views=('gross', 'ceded'), bs=0.5) on an unbounded severity clipped the gross axis to 512 buckets and answered off a joint with a deficit of 0.535.

Notes

Occurrence only. Any aggregate reinsurance on the object is ignored: this is the joint law of the per-occurrence ceded / net aggregates. (The aggregate-cover bivariate is degenerate – at the aggregate level ceded and net are deterministic functions of the aggregate, supported on a curve – so it is out of scope.)

Method. Per claim, (c(X), n(X)) lies on the line c + n = X. Placing the gross severity mass at (c(x_k), n(x_k)) (rebucketed onto the 2D grid by the active reins_bucket scheme) builds the bivariate severity S. The joint aggregate density is iFFT2(freq_pgf(n, FFT2(S))) – exactly the univariate _fft_aggregate() with the 1D transforms replaced by 2D transforms, valid because freq_pgf(n, z) is elementwise in z. The zero-risk (n == 0) and fixed-count-one shortcuts mirror _fft_aggregate. Marginalising the result over one axis recovers the corresponding univariate occurrence ceded / net aggregate (exact validation targets; see the Cross-check notes in dev/done/reins-bivariate.md).

This wraps the object as a aggregate.bivariate.BivariateAggregate in netceded mode (the engine is aggregate.bivariate.build_netceded_joint()).

occ_joint(views=('gross', 'ceded'), **sizing)[source]

The occurrence joint, built once per sizing and held.

occ_bivariate() behind a memo. Two surfaces want the same joint of one object, the natural allocation frame and the kappa chart, and a 2-D FFT is not something to pay for twice because two callers asked the same question (dev/plan-pricing-natural-allocation.md, decision 6, agreed by the author 2026-08-14).

Parameters:
  • views ((str, str), default :py:class:`(`’gross’, ``'ceded')``) – The axis view pair. The default is the pair the allocation and the kappa band both condition on, which is why it differs from occ_bivariate()’s.

  • **sizing – Passed to occ_bivariate(), and part of the memo key, so a resize is an honest rebuild rather than a stale hit.

Returns:

BivariateAggregate

Notes

The cache is cleared by update_work(), so a re-updated object never answers off a joint built on its old grid. A store_dir build is not held: a disk backed joint owns a directory whose lifetime is the caller’s, and holding a reference to one would quietly keep it alive past the point the caller expected to be done with it.

palm_kappa(target, conditioning=None)[source]

Conditional mean of one per-claim compound given another, all 1-D.

The Palm conditional-mean ladder kappa(s) = E[sum target(X_j) | sum conditioning(X_j) = s] on the model grid, for deterministic per-claim functions of the gross severity sharing this object’s event process (e.g. a layer’s cession against the occurrence-retained claim). Assembles the pieces for aggregate._aggregate_compute.palm_conditional_mean(): the conditioning image density and the target image measure via _rebucket_to_grid(), the pgf pair off frequency, and base_mean.

Parameters:
  • target (callable) – Per-claim target function c(x), vectorized over the grid (e.g. a ceder from make_ceder_netter()).

  • conditioning (callable, optional) – Per-claim conditioning function n_fn(x); None (default) conditions on the gross aggregate itself (the identity), under which kappa of the identity target is s exactly (the Panjer self-check).

Returns:

  • kappa (ndarray) – E[S_target | S_conditioning = s] at s = self.xs; NaN where the conditioning density is negligible.

  • conditioning_density (ndarray) – The compound density of the conditioning subject on self.xs (use it to locate quantiles of the conditioning aggregate).

Raises:
  • NotImplementedError – On a signed or windowed grid (i0 or x_min active), or for a frequency family without Frequency.freq_pgf_prime() (callers catch and degrade to a marginal ladder).

  • ValueError – If the object has not been updated (no severity density).

Notes

The shared-event analogue of Portfolio’s independence trick: linearity of the conditional mean decomposes claim by claim, the rest of the book enters only through its conditioning sum, and the Fourier-side count factor is P_N' in place of the leave-one-out transform. One shared inverse FFT plus one FFT for the target; the 2-D occ_bivariate joint is never formed. Derivation and scope: dev/plan-a362-pnl-punchups.md.

property reins_stats_df

Per-layer reinsurance layering summary (empirical, model-grid).

A layering analysis with one column per reinsurance layer plus the gross book and the ceded / net totals.

4.4. Columns

MultiIndex (view, layer) – view is occ / agg:

  • occurrence: Gross (the gross book – always present), then, when occ_reins, layer.1 … layer.k (one per layer) and Ceded / Net totals.

  • aggregate (when agg_reins): layer.1 … layer.m, Ceded / Net. There is no aggregate ``Subject`` column – the subject (the occurrence output the aggregate cover applies to) is the column flagged ('meta', 'output') == 1 in the occurrence block, or Gross when there is no occurrence program.

4.4. Rows

MultiIndex (component, measure):

  • meta: share (proportion covered), limit, attach, pr_attach and pr_detach – probabilities per modeled claim, read from the picks-adjusted bucketed subject severity (the grid): P(subject > attach) (exclusive) and P(subject >= attach + limit) (inclusive). One basis serves the whole frame: the same pr_attach conditions the layer severity and thins the layer frequency, so freq mean = n * pr_attach and sev_cond = sev_uncond / pr_attach hold exactly. pr_detach is NaN for unlimited layers / net totals. pr_loss (P(aggregate > 0) from the column’s aggregate density), lol (loss on line = expected layer aggregate loss / placed limit), and output (0/1 flag marking each stage’s output view – two 1s for an occ+agg program; Gross carries the 1 when there is no occurrence program). Gross carries the widest policy terms over the mixture components – max limit (inf if any component is unlimited) over min attach (share 1). The occurrence Ceded total deliberately carries the placed capacity sum(share * limit) and the minimum layer attachment: the placed sum is the program’s capacity, which a max would understate.

  • ('freq'|'sev'|'agg', ex1|ex2|ex3|mean|cv|skew) – moments (ex1 duplicates mean for easy filter(regex=...) access).

Notes

Occurrence layers are conditional on a loss reaching the layer: the frequency is the expected penetrating count n' = E[N] * P(subject > attach) and the severity is the unconditional layer severity divided by the same P(subject > attach) (conditional given attach), which leaves the layer aggregate mean n' * sev equal to the unconditional E[N] * E[ceded]. The agg row is the column’s actual aggregate distribution (FFT of the unconditional layer ceded severity), so its higher moments and pr_loss are exact.

The occurrence ``Ceded`` / ``Net`` columns are unconditional totals: the same claim count as Gross and unconditional severities, so Ceded sev + Net sev == Gross sev. Aggregate layer means sum to the Ceded aggregate mean.

The aggregate block leaves ``freq`` and ``sev`` all ``NaN`` – a cover on the aggregate has no per-claim frequency / severity that combine in the usual way.

Lazily built; invalidated by the reins_bucket setter and on update. Returns None when no reinsurance is configured.

property reins_summary_df

Per-stage reinsurance summary – the daily driver.

Mirrors the economic view of validation_df: compare the theoretic reference (the leading view – Gross for occurrence, Subject for aggregate) against the model output of each view. One block per applicable stage; each block is a view x component table sharing the same eight columns as validation_df:

  • EX / Est EX / Change EX – the theoretic reference mean (constant down each component), the per-view model-output mean, and (Est - reference) / reference;

  • CV / Est CV / Change CV – the same three for the CV;

  • Sk / Est Sk – reference and model-output skew (no change column).

Change carries two readings off one arithmetic: on the leading (Gross / Subject) row the reference and the model output are the same view, so it is the numerical validation / rebucketing error (~0 under linear); on the ceded / net rows it is the % impact of the cession on that moment. This is the per-view, per-component analogue of the single Change column in validation_df.

Layout (per the gross/subject convention; view and component labels are lower-case to match the other frames):

  • Occurrence block – leads with gross (the reference): rows (gross|ceded|net) x (freq|sev|agg).

  • Aggregate block – leads with subject (the reference): rows (subject|ceded|net) x (agg) (sev not applicable; freq degenerate -> NaN).

Frequency is reported on the Est (model-output) basis unconditionally (mean E[N] only, so freq * sev == agg within a view; cv / skew NaN) – consistent with reins_stats_df(). The leading gross row’s Est frequency is left NaN to mirror validation_df exactly. (Only the per-layer layer.k columns of reins_stats_df() are conditional; reins_summary_df is always unconditional.)

Index is MultiIndex (stage, view, component). Derived from reins_stats_df() / the internal view-stats frame. Returns None when no reinsurance is configured.

rescale(scale, kind='homog')[source]

Return a rescaled Aggregate object - used to compute derivatives.

All need to be safe multiplies because of array specification there is an array that is not a numpy array

TODO have parser return numpy arrays not lists!

Parameters:
  • scale – amount of scale

  • kind – homog of inhomog

Returns:

__init__(name, exp_el=0.0, exp_premium=0.0, exp_lr=0.0, exp_en=0.0, exp_attachment=None, exp_limit=inf, sev_name='', sev_a=nan, sev_b=0.0, sev_mean=0.0, sev_cv=0.0, sev_loc=0.0, sev_scale=0.0, sev_xs=None, sev_ps=None, sev_wt=1.0, sev_lb=0.0, sev_ub=inf, sev_conditional=True, sev_signed=False, sev_reflect=False, sev_pick_attachments=None, sev_pick_losses=None, occ_reins=None, occ_kind='', occ_reins_label=None, freq_name='', freq_a=0.0, freq_b=0.0, freq_zm=False, freq_p0=nan, freq_pin_mean=True, exp_years=0.0, exp_rate=0.0, wait_name='', wait_a=nan, wait_b=0.0, wait_mean=0.0, wait_cv=0.0, wait_loc=0.0, wait_scale=0.0, wait_xs=None, wait_ps=None, wait_wt=1.0, wait_lb=0.0, wait_ub=inf, wait_conditional=True, wait_attachment=None, wait_limit=inf, agg_reins=None, agg_kind='', agg_reins_label=None, reins_bucket=None, dsev_bucket=None, value_type='loss', approximate='exact', label=None, label_map=None, note='', hints='', tags=(), _tweedie=None, exp_retention_end=None)[source]

The Aggregate distribution class manages creation and calculation of aggregate distributions. It allows for very flexible creation of Aggregate distributions. Severity can express a limit profile, a mixed severity or both. Mixed frequency types share a mixing distribution across all broadcast terms to ensure an appropriate inter- class correlation.

Parameters:
  • name – name of the aggregate

  • exp_el – expected loss or vector

  • exp_premium – premium volume or vector. With exp_lr it sizes the expected loss (exp_el = exp_premium * exp_lr); alone, alongside a claims or loss sizing head, it is informational (the DecL FYI premium, 5 claims 20000 premium): the loss ratio back-fills from the realized expected loss and the law is untouched.

  • exp_lr – loss ratio or vector (requires premium)

  • exp_en – expected claim count per segment (self.n = total claim count)

  • exp_attachment – occurrence attachment; None indicates no limit clause, which is treated different from an attachment of zero.

  • exp_limit – occurrence limit

  • exp_retention_end – ground-up loss at which the retention reaches zero, per layer ([Franchise-Deductible]); None or inf is the ordinary deductible, the attachment itself is the franchise.

  • sev_name – severity name or sev.BUILTIN_SEV or meta.var agg or port or similar or vector or matrix

  • sev_a – scipy stats shape parameter

  • sev_b – scipy stats shape parameter

  • sev_mean – average (unlimited) severity

  • sev_cv – unlimited severity coefficient of variation

  • sev_loc – scipy stats location parameter

  • sev_scale – scipy stats scale parameter

  • sev_xs – xs and ps must be provided if sev_name is (c|d)histogram, xs are the bucket break points

  • sev_ps – ps are the probability densities within each bucket; if buckets equal size no adjustments needed

  • sev_wt – weight for mixed distribution

  • sev_lb – lower bound for severity (length of sev_lb must equal length of sev_ub and weights)

  • sev_ub – upper bound for severity

  • sev_conditional – if True, severity is conditional, else unconditional.

  • sev_signed – if True the severity is signed (never clamps its negative support; a profit is a negative loss). Set by the ssev DecL keyword. Orthogonal to value_type. dsev with a negative atom self-signs regardless of this flag.

  • sev_pick_attachments – if not None, a list of attachment points to define picks

  • sev_pick_losses – if not None, a list of losses by layer

  • occ_reins – layers: share po layer xs attach or XXXX

  • occ_kind – ceded to or net of

  • freq_name – name of frequency distribution

  • freq_a – cv of freq dist mixing distribution

  • freq_b – claims per occurrence (delaporte or sig), scale of beta or lambda (Sichel)

  • freq_zm – True/False zero modified flag

  • freq_p0 – if freq_zm, provides the modified value of p0; default is nan

  • freq_pin_mean – True (default): solve for the base mean whose realized E[N] matches the exposure clause, so n claims means n claims. False, the DecL ! after zm / zt: the clause sets the un-modified (base) mean and the (a, b, 1) reweighting shifts it, so the realized E[N] is an output, the textbook parameterization. Ignored unless freq_zm. The default changed at 1.0.0a325 ([ZT-ZM-Recalibrate-Default]); before that it was False and the DecL marker meant the opposite.

  • exp_years – renewal horizon T (the DecL T years exposure); requires freq_name='renewal' and a wait_* law (strict pairing). The claim count is the Sparre-Andersen renewal count N(T) of the wait law.

  • exp_rate – informational premium rate (at NUMBER rate): sets exp_premium = exp_years * exp_rate; never sizes the count. Stored for writer round-trip.

  • wait_name – waiting-time distribution name; the wait_* family mirrors sev_* exactly (the wait clause reuses the severity mini-language on the time axis)

  • wait_a – wait shape parameter (as sev_a)

  • wait_b – wait shape parameter (as sev_b)

  • wait_mean – wait mean (as sev_mean)

  • wait_cv – wait cv (as sev_cv)

  • wait_loc – wait location (as sev_loc)

  • wait_scale – wait scale (as sev_scale)

  • wait_xs – dwait outcomes (as sev_xs)

  • wait_ps – dwait probabilities; may sum to q < 1 for a defective (terminating) renewal process (dwait ... !)

  • wait_wt – wait mixture weight (as sev_wt)

  • wait_lb – wait splice lower bound (as sev_lb)

  • wait_ub – wait splice upper bound (as sev_ub)

  • wait_conditional – as sev_conditional; False = defective splice, or the unconditional layer when a wait layer is set

  • wait_attachment – wait layer attachment a (DecL wait y xs a); as exp_attachment on the wait law – conditional (W - a | W > a) by default, min((W - a)+, y) with wait_conditional=False (zero waits cluster). Cannot combine with a splice window

  • wait_limit – wait layer limit y – caps the (layered) wait, an atom at y of the escaping mass window (mass outside [lb, ub] terminates the process rather than renormalizing)

  • agg_reins – layers

  • agg_kind – ceded to or net of

  • value_type – 'loss' (default) or 'payoff'; the DecL orientation suffix sets 'payoff' (more is better) for an asset-return / direct-payoff primitive. Inert for the distribution, consumed at the pricing layer (the dual distortion). A premium-minus-loss position is the separate PnL veneer (see make_pnl), not a value on the aggregate.

  • approximate – 'exact' (default), 'norm', 'lognorm', 'gamma', 'sgamma' or 'slognorm'. When not 'exact', the freq x sev convolution is replaced at construction by a single continuous severity fitted to the aggregate’s moments (method of moments), carried on a fixed frequency of 1. The shifted families sgamma / slognorm match mean, cv and skew (normal as the symmetric limit, a reflected fit for left skew); lognorm and gamma match mean and cv only (the declared aggregate’s skew is not reproduced); norm matches mean and cv with zero skew. The fitted severity’s signedness mirrors the input’s: an unsigned (plain sev) input clamps any fitted sub-zero mass to an atom at 0, and the moment drift shows in validation_df. The original program is preserved in note. Incompatible with occurrence reinsurance (which acts pre-convolution); aggregate reinsurance rides along unchanged. Set by the approximate DecL keyword. The chosen kind is exposed on the instance as the attribute self.approximation ('' when exact, else the kind) – a distinct name from the approximate() method (the MoM-surrogate factory) so the two do not collide. See dev/done/plan-approximate.md.

  • label – optional human display label (the DecL as "..." clause); None falls back to name. Presentation only – repr / exhibit titles prefer it over name – never an identity / reference target. See dev/plan-decl-labels.md.

  • occ_reins_label – optional per-occurrence-layer display labels (the reins-clause as clause); a list parallel to occ_reins (entries None where unlabeled), or None. Pooled into label_map['occ_reins'] as a sparse {layer_index: label} dict (read via a.labels.occ_reins); names the cession group / leg rows in a P&L ledger.

  • agg_reins_label – optional per-aggregate-layer display labels, parallel to agg_reins (see occ_reins_label; pooled into label_map['agg_reins']).

  • note – free-text note, from a note{...} clause

  • hints – raw hints{...} build-settings string (key=value; form). Pure annotation here; the underwriter parses it on build (caller-supplied build() kwargs win).

  • _tweedie – private provenance, a aggregate.tweedie.TweedieParameters or None. Set only by the parser, when a tweedie clause (rather than its compound-Poisson-gamma expansion) is what the author wrote; read only by decl_writer and as_tweedie(). Not part of the public constructor contract. It is an argument rather than an underwriter-applied stamp because a parsed spec is splatted straight into this constructor at eleven call sites and there is no choke point to pop it at. See dev/done/plan-tweedie.md.

tags

Tag slugs from the DecL tags{...} trailer, for grouping and selection (Underwriter.discover(tags=...)). Empty tuple if none.

property base_mean

The un-modified expected claim count – the mean freq_pgf consumes.

Equals n for every unmodified frequency, so ordinary code need not distinguish them. Under zm / zt they part company: n is the realized (shifted) E[N] that the aggregate actually has, while this is the base mean the exposure clause set and the one the (a, b, 1) PGF is parameterized by. Every PGF evaluation – the FFT convolution, the bivariate 2-D compound, the pedagogy transforms – must pass this, not n.

See modify_mean() for the map between the two.

property info

Fixed-layout multi-line summary string.

Every row is always present, in the same order, for every Aggregate; a value that is not (yet) available – e.g. the grid block before update – renders as n/a. The row catalogue and value enumerations are documented in dev/info-strings.rst. Shares the label/value convention (aggregate.constants.info_row()) with Portfolio and Distortion.

property validation_score

how well the grid reproduces theory (float).

In units of the validation tolerance, so score <= 1 means the object passes at its own validation_eps and 1 is exactly the pass boundary. Where valid says whether a line was crossed, this says by how far, which is what makes it comparable across grids and the quantity sharpen() minimizes.

Six terms: severity and aggregate mean, CV and skewness, each relative error divided by its own tolerance, combined as a power mean with power=2. See validation_score() for the other powers and validation_score_terms() for the per-term detail.

Type:

Continuous validation score

property validation_description

Short one-line validation verdict (str).

The terse phrase the info row and the one-line intro carry: 'not unreasonable', 'fails agg cv'. The verbose form is validation_explanation. Validation is computed if needed.

property validation_explanation

Long-narrative explanation of the validation result (str).

The consistent narrative surface, mirroring tail_explanation / bs_explanation, and the verbose form of validation_description. Validation is computed if needed.

property one_claim

Whether the claim count is identically one.

Returns:

bool – True iff N = 1 with probability one, however the frequency happens to be spelled.

Notes

This is the convolution gate for the exact severity copy shortcut. With exactly one claim the aggregate is the (post occurrence reinsurance) severity, so iFFT(P_N(FFT(sev))) is an identity executed numerically: it returns the severity plus machine epsilon dust. Copying the array instead is exact and cheaper. See aggregate._aggregate_compute.freq_sev_convolution() (1-D) and aggregate.bivariate.netceded_joint_density() (2-D), which share this predicate so the two never drift apart.

Two spellings reach the same law and both answer True:

  • 1 claim ... fixed: a FrequencyFixed whose component claim counts sum to one. A limit profile splitting that single claim over several components still qualifies, because sev_density is then the corresponding severity mixture.

  • dfreq [1]: a FrequencyEmpirical carrying all its mass on the outcome one. FrequencyRenewal is an empirical frequency post build, so a renewal count that degenerates to one claim qualifies too.

The empirical test is on the support, never on the mean: dfreq [0 2] [.5 .5] has mean one and is emphatically not one claim. Zero probability atoms are ignored, since validate_discrete_distribution() makes the outcomes distinct and ascending but never drops a zero mass.

discretize(sev_calc, discretization_calc, normalize)[source]

Discretize the severity distributions and weight.

sev_calc describes how the severity is discretized. The options are discrete=round, forward, backward or moment.

sev_calc='continuous' (same as forward, kept for backwards compatibility) is used when you think of the resulting distribution as continuous across the buckets (which we generally don’t). The buckets are not shifted and so \(Pr(X=b_i) = Pr( b_{i-1} < X \le b_i)\). Note that \(b_{i-1}=-bs/2\) is prepended.

We use the discretized distribution as though it is fully discrete and only takes values at the bucket points. Hence, we should use sev_calc=’discrete’. The buckets are shifted left by half a bucket, so \(Pr(X=b_i) = Pr( b_i - b/2 < X \le b_i + b/2)\).

The other wrinkle is the righthand end of the range. If we extend to np.inf then we ensure we have probabilities that sum to 1. But that method introduces a probability mass in the last bucket that is often not desirable (we expect to see a smooth continuous distribution, and we get a mass). The other alternative is to use endpoint = 1 bucket beyond the last, which avoids this problem but can leave the probabilities short. We opt here for the latter and normalize (rescale).

discretization_calc controls whether individual probabilities are computed using backward-differences of the survival function or forward differences of the distribution function, or both. The former is most accurate in the right-tail and the latter for the left-tail of the distribution. We are usually concerned with the right-tail, so prefer survival. Using both takes the greater of the two esimates giving the best of both worlds (underflow makes distribution zero in the right-tail and survival zero in the left tail, so the maximum gives the best estimate) at the expense of computing time.

Sensible defaults: sev_calc=discrete, discretization_calc=survival, normalize=True.

Parameters:
  • sev_calc – discrete=round, forward, backward, or continuous and method becomes discrete otherwise

  • discretization_calc – survival, distribution or both; in addition the method then becomes survival

  • normalize – if True, normalize the severity so sum probs = 1. This is generally what you want; but when dealing with thick tailed distributions it can be helpful to turn it off.

Returns:

snap(x)[source]

Snap value x to the index of density_df, i.e., as a multiple of self.bs.

Parameters:

x

Returns:

property prob_eq_0

P(X == 0) – the probability the outcome is exactly zero.

On demand, from the realised grid, after update(). On a loss object this reads as “no loss” (the ground-up P(N = 0), plus any severity atom at zero and any mass the reinsurance nets to zero); on a payoff / P&L object as “exactly break even”. Shared, sign-neutral surface with Portfolio.prob_eq_0 and prob_eq_0.

Returns:

float or None – None before update() (there is no realised grid yet).

Notes

Reads the mass at the zero grid point through the canonical GridDistribution (pmf(0)) – identical to pmf() at zero. On a discrete book that is the exact atom. On a continuous severity the zero bucket also absorbs every outcome that discretizes to zero (losses below bs, or below bs / 2 under the round scheme), so the answer is P(N = 0) plus that sliver – the usual FFT-grid reading, and it tightens as bs shrinks.

property value_type

'loss' or 'payoff'.

Records how the aggregate should be read – actuarial loss (“more is worse”, the default) vs. payoff / asset (“more is better”). It is inert for the distribution itself: density, moments, quantiles, deficit and plotting do not depend on it. It is consumed only when applying distortions / pricing (specified in the Portfolio plan and downstream pricing work), where a 'payoff' object is negated / the dual distortion applied.

The role is stored as a boolean (_is_loss_value); this getter returns the configured label for the role ([labels] in the config, defaults 'loss' / 'payoff'). The setter accepts the canonical tokens or the configured labels. Pricing code must branch on _is_loss_value, never on the label text.

See dev/plan-negative-x-agg.md §5.5.

Type:

Sign convention for the variable

make_pnl(consideration=None, *, gross=None, ceded=None, expense_spec=None, gcn_economics=None, consideration_label=None, loss_label=None)[source]

Wrap this aggregate as the risky leg of a PnL position.

Object sugar delegating to the builders in aggregate._pnl_builders. Two construction modes, both the consolidated (single-group) pnl face ([Decision-PnL-Is-Consolidated]):

  • plain – make_pnl(consideration=C): a one-group sell ledger over this aggregate’s own density (for a reinsurance-bearing aggregate that is the net density – what comes out of the aggregate).

  • consolidated reinsurance – make_pnl(gross=Pg, ceded=Pc) on a reinsurance-bearing aggregate (build_consolidated_pnl()): one net-premium consideration leg (gross - ceded premiums) against the engine’s net loss over its deepest net marginal, with ceding commissions booked as an obligation-side commission leg ([Commission-Obligation-Side]). The per-step walk (gross -> each cover -> Total) is the DecL xpnl face.

Parameters:
  • consideration (float, array-like, or callable, optional) – The plain-mode consideration, signed (+ received, - paid); a vector sums to one book amount; a callable f(x) is a loss-sensitive consideration applied bucket-wise. Mutually exclusive with gross/ceded.

  • gross (float, optional) – The gross premium received and ceded premium paid (positive magnitudes) for the group-ledger view.

  • ceded (float, optional) – The gross premium received and ceded premium paid (positive magnitudes) for the group-ledger view.

  • consideration_label (str, optional) – Name for the premium leg (the DecL premium as clause); defaults to 'consideration' (plain) / 'premium' (ledger).

  • loss_label (str, optional) – Name for the loss leg (the DecL engine as clause); defaults to 'loss'.

Returns:

PnL – Always a PnL value object; the ledger form carries its resolved cession economics as pnl.economics.

Notes

build('pnl NAME C premium less <body>') is sugar for build('agg NAME <body>').make_pnl(consideration=C).

update(log2=16, bs=0, bucket_sizing_p=0.99999, debug=False, x_min='auto', x_max=None, window_convention=None, sharpen=False, **kwargs)[source]

Convenience function, delegates to update_work. Avoids having to pass xs.

Parameters:
  • log2

  • bs

  • bucket_sizing_p – p value passed to the moment-window bucket sizer. If > 1 converted to 1 - 10**-p.

  • debug

  • x_min – lower edge of the output window. 'auto' (default) resolves to 0 for an ordinary non-negative aggregate (today’s behaviour) and to None (automatic two-sided window) for a signed aggregate – so a P&L declared with ssev / negative dsev just works from build. None forces the automatic window; a number forces that origin (snapped to a multiple of bs; may be negative).

  • x_max – upper edge of the output window; informational, the grid length is fixed by log2. Currently unused when x_min is given explicitly (the window is [x_min, x_min + (2**log2)*bs)).

  • window_convention – 'loss' / 'payoff' to override the sign convention orienting the automatic windowed placement (per-edge coverage and padding skew). None (default) derives it from value_type. See dev/plan-bucket-window-2.md §1A.

  • sharpen – opt in to auto-sharpening. False (default) leaves the grid exactly as the estimator chose it. True runs sharpen() once the update completes, which probes the eight neighbouring (bs, log2) cells and moves to a better one on a large win. Off by default, and not turned on by build, because a probe costs eight extra updates.

  • kwargs – passed through to update

Returns:

Signedness is carried by the severity declaration (ssev /negative dsev), not by an argument here – see _signed.

update_work(xs, padding=1, sev_calc='discrete', discretization_calc='survival', normalize=True, force_severity=False, reins_bucket=None, dsev_bucket=None, debug=False, x_min=0, x_max=None)[source]

Compute a discrete approximation to the aggregate density via FFT.

See discretize for sev_calc, discretization_calc and normalize.

Empirical-moment note: the aggregate raw moments – and hence the empirical CV/skew shown in stats_df and validation_df – are taken from a de-fuzzed copy of the FFT density (values below machine epsilon zeroed). Without this, sub-eps floating-point fuzz in far-tail buckets is amplified by x**3 in the third moment and corrupts the empirical skew on wide grids: a symmetric distribution’s skew can drift from ~1e-15 to ~1e-4 as log2 grows, purely from buckets the distribution never reaches. The fuzz is safe to drop because the FFT is exact up to rounding and the exact aggregate has no negative density even under aliasing, so any stray value is small. self.agg_density is deliberately left as the raw FFT output (consistent with ftagg_density); only the moment computation sees the cleaned copy. See the inline comment at the moment computation for full detail.

Quick simple test with log2=13 update took 5.69 ms and _eff took 2.11 ms. So quicker but not an issue unless you are doing many buckets or aggs.

Parameters:
  • xs – range of x values used to discretize

  • padding – for FFT calculation

  • sev_calc – discrete=round, forward, backward, or continuous and method becomes discrete otherwise

  • discretization_calc – survival, distribution or both; in addition the method then becomes survival

  • normalize – if True, normalize the severity so sum probs = 1. This is generally what you want; but when dealing with thick tailed distributions it can be helpful to turn it off.

  • force_severity – make severities for plotting even when only the aggregate is requested

  • reins_bucket – optional override of the net/ceded rebucketing scheme (‘linear’ or ‘nearest’); defaults to the current self.reins_bucket.

  • dsev_bucket – optional override of the discrete-severity atom placement scheme (‘linear’ or ‘nearest’); defaults to the current self.dsev_bucket. See dsev_bucket.

  • debug – run reinsurance in debug model if True.

  • x_min – None requests an automatic two-sided output window (NYI in this stage – treated as the grid origin xs[0]); otherwise informational (the grid origin is read from xs[0]).

  • x_max – informational upper window edge.

Returns:

property valid

Check if the model appears valid. An answer of True means the model is “not unreasonable”. It does not guarantee the model is valid. On the other hand, False means it is definitely suspect. (The interpretation is similar to the null hypothesis in a statistical test). Called and reported automatically by qd for Aggregate objects.

Checks the relative errors (from the canonical stats_df) for:

  • severity mean < eps

  • severity cv < 10 * eps

  • severity skew < 100 * eps (skewness is more difficult to estimate)

  • aggregate mean < eps.

  • the convolution residual < ALIASING_EPS (aggregate._validation.convolution_residual(), the ALIASING test), whenever the pmf deficit is dust.

  • aggregate cv < 10 * eps

  • aggregate skew < 100 * esp

The default uses eps = 1e-4 relative error. This can be changed by setting the validation_eps variable.

All reads come from stats_df – the single source of truth – not validation_df (display).

The CV and skew tests are applied only when the theoretical value is finite and its magnitude exceeds VALIDATION_NOISE – a theoretically-zero skew (symmetric severity) or CV (deterministic severity) is skipped, because the FFT’s empirical estimate of a zero higher moment is grid-dependent noise with no meaningful relative error. When the test applies, np.isclose with relative tolerance 10*eps (CV) / 100*eps (skew, harder to estimate) measures agreement.

The ALIASING test measures the convolution step directly rather than comparing two mean errors, and fires only when the pmf deficit is arithmetic dust. Wrap conserves mass while moving the mean; truncation drops mass, which DEFECTIVE and AGG_MEAN already own. See aggregate._validation.convolution_residual() and dev/done/plan-validation-punchup.md.

Run with logger level 20 (info) for more information on failures.

A Type 1 error (rejecting a valid model) is more likely than Type 2 (failing to reject an invalide one).

Returns:

True (interpreted as not unreasonable) if all tests are passed, else False.

unwrap(p=1e-07, audit=True)[source]

Unwrap self created with log2 that is too small to contain the answer.

Parameters:
  • p – Percentile threshold. The estimated p and 1-p quantiles are used to determine the effective support [L, R]. R-L must fit in the space available, i.e., R-L <= N * self.bs.

  • audit – If audit, return comparison of empirical moments of shifted answer with a.actual_m etc. analytic moments.

Returns:

Unwrap named tuple containing fields y the density as a Series, mode of shifting/unwrapping, prob_captured the probability in the effective support (which should be close to 1), L, R the boundary of the effective support.

picks(attachments, layer_loss_picks, debug=False)[source]

Adjust the computed severity to hit picks targets in layers defined by a. Delegates work to _picks_work(). See that function for details.

Raises:

ValueError – If any attachment misses the realized grid or lies above the top of the window. The message names the offenders and a bucket size that would work.

Notes

Every attachment must be a grid point. Picks defines the layers the reweighting is solved on, so a boundary strictly inside a bucket has no faithful reading: that bucket’s mass sits at a single point and cannot be divided between the layer below and the layer above without inventing a within-bucket distribution. The update therefore raises a ValueError naming a compatible bucket rather than snapping the boundary, which would silently restate the tower the caller asked for (author ruling 2026-08-24).

A reinsurance tower over the same attachments is exempt, and the contrast is the point. apply_reins_work evaluates piecewise linear ceder and netter functions at every grid point and lands the off grid results back on the lattice, which is faithful because a contract is a function of the loss and can be evaluated anywhere. A pick is a constraint on an integral between two boundaries, so the boundaries have to exist on the grid.

The bucket sizer is deliberately blind to picks: an off grid result under auto sizing is an error naming a compatible bucket, not an input to _bs_window.

freq_pmf(log2)[source]

Return the frequency probability mass function (pmf) computed using 2**log2 buckets. Uses self.en to compute the expected frequency. The Frequency does not know the expected claim count, so this is a method of Aggregate.

apply_occ_reins(debug=False)[source]

Apply the entire occ reins structure and save output For by layer detail see reins_stats_df. Makes sev_density_gross, sev_density_net and sev_density_ceded, and updates sev_density to the requested view.

Not reflected in statistics df.

Parameters:

debug – More verbose.

Returns:

apply_agg_reins(debug=False, padding=1)[source]

Apply the entire agg reins structure and save output. For by layer detail see reins_stats_df. Makes agg_density_gross, agg_density_net and agg_density_ceded, and updates agg_density to the requested view.

Not reflected in statistics df: the post-reins empirical moments (est_* and the stats_df['empirical'] column) are written by update_work from the same density updated here.

Returns:

property reins_description

Short narrative description of the reinsurance (str).

The consistent narrative surface, mirroring tail_description / bs_description: returns the kind='both', width=0 text. For the parameterized form (a single kind, or wrapped to a width) use the private worker _reins_description().

property reins_explanation

the terms, then what they do (str).

The verbose half of the pair (a172 [FCC-Contract-Gaps]). reins_description says what the program declares; this adds what the cession does, read off reins_summary_df: expected loss gross, ceded and net at each stage, and the share of the gross that the cession carries.

Returns:

str – 'No reinsurance.' on a clean book, so the caller never has to branch on reins_kinds first.

Notes

Occurrence and aggregate are reported as separate stages because they are applied in sequence: the aggregate cover attaches to the subject, which is the occurrence stage’s net, not to the gross. Reading a single “ceded” number across both stages is the standard way to double count.

Type:

Long narrative of the reinsurance

property reins_kinds

Text description of kinds of reinsurance applied.

Returns:

str – One of 'None', 'Occurrence only', 'Aggregate only', or 'Occurrence and aggregate'.

apply_distortion(dist, *, view='ask', S_calculation='forwards', allow_deficit=False)[source]

Apply distortion to the aggregate density; appends gS, gp_total and exag columns to density_df.

Routes through the exact-discrete Choquet helper (choquet_weights()): exag(a) = rho_g(X ∧ a) = Σ_{x≤a} x·gp + a·g(S(a)), a direct sum carrying the origin – valid on windowed and signed supports (the old cumsum(gS)·bs idiom assumed a zero-origin grid). The effective g resolves view × the object’s value-type role (effective_g()), so a payoff-role aggregate prices through the dual automatically.

Parameters:
  • dist (Distortion) – The distortion to apply.

  • view ({'ask', 'bid'}) – Pricing view; composes with the value-type role by XOR.

  • S_calculation ({'forwards', 'backwards'}) – Deficit-parking direction; see choquet_weights().

  • allow_deficit (bool) – Explicit truncation policy for a materially defective pmf. Default False raises DefectiveDistributionError.

Raises:

ValueError – For a mass distortion on an unbounded support: the mass lands on the last represented bucket, which is a different bounded problem, not an approximation. Certify self.bounded = True if the support is in fact bounded.

pollaczeck_khinchine(rho, cap=0, excess=0, stop_loss=0, kind='index', padding=1)[source]

Return the Pollaczeck-Khinchine Capital function relating surplus to eventual probability of ruin. Requires a Poisson frequency (raises ValueError otherwise); for a renewal (wait-clause) frequency use wiener_hopf().

See Embrechts, Kluppelberg, Mikosch 1.2, page 28 Formula 1.11

Parameters:
  • rho – rho = prem / loss - 1 is the margin-to-loss ratio

  • cap – cap = cap severity at cap, which replaces severity with X | X <= cap

  • excess – excess = replace severity with X | X > cap (i.e. no shifting)

  • stop_loss – stop_loss = apply stop loss reinsurance to cap, so X > stop_loss replaced with Pr(X > stop_loss) mass

  • kind

  • padding – for update (the frequency tends to be high, so more padding may be needed)

Returns:

RuinFunction named tuple (ruin, find_u, mean, density): ruin vector as pd.Series, function to lookup capitals (no interpolation if kind==index; else interp), discretized severity mean, and the integrated-severity (equilibrium) density dfi. Unpacks positionally like the historical bare tuple.

wiener_hopf(rho, kind='index', log2=None)[source]

Eventual ruin probability for a Sparre-Andersen (renewal) model by cepstral Wiener-Hopf factorization.

The renewal counterpart of pollaczeck_khinchine(): the frequency must be a renewal (years ... wait ...) frequency, and the premium rate is c = (1 + rho) E[X] / E[W] – the loaded long-run loss rate, so rho keeps its PK margin-to-loss meaning.

Parameters:
  • rho (float) – Margin-to-loss ratio: premium = (1 + rho) times the expected loss rate E[X] / E[W].

  • kind ({'index', 'interpolate'}) – find_u lookup convention, as in pollaczeck_khinchine(): 'index' snaps to the grid, anything else interpolates.

  • log2 (int, optional) – Half-grid exponent. The wrapped circle has 2**(log2 + 1) buckets and psi is returned on u = arange(2**log2) * bs. Defaults to self.log2, so the u-grid coincides with the severity grid (and with the PK u-grid). Must be at least self.log2; raise it for heavy-tailed severities (see the wrap-around warning).

Returns:

RuinFunction – Named tuple (ruin, find_u, mean, density): psi(u) as a pd.Series on the u-grid, the capital-lookup closure, the discretized severity mean, and the pmf of the all-time maximum of the loss walk on the u-grid. Unpacks positionally exactly like pollaczeck_khinchine().

Raises:

ValueError – If the frequency is not renewal (for Poisson use pollaczeck_khinchine()); if the object has not been updated; if the severity grid is signed; if the wait law is defective (terminating renewal process); or if the net profit condition E[Y] < 0 fails on the grid (margin too small).

Notes

Sparre-Andersen surplus U(t) = u + ct - sum_{i <= N(t)} X_i with iid waits W. Ruin can occur only at claim instants, so the problem collapses to the random walk with per-claim step Y = X - cW: psi(u) = P(M > u) with M the all-time maximum of the partial sums – the Lindley / GI-G-1 stationary waiting time, finite iff E[Y] < 0 (net profit condition c E[W] > E[X], i.e. rho > 0).

M is still a geometric number of iid ascending ladder heights, but outside the Poisson world the ladder height law has no closed form – the two pieces of Poisson magic behind PK (ladder heights = the equilibrium distribution, ladder epoch probability 1/(1+rho)) do not survive. Instead the ladder structure is extracted numerically from the Wiener-Hopf factorization of the walk by the cepstral method: see aggregate._renewal.ruin_cepstral() for the algorithm (Spitzer identity, support separation in the cepstrum, z = 1 regularization).

Discretization: the severity rides its existing rounding pmf (sev_density_df.p_sev, exactly as PK); the scaled wait cW is discretized by the same rounding scheme via _discretize_wait_pmf(). Rounding keeps means correct to O(bs^2) and makes the lattice walk aperiodic, so the symbol 1 - phi_Y has no unit-circle zeros besides z = 1. The kernel’s z = 1 patch uses the grid mean step (signed @ fy), which also absorbs the (negligible) wait mass dropped beyond the grid top. psi is valid for u below 2**log2 * bs; psi at the top of the grid is the wrap-around diagnostic – the cepstrum coefficients decay like the severity tail, so a heavy-tailed severity needs a wider grid (larger log2) before wrap-around contamination sets in, and a warning is issued when the top-of-grid psi exceeds 1e-6.

For exponential waits the model is compound Poisson and this method agrees with pollaczeck_khinchine() up to discretization. See Embrechts, Kluppelberg, Mikosch 1.2 for the model and Spitzer (1956) for the identity.

eventual_ruin(rho=None, *, lr=None, p=None, u=None, log2=None, n_sims=1000, seed=20260905)[source]

Probability of eventual ruin at one capital level: the receipt.

States a premium margin (rho or lr, exactly one) and a capital level (p or u, exactly one), computes the exact probability of eventual ruin there (Poisson frequency via pollaczeck_khinchine(), renewal via wiener_hopf(); anything else raises), validates it with a capped simulation of the same discretized model, and returns the RuinResult the ruin exhibit dispatches on.

Parameters:
  • rho (float, optional) – Margin-to-loss ratio; the premium rate is c = (1 + rho) E[X] / E[W]. Must be positive (net profit). Exactly one of rho or lr.

  • lr (float, optional) – The same margin stated as a loss ratio, lr = 1 / (1 + rho), in (0, 1). Exactly one of rho or lr.

  • p (float, optional) – Probability of eventual default; resolved to an initial surplus through the ruin function’s capital lookup find_u(p) on the grid. Exactly one of p or u.

  • u (float, optional) – Initial surplus directly. Exactly one of p or u.

  • log2 (int, optional) – Renewal path only: forwarded to wiener_hopf() for heavy tails.

  • n_sims (int, default 1000) – Simulated paths for the reasonableness check. The small default follows the teaching-aid sizing ruling (dev/plan-pk-tab.md ruling 4); raise it for a tighter standard error.

  • seed (int or None, default fixed) – rng seed. The fixed default keeps served documents hash-stable; None draws a fresh seed and reports it on the result.

Returns:

RuinResult – Carrying ruin_df (the one-column stats strip), the resolved u and exact psi beside the simulated estimate and its standard error, the margin both ways, and the seed used.

Notes

On the Poisson path the frame also carries the Lundberg exponent R and the classical bound exp(-R u) when the adjustment equation M_X(R) = 1 + (1 + rho) E[X] R has a bracketed root on the discretized severity (see _lundberg_exponent()); a book whose grid puts the root past the overflow guard simply omits the two rows. The simulation samples the discretized severity and wait laws, so the simulated model is exactly the model the exact psi prices.

Added in version 1.0: Provisional companion to the ruin exhibit and chart (dev/plan-pk-tab.md).

plot(xmax=None, log=False, full_range=False, reflect=False, return_period=False, invert=False)[source]

Plot the aggregate and its severity: the mass, and the Lee diagram.

Parameters:
  • xmax (float, optional) – Upper end of the loss window, in place of the computed one. How a gross and a net aggregate are read against one common scale.

  • log (bool) – Read both axes of the mass panel on log, which is the log density panel this plot used to draw as a third picture.

  • full_range (bool) – Show the whole grid rather than the q(0.001) to q(0.999) crop.

  • reflect (bool) – Read the Lee panel against the exceeding probability rather than the non-exceeding one, so the curve drawn is the survival function. With invert it is S(x) the usual way round, and that axis offers a log reading where the non-exceeding one does not.

  • return_period (bool) – Read the Lee panel against return period rather than non-exceedance probability, which spreads the rare tail so it can be read off directly. This was quantile_x='return'.

  • invert (bool) – Exchange the Lee panel’s axes, which draws the distribution function: a quantile function and a cdf are inverses, so it is the same pairs read the other way round.

Returns:

matplotlib.figure.Figure – Also stashed on self.figure.

Notes

Draws the chart document charts.chart_agg emits, through the one generic renderer, so this picture and the one a browser draws come from a single set of semantic decisions rather than two that must be kept in step by hand.

The two panels are one reading of one book: the loss axis is a single axis, the density panel’s x and the Lee panel’s y, so a window moves both. Which drawing each series gets (stems for a small discrete book, steps, or a line once a bucket is sub-pixel) is the renderer’s, from the declared support and the room on screen, which is what retired the old discrete and continuous branches.

with_hints(**extra)[source]

This aggregate’s program with its realized grid pinned into hints{}.

The certification helper for the reference-severity feature. A sev agg.NAME reference requires the referenced declaration to carry explicit log2 and bs hints, so that the severity it stands for is pinned by the recipe base rather than by whatever ambient defaults happened to be in force. This is how a declaration acquires them: get the inner right interactively, then build(inner.with_hints()) re-registers it with its resolution pinned, turning a candidate inner into a certified one.

Parameters:

**extra – Further hints{} settings (any key the language allows, e.g. padding=2, normalize=False), merged over log2, bs and normalize, which come from the object’s current state.

Returns:

str – One line of DecL, ready to hand back to build.

Raises:

ValueError – If the object has not been updated, carries no DecL program, or is given a hint key DecL does not have.

Examples

>>> from aggregate import build
>>> a = build('agg WH.Doc 10 claims sev lognorm 50 cv 1 poisson')
>>> 'hints{' in a.with_hints()
True

Notes

The existing trailer survives: the clause is merged key by key, so a declared padding stays and only the grid moves.

as_severity(limit=inf, attachment=0, conditional=False)[source]

Use this aggregate’s output loss distribution as a severity.

The programmatic twin of the DecL sev agg.NAME reference, and the mirror of aggregate.portfolio.Portfolio.as_severity(). The motivating shape is a compound-of-a-compound: a per-policy aggregate becomes the per-policy severity of a book of policies, which is how a US personal-auto split limit (100 per claimant, 300 per policy) is written.

Parameters:
  • limit (float, default np.inf) – Layer width applied to the resulting severity.

  • attachment (float, default 0) – Layer attachment applied to the resulting severity.

  • conditional (bool, default False) – Whether layered moments divide out P(X > attachment).

Returns:

SeverityMeta – A discrete severity whose atoms are agg_density on xs. Under reinsurance that is the output view (ceded or net), which is the whole point: the object’s answer to “what do you output” already has the cession baked in.

Raises:

ValueError – If the aggregate has not been updated. The conversion reads the distribution the object outputs, and it does not have one yet.

Notes

Nothing is recomputed and nothing on self is touched. See aggregate.distributions.SeverityMeta for what the resulting severity is (a fully formed discrete severity with exact moments) and the DecL reference documentation for the declarative route, which adds a hygiene rule this programmatic path cannot enforce: the source must already be at the resolution you meant.

create_frequency()[source]

Materialize this object’s claim-count distribution as an Aggregate.

The engine carries frequency only as a PGF (applied in the Fourier domain), so there is no q / tvar / cdf / percentiles for the count itself. This builds the marginal count distribution as a first-class Aggregate (via the dsev [1] point-mass trick) so every inherited method works on the count.

Returns:

Aggregate – A built aggregate named f'{self.name}.freq' whose aggregate density is this object’s claim-count distribution: agg_density[k] = P(N = k). Use .q, .tvar, .cdf, .plot, .density_df, .summary_df etc. on it directly.

Examples

>>> fa = a.create_frequency()
>>> fa.q([0.01, 0.5, 0.99])   # count percentiles
>>> fa.tvar(0.99)             # tail count
>>> fa.plot()                 # the count distribution, plotted

Notes

Built through the normal build front door, so grid windowing, bs and log2 selection all happen automatically – a high-mean count (large exposure) needs no special handling. The returned object is a snapshot: rebuild it if the parent changes. See _frequency_program() for what is kept and dropped from the spec.

as_tweedie()[source]

The reproductive Tweedie parameters of this aggregate, or None.

A Tweedie with 1 < p < 2 is a compound Poisson distribution with gamma severity, so any aggregate of that shape has reproductive parameters whether or not it was declared with the tweedie keyword. This reports them.

Returns:

TweedieParameters or None – (p, mean, dispersion), with variance = dispersion * mean ** p. None when the aggregate is not a plain compound Poisson-gamma.

Notes

Two sources, one answer. When the object was declared as tweedie <p> <mean> <dispersion> the declared triple is returned verbatim, off the private _tweedie provenance key the parser records. Otherwise the triple is derived from the engine by aggregate.tweedie.tweedie_convert(), running its (lambda, alpha, beta) to (p, mu, sigma^2) direction over the Poisson mean and the gamma shape and scale.

That second path is the point. 10.05 claims sev gamma 0.0995 cv 0.0709 poisson is the same distribution as tweedie 1.005 1 0.1 and answers here too, so the feature is about the mathematics rather than about which spelling was used. The unparser deliberately does not work this way: it renders from provenance alone, because rewriting an aggregate into a spelling its author did not choose is not its job. See dev/done/plan-tweedie.md.

Recognition is refused for anything that is no longer a bare compound Poisson-gamma: reinsurance or a layer at either level, a limit or attachment on the severity, a mixed or weighted severity, a location shift or splice, a reflected severity, a limit profile, a zero-modified or truncated frequency, and any method-of-moments approximate fit (whose engine is a fitted single severity on a fixed count, not the requested compound at all).

Examples

>>> from aggregate import build
>>> from aggregate.tweedie import Tweedie
>>> a = build('agg Doc tweedie 1.5 100 0.5')
>>> a.as_tweedie()
TweedieParameters(p=1.5, mean=100.0, dispersion=0.5)

The named tuple splats, which is how you reach the analytic object and its exact density:

tw = Tweedie(*a.as_tweedie())
property validation_df

Moment-vs-estimate table for Freq / Sev / Agg (the QA view).

The validation frame: it proves the FFT reproduced the analytic moments (“if the first three moments match, the aggregate is not unreasonable”). Surfaced on demand and in qd() when the object fails validation; the daily-driver headline is summary_df. Three-row Freq / Sev / Agg frame.

Two display modes, same 8-column shape and same column arithmetic:

  • No reinsurance – validation view. Columns are theoretical EX | Est EX | Err EX | CV | Est CV | Err CV | Sk | Est Sk. Err is the noise-aware relative error of empirical vs theoretical.

  • With reinsurance – economic view. Columns become Gross EX | <label> EX | Change EX | Gross CV | <label> CV | Change CV | Gross Sk | <label> Sk, where Gross is the theoretical before any cover and <label> is the model output – Net (all covers net of), Ceded (all ceded to), or Output (mixed, occ and agg passing different kinds). Change = (output - gross) / gross – arithmetically the same column as Err (so the eyeball degenerates cleanly to the validation view when reins is absent), but now read as the % change driven by the cession. Labels are the REINS_LABEL_* constants.

Sources from the canonical self.stats_df: mixed for Gross, empirical for the realised (model-output) view.

property summary_df

At-a-glance risk view – moments + key percentiles, Freq / Sev / Agg.

The daily-driver headline (the lead frame in qd() and _repr_html_()). The compound-model identity made legible: each row answers a different question – count risk (Freq), single-claim severity (Sev), total loss (Agg) – and the percentiles trace where the tail comes from (a heavy Agg skew you can see is inherited from Sev). The validation moment-error table is now validation_df; the tail-behavior classifier is tail_behavior_df.

Index Freq / Sev / Agg (the X index).

Columns Mean | SD | CV | Skew | P01 | Median | P99.

  • SD and CV are both always present (stable layout). CV = SD / Mean is blank when |Mean| is ~0 relative to SD (a signed / near-break-even position – see _cv_or_nan()); SD never blanks. Skew is well defined even at mean 0, so it stays.

  • Moments are the computed (realised FFT-grid) moments for the Sev and Agg rows – Mean is the est_* estimate, the same value validation audits against, not the analytic moment. The Freq row stays PGF-exact (the engine never materializes a count distribution to estimate from, so analytic is the realised value). Before update() there is no grid, so the whole frame falls back to the analytic (theoretical) moments from stats_df.

  • Percentiles come from the FFT grid (exact, not simulated): Agg via q(), Sev via q_sev() (mixtures included, already on the grid). They populate only after update().

Frequency-row percentiles are blank by design: frequency is carried as a PGF (freq_pgf), applied in the Fourier domain – the engine never materializes a count distribution, so there is nothing to take a quantile of. The Freq row still carries E[X] / SD / CV / Skew (PGF-exact), which is what that row is for (count volatility). To get the count distribution as a first-class object, use create_frequency(), then .q(...) / .tvar(...) on it.

Returns:

pandas.DataFrame – Three-row Freq / Sev / Agg frame, Mean carried in .attrs.

property tail_df

Return-period / exceedance table on the default ladder (a property).

The first-class-citizen form of tail_periods_df(): no arguments, the standard DEFAULT_RETURN_PERIODS ladder. Pass your own ladder with tail_periods_df(periods=...).

Returns:

pandas.DataFrame or None – None before update().

tail_periods_df(periods=None)[source]

Return-period / exceedance table for the aggregate (the centerpiece).

The language of reinsurance submissions, cat-model output, and Solvency II / rating-agency capital. Aggregate-only (tail risk is a property of the total), so it complements summary_df (“made of”) with “how bad does it get”. The tail numbers – including the 1-in-1000 TVaR – come from the FFT grid, exact, not simulated (no Monte-Carlo wobble).

Index the non-exceedance probability P, running symmetrically from 0.001 to 0.999 on the default ladder (DEFAULT_RETURN_PERIODS; pass periods= to override): every rung contributes both its lower-tail probability 1 / T and its upper-tail probability 1 - 1 / T. The 1-in-200 (99.5%, Solvency II) and 1-in-250 (99.6%, US capital-adequacy / rating) rows are highlighted in the HTML rendering, on both sides.

Columns T | VaR | TVaR | xsVaR | VaR/Mean.

  • T the return period as the row reads: 1 / P below the median, 1 / (1 - P) above it.

  • VaR = q(P): the quoted number.

  • TVaR = tvar(P): the priced number, adjacent to VaR so the VaR-to-TVaR gap (tail fatness) reads at a glance.

  • xsVaR = VaR - E[X]: capital, the excess of VaR over expected.

  • VaR/Mean: leverage.

Parameters:

periods (array_like of float, optional) – Return-period ladder. Defaults to DEFAULT_RETURN_PERIODS.

Returns:

pandas.DataFrame or None – Indexed by non-exceedance probability P; E[X] carried in .attrs. None before update() (the realised grid is not yet built).

Notes

The ladder is symmetric in P and carries no orientation of its own: a loss object is read off the upper rows, where the bad outcome is a rare large loss, and a payoff object off the lower rows, where it is a rare small result. Both readings sit in one table, which is what lets a signed position be read from either side. Downside TVaR for a signed payoff position is refined in the P&L veneer (see dev/plan-pnl-*).

property approximation_df

All five method-of-moments fits against the exact law (a property).

Columns exact (the anchor, the realized grid) then APPROXIMATION_FAMILIES; rows the meta / stats / quantiles / rel err blocks of approximation_frame(), the last two on the tail_df ladder. On demand, no options, always all five families; rel err reads each family quantile as a relative error against exact.

Returns:

pandas.DataFrame or None – None before update().

Notes

Each family column is the law of the emitted program under the mirroring rule (dev/done/plan-approximate-punchup.md): an unsigned (plain sev) subject clamps a fit’s sub-zero mass to an atom at 0, so even a three-parameter (shifted) family can differ from the exact column, which is exactly what the frame teaches; the two-parameter lognorm / gamma differ in skew by construction, norm targets zero. ks is the Kolmogorov distance sup |F - G| on the grid at the bucket convention F(x_k) = P(X <= x_k), the Berry-Esseen quantity.

Cached after the first access (the exhibit and the two approximation* charts all read it); invalidated wherever density_df is. Treat the returned frame as read-only.

property approximation_density_df

Grid-mass densities of the five fits beside the exact (a property).

The plotting feed for the approximation chart: index the output grid, column exact the realized density, then one column per family holding pdf(x) * bs of the emitted law (reflect and mirror-clamp applied), so the columns overlay the discrete density directly. See approximation_density_frame().

Returns:

  • pandas.DataFrame or None – None before update().

  • Cached after the first access (both ``approximation*` charts read`

  • it); invalidated wherever ``density_df` is. Treat the returned`

  • frame as read-only.

aggregate_error_analysis(log2, bs2_from=None, **kwargs)[source]

Analysis of aggregate error across a range of bucket sizes. If bs2_from is None size a starting bs from the analytic moment window (estimate_agg_window) and scan plus/minus 3 doublings. Note: if the distribution does not have a second moment, you must enter bs2_from.

Parameters:
  • log2

  • bs2_from – lower bound on bs to use, in log2 terms; estimated from the analytic moment window if not input.

  • kwargs – passed to update

severity_error_analysis(sev_calc='round', discretization_calc='survival', normalize=True)[source]

Analysis of severity component errors, uses the current bs in self. Gives detailed, component by component, error analysis of severities. Includes discretization error (bs large relative to mean) and truncation error (tail integral large).

Total S shows the aggregate not severity. Generally about self.n * (1 - sum_p) (per Feller).

q(p, kind='lower')[source]

Return quantile function of density_df.p_total.

Definition 2.1 (Quantiles) x(α) = qα(X) = inf{x ∈ R : P[X ≤ x] ≥ α} is the lower α-quantile of X x(α) = qα(X) = inf{x ∈ R : P[X ≤ x] > α} is the upper α-quantile of X.

kind=='middle' has been removed.

Parameters:
  • p

  • kind – ‘lower’ or ‘upper’.

Returns:

center_window(p=1e-06)[source]

Return the central window of density_df holding 1 - p of the mass.

A thin, no-recompute re-slicer over the finished aggregate: it runs balanced_window() on the realized p_total and returns the rows of density_df in the equal-tail window [q(p/2), q(1 - p/2)] – p/2 of the mass trimmed off each tail, 1 - p kept and centred. Useful for tightening the display window of any computed aggregate (and the post-calc primitive the bivariate axis sizing is built on).

Parameters:

p (float, default 1e-6) – Total discarded tail mass, split equally between the two tails. Must satisfy 0 < p < 1. The literal discarded mass, not a coverage – see balanced_window().

Returns:

pandas.DataFrame – The slice density_df.loc[lo:hi] (a view onto the existing frame; no recomputation). All columns are preserved.

Notes

Edges are snapped to the bucket size bs so the window aligns with the grid. Does not mutate the aggregate – density_df is unchanged.

q_sev(p)[source]

Compute quantile of severity distribution, returning element in the index. Very similar code to q, but only lower quantiles.

Parameters:

p

Returns:

tvar_sev(p)[source]

TVaR of severity - now available for free!

added June 2023

Fixed 1.0.0a91: previously this read the aggregate var/tvar cache (_var_tvar_function['tvar'] built from p_total) rather than the severity grid, so it returned the aggregate TVaR. It now correctly uses the severity grid (sev_density_df.p_sev) – the numbers move.

tvar(p, kind='')[source]

Updated June 2023, 0.13.0

Compute the tail value at risk at threshold p

Definition 2.6 (Tail mean and Expected Shortfall) Assume E[X−] < ∞. Then x¯(α) = TM_α(X) = α^{−1}E[X 1{X≤x(α)}] + x(α) (α − P[X ≤ x(α)]) is α-tail mean at level α the of X. Acerbi and Tasche (2002)

We are interested in the right hand exceedence [?? note > vs ≥] α^{−1}E[X 1{X > x(α)}] + x(α) (P[X ≤ x(α)] − α)

McNeil etc. p66-70 - this follows from def of ES as an integral of the quantile function

q is exact quantile (most of the time) q1 is the smallest index element (bucket multiple) greater than or equal to q

tvar integral is int_p^1 q(s)ds = int_q^infty xf(x)dx = q + int_q^infty S(x)dx we use the last approach. np.trapz approxes the integral. And the missing piece between q and q1 approx as a trapezoid too.

Parameters:
  • p

  • kind

Returns:

sample(n, replace=True)[source]

Draw a sample of n items from the aggregate distribution. Wrapper around pd.DataFrame.sample.

property sev

The exact continuous severity distribution, as a set of functions.

The look-through past the discretization: the weighted mixture of the component Severity objects evaluated from the input distributions, not from the bs-grid PMF in sev_density_df. Use it whenever the answer should not be quantized to a bucket.

Returns:

SevFunctions – A namedtuple of five callables, each accepting a scalar or an array:

cdf(x), sf(x), pdf(x)

The forward functions.

ppf(p), isf(s)

Their inverses. ppf is the quantile function indexed by non-exceedance probability, isf by exceedance probability. Added in 1.0.0a180.

See also

Aggregate.q_sev

the same quantile snapped to the bs grid.

Notes

Exact versus grid. q_sev() reads the discretized severity PMF through a GridDistribution, so it can only ever return a lattice point. sev.ppf calls the underlying continuous distribution, so it returns the true quantile. On agg T1 2 claims sev lognorm 1000 cv 1.31 poisson at bs = 8, sev.ppf(0.99) = 6207.853 against q_sev(0.99) = 6208. The grid answer is the right one when the question is about the computed aggregate; the exact one is right when the question is about the input severity, as it is for occurrence PML and exceedance curves. See oep().

Single component. The five functions are the component Severity methods directly. Severity subclasses scipy.stats.rv_continuous and routes ppf / isf through its layered overrides, so limits, attachments and splices are all respected.

Mixture. The forward functions are the wts-weighted sums of the component functions. The inverses have no closed form and are solved by _mixture_inverse(), which brackets the root with the component inverses and calls scipy.optimize.brentq(). All five close over the same wts vector, so the inverses invert exactly the mixture the forward functions evaluate.

Weights. sev_wt carries the mixture weights directly. Under broadcasting (exposure crossed with severity) the per-component sev_wt are all 1, so their sum is the component count; that case is detected and the weights are taken from the per-component expected claim counts instead.

cdf(x, kind='previous')[source]

Cumulative distribution function at x.

Parameters:
  • x (float or array_like) – Loss size. May lie outside the computed grid; see Notes.

  • kind (str, default 'previous') – Interpolation passed to scipy.interpolate.interp1d(). The default steps, which is right for a discrete law on a lattice. (2022-10 change: kind introduced; the default was linear.)

Returns:

float or ndarray

Notes

Outside the grid the answer is only given where it is known. Below the grid’s lower edge this returns 0 when the law provably has no mass there, which is the ordinary case of a non-negative aggregate on a grid starting at the origin, and nan otherwise. “Otherwise” means a support window placed above the origin, or a signed law whose two-sided window the sizer positioned: in both, mass below the edge was discretized away or never computed, and the grid does not say how much. Reporting 0 there would assert an absence that is not known, so nan is reported instead. The rule lives in aggregate.utilities.below_grid_fill().

Above the grid the value is the top of the cumulative sum, which is the total mass actually computed and is therefore slightly below 1 for an unbounded law. That is the cdf of the discretized law this object is, not an estimate of the true law’s upper tail.

q() is unaffected: it inverts within the grid and never extrapolates.

See also

sf, aggregate.utilities.below_grid_fill

sf(x)[source]

Survival function at x, computed as 1 - cdf(x).

Parameters:

x (float or array_like) – Loss size.

Returns:

float or ndarray

Notes

Inherits cdf()’s outside-the-grid behavior exactly, so sf is nan wherever cdf is: below a window, and below a signed law’s grid. See cdf().

pdf(x)[source]

Probability density function, assuming a continuous approximation of the bucketed density.

Parameters:

x

Returns:

pmf(x)[source]

Probability mass function, treating aggregate as discrete x must be in the index (?)

json()[source]

Write spec to json string.

Returns:

approximate(approx_type='slognorm', output='scipy')[source]

Create an approximation to self using method of moments matching.

Compare to Portfolio.approximate which returns a single sev fixed freq agg, this returns a scipy dist by default.

Use case: exam questions with the normal approximation!

Parameters:
  • approx_type – norm, lognorm, slognorm (shifted lognormal), gamma, sgamma. If ‘all’ then returns a dictionary of each admissible approx (families that cannot be represented for this distribution/output are skipped).

  • output – scipy - frozen scipy.stats continuous rv object; sev_decl - DecL severity fragment (to substitute into an agg; no name) sev_kwargs - dictionary of parameters to create Severity agg_decl - DecL program agg NM 1 claim sev ... fixed agg (or any other string) - Aggregate object built from the agg_decl program via build, so it is parser-born: nonempty program, decompiles, and lands in the underwriter’s knowledge under the fit name.

Returns:

as above.

A shifted family (slognorm / sgamma) requested for a symmetric distribution degenerates to its normal limit and emits a UserWarning (pass approx_type='norm' to select the normal explicitly). A left-skewed distribution is fitted by reflection and renders in DecL through the ordinary reflection syntax loc - scale * name shape; only output='scipy' raises ValueError for it (scipy has no frozen reflected rv).

The emitted severity keyword mirrors the input. The DecL and object outputs spell the severity ssev when this aggregate is signed (_signed()), else plain sev; output='sev_kwargs' mirrors the same rule through sev_signed. A fit whose law reaches below zero emitted under plain sev clamps its sub-zero tail to an atom at 0 (a loss stays a loss), at the cost of a small moment drift that the built surrogate’s validation_df reports.

entropy_fit(n_moments, tol=1e-10, verbose=False)[source]

Find the max entropy fit to the aggregate based on n_moments fit. The constant is added (sum of probabilities constraint), for two moments there are n_const = 3 constrains.

Based on discussions with, and R code from, Jon Evans

Run

ans = obj.entropy_fit(2)
ans['ans_df'].plot()

to compare the fits.

Parameters:
  • n_moments – number of moments to match

  • tol

  • verbose

Returns:

var_dict(p, kind='lower', snap=False)[source]

Make a dictionary of value at risks for the aggregate, mirrors Portfolio.var_dict. Here is just marshals calls to the appropriate var or tvar function.

Allows the price function to run consistently with Portfolio version.

Example Use:

for p, arg in zip([.996, .996, .996, .985], ['var', 'lower', 'upper', 'tvar']):
    print(port.var_dict(p, arg,  snap=True))
Parameters:
  • p

  • kind – var (defaults to lower), upper, lower, tvar

  • snap – snap tvars to index

Returns:

price(p, g, kind='var')[source]

Price using regulatory and pricing g functions, mirroring Portfolio.price. Unlike Portfolio, cannot calibrate. Applying specified Distortions only. If calibration is needed, embed Aggregate in a one-line Portfolio object.

Compute E_price (X wedge E_reg(X) ) where E_price uses the pricing distortion and E_reg uses the regulatory distortion.

Regulatory capital distortion is applied on unlimited basis: reg_g can be:

  • if input < 1 it is a number interpreted as a p value and used to determine VaR capital

  • if input > 1 it is a directly input capital number

  • d dictionary: Distortion; spec { name = dist name | var, shape=p value a distortion used directly

pricing_g is { name = ph|wang and shape=}, if shape (lr or roe not allowed; require calibration).

if ly, must include ro in spec

Parameters:
  • p – a distortion function spec or just a number; if >1 assets, if <1 a prob converted to quantile

  • kind – var lower upper tvar

  • g – pricing distortion function

Returns:

price_pentagon(*, p=None, a=None, P=None, M=None, Q=None, LR=None, PQ=None, ROE=None, reins_view=None)[source]

Complete the pricing octet at a capital level given one target.

Fix the capital level with exactly one of p (a VaR probability, a = self.q(p)) or a (an asset level, snapped to the grid), then supply exactly one pricing target – premium P, cost of capital ROE (a.k.a. CoC), or a loss ratio via LR (equivalently M, Q or PQ). Returns the canonical one-row ('total') pentagon DataFrame (columns PENTAGON_STATS). The target keywords match the canonical stat names.

Pure accounting completion against the object’s expected loss at the chosen capital level – no distortion is involved (contrast price(), which prices with a Distortion). The triple {L, a, target} is solved by Pentagon.solve().

Parameters:
  • p (float, optional) – VaR probability fixing the capital level; mutually exclusive with a.

  • a (float, optional) – Asset level fixing the capital; mutually exclusive with p.

  • P (float, optional) – Exactly one pricing target – premium, margin, capital, loss ratio, premium-to-capital, or cost of capital (M/Q).

  • M (float, optional) – Exactly one pricing target – premium, margin, capital, loss ratio, premium-to-capital, or cost of capital (M/Q).

  • Q (float, optional) – Exactly one pricing target – premium, margin, capital, loss ratio, premium-to-capital, or cost of capital (M/Q).

  • LR (float, optional) – Exactly one pricing target – premium, margin, capital, loss ratio, premium-to-capital, or cost of capital (M/Q).

  • PQ (float, optional) – Exactly one pricing target – premium, margin, capital, loss ratio, premium-to-capital, or cost of capital (M/Q).

  • ROE (float, optional) – Exactly one pricing target – premium, margin, capital, loss ratio, premium-to-capital, or cost of capital (M/Q).

  • reins_view (str, optional) – Which of a cession’s distributions to complete the octet on, one of reins_views. The default None is the object’s own. Both the asset level and the expected loss resolve on the chosen view, matching calibrate_distortions().

Returns:

pandas.DataFrame – One 'total' row, eight canonical pentagon columns.

Raises:

ValueError – If not exactly one of p/a is given, or not exactly one pricing target is supplied.

price_ccoc(ccoc, *, p)[source]

Price at a constant cost of capital ccoc and VaR level p.

No distortion is involved – a thin alias for self.price_pentagon(p=p, ROE=ccoc) returning the canonical one-row ('total') pentagon DataFrame. Parity with Portfolio.price_ccoc().

prob_loss_assets(*, p=None, L=None, a=None)[source]

Given any one of p, L, a, return the consistent triple.

Free choice over the capital anchor: pass exactly one of the VaR probability p, the limited expected loss L = E[min(X, a)], or the asset level a – any one determines the other two. Thin delegator to prob_loss_assets() over this aggregate’s p_total grid (the single lev source).

Returns:

ProbLossAssets – Namedtuple (p, L, a), grid-snapped and mutually consistent (L == lev(a), p == cdf(a)).

price_pentagon_ex(*, p=None, a=None, L=None, M=None, P=None, Q=None, LR=None, PQ=None, ROE=None, reins_view=None)[source]

Complete the pricing octet over the full pentagon vocabulary.

The full-power front door over price_pentagon(): free over the capital anchor (p, a, or the limited expected loss L) and accepting any soluble pentagon configuration, warning when an accounting-determined L does not reconcile with E[min(X, a)]. Thin delegator to the shared single-distribution pricing concern; see aggregate._pricing.price_pentagon_ex() for the full contract.

Returns:

pandas.DataFrame – One 'total' row: a leading p column then the eight canonical pentagon stats.

calibrate_distortions(coc=None, *, lr=None, p=None, a=None, kind='lower', names=('ccoc', 'ph', 'wang', 'dual', 'tvar'), reins_view=None)[source]

Calibrate the standard pricing distortion set to a pricing target.

The Aggregate counterpart of Portfolio.calibrate_distortions() – calibration to this distribution (the aggregate is its own total), with no per-unit allocation (that stays a Portfolio concern). Calibrating directly here means no more wrapping a single Aggregate in a one-unit Portfolio to obtain a calibrated distortion set.

Parameters:
  • coc (float, optional) – Target cost of capital COC = (P - L) / Q. Exactly one of coc or lr.

  • lr (float, optional) – Target loss ratio LR = L / P, converted to a cost of capital through the pentagon at the resolved anchor. Exactly one of coc or lr.

  • p (float, optional) – Probability at which the calibration applies; converted to an asset level via self.q(p, kind). Exactly one of p or a.

  • a (float, optional) – Asset level; snapped to the grid. Exactly one of p or a.

  • kind ({'lower', 'upper'}, optional) – VaR kind when p is provided. Default 'lower'.

  • reins_view (str, optional) – Which of a cession’s distributions to calibrate on, one of reins_views. The default None is this aggregate’s own density, which under a cession is whichever view the program asked for: a net of program holds its net, a ceded to program holds its ceded.

Returns:

CalibrationResult – Carrying distortion_df (one row per distortion in [ccoc, ph, wang, dual, tvar]), the shared one-row calibration_df target, the distortions themselves, and the inputs they were fitted at. The same three are stored on self.distortion_df / self.calibration_df / self.distortions as before, so nothing that read them moves; both frames carry attrs['reins_view'], recording which distribution they were fitted to. Same schema as Portfolio.calibrate_distortions().

The return type changed at 1.0.0a259: it was the bare distortion_df, which is now an attribute of the result.

Notes

The expected loss anchoring the premium target is computed on the full aggregate grid (the E[min(X, a)] / add_exa convention), matching a one-unit Portfolio’s exa_total.

names selects the distortion families to calibrate (default the standard set); signed / payoff supports are handled transparently (see aggregate._pricing.calibrate_distortions()).

Calibrating gross and net separately and differencing the premiums is the allowance for reinsurance in the rate. The asset level resolves on the chosen view, so p= holds the threshold fixed across views rather than the capital.

Examples

a = build('agg Re 100 claims sev lognorm 50 cv 2 poisson '
          'occurrence net of 100 xs 100')
a.reins_views                                    # ['gross', 'ceded', 'net']
gross = a.calibrate_distortions(0.10, p=0.999, reins_view='gross')
net = a.calibrate_distortions(0.10, p=0.999, reins_view='net')
evaluate(P=None, *, p=None, a=None, names=None, reins_view=None)[source]

Evaluate the position P - X: the breakeven acceptability panel.

Pricing asks what the obligation is worth; evaluation asks how much stress the position you hold survives. Solves, per distortion family, for the shape at which the risk-adjusted margin reaches 0. The breakeven gini_p is the family-agnostic acceptability index of Cherny and Madan.

Parameters:
  • P (float, optional) – The premium held against this aggregate. Defaults to exp_premium, and raises when that is unset: a position has to have a consideration before it can be evaluated.

  • p (float, optional) – Asset probability, resolved on the evaluated distribution (a = q(p)). At most one of p or a.

  • a (float, optional) – Asset level, snapped to the grid. At most one of p or a; with neither, the position is measured against its whole distribution, which is the unlimited reading.

  • names (sequence of str, optional) – Distortion families. Defaults to EVAL_FAMILIES (ph / wang / dual / tvar) unanchored, and to EVAL_FAMILIES_ANCHORED, which adds ccoc, when an anchor is given.

  • reins_view (str, optional) – Which of a cession’s distributions to evaluate, one of reins_views. The default None is this aggregate’s own. P is not adjusted with the view: evaluating the gross distribution against the net premium asks what stress the position would survive if the cover failed to respond, which is a question worth being able to ask deliberately, and a wrong answer to ask by accident. Pass the premium that goes with the view.

Returns:

EvaluationResult – Carrying evaluation_df, the panel: tidy (long) form, MultiIndex rows (Step, distortion) with Step this aggregate’s name, and columns role / param_name / param / gini_p / error / status. role is always 'sell' here: an aggregate is an obligation written, so the position is held the way it is booked. The same shape PnL.evaluate() and Portfolio.evaluate() return, so panels concatenate. The result also carries the premium and the view the panel was measured on.

The return type changed at 1.0.0a259: it was the bare panel, which is now .evaluation_df. A frame cannot be dispatched on, and the evaluate exhibit dispatches on the result.

Warns:

DegenerateEvaluationWarning – When no breakeven level exists: P <= E[X] (unacceptable at any stress) or the position cannot lose (acceptable at every stress). Both report NaN.

Notes

Anchoring is what makes evaluation and calibration comparable. calibrate_distortions() solves rho_g(min(X, a)) = P at the asset level it resolved. Evaluating at the same anchor solves the same equation, so the panel recovers that calibration’s parameters family for family, which is the round trip worth having. Unanchored, the implicit asset level is the top of the FFT grid, and the two answers differ by whatever the tail beyond a is worth.

See also

aggregate._pricing.evaluate_margin

the solve and its math.

calibrate_distortions

the pricing counterpart, given a CoC target.

4.4.6. Severity fits and approximations

Public facade for the distribution classes (Phase 1 kind split).

This module defines no logic of its own. The implementation lives in the underscore-prefixed kind modules; the underscore answers “import from here?” with “no – import from the package”. Every historical import path (aggregate.Aggregate, aggregate.distributions.Aggregate, from aggregate.distributions import <name>) keeps working through the re-exports below.

Internal import order (the only place the intra-distributions ordering lives): _fits -> _severity -> _frequency -> _aggregate.

aggregate.distributions.lognorm_fit(m, cv)[source]

Lognormal (mu, sigma) parameters from mean m and cv cv.

Notes

For ss.lognorm(sigma, scale=exp(mu)) matching mean m and CV cv, \(\sigma^2 = \log(1 + \mathrm{cv}^2)\) and \(\mu = \log(m) - \sigma^2 / 2\).

aggregate.distributions.sln_fit(m, cv, skew)[source]

method of moments shifted lognormal fit matching given mean, cv and skewness

Parameters:
  • m

  • cv

  • skew

Returns:

aggregate.distributions.sgamma_fit(m, cv, skew)[source]

method of moments shifted gamma fit matching given mean, cv and skewness

Parameters:
  • m

  • cv

  • skew

Returns:

aggregate.distributions.gamma_fit(m, cv)[source]

gamma parameters from mean and cv.

aggregate.distributions.beta_fit(m, cv)[source]

alpha and beta parameters from mean and cv.

aggregate.distributions.invgamma_fit(cv)[source]

Inverse gamma shape parameter from cv.

Notes

For ss.invgamma(a) the squared coefficient of variation satisfies \(\mathrm{cv}^2 = 1 / (a - 2)\), giving \(a = 1/\mathrm{cv}^2 + 2\). Valid for \(a > 2\), i.e. when the variance exists.

aggregate.distributions.invgauss_fit(cv)[source]

Inverse Gaussian shape parameter from cv.

Notes

For ss.invgauss(mu) the cv equals \(\sqrt{\mu}\), so \(\mu = \mathrm{cv}^2\).

aggregate.distributions.lognorm_lev(mu, sigma, n, limit)[source]

return E(min(X, limit)^n) for lognormal using exact calculation currently only for n=1, 2

Parameters:
  • mu

  • sigma

  • n

  • limit

Returns:

aggregate.distributions.lognorm_approx(ser)[source]

Lognormal approximation to series, index = loss values, values = density.

aggregate.distributions.approximate_from_mcvsk(m, cv, skew, name, agg_str, note, approx_type, output, warn_degenerate=False, signed_input=False)[source]

Dispatch (m, cv, skew) to a method-of-moments fit, in the requested form.

Backs Aggregate.approximate and Portfolio.approximate. A thin output adapter over the single fit core _approximate_sev_kwargs(), which owns the family fits and the guards (symmetric -> normal, left-skew -> reflected); this function only formats the core’s sev_* kwargs into the requested surface, so the two approximate surfaces and the approximate DecL keyword share one implementation.

Parameters:
  • m (float) – Analytic (or empirical) aggregate mean, coefficient of variation, and skewness.

  • cv (float) – Analytic (or empirical) aggregate mean, coefficient of variation, and skewness.

  • skew (float) – Analytic (or empirical) aggregate mean, coefficient of variation, and skewness.

  • name (str) – Naming / note scaffolding for the DecL and Aggregate output forms. agg_str is the program prefix up to but excluding the severity keyword ('agg NM 1 claim '); the adapter chooses sev or ssev from signed_input.

  • agg_str (str) – Naming / note scaffolding for the DecL and Aggregate output forms. agg_str is the program prefix up to but excluding the severity keyword ('agg NM 1 claim '); the adapter chooses sev or ssev from signed_input.

  • note (str) – Naming / note scaffolding for the DecL and Aggregate output forms. agg_str is the program prefix up to but excluding the severity keyword ('agg NM 1 claim '); the adapter chooses sev or ssev from signed_input.

  • approx_type ({'norm', 'lognorm', 'gamma', 'sgamma', 'slognorm'}) – The fitted family. 'all' is handled by the calling method, not here.

  • output (str) – 'scipy' -> frozen scipy.stats rv; 'sev_kwargs' -> the Severity kwargs dict; 'sev_decl' -> a DecL severity fragment; 'agg_decl' -> a full agg ... fixed DecL program; 'agg' (or any other string) -> a fixed-frequency Aggregate built from the agg_decl program via build, so it is parser-born: it carries program, decompiles, and lands in the underwriter’s knowledge under the fit name.

  • warn_degenerate (bool) – When True (the interactive .approximate() method) emit a UserWarning if an explicitly-requested shifted family (slognorm / sgamma) degenerates to a normal because the distribution is symmetric.

  • signed_input (bool) – Whether the source object is signed (Aggregate._signed() / Portfolio._signed()). The emitted severity keyword mirrors the input, not the fit: ssev when True, plain sev when False. output='sev_kwargs' mirrors the same rule through the sev_signed flag.

Returns:

object – Per output.

Notes

The emitted keyword mirrors the input. A loss aggregate stays a loss: a fit whose law reaches below zero (the normal, or a reflected shifted fit) emitted under plain sev clamps its sub-zero tail to an atom at 0, at the cost of a small moment drift that validation_df of the built object reports like any other discretization error. An ssev input keeps the fitted law exact. The fit core keeps returning its own sev_signed / sev_reflect flags; the adapters decide the keyword.

A left-skewed (reflected) fit renders in DecL through the ordinary reflection syntax loc - scale * name shape, so every output form serves it except output='scipy': scipy has no frozen reflected rv, so that one still raises ValueError; use the 'agg_decl' or object outputs, or approx_type='norm'.