pub struct PredictiveDistribution { /* private fields */ }Expand description
A joint distribution over components (origin years, lines, layers), stored as simulated draws.
Draws are simulation-major: row i holds every component’s value in
simulation i, so the components keep their dependence. The total’s
99.5% quantile is computed from row sums, not by adding the components’
99.5% quantiles. See docs/design/predictive-distribution.md.
The Distribution and Empirical methods describe the total
over all components. Use marginal for one
component, or aggregate to sum over dimensions.
§Example
use prospicio_prob::{Distribution, Empirical, Lognormal, PredictiveDistribution, Provenance};
let sev = Lognormal::from_mean_cv(100.0, 0.5).unwrap();
let pd = PredictiveDistribution::simulate(
vec!["origin".into()],
vec![vec![2023.into()], vec![2024.into()]],
10_000,
42,
Provenance::new("example"),
|rng, row| {
for cell in row {
*cell = sev.quantile(rng.next_open01()).unwrap();
}
},
)
.unwrap();
assert!((pd.mean() - 200.0).abs() < 2.0);
let origin_2024 = pd.marginal(&vec![2024.into()]).unwrap();
assert!(pd.tvar(0.995).unwrap() > origin_2024.tvar(0.995).unwrap());Implementations§
Source§impl PredictiveDistribution
impl PredictiveDistribution
Sourcepub fn capital(
&self,
d: &Distortion,
method: AllocationMethod,
) -> Result<Allocation>
pub fn capital( &self, d: &Distortion, method: AllocationMethod, ) -> Result<Allocation>
The risk measure ρ = d of the total, each component’s stand-alone
measure, and the total’s allocation to the components by method.
Components must add up to the portfolio being allocated (segments, not a tower result holding gross, ceded and net side by side).
use prospicio_prob::capital::AllocationMethod;
use prospicio_prob::{Distortion, KeyValue, PredictiveDistribution, Provenance};
// Two lines over four simulations; the totals are 3, 5, 7, 9.
let pd = PredictiveDistribution::from_draws(
vec!["lob".into()],
vec![vec![KeyValue::from("motor")], vec![KeyValue::from("property")]],
vec![1.0, 2.0, 4.0, 1.0, 2.0, 5.0, 3.0, 6.0],
Provenance::new("example"),
)
.unwrap();
let tvar = Distortion::tvar(0.5).unwrap();
let a = pd.capital(&tvar, AllocationMethod::Euler).unwrap();
assert_eq!(a.total, 8.0);
assert_eq!(a.standalone, [3.5, 5.5]);
assert_eq!(a.allocated, [2.5, 5.5]);
assert_eq!(a.diversification_benefit(), 1.0);Fails for AllocationMethod::Covariance when the total does not
vary, for AllocationMethod::Proportional when the stand-alone
measures sum to 0, and for AllocationMethod::Shapley with more
than SHAPLEY_MAX_COMPONENTS components.
Source§impl PredictiveDistribution
impl PredictiveDistribution
Sourcepub fn join(
parts: &[(&str, &PredictiveDistribution)],
dim: &str,
pairing: Pairing,
) -> Result<Self>
pub fn join( parts: &[(&str, &PredictiveDistribution)], dim: &str, pairing: Pairing, ) -> Result<Self>
Joins parts (label, distribution) into one distribution with a new
leading dimension dim holding each part’s label, followed by the
union of the parts’ dimensions in order of first appearance. A part
without one of those dimensions has the empty text "" there.
Simulation i of the result is simulation i of every part. With
Pairing::Independent the parts are independent only if they were
simulated independently, so two parts with the same seed and stream
scheme (which would draw the same random numbers in each
simulation) are refused; set the dependence you want with
reorder_groups. With
Pairing::SameSimulations the parts come from the same scenarios
and keep their pairing.
use prospicio_prob::portfolio::Pairing;
use prospicio_prob::{Empirical, KeyValue, PredictiveDistribution, Provenance};
let reserve = PredictiveDistribution::from_draws(
vec!["origin".into()],
vec![vec![KeyValue::Int(2023)], vec![KeyValue::Int(2024)]],
vec![10.0, 20.0, 12.0, 25.0],
Provenance::new("odp_bootstrap").seed(1, "chacha20/sim-index/v1"),
)
.unwrap();
let premium = PredictiveDistribution::from_draws(
vec!["lob".into()],
vec![vec![KeyValue::from("motor")]],
vec![50.0, 40.0],
Provenance::new("collective").seed(2, "chacha20/sim-index/v1"),
)
.unwrap();
let parts = [("reserve", &reserve), ("premium", &premium)];
let p = PredictiveDistribution::join(&parts, "risk", Pairing::Independent).unwrap();
assert_eq!(p.dims(), ["risk", "origin", "lob"]);
assert_eq!(p.total().draws(), [80.0, 77.0]);
let by_risk = p.aggregate(&["risk"]).unwrap();
assert_eq!(by_risk.draw_matrix(), [30.0, 50.0, 37.0, 40.0]);Sourcepub fn reorder_groups(
&self,
dim: &str,
correlation: &[f64],
seed: u64,
) -> Result<Self>
pub fn reorder_groups( &self, dim: &str, correlation: &[f64], seed: u64, ) -> Result<Self>
Sets the dependence between the groups of dimension dim (the
segments of join, lines, layers) by Iman–Conover on
the groups’ totals: each group’s simulations are reordered as whole
rows, so every group keeps its own distribution and its internal
joint structure, and the groups’ totals take a rank correlation
close to correlation (G × G, groups in order of first
appearance; Spearman’s rho near (6/π) asin(r/2)). Group g > 0
shuffles with stream g of seed.
use prospicio_prob::{KeyValue, PredictiveDistribution, Provenance};
let n = 2000;
let pd = PredictiveDistribution::from_draws(
vec!["seg".into(), "item".into()],
vec![
vec![KeyValue::from("a"), KeyValue::Int(0)],
vec![KeyValue::from("a"), KeyValue::Int(1)],
vec![KeyValue::from("b"), KeyValue::Int(0)],
],
(0..n).flat_map(|i| [i as f64, 2.0 * i as f64, ((i * 7919) % n) as f64]).collect(),
Provenance::new("example"),
)
.unwrap();
let r = pd.reorder_groups("seg", &[1.0, 0.0, 0.0, 1.0], 5).unwrap();
// Segment a's rows move together: item 1 is still twice item 0.
assert!((0..n).all(|i| r.row(i).unwrap()[1] == 2.0 * r.row(i).unwrap()[0]));Source§impl PredictiveDistribution
impl PredictiveDistribution
Sourcepub fn from_draws(
dims: Vec<String>,
components: Vec<ComponentKey>,
draws: Vec<f64>,
provenance: Provenance,
) -> Result<Self>
pub fn from_draws( dims: Vec<String>, components: Vec<ComponentKey>, draws: Vec<f64>, provenance: Provenance, ) -> Result<Self>
A distribution from draws already laid out simulation-major:
draws[i * n_components + j] is component j in simulation i.
Fails if dims repeats a name, a key does not have one value per
dimension, keys repeat, there are no components, draws is empty
or not a whole number of rows, or a draw is not finite.
Sourcepub fn simulate<F>(
dims: Vec<String>,
components: Vec<ComponentKey>,
n_sims: usize,
seed: u64,
provenance: Provenance,
simulate: F,
) -> Result<Self>
pub fn simulate<F>( dims: Vec<String>, components: Vec<ComponentKey>, n_sims: usize, seed: u64, provenance: Provenance, simulate: F, ) -> Result<Self>
Runs n_sims simulations in parallel and collects them.
Simulation i calls simulate(rng, row) with
rng = StreamRng::new(seed, i) and fills row, one value per
component. Each simulation owns its stream, so the result is
identical for any number of threads and any row can be replayed
alone. The seed and SIM_INDEX_SCHEME are recorded in the
provenance.
Sourcepub fn simulate_with<F>(
parallel: bool,
dims: Vec<String>,
components: Vec<ComponentKey>,
n_sims: usize,
seed: u64,
provenance: Provenance,
simulate: F,
) -> Result<Self>
pub fn simulate_with<F>( parallel: bool, dims: Vec<String>, components: Vec<ComponentKey>, n_sims: usize, seed: u64, provenance: Provenance, simulate: F, ) -> Result<Self>
PredictiveDistribution::simulate, run in parallel only when
parallel is true; otherwise every simulation runs in order on the
calling thread (for a closure that calls a crate::Custom whose
callbacks must stay there). The draws are the same either way.
Sourcepub fn components(&self) -> &[ComponentKey] ⓘ
pub fn components(&self) -> &[ComponentKey] ⓘ
Component keys, in column order.
Sourcepub fn n_components(&self) -> usize
pub fn n_components(&self) -> usize
Number of components (columns).
Sourcepub fn provenance(&self) -> &Provenance
pub fn provenance(&self) -> &Provenance
Where this result came from.
Sourcepub fn row(&self, sim: usize) -> Option<&[f64]>
pub fn row(&self, sim: usize) -> Option<&[f64]>
Every component’s value in simulation sim, or None past the end.
Sourcepub fn draw_matrix(&self) -> &[f64]
pub fn draw_matrix(&self) -> &[f64]
All draws, simulation-major: draw_matrix()[i * n_components() + j]
is component j in simulation i. (The Empirical::draws of a
PredictiveDistribution are the per-simulation totals instead.)
Sourcepub fn component_index(&self, key: &ComponentKey) -> Option<usize>
pub fn component_index(&self, key: &ComponentKey) -> Option<usize>
Column of the component with this key.
A key that matches no component exactly may still name a
Period by its label, as text or an integer ("2021" or 2021
for a year, "2021Q3" for a quarter): bindings and files carry
periods as labels. The label match is used only when it names one
component.
use prospicio_core::{Grain, Month, Period};
use prospicio_prob::{KeyValue, PredictiveDistribution, Provenance};
let origin = |y| KeyValue::Period(Period::containing(Month::january(y), Grain::Year));
let pd = PredictiveDistribution::from_draws(
vec!["origin".into()],
vec![vec![origin(2020)], vec![origin(2021)]],
vec![1.0, 2.0],
Provenance::new("example"),
)
.unwrap();
assert_eq!(pd.component_index(&vec![KeyValue::from(2021)]), Some(1));
assert_eq!(pd.component_index(&vec![KeyValue::from("2020")]), Some(0));
assert_eq!(pd.component_index(&vec![KeyValue::from("2022")]), None);Sourcepub fn marginal(&self, key: &ComponentKey) -> Option<Sampled>
pub fn marginal(&self, key: &ComponentKey) -> Option<Sampled>
One component’s draws, in simulation order, or None if no
component has this key.
Sourcepub fn aggregate(&self, keep: &[&str]) -> Result<Self>
pub fn aggregate(&self, keep: &[&str]) -> Result<Self>
Sums the components within each simulation over every dimension not
in keep, keeping the joint structure.
aggregate(&["lob"]) gives one component per line of business, summed
over origins. aggregate(&[]) gives a single component, the total.
Groups appear in the order their first component appears.
Sourcepub fn resample(&self, rng: &mut StreamRng, n: usize) -> Result<Self>
pub fn resample(&self, rng: &mut StreamRng, n: usize) -> Result<Self>
n whole rows drawn with replacement from stream rng, so the
components keep their dependence.
Sourcepub fn allocate(&self, d: &Distortion) -> Vec<f64>
pub fn allocate(&self, d: &Distortion) -> Vec<f64>
Allocates the distortion risk measure of the total
to the components by co-measure (Euler allocation): one
contribution per component, in components
order, summing to d applied to the total.
Simulations are ranked by their total, take the distortion’s
weights by rank (Distortion::weights), and each component’s
contribution is the weighted sum of its own draws. For
Distortion::Tvar(p) the contributions are the CoTVaRs,
E[X_j | total in its top 1 - p]. Simulations with equal totals
share their weights equally, so the result does not depend on how
ties are ordered.
Components must add up to the portfolio being allocated: allocate a set of segments, not a tower result that holds gross, ceded and net side by side.
use prospicio_prob::{Distortion, PredictiveDistribution, Provenance, KeyValue};
// Two lines over four simulations; the totals are 3, 5, 7, 9.
let pd = PredictiveDistribution::from_draws(
vec!["lob".into()],
vec![vec![KeyValue::from("motor")], vec![KeyValue::from("property")]],
vec![1.0, 2.0, 4.0, 1.0, 2.0, 5.0, 3.0, 6.0],
Provenance::new("example"),
)
.unwrap();
// TVaR at 50%: the two worst years, 7 = 2 + 5 and 9 = 3 + 6.
let co = pd.allocate(&Distortion::tvar(0.5).unwrap());
assert_eq!(co, [2.5, 5.5]);Source§impl PredictiveDistribution
impl PredictiveDistribution
Sourcepub fn marginal_expected_shortfall(&self, p: f64) -> Result<Vec<f64>>
pub fn marginal_expected_shortfall(&self, p: f64) -> Result<Vec<f64>>
Marginal expected shortfall of each component at level p:
E[X_j | total ≥ VaR_p(total)] (Acharya et al., 2017), the
components’ expected losses in the portfolio’s worst 1 - p. It is
the CoTVaR, the Euler allocation of the total’s TVaR
(allocate with Distortion::Tvar), and sums
to it.
Sourcepub fn covar(&self, component: &ComponentKey, p: f64, q: f64) -> Result<f64>
pub fn covar(&self, component: &ComponentKey, p: f64, q: f64) -> Result<f64>
CoVaR of a component (Adrian and Brunnermeier, 2016, in the form of
Girardi and Ergün, 2013): the total’s VaR at level q over the
simulations where the component is in distress, at or above its own
VaR at level p. Compare it with the total’s unconditional VaR at
q to see how much one segment’s bad years drag the portfolio.
use prospicio_prob::{KeyValue, PredictiveDistribution, Provenance};
// Two components over four simulations.
let pd = PredictiveDistribution::from_draws(
vec!["lob".into()],
vec![vec![KeyValue::from("a")], vec![KeyValue::from("b")]],
vec![1.0, 0.0, 2.0, 1.0, 3.0, 5.0, 4.0, 1.0],
Provenance::new("example"),
)
.unwrap();
// a is at or above its 75% VaR (3) in the last two simulations,
// whose totals are 8 and 5; their median (q = 0.5) is 5.
assert_eq!(pd.covar(&vec![KeyValue::from("a")], 0.75, 0.5).unwrap(), 5.0);Sourcepub fn esscher_allocation(&self, h: f64) -> Result<Vec<f64>>
pub fn esscher_allocation(&self, h: f64) -> Result<Vec<f64>>
Esscher allocation: E[X_j e^(h·total)] / E[e^(h·total)] per
component, the components’ means under the Esscher transform of the
total. They sum to the total’s Esscher premium
(risk::esscher), and with h = 0 they are
the means.
Source§impl PredictiveDistribution
impl PredictiveDistribution
Sourcepub fn blend(
models: &[&PredictiveDistribution],
weights: &[f64],
seed: u64,
) -> Result<Self>
pub fn blend( models: &[&PredictiveDistribution], weights: &[f64], seed: u64, ) -> Result<Self>
Blends several models’ predictive distributions with weights
(from stacking or pseudo-BMA, say): simulation i is simulation i
of model k, with k drawn with probability weights[k] from
stream i of seed. Each row stays a joint draw of one model, so
sums across components remain coherent.
Every distribution must have the same dimensions, components (in the same order) and number of simulations. Weights are normalized; they must be non-negative and not all zero.
use prospicio_prob::{Empirical, KeyValue, PredictiveDistribution, Provenance};
let one = |v: f64| {
PredictiveDistribution::from_draws(
vec!["lob".into()],
vec![vec![KeyValue::from("a")]],
vec![v; 1000],
Provenance::new("example"),
)
.unwrap()
};
let blend = PredictiveDistribution::blend(&[&one(0.0), &one(1.0)], &[0.25, 0.75], 7).unwrap();
let share = blend.total().draws().iter().sum::<f64>() / 1000.0;
assert!((share - 0.75).abs() < 0.05);Source§impl PredictiveDistribution
impl PredictiveDistribution
Sourcepub fn blend_by_component(
models: &[&PredictiveDistribution],
weights: &[Vec<f64>],
seed: u64,
) -> Result<Self>
pub fn blend_by_component( models: &[&PredictiveDistribution], weights: &[Vec<f64>], seed: u64, ) -> Result<Self>
Blends models with weights that differ by component, as
hierarchical stacking gives them (weights[j] is component j’s
weight vector, one entry per model). In simulation i every
component draws its model from the same uniform (stream i of
seed) against its own cumulative weights, so components with the
same weights take the same model and dependence across components
is kept as far as the weights allow. With equal weights everywhere
it is blend.
Trait Implementations§
Source§impl Clone for PredictiveDistribution
impl Clone for PredictiveDistribution
Source§fn clone(&self) -> PredictiveDistribution
fn clone(&self) -> PredictiveDistribution
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for PredictiveDistribution
impl Debug for PredictiveDistribution
Source§impl Distribution for PredictiveDistribution
impl Distribution for PredictiveDistribution
Source§fn sample(&self, rng: &mut StreamRng, n: usize) -> Vec<f64>
fn sample(&self, rng: &mut StreamRng, n: usize) -> Vec<f64>
n draws from stream rng, by inverse transform unless the family
overrides it (crate::Gamma draws by Marsaglia and Tsang). Read moreSource§fn survival(&self, x: f64) -> f64
fn survival(&self, x: f64) -> f64
P(X > x). Representations with a direct form override the
default 1 - cdf(x), which loses all precision far in the tail.Source§fn is_parallel_safe(&self) -> bool
fn is_parallel_safe(&self) -> bool
crate::Custom
whose callbacks must stay on the calling thread (an R function), so
the parallel simulations run single-threaded when they meet one.Source§impl Empirical for PredictiveDistribution
impl Empirical for PredictiveDistribution
Source§fn draws(&self) -> &[f64]
fn draws(&self) -> &[f64]
i came from
simulation i, so two distributions from the same simulations can
be paired draw by draw.Source§fn distortion(&self, d: &Distortion) -> f64
fn distortion(&self, d: &Distortion) -> f64
Distortion::apply_sorted. Read moreAuto Trait Implementations§
impl !Freeze for PredictiveDistribution
impl RefUnwindSafe for PredictiveDistribution
impl Send for PredictiveDistribution
impl Sync for PredictiveDistribution
impl Unpin for PredictiveDistribution
impl UnsafeUnpin for PredictiveDistribution
impl UnwindSafe for PredictiveDistribution
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more