Skip to main content

PredictiveDistribution

Struct PredictiveDistribution 

Source
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

Source

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

Source

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]);
Source

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

Source

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.

Source

pub fn simulate<F>( dims: Vec<String>, components: Vec<ComponentKey>, n_sims: usize, seed: u64, provenance: Provenance, simulate: F, ) -> Result<Self>
where F: Fn(&mut StreamRng, &mut [f64]) + Sync,

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.

Source

pub fn simulate_with<F>( parallel: bool, dims: Vec<String>, components: Vec<ComponentKey>, n_sims: usize, seed: u64, provenance: Provenance, simulate: F, ) -> Result<Self>
where F: Fn(&mut StreamRng, &mut [f64]) + Sync,

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.

Source

pub fn dims(&self) -> &[String]

Dimension names, e.g. ["lob", "origin"].

Source

pub fn components(&self) -> &[ComponentKey] ⓘ

Component keys, in column order.

Source

pub fn n_sims(&self) -> usize

Number of simulations (rows).

Source

pub fn n_components(&self) -> usize

Number of components (columns).

Source

pub fn provenance(&self) -> &Provenance

Where this result came from.

Source

pub fn row(&self, sim: usize) -> Option<&[f64]>

Every component’s value in simulation sim, or None past the end.

Source

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.)

Source

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);
Source

pub fn marginal(&self, key: &ComponentKey) -> Option<Sampled>

One component’s draws, in simulation order, or None if no component has this key.

Source

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.

Source

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.

Source

pub fn total(&self) -> &Sampled

The total over all components, one value per simulation.

Source

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

Source

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.

Source

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);
Source

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

Source

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

Source

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

Source§

fn clone(&self) -> PredictiveDistribution

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for PredictiveDistribution

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Distribution for PredictiveDistribution

Source§

fn mean(&self) -> f64

Expected value.
Source§

fn variance(&self) -> f64

Variance.
Source§

fn cdf(&self, x: f64) -> f64

P(X <= x).
Source§

fn quantile(&self, p: f64) -> Result<f64>

Smallest x with cdf(x) >= p. Read more
Source§

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 more
Source§

fn std_dev(&self) -> f64

Standard deviation.
Source§

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

Whether the distribution may be evaluated on several threads at once. True for every native family; false for a 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

Source§

fn draws(&self) -> &[f64]

The draws, in the order they were simulated. Draw i came from simulation i, so two distributions from the same simulations can be paired draw by draw.
Source§

fn sorted(&self) -> &[f64]

The draws sorted ascending.
Source§

fn mean_of(&self, f: impl Fn(f64) -> f64) -> f64

Mean of f(x) over the draws. Read more
Source§

fn var(&self, p: f64) -> Result<f64>

Value at risk at level p; see var_sorted.
Source§

fn tvar(&self, p: f64) -> Result<f64>

Tail value at risk at level p; see tvar_sorted.
Source§

fn distortion(&self, d: &Distortion) -> f64

Distortion risk measure of the draws; see Distortion::apply_sorted. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V