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._aggregatehold theFrequency/Severity/Aggregateclasses (kind subclasses use theBase<Kind>registry dispatch);aggregate._fitsholds 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). TheAggregatemethods 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.
|
Manages Frequency distributions: creates moment function and MGF. |
|
|
|
Compound (aggregate) probability distribution. |
|
Lognormal |
|
method of moments shifted lognormal fit matching given mean, cv and skewness |
|
method of moments shifted gamma fit matching given mean, cv and skewness |
|
gamma parameters from mean and cv. |
|
alpha and beta parameters from mean and cv. |
|
Inverse gamma shape parameter from cv. |
|
Inverse Gaussian shape parameter from cv. |
|
return E(min(X, limit)^n) for lognormal using exact calculation currently only for n=1, 2 |
|
Lognormal approximation to series, index = loss values, values = density. |
|
Dispatch |
4.4.3. Frequency class
- class aggregate.distributions.Frequency(freq_name=None, *args, **kwargs)[source]
Bases:
HelpMixinManages 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 parametersbernoulli: exp_en interpreted as a probability, must be < 1binomial: Binomial(n/p, p) where p = freq_a, and n = exp_enpoisson: 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 isgeometric zt, the same law by memorylessness.logarithmci: logarithmic(theta), supported on 1, 2, …; theta solved numericallyneymana: 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 meanpascal: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 distributiondelaporte: 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 distributionsig: 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_bsichel.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
cfrom_zm_weight(). Closed form, defined for everyp0Min[0, 1)– no solver, and no infeasible region.- Parameters:
base_mean (
float, optional) – The un-modified mean. Defaults tobase_mean, the value in force for this object.- Returns:
float– The realizedE[N]. Returnsbase_meanunchanged 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]istarget_mean.The counterpart of
modify_mean(), and the DecL default: a barepoisson zm 0.5states the realized mean and comes here. The!marker (poisson zm 0.5 !) opts out and takesmodify_mean()instead. This is the one place a numerical solve survives in the zero-modification path, and sincea325it 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 belowLis genuinely unattainable – for a zero-truncated PoissonL = 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_meanlies at or below the attainable infimum.
- apply_deductible(survival)[source]
Loss Models §8.6: the payment count
N^Pimplied 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 withbase_meanandenset, describingN^P.- Raises:
ValueError – If
survivalis outside(0, 1], orbase_meanis 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, whenE[N] = n.Same contract as
freq_pgf():nis the un-modified (base) mean and the evaluation is elementwise in the transformed argumentz. 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 forfreq_pgf()(passAggregate.base_mean, never the realizedn, under zero modification).z (
scalarorndarray,realorcomplex) – Evaluation points (e.g. an rfft vector).
- Returns:
ndarray– \(P_N'(z)\), elementwise.- Raises:
NotImplementedError – For families outside the supported set (
sig,beta,sicheland 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 recursionk p_k = (a k + b) p_(k-1): multiplying byz^(k-1)and summing givesP'(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 factP' = n P. The identity holds for the base pgf, so the base implementation reads the class-levelfreq_pgf, never an instance-level zero-modified wrapper;_install_zm_wrapperslayersc 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 thefreq_pmfFFT inversion, evaluated withevaluate_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
NotImplementedErrorfromfreq_pgf_prime()read this instead, so chart availability and the emitter’s route switch agree without a try/except at predicate time.- Returns:
bool–Truefor the (a, b, 0) families (the_panjer_ab0flag) and for any subclass carrying a closed-formfreq_pgf_prime()override (fixed, bernoulli, empirical/renewal, and the gamma, delaporte and ig mixed Poissons);Falsefor the rest (logarithmic, sig, beta, sichel and variants, neymana, pascal).
Notes
Reads the override off the class, never the instance:
_install_zm_wrapperslayers ac G'wrapper onto the instance, and a zero-modified member of an unsupported family must still answerFalse(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 owningAggregatesupplies (the moment rows beforeenis stamped) – renders asn/a. Shares the label/value convention (aggregate.constants.info_row()) withAggregate/Portfolio/Severity. The row catalogue is documented indev/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 toname. 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’sn), so a standalone frequency reports only its family class; the full count support appears in the aggregate’stail_behavior_df. The verbose form istail_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 owningAggregatehas stampeden– 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_0family: the same question one level down fromprob_eq_0(P(X = 0)), under the same name and the same zero-argument shape.Evaluated at
base_mean, the un-modified mean the owningAggregatestamps at construction; a standalone frequency raises until it is set. For an unmodified frequencybase_meanequalsen, so this is simplyP(N = 0)at the expected claim count.Under
zm/ztthe answer isfreq_p0by construction – that is what zero modification means – not_prob_eq_0evaluated 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_0default isNone; only the zero-modifiable kinds override it – seesupports_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):Aggregatecalls 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 a149tail_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), columnsp(this frequency’s pmf) andpo_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 owningAggregatestamps at construction, which equalsenunlesszm/ztis in force (a standalone frequency raises until one of them is set). The pmf invertsfreq_pgfon 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.infwhen unbounded), which is not the largest materialized atom. Reporting only:tail_behavior_dfand 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. Seedev/done/plan-agg-port-as-sev.mdsection 4.6.
- reference_bs: float = None
The referenced source’s own bucket size, known exactly on the resolution path (never inferred from
np.diffof 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 toSeverityScipy,SeverityDHistogram,SeverityCHistogram,SeverityFixed,SeverityMeta, orSeverityCopy. The chosen subclass’s_buildpopulatesself.fzfrom 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 formdhistogram/chistogram/fixed, or an existingSeverity/Aggregate/Portfolioinstance for the copy / meta paths.exp_attachment (
float | None) – Layer attachment point.Nonemeans “no layer clause” and conditions onX >= 0;0means “conditional onX > 0” (this distinction matters for distributions with mass at zero).exp_limit (
float) – Layer width (the “y” iny 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 outP(X > attachment).name (
str) – Identifier (e.g. set bysev SOMENAME …in DecL).note (
str) – Free-text annotation (from anote{...}clause).hints (
str) – Rawhints{...}build-settings string; retained as annotation (a standaloneSeverityhas no build settings to apply).exp_retention_end (
float) – Ground-up lossd2at which the retention reaches zero ([Franchise-Deductible]).inf(default) is the ordinary deductible;d2 == exp_attachmentis the franchise, which pays the whole loss, up to the limit, once it exceeds the attachment. A finite value needs a layer clause andd2 >= attachment. The limit is the largest payment in every form;detachmentis 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 onself.fzretain the original values. If the policy changes, build a newSeverityrather than reassigning fields on an existing one.- Raises:
ValueError – If
sev_lb/sev_ubdescribe 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 asn/a. Shares the label/value convention (aggregate.constants.info_row()) withAggregate/Portfolio/Frequency. The row catalogue is documented indev/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_momentschecks at construction).
- property actual_skew
Severity skewness (analytic).
- property tail_class
This severity’s
TailClassrung.Deterministic family lookup (param-aware): structural-bounded test first (so
boundedis spec-only), then scipy family. Seeaggregate.tail.classify_severity().
- property bounded: bool
Whether the (post-layer, post-splice) severity has bounded support.
Derived view:
Trueifftail_classisBOUNDED— i.e.fixed/dhistogram/chistogram(finite by construction), ascipyfamily in_BOUNDED_SCIPY_SEVS, a finite layerexp_limitor splicesev_ub, or ameta/copywrapping a bounded object.
- property tail_description: str
family, declared support, claim-space support, tail class.
E.g.
lognorm, [0, inf), subexponential right tailfor an unlimited severity, orlognorm, [0, 500], bounded; 500 xs 250once a layer is declared anddhistogram, [1, 6], bounded; atoms [1 2 3 4 5 6]for adsev. Derived from the sameseverity_tail_row()as the aggregate’stail_behavior_dfcomprows, with the declared layer / atom phrase appended when it says something the interval does not (a149: this absorbed the oldsupport_description). The verbose form istail_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:
Histogram fast-path. If
sev1was precomputed during_build(histograms and meta-severities populatesev1/sev2/sev3directly from the support) and the layer is the trivial[0, inf], return those values immediately.Analytic shortcut. For
lognorm,pareto,gamma, andexpon(with no shift/truncation), delegate to_moms_analytic(), which computes layered moments in closed form via partial expected values. Defensivenp.infoverrides are applied when the underlying moment does not formally exist (e.g. pareto shape <= 1).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]) (
tupleoffloat) –np.nansignals an unreliable numerical result;np.infmeans the moment is genuinely undefined.Mathematical background-----------------------With :math:`X(a,d) = min(d,(X-a)_+)`andusing thequantile-space changeofvariables :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`` andthenapplies the binomial expansiontorecover :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. Withinvertit isS(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 onself.figure.
Notes
Draws the document
charts.chart_severityemits. 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,ProgramMixinCompound (aggregate) probability distribution.
Implements the FFT-based algorithm of Mildenhall (2024): discretize severity, FFT, apply frequency PGF, inverse FFT. See
_freq_sev_convolutionfor the five-line core;update_workfor the orchestration (severity prep → occurrence reinsurance → convolution → aggregate reinsurance → audit). Validation by theoretical-vs-empirical moment comparison (paper §4.7) lives in theempiricalanderrorcolumns ofstats_dfwritten at the end ofupdate_work.Construction is via the
__init__arguments below, or — more usually — viabuild()parsing DecL.Public surface that Portfolio and Bounds depend on. Three stats surfaces —
infofor text,summary_dffor the daily risk view, andstats_dffor 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_dfis the companion return-period table;validation_dfthe 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 byupdate_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 legacyaudit_df/report_df/report_ser/statistics/statistics_df/statistics_total_dfsurface has been removed; consultstats_dfinstead.- 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)TailClasstriple.The authoritative tail-thickness computation: frequency and severity rungs by deterministic family lookup, the aggregate rung by the
maxcombine rule..boundedand the tail text are derived from this. Returns aTailClassesnamedtuple with.freq/.sev/.aggfields.
- property bounded: bool
Whether the aggregate has bounded support.
Derived view:
Trueiff the aggregate tail class isBOUNDED, 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 layerexp_limitor splicesev_ub. Conservative:Falsewhenever boundedness cannot be proved from the spec. Setself.bounded = Trueto 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_dfreport – 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 toinfo().
- 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 realizedusedgrid, indexed bymethod, culled to the user-facing columns: whether the methodapplies, whether it wasselected, the method window (x_min/x_max) and its widthW, the grid (bs/log2), the log2 the windowlog2_need``s at that ``bs, what fraction of the distribution the window holds (coverage), any estimatedclippedfar-tail mass, and a one-linenote.Returns
Nonebeforeupdate(). Seebs_description/bs_explanationfor 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.coverageis 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/selectedbool, the window and grid columns float,log2/log2_neednullableInt64(an exponent reads as an integer, and a method that never ran records none),coverage/notestring. The frame used to be built by transposing a method-per-column block, which typed the lotobjectand 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 isbs_explanation; the ANSI-coloured variant isaggregate.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 perlog2, and within each a line search out from the current bucket:bsis doubled untilvalidation_scorestops improving, then halved likewise, capped atbs_limiteach way. It then takes the best score among the cells that do not growlog2, growing by one only when nothing at the current size or smaller reachesgood_enough. A discrete severity whose atoms are a whole number of buckets is already exact, so the bucket is pinned and onlylog2is probed.Populates
sharpen_df,sharpen_descriptionandsharpen_explanation. Returnsself, so the call chains; the object is moved in place, soa = a.sharpen()rebinds the same object rather than producing a second one.It also pins the outcome onto the object’s own text.
program,noteandhintsare rewritten together so the three agree andbuild(a.program)reproduces the sharpened object: a moved grid becomeshints{log2=...; bs=...}, a confirmed one anote{sharpen: ...}and no hints. Probing twice replaces the verdict rather than stacking a second one. Seeaggregate._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_log2from the probe centre, the realizedbs/log2/extent/x_min, thescoreand its six normalized terms (u_sev_meanthroughu_agg_skew), thealiasingratio, thevalidationverdict, awarningscount,seconds, theselectedwinner, and anotecarrying 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.Nonebeforesharpen()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
log2was or was not grown, and what changed. The short form issharpen_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_descriptionandsharpen_explanation.sharpen()pins its outcome ontoprogramas it finishes, so this is that program rendered to read: thespreadlayout with the trailer left in, since here the trailer is the payload.hints{log2=...; bs=...}when the grid moved, anote{sharpen: ...}and deliberately no hints when it did not.''beforesharpen()runs.Which of the four to reach for: this one for text to read or share,
programfor the one-line stamp,hintsfor the settings alone,notefor the verdict alone.Delegated to
aggregate._program.sharpen_program(); the three outcomes are documented onaggregate._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 isinherit premiumwhen the exposure states one, and otherwise expected loss overloss_ratio;expense_ratio=0omits the expense clause.net_combined_ratioadditionally prices the reinsurance, which is what silences theZeroPremiumCessionWarningan 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 wrappingpnlare documented in full, and toaggregate._program._pnl_technical()for the ladder’s algebra.- Parameters:
loss_ratio (
float, default0.70) – Sizes the premium, and only when there is none to inherit andnet_combined_ratioisNone.expense_ratio (
float, default0.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 (
floatorsequence, optional) – Each tier’s combined ratio, defaulting tonet_combined_ratio. A sequence gives one value per layer, in declaration order.agg_combined_ratio (
floatorsequence, optional) – Each tier’s combined ratio, defaulting tonet_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 currencydeposit, or arateof 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
cessionadded.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 (
stroriterableofstr) – One cession clause per tier, each opening withoccurrenceoraggregate. 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 effectiveseverity(only when there is more than one component); and theaggregate. Columns:min/maxstructural support (-inf/infat an unbounded end),left_tail/right_tail(the per-side tail class –boundedat a finite end, else the family decay rung),bounded(the support finite both ends), and (aggregate row only) the conservativeconcentratedflag andcv = sd / mean. Anotecarries power-lawalpha/ 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. Seeaggregate.tailanddev/bucket-selection.rst.- Returns:
pandas.DataFrame– Indexed bycomponent.
- 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/2positional bias). MirrorsPortfolio.allocation_method().Reinsurance is baked in during
update(), so a change afterbuildrequires 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 ofreins_bucket.'linear'(default) splits each off-grid atom’s mass across its two bracketing grid bucketsk,k+1with weights1-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 tobs/2positional bias). On-grid atoms (f == 0, e.g. integer atoms withbs == 1) give identical results under both schemes.Discretization happens in
update(), so a change afterbuildrequires 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 byupdate.
- property density_df
Per-bucket density / distribution / risk-measure frame.
Built lazily on first access after
update. Stored inself._density_df; treat as read-only.Columns (in construction order):
Column
Set from
Read by
lossself.xs(also the index)plot,q, boundsp_totalself.agg_density- Portfolio (when Aggregate
is in a port), bounds,
plot, user code
palias of
p_totalPortfolio API compat
log_pnp.log(p)plotlog scaleFp.cumsum()q,var,tvarS1 - p_total.cumsum()q,tvar,plotlevcumsum(loss·p) + loss·Spricing
exaalias of
levPortfolio API compat
exleacumsum(loss·p) / Fpricing
eself.est_m(constant column)exgtaexgta(e - cumsum(loss·p)) / Spricing
exeqaloss(sinceE[X|X=a] = a)Portfolio API compat
lev,exleaandexgtaare direct sums that carry the window originx0(numerics-2): on a windowed or signed gridE[X ∧ a] = Σ_{x≤a} x·p + a·S(a), nevercumsum(S)·bs(which silently assumes the grid starts at 0). Ratio denominators are guarded:exlea(exgta) is NaN whereF(S) is at or below the validation noise floor. Theepdcolumn was removed at numerics-2 (no consumers;max(0, e - lev)/eif needed).Duplicated columns (
p == p_total,exa == lev,exeqa == loss) are intentional: Portfolio’sfilter(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 indexi0), 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 indensity_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)(-infat 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_cededis a point mass at 0 andp_sev_net == p_sev_gross.- p_agg_gross, p_agg_ceded_occ, p_agg_net_occaggregate of each
occurrence severity view.
p_agg_grossis the true gross aggregate (_fft_aggregateof the gross severity).- p_agg_subject, p_agg_ceded, p_agg_netaggregate-cover views.
p_agg_subjectis the aggregate input to the aggregate cover (= aggregate of the requested occurrence output); it equalsp_agg_grossonly when there is no occurrence cover.
Notes
All aggregate columns route through
_fft_aggregateso the zero-risk / fixed-1 shortcuts apply consistently. Renamed from the legacyreinsurance_df:p_agg_gross_occ -> p_agg_grossand the oldp_agg_gross(the agg-cover input)-> p_agg_subject. ReturnsNonewhen 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 oncalibrate_distortions()andevaluate(), 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. Mirrorsavailable_charts()andavailable_exhibits(), which answer the same shape of question about the same object.cededandnetare end to end, so on an occurrence-only program they are the occurrence stage’s cession and retention, not the empty aggregate-stage columns ofreins_density_df.- Returns:
listofstr
See also
aggregate._reinsurance.reins_view_columnsthe 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_dfcomputes the cession,price()prices any pmf, and this walks one through the other. The DecLceded premiumclause 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 aftercalibrate_distortions(), with nodistortionargument, which prices every view with every calibrated family.- Parameters:
distortion (
Distortion,str,dict, orNone) – A distortion, a name indistortions, a mapping, orNonefor the whole calibrated set.p (
float, optional) – Asset probability; each view resolves its owna = q(p).a (
float, optional) – A common asset level. At most one ofpora; with neither the price is unlimited.views (
sequenceofstr, optional) – Defaults to all ofreins_views.
- Returns:
pandas.DataFrame–(distortion, view)rows bya/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 onself.figure.
Notes
Draws the document
charts.chart_reinsemits. 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 (+) cededthan 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 netN = sum n(X_i)– where each gross claimX_iis 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 inreins_density_df. The three views satisfyceded + net = gross, so any two determine the third; the pick of which two isviews.- Parameters:
views (
(str,str), default :py:class:`(`’net’, ``'ceded')``) – The(x, y)axis view pair, each one of'gross'/'ceded'/'net'. Axis 0 (x) isviews[0], axis 1 (y) isviews[1]. The default('net', 'ceded')matches the DecLnetcededform;('gross', 'ceded')matchesgrosscededand('gross', 'net')matchesgrossnet.bs (
float, optional) – Bucket-size override (a single commonbsfor 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 commonbssized 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 offbs_explanation.log2_x (
int, optional) – Axis-0 / axis-1 log2 grid lengths (grid has1 << log2points). Default: measured from the two views’ occurrence aggregate margins viabalanced_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 has1 << log2points). Default: measured from the two views’ occurrence aggregate margins viabalanced_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_log2cells.Noneuses theBivariateSettings.total_log2default (20). Raise it for a fine joint, and pair it withstore_dirpast 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 todensity.zarrthere and never materializes in RAM. Requires themassiveextra.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 innetcededmode, with the jointdensity, the two axis grids (axis_xs),marginals/moments(E[X^i Y^j]) /corr/summary_df/stats_df/infoand a two-panelplot(comonotone per-claim severity and joint aggregate). Equivalent to the DecLnetceded/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 linec + n = X. Placing the gross severity mass at(c(x_k), n(x_k))(rebucketed onto the 2D grid by the activereins_bucketscheme) builds the bivariate severityS. The joint aggregate density isiFFT2(freq_pgf(n, FFT2(S)))– exactly the univariate_fft_aggregate()with the 1D transforms replaced by 2D transforms, valid becausefreq_pgf(n, z)is elementwise inz. 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 theCross-checknotes indev/done/reins-bivariate.md).This wraps the object as a
aggregate.bivariate.BivariateAggregateinnetcededmode (the engine isaggregate.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 fromocc_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. Astore_dirbuild 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 foraggregate._aggregate_compute.palm_conditional_mean(): the conditioning image density and the target image measure via_rebucket_to_grid(), the pgf pair offfrequency, andbase_mean.- Parameters:
target (
callable) – Per-claim target functionc(x), vectorized over the grid (e.g. a ceder frommake_ceder_netter()).conditioning (
callable, optional) – Per-claim conditioning functionn_fn(x);None(default) conditions on the gross aggregate itself (the identity), under whichkappaof the identity target issexactly (the Panjer self-check).
- Returns:
kappa (
ndarray) –E[S_target | S_conditioning = s]ats = self.xs;NaNwhere the conditioning density is negligible.conditioning_density (
ndarray) – The compound density of the conditioning subject onself.xs(use it to locate quantiles of the conditioning aggregate).
- Raises:
NotImplementedError – On a signed or windowed grid (
i0orx_minactive), or for a frequency family withoutFrequency.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 isP_N'in place of the leave-one-out transform. One shared inverse FFT plus one FFT for the target; the 2-Docc_bivariatejoint 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)–viewisocc/agg:occurrence:
Gross(the gross book – always present), then, whenocc_reins,layer.1…layer.k(one per layer) andCeded/Nettotals.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') == 1in the occurrence block, orGrosswhen there is no occurrence program.
4.4. Rows
MultiIndex (component, measure):meta:share(proportion covered),limit,attach,pr_attachandpr_detach– probabilities per modeled claim, read from the picks-adjusted bucketed subject severity (the grid):P(subject > attach)(exclusive) andP(subject >= attach + limit)(inclusive). One basis serves the whole frame: the samepr_attachconditions the layer severity and thins the layer frequency, sofreq mean = n * pr_attachandsev_cond = sev_uncond / pr_attachhold exactly.pr_detachisNaNfor 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), andoutput(0/1flag marking each stage’s output view – two 1s for an occ+agg program;Grosscarries the 1 when there is no occurrence program).Grosscarries the widest policy terms over the mixture components – maxlimit(infif any component is unlimited) over minattach(share1). The occurrenceCededtotal deliberately carries the placed capacitysum(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 (ex1duplicatesmeanfor easyfilter(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 sameP(subject > attach)(conditional given attach), which leaves the layer aggregate meann' * sevequal to the unconditionalE[N] * E[ceded]. Theaggrow is the column’s actual aggregate distribution (FFT of the unconditional layer ceded severity), so its higher moments andpr_lossare exact.The occurrence ``Ceded`` / ``Net`` columns are unconditional totals: the same claim count as
Grossand unconditional severities, soCededsev +Netsev ==Grosssev. Aggregate layer means sum to theCededaggregate 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_bucketsetter and onupdate. ReturnsNonewhen 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 –Grossfor occurrence,Subjectfor aggregate) against the model output of each view. One block per applicable stage; each block is aview x componenttable sharing the same eight columns asvalidation_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).
Changecarries 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 underlinear); 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 singleChangecolumn invalidation_df.Layout (per the gross/subject convention;
viewandcomponentlabels 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 (meanE[N]only, sofreq * sev == aggwithin a view; cv / skewNaN) – consistent withreins_stats_df(). The leadinggrossrow’sEstfrequency is leftNaNto mirrorvalidation_dfexactly. (Only the per-layerlayer.kcolumns ofreins_stats_df()are conditional;reins_summary_dfis always unconditional.)Index is
MultiIndex (stage, view, component). Derived fromreins_stats_df()/ the internal view-stats frame. ReturnsNonewhen 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
Aggregatedistribution 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_lrit sizes the expected loss (exp_el = exp_premium * exp_lr); alone, alongside aclaimsorlosssizing 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]);Noneorinfis 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
ssevDecL keyword. Orthogonal tovalue_type.dsevwith 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 claimsmeans n claims. False, the DecL!afterzm/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 unlessfreq_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 yearsexposure); requiresfreq_name='renewal'and await_*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): setsexp_premium = exp_years * exp_rate; never sizes the count. Stored for writer round-trip.wait_name – waiting-time distribution name; the
wait_*family mirrorssev_*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 –
dwaitoutcomes (assev_xs)wait_ps –
dwaitprobabilities; 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 setwait_attachment – wait layer attachment
a(DecLwait y xs a); asexp_attachmenton the wait law – conditional(W - a | W > a)by default,min((W - a)+, y)withwait_conditional=False(zero waits cluster). Cannot combine with a splice windowwait_limit – wait layer limit
y– caps the (layered) wait, an atom atyof 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 separatePnLveneer (seemake_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 familiessgamma/slognormmatch mean, cv and skew (normal as the symmetric limit, a reflected fit for left skew);lognormandgammamatch mean and cv only (the declared aggregate’s skew is not reproduced);normmatches mean and cv with zero skew. The fitted severity’s signedness mirrors the input’s: an unsigned (plainsev) input clamps any fitted sub-zero mass to an atom at 0, and the moment drift shows invalidation_df. The original program is preserved innote. Incompatible with occurrence reinsurance (which acts pre-convolution); aggregate reinsurance rides along unchanged. Set by theapproximateDecL keyword. The chosen kind is exposed on the instance as the attributeself.approximation(''when exact, else the kind) – a distinct name from theapproximate()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);Nonefalls back toname. Presentation only – repr / exhibit titles prefer it overname– never an identity / reference target. See dev/plan-decl-labels.md.occ_reins_label – optional per-occurrence-layer display labels (the reins-clause
asclause); a list parallel toocc_reins(entriesNonewhere unlabeled), orNone. Pooled intolabel_map['occ_reins']as a sparse{layer_index: label}dict (read viaa.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(seeocc_reins_label; pooled intolabel_map['agg_reins']).note – free-text note, from a
note{...}clausehints – raw
hints{...}build-settings string (key=value;form). Pure annotation here; the underwriter parses it on build (caller-suppliedbuild()kwargs win)._tweedie – private provenance, a
aggregate.tweedie.TweedieParametersorNone. Set only by the parser, when atweedieclause (rather than its compound-Poisson-gamma expansion) is what the author wrote; read only bydecl_writerandas_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_pgfconsumes.Equals
nfor every unmodified frequency, so ordinary code need not distinguish them. Underzm/ztthey part company:nis 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, notn.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 beforeupdate– renders asn/a. The row catalogue and value enumerations are documented indev/info-strings.rst. Shares the label/value convention (aggregate.constants.info_row()) withPortfolioandDistortion.
- property validation_score
how well the grid reproduces theory (float).
In units of the validation tolerance, so
score <= 1means the object passes at its ownvalidation_epsand1is exactly the pass boundary. Wherevalidsays whether a line was crossed, this says by how far, which is what makes it comparable across grids and the quantitysharpen()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. Seevalidation_score()for the other powers andvalidation_score_terms()for the per-term detail.- Type:
Continuous validation score
- property validation_description
Short one-line validation verdict (str).
The terse phrase the
inforow and the one-line intro carry:'not unreasonable','fails agg cv'. The verbose form isvalidation_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 ofvalidation_description. Validation is computed if needed.
- property one_claim
Whether the claim count is identically one.
- Returns:
bool–TrueiffN = 1with 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. Seeaggregate._aggregate_compute.freq_sev_convolution()(1-D) andaggregate.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: aFrequencyFixedwhose component claim counts sum to one. A limit profile splitting that single claim over several components still qualifies, becausesev_densityis then the corresponding severity mixture.dfreq [1]: aFrequencyEmpiricalcarrying all its mass on the outcome one.FrequencyRenewalis 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, sincevalidate_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_calcdescribes 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_calccontrols 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-upP(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 withPortfolio.prob_eq_0andprob_eq_0.- Returns:
floatorNone–Nonebeforeupdate()(there is no realised grid yet).
Notes
Reads the mass at the zero grid point through the canonical
GridDistribution(pmf(0)) – identical topmf()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 belowbs, or belowbs / 2under the round scheme), so the answer isP(N = 0)plus that sliver – the usual FFT-grid reading, and it tightens asbsshrinks.
- 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
PnLposition.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-groupsellledger 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 DecLxpnlface.
- Parameters:
consideration (
float,array-like, orcallable, optional) – The plain-mode consideration, signed (+received,-paid); a vector sums to one book amount; a callablef(x)is a loss-sensitive consideration applied bucket-wise. Mutually exclusive withgross/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 premiumasclause); defaults to'consideration'(plain) /'premium'(ledger).loss_label (
str, optional) – Name for the loss leg (the DecL engineasclause); defaults to'loss'.
- Returns:
PnL– Always aPnLvalue object; the ledger form carries its resolved cession economics aspnl.economics.
Notes
build('pnl NAME C premium less <body>')is sugar forbuild('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 to0for an ordinary non-negative aggregate (today’s behaviour) and toNone(automatic two-sided window) for a signed aggregate – so a P&L declared withssev/ negativedsevjust works frombuild.Noneforces the automatic window; a number forces that origin (snapped to a multiple ofbs; may be negative).x_max – upper edge of the output window; informational, the grid length is fixed by
log2. Currently unused whenx_minis 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 fromvalue_type. Seedev/plan-bucket-window-2.md§1A.sharpen – opt in to auto-sharpening.
False(default) leaves the grid exactly as the estimator chose it.Truerunssharpen()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 bybuild, because a probe costs eight extra updates.kwargs – passed through to update
- Returns:
Signedness is carried by the severity declaration (
ssev/negativedsev), 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_dfandvalidation_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 byx**3in 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_densityis deliberately left as the raw FFT output (consistent withftagg_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. Seedsev_bucket.debug – run reinsurance in debug model if True.
x_min –
Nonerequests an automatic two-sided output window (NYI in this stage – treated as the grid originxs[0]); otherwise informational (the grid origin is read fromxs[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(), theALIASINGtest), 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_epsvariable.All reads come from
stats_df– the single source of truth – notvalidation_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.isclosewith relative tolerance10*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()anddev/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
ValueErrornaming 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_workevaluates 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
Frequencydoes not know the expected claim count, so this is a method ofAggregate.
- 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 thestats_df['empirical']column) are written byupdate_workfrom 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 thekind='both', width=0text. 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_descriptionsays what the program declares; this adds what the cession does, read offreins_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 onreins_kindsfirst.
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_totalandexagcolumns todensity_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 oldcumsum(gS)·bsidiom assumed a zero-origin grid). The effectivegresolvesview× 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; seechoquet_weights().allow_deficit (
bool) – Explicit truncation policy for a materially defective pmf. Default False raisesDefectiveDistributionError.
- 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 = Trueif 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
ValueErrorotherwise); for a renewal (wait-clause) frequency usewiener_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:
RuinFunctionnamed 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) densitydfi. 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 isc = (1 + rho) E[X] / E[W]– the loaded long-run loss rate, sorhokeeps its PK margin-to-loss meaning.- Parameters:
rho (
float) – Margin-to-loss ratio: premium =(1 + rho)times the expected loss rateE[X] / E[W].kind (
{'index', 'interpolate'}) –find_ulookup convention, as inpollaczeck_khinchine():'index'snaps to the grid, anything else interpolates.log2 (
int, optional) – Half-grid exponent. The wrapped circle has2**(log2 + 1)buckets and psi is returned onu = arange(2**log2) * bs. Defaults toself.log2, so the u-grid coincides with the severity grid (and with the PK u-grid). Must be at leastself.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 likepollaczeck_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 conditionE[Y] < 0fails on the grid (margin too small).
Notes
Sparre-Andersen surplus
U(t) = u + ct - sum_{i <= N(t)} X_iwith iid waitsW. Ruin can occur only at claim instants, so the problem collapses to the random walk with per-claim stepY = X - cW:psi(u) = P(M > u)withMthe all-time maximum of the partial sums – the Lindley / GI-G-1 stationary waiting time, finite iffE[Y] < 0(net profit conditionc E[W] > E[X], i.e.rho > 0).Mis 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 probability1/(1+rho)) do not survive. Instead the ladder structure is extracted numerically from the Wiener-Hopf factorization of the walk by the cepstral method: seeaggregate._renewal.ruin_cepstral()for the algorithm (Spitzer identity, support separation in the cepstrum,z = 1regularization).Discretization: the severity rides its existing rounding pmf (
sev_density_df.p_sev, exactly as PK); the scaled waitcWis discretized by the same rounding scheme via_discretize_wait_pmf(). Rounding keeps means correct toO(bs^2)and makes the lattice walk aperiodic, so the symbol1 - phi_Yhas no unit-circle zeros besidesz = 1. The kernel’sz = 1patch uses the grid mean step (signed @ fy), which also absorbs the (negligible) wait mass dropped beyond the grid top. psi is valid forubelow2**log2 * bs;psiat 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 (largerlog2) 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 (
rhoorlr, exactly one) and a capital level (poru, exactly one), computes the exact probability of eventual ruin there (Poisson frequency viapollaczeck_khinchine(), renewal viawiener_hopf(); anything else raises), validates it with a capped simulation of the same discretized model, and returns theRuinResulttheruinexhibit dispatches on.- Parameters:
rho (
float, optional) – Margin-to-loss ratio; the premium rate isc = (1 + rho) E[X] / E[W]. Must be positive (net profit). Exactly one ofrhoorlr.lr (
float, optional) – The same margin stated as a loss ratio,lr = 1 / (1 + rho), in(0, 1). Exactly one ofrhoorlr.p (
float, optional) – Probability of eventual default; resolved to an initial surplus through the ruin function’s capital lookupfind_u(p)on the grid. Exactly one ofporu.u (
float, optional) – Initial surplus directly. Exactly one ofporu.log2 (
int, optional) – Renewal path only: forwarded towiener_hopf()for heavy tails.n_sims (
int, default1000) – Simulated paths for the reasonableness check. The small default follows the teaching-aid sizing ruling (dev/plan-pk-tab.mdruling 4); raise it for a tighter standard error.seed (
intorNone, defaultfixed) – rng seed. The fixed default keeps served documents hash-stable;Nonedraws a fresh seed and reports it on the result.
- Returns:
RuinResult– Carryingruin_df(the one-column stats strip), the resolveduand exactpsibeside 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
Rand the classical boundexp(-R u)when the adjustment equationM_X(R) = 1 + (1 + rho) E[X] Rhas 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
ruinexhibit 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 theq(0.001)toq(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. Withinvertit isS(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 wasquantile_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 onself.figure.
Notes
Draws the chart document
charts.chart_aggemits, 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.NAMEreference requires the referenced declaration to carry explicitlog2andbshints, 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, thenbuild(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 overlog2,bsandnormalize, which come from the object’s current state.- Returns:
str– One line of DecL, ready to hand back tobuild.- 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
paddingstays 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.NAMEreference, and the mirror ofaggregate.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, defaultnp.inf) – Layer width applied to the resulting severity.attachment (
float, default0) – Layer attachment applied to the resulting severity.conditional (
bool, defaultFalse) – Whether layered moments divide outP(X > attachment).
- Returns:
SeverityMeta– A discrete severity whose atoms areagg_densityonxs. 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
selfis touched. Seeaggregate.distributions.SeverityMetafor 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-classAggregate(via thedsev [1]point-mass trick) so every inherited method works on the count.- Returns:
Aggregate– A built aggregate namedf'{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_dfetc. 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
buildfront door, so grid windowing,bsandlog2selection 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 < 2is a compound Poisson distribution with gamma severity, so any aggregate of that shape has reproductive parameters whether or not it was declared with thetweediekeyword. This reports them.- Returns:
TweedieParametersorNone–(p, mean, dispersion), withvariance = dispersion * mean ** p.Nonewhen 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_tweedieprovenance key the parser records. Otherwise the triple is derived from the engine byaggregate.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 poissonis the same distribution astweedie 1.005 1 0.1and 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. Seedev/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
approximatefit (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 issummary_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.Erris 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, whereGrossis the theoretical before any cover and<label>is the model output –Net(all covers net of),Ceded(all ceded to), orOutput(mixed, occ and agg passing different kinds).Change = (output - gross) / gross– arithmetically the same column asErr(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 theREINS_LABEL_*constants.
Sources from the canonical
self.stats_df:mixedfor Gross,empiricalfor 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 heavyAggskew you can see is inherited fromSev). The validation moment-error table is nowvalidation_df; the tail-behavior classifier istail_behavior_df.Index
Freq/Sev/Agg(theXindex).Columns
Mean | SD | CV | Skew | P01 | Median | P99.SDandCVare both always present (stable layout).CV = SD / Meanis blank when|Mean|is ~0 relative toSD(a signed / near-break-even position – see_cv_or_nan());SDnever blanks.Skewis well defined even at mean 0, so it stays.Moments are the computed (realised FFT-grid) moments for the
SevandAggrows –Meanis theest_*estimate, the same value validation audits against, not the analytic moment. TheFreqrow stays PGF-exact (the engine never materializes a count distribution to estimate from, so analytic is the realised value). Beforeupdate()there is no grid, so the whole frame falls back to the analytic (theoretical) moments fromstats_df.Percentiles come from the FFT grid (exact, not simulated):
Aggviaq(),Sevviaq_sev()(mixtures included, already on the grid). They populate only afterupdate().
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 carriesE[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, usecreate_frequency(), then.q(...)/.tvar(...)on it.- Returns:
pandas.DataFrame– Three-row Freq / Sev / Agg frame,Meancarried 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 standardDEFAULT_RETURN_PERIODSladder. Pass your own ladder withtail_periods_df(periods=...).- Returns:
pandas.DataFrameorNone–Nonebeforeupdate().
- 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 from0.001to0.999on the default ladder (DEFAULT_RETURN_PERIODS; passperiods=to override): every rung contributes both its lower-tail probability1 / Tand its upper-tail probability1 - 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.Tthe return period as the row reads:1 / Pbelow the median,1 / (1 - P)above it.VaR = q(P): the quoted number.TVaR = tvar(P): the priced number, adjacent toVaRso 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_likeoffloat, optional) – Return-period ladder. Defaults toDEFAULT_RETURN_PERIODS.- Returns:
pandas.DataFrameorNone– Indexed by non-exceedance probabilityP;E[X]carried in.attrs.Nonebeforeupdate()(the realised grid is not yet built).
Notes
The ladder is symmetric in
Pand 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 (seedev/plan-pnl-*).
- property approximation_df
All five method-of-moments fits against the exact law (a property).
Columns
exact(the anchor, the realized grid) thenAPPROXIMATION_FAMILIES; rows themeta/stats/quantiles/rel errblocks ofapproximation_frame(), the last two on thetail_dfladder. On demand, no options, always all five families;rel errreads each family quantile as a relative error againstexact.- Returns:
pandas.DataFrameorNone–Nonebeforeupdate().
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 theexactcolumn, which is exactly what the frame teaches; the two-parameterlognorm/gammadiffer inskewby construction,normtargets zero.ksis the Kolmogorov distancesup |F - G|on the grid at the bucket conventionF(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 whereverdensity_dfis. 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
approximationchart: index the output grid, columnexactthe realized density, then one column per family holdingpdf(x) * bsof the emitted law (reflect and mirror-clamp applied), so the columns overlay the discrete density directly. Seeapproximation_density_frame().- Returns:
pandas.DataFrameorNone–Nonebeforeupdate().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 Nonesize 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_dfholding1 - pof the mass.A thin, no-recompute re-slicer over the finished aggregate: it runs
balanced_window()on the realizedp_totaland returns the rows ofdensity_dfin the equal-tail window[q(p/2), q(1 - p/2)]–p/2of the mass trimmed off each tail,1 - pkept 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, default1e-6) – Total discarded tail mass, split equally between the two tails. Must satisfy0 < p < 1. The literal discarded mass, not a coverage – seebalanced_window().- Returns:
pandas.DataFrame– The slicedensity_df.loc[lo:hi](a view onto the existing frame; no recomputation). All columns are preserved.
Notes
Edges are snapped to the bucket size
bsso the window aligns with the grid. Does not mutate the aggregate –density_dfis 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 fromp_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
Severityobjects evaluated from the input distributions, not from thebs-grid PMF insev_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.
ppfis the quantile function indexed by non-exceedance probability,isfby exceedance probability. Added in 1.0.0a180.
See also
Aggregate.q_sevthe same quantile snapped to the
bsgrid.
Notes
Exact versus grid.
q_sev()reads the discretized severity PMF through aGridDistribution, so it can only ever return a lattice point.sev.ppfcalls the underlying continuous distribution, so it returns the true quantile. Onagg T1 2 claims sev lognorm 1000 cv 1.31 poissonatbs = 8,sev.ppf(0.99) = 6207.853againstq_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. Seeoep().Single component. The five functions are the component
Severitymethods directly.Severitysubclassesscipy.stats.rv_continuousand routesppf/isfthrough 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 callsscipy.optimize.brentq(). All five close over the samewtsvector, so the inverses invert exactly the mixture the forward functions evaluate.Weights.
sev_wtcarries the mixture weights directly. Under broadcasting (exposure crossed with severity) the per-componentsev_wtare 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 (
floatorarray_like) – Loss size. May lie outside the computed grid; see Notes.kind (
str, default'previous') – Interpolation passed toscipy.interpolate.interp1d(). The default steps, which is right for a discrete law on a lattice. (2022-10 change:kindintroduced; the default waslinear.)
- Returns:
floatorndarray
Notes
Outside the grid the answer is only given where it is known. Below the grid’s lower edge this returns
0when the law provably has no mass there, which is the ordinary case of a non-negative aggregate on a grid starting at the origin, andnanotherwise. “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. Reporting0there would assert an absence that is not known, sonanis reported instead. The rule lives inaggregate.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
1for 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 as1 - cdf(x).- Parameters:
x (
floatorarray_like) – Loss size.- Returns:
floatorndarray
Notes
Inherits
cdf()’s outside-the-grid behavior exactly, sosfisnanwherevercdfis: below a window, and below a signed law’s grid. Seecdf().
- 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 (?)
- 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 ... fixedagg (or any other string) - Aggregate object built from the agg_decl program viabuild, so it is parser-born: nonemptyprogram, 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 aUserWarning(passapprox_type='norm'to select the normal explicitly). A left-skewed distribution is fitted by reflection and renders in DecL through the ordinary reflection syntaxloc - scale * name shape; onlyoutput='scipy'raisesValueErrorfor it (scipy has no frozen reflected rv).The emitted severity keyword mirrors the input. The DecL and object outputs spell the severity
ssevwhen this aggregate is signed (_signed()), else plainsev;output='sev_kwargs'mirrors the same rule throughsev_signed. A fit whose law reaches below zero emitted under plainsevclamps 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’svalidation_dfreports.
- 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_gcan 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_gis { 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)) ora(an asset level, snapped to the grid), then supply exactly one pricing target – premiumP, cost of capitalROE(a.k.a. CoC), or a loss ratio viaLR(equivalentlyM,QorPQ). Returns the canonical one-row ('total') pentagonDataFrame(columnsPENTAGON_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 aDistortion). The triple{L, a, target}is solved byPentagon.solve().- Parameters:
p (
float, optional) – VaR probability fixing the capital level; mutually exclusive witha.a (
float, optional) – Asset level fixing the capital; mutually exclusive withp.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 ofreins_views. The defaultNoneis the object’s own. Both the asset level and the expected loss resolve on the chosen view, matchingcalibrate_distortions().
- Returns:
pandas.DataFrame– One'total'row, eight canonical pentagon columns.- Raises:
ValueError – If not exactly one of
p/ais given, or not exactly one pricing target is supplied.
- price_ccoc(ccoc, *, p)[source]
Price at a constant cost of capital
ccocand VaR levelp.No distortion is involved – a thin alias for
self.price_pentagon(p=p, ROE=ccoc)returning the canonical one-row ('total') pentagonDataFrame. Parity withPortfolio.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 lossL = E[min(X, a)], or the asset levela– any one determines the other two. Thin delegator toprob_loss_assets()over this aggregate’sp_totalgrid (the singlelevsource).- 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 lossL) and accepting any soluble pentagon configuration, warning when an accounting-determinedLdoes not reconcile withE[min(X, a)]. Thin delegator to the shared single-distribution pricing concern; seeaggregate._pricing.price_pentagon_ex()for the full contract.- Returns:
pandas.DataFrame– One'total'row: a leadingpcolumn 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
Aggregatecounterpart ofPortfolio.calibrate_distortions()– calibration to this distribution (the aggregate is its own total), with no per-unit allocation (that stays aPortfolioconcern). Calibrating directly here means no more wrapping a singleAggregatein a one-unitPortfolioto obtain a calibrated distortion set.- Parameters:
coc (
float, optional) – Target cost of capitalCOC = (P - L) / Q. Exactly one ofcocorlr.lr (
float, optional) – Target loss ratioLR = L / P, converted to a cost of capital through the pentagon at the resolved anchor. Exactly one ofcocorlr.p (
float, optional) – Probability at which the calibration applies; converted to an asset level viaself.q(p, kind). Exactly one ofpora.a (
float, optional) – Asset level; snapped to the grid. Exactly one ofpora.kind (
{'lower', 'upper'}, optional) – VaR kind whenpis provided. Default'lower'.reins_view (
str, optional) – Which of a cession’s distributions to calibrate on, one ofreins_views. The defaultNoneis this aggregate’s own density, which under a cession is whichever view the program asked for: anet ofprogram holds its net, aceded toprogram holds its ceded.
- Returns:
CalibrationResult– Carryingdistortion_df(one row per distortion in[ccoc, ph, wang, dual, tvar]), the shared one-rowcalibration_dftarget, thedistortionsthemselves, and the inputs they were fitted at. The same three are stored onself.distortion_df/self.calibration_df/self.distortionsas before, so nothing that read them moves; both frames carryattrs['reins_view'], recording which distribution they were fitted to. Same schema asPortfolio.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_exaconvention), matching a one-unitPortfolio’sexa_total.namesselects the distortion families to calibrate (default the standard set); signed / payoff supports are handled transparently (seeaggregate._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_pis the family-agnostic acceptability index of Cherny and Madan.- Parameters:
P (
float, optional) – The premium held against this aggregate. Defaults toexp_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 ofpora.a (
float, optional) – Asset level, snapped to the grid. At most one ofpora; with neither, the position is measured against its whole distribution, which is the unlimited reading.names (
sequenceofstr, optional) – Distortion families. Defaults toEVAL_FAMILIES(ph/wang/dual/tvar) unanchored, and toEVAL_FAMILIES_ANCHORED, which addsccoc, when an anchor is given.reins_view (
str, optional) – Which of a cession’s distributions to evaluate, one ofreins_views. The defaultNoneis this aggregate’s own.Pis 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– Carryingevaluation_df, the panel: tidy (long) form,MultiIndexrows(Step, distortion)withStepthis aggregate’s name, and columnsrole/param_name/param/gini_p/error/status.roleis always'sell'here: an aggregate is an obligation written, so the position is held the way it is booked. The same shapePnL.evaluate()andPortfolio.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 reportNaN.
Notes
Anchoring is what makes evaluation and calibration comparable.
calibrate_distortions()solvesrho_g(min(X, a)) = Pat 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 beyondais worth.See also
aggregate._pricing.evaluate_marginthe solve and its math.
calibrate_distortionsthe 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 meanmand cvcv.Notes
For
ss.lognorm(sigma, scale=exp(mu))matching meanmand CVcv, \(\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.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.approximateandPortfolio.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’ssev_*kwargs into the requested surface, so the twoapproximatesurfaces and theapproximateDecL 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_stris the program prefix up to but excluding the severity keyword ('agg NM 1 claim '); the adapter choosessevorssevfromsigned_input.agg_str (
str) – Naming / note scaffolding for the DecL and Aggregate output forms.agg_stris the program prefix up to but excluding the severity keyword ('agg NM 1 claim '); the adapter choosessevorssevfromsigned_input.note (
str) – Naming / note scaffolding for the DecL and Aggregate output forms.agg_stris the program prefix up to but excluding the severity keyword ('agg NM 1 claim '); the adapter choosessevorssevfromsigned_input.approx_type (
{'norm', 'lognorm', 'gamma', 'sgamma', 'slognorm'}) – The fitted family.'all'is handled by the calling method, not here.output (
str) –'scipy'-> frozenscipy.statsrv;'sev_kwargs'-> theSeveritykwargs dict;'sev_decl'-> a DecL severity fragment;'agg_decl'-> a fullagg ... fixedDecL program;'agg'(or any other string) -> a fixed-frequencyAggregatebuilt from theagg_declprogram viabuild, so it is parser-born: it carriesprogram, decompiles, and lands in the underwriter’s knowledge under the fit name.warn_degenerate (
bool) – WhenTrue(the interactive.approximate()method) emit aUserWarningif 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:ssevwhenTrue, plainsevwhenFalse.output='sev_kwargs'mirrors the same rule through thesev_signedflag.
- Returns:
object– Peroutput.
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
sevclamps its sub-zero tail to an atom at 0, at the cost of a small moment drift thatvalidation_dfof the built object reports like any other discretization error. Anssevinput keeps the fitted law exact. The fit core keeps returning its ownsev_signed/sev_reflectflags; 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 exceptoutput='scipy': scipy has no frozen reflected rv, so that one still raisesValueError; use the'agg_decl'or object outputs, orapprox_type='norm'.