stochastic-rs

Copulas

All 15 bivariate and 8 multivariate copulas with the BivariateExt / MultivariateExt traits — Archimedean, extreme-value, elliptical, and vine families.

Copulas

The stochastic-rs-copulas crate ships bivariate and multivariate copulas plus correlation utilities and an empirical copula.

Bivariate (BivariateExt<T>)

15 copulas — every variant of the crate's own bivariate::CopulaType enum (grep -n "pub enum CopulaType" -A 15 stochastic-rs-copulas/src/bivariate.rs). "Archimedean" below means the type overrides generator(), the trait's Archimedean-generator hook — the default implementation returns an error, so a concrete override is the tree-derived signal, not a label taken from each module's own prose:

CopulaFamilyTail dependence
AmhArchimedeanNone
Bb1Archimedean (two-parameter)Both, asymmetric (λL=2−1/(θδ)\lambda_L = 2^{-1/(\theta\delta)}, λU=2−21/δ\lambda_U = 2 - 2^{1/\delta})
Bb7Archimedean (two-parameter)Both, asymmetric (λU=2−21/θ\lambda_U = 2 - 2^{1/\theta}, λL=2−1/δ\lambda_L = 2^{-1/\delta})
ClaytonArchimedeanLower only (λL=2−1/θ\lambda_L = 2^{-1/\theta})
FgmPerturbationNone (bounded perturbation of independence)
FrankArchimedeanNone (symmetric)
GalambosExtreme-valueUpper only (λU=2−1/θ\lambda_U = 2^{-1/\theta})
GaussianEllipticalNone, for any ρ\rho
GumbelArchimedeanUpper only (λU=2−21/θ\lambda_U = 2 - 2^{1/\theta})
HuslerReissExtreme-valueUpper only (λU=2(1−Φ(1/λ))\lambda_U = 2(1-\Phi(1/\lambda)))
IndependenceTrivialNone
JoeArchimedeanUpper only (λU=2−21/θ\lambda_U = 2 - 2^{1/\theta})
MarshallOlkinSingular (shared-shock)Upper only (λU=min⁡(α,β)\lambda_U = \min(\alpha,\beta)); has a singular component on uα=vβu^\alpha = v^\beta
PlackettConstant odds ratioNone
TCopulaEllipticalBoth, symmetric (λL=λU\lambda_L = \lambda_U)

The bivariate samplers are consolidated under BivariateExt; the v1.x NCopula2DExt was removed in v2.0.

Choosing a copula

Every family except Independence is fit the same way — moment-matching Kendall's tau, then inverting to the family's own shape parameter — so how you fit isn't the distinguishing axis. The crate's own doc comment (stochastic-rs-copulas/src/bivariate.rs) has the full comparison, including which common heuristics turn out to be wrong on the types in this crate. Short version:

NeedReach for
Both positive and negative dependenceFrank, Gaussian, TCopula, Plackett — the only four spanning the full (-1, 1) range of τ
Only ever positive dependence, lower-tailClayton — the only family here with lower-tail dependence at all
Only ever positive dependence, upper-tailGalambos, HuslerReiss, Gumbel, Joe — note Gumbel and Joe share the exact same upper-tail formula, so tail dependence alone won't distinguish them
Symmetric tail dependence (both tails, equal)TCopula — the only one
Weak dependence in either direction (τ within roughly ±0.2-0.3)Amh, Fgm — both structurally cannot represent stronger dependence, in either direction
Fast sampling at scale (millions of draws)Clayton, Gaussian or Independence — the only three with a real closed-form percent_point; everything else pays a per-draw root-find
A non-exchangeable, shared-shock dependence structureMarshallOlkin's with_alpha_beta constructor — the one family here that isn't symmetric in its two arguments in general

All 13 require inputs already probability-integral-transformed to [0, 1] — fit runs a Kolmogorov-Smirnov uniformity check before touching Kendall's tau. Independence is the one exception: its fit never reads the data at all.

Multivariate (MultivariateExt<T>)

8 copulas — every variant of multivariate::CopulaType (grep -n "pub enum CopulaType" -A 10 stochastic-rs-copulas/src/multivariate.rs). Two of these are not real pair-copula-construction vines despite the name — each says so in its own module doc:

CopulaFamilyNotes
GaussianMultivariateEllipticalCholesky-based
TMultivariateEllipticalMultivariate Student-t
NestedArchimedeanArchimedean, nestedOuter + per-cluster inner generators (NAC)
CVineVine, PCCCanonical vine — genuine pair-copula construction
DVineVine, PCCD-vine — genuine pair-copula construction
RVineVine, PCCRegular vine — graph-theoretic generalisation of C-/D-vine
TreeMultivariateNot a real vineMST-derived implied correlation collapsed to a Gaussian copula — module doc: "Gaussian-collapsed implied-correlation copula, NOT a real R-vine"
VineMultivariateNot a real vineStar-C-vine-derived implied correlation collapsed to a Gaussian copula — same disclaimer, same doc

Multivariate copulas ship in the default build — the linear algebra is the pure-Rust faer, no feature flag and no system BLAS.

Examples

Clayton — lower-tail dependence

θ>0\theta > 0 produces lower-tail dependence, useful for modelling joint crashes in equity returns.

tests/doctest_copulas_clayton.rs
// docs: copulas#clayton-lower-tail-dependence
//! Backs the Clayton example on the copulas catalog page.

use stochastic_rs::copulas::bivariate::clayton::Clayton;
use stochastic_rs::copulas::correlation::kendall_tau;
use stochastic_rs::traits::BivariateExt;

#[test]
fn clayton_set_tau_then_sample() {
  let mut cop = Clayton::new();
  cop.set_tau(0.5); // tau => theta via Kendall inversion
  cop.set_theta(cop.compute_theta());
  let uv = cop.sample_with_seed(10_000, 42).unwrap(); // Array2<f64>, shape (10_000, 2)
  assert_eq!(uv.dim(), (10_000, 2));
  assert!(uv.iter().all(|&x| (0.0..=1.0).contains(&x)));

  let corr = kendall_tau(&uv); // pairwise Kendall tau matrix, shape (2, 2)
  let tau_hat = corr[[0, 1]];
  assert!((tau_hat - 0.5).abs() < 0.05, "tau_hat = {tau_hat}");
}
import stochastic_rs as srs

cop = srs.Clayton(theta=2.0)
uv = cop.sample(10_000, seed=42)              # shape (10_000, 2)
# kendall_tau_matrix takes an (n, k) matrix and returns the k×k pairwise-tau
# matrix; uv is already (n, 2), so [0, 1] is the one off-diagonal entry.
tau = srs.kendall_tau_matrix(uv)[0, 1]
print("Kendall's tau ≈", tau)                 # ≈ 0.5

BB1 / BB7 — two-parameter tail asymmetry

The one-parameter Archimedean families tie the two tails together: Clayton has only lower-tail, Gumbel only upper-tail dependence. BB1 (Clayton–Gumbel) and BB7 (Joe–Clayton) carry two parameters (θ,δ)(\theta, \delta) and set the two tails separately — BB1 has λL=2−1/(θδ)\lambda_L = 2^{-1/(\theta\delta)}, λU=2−21/δ\lambda_U = 2 - 2^{1/\delta}; BB7 has λU=2−21/θ\lambda_U = 2 - 2^{1/\theta}, λL=2−1/δ\lambda_L = 2^{-1/\delta}. fit is a two-parameter maximum-likelihood estimate (Nelder–Mead on the log-density); set_tau + compute_theta inverts Kendall's τ at the current δ (closed form for BB1, τ=1−2/(δ(θ+2))\tau = 1 - 2/(\delta(\theta+2)); numerical for BB7).

gof::gof_cramer_von_mises is the Rosenblatt-transform Cramér–von Mises test of Genest, Rémillard & Beaudoin (2009) with a parametric bootstrap p-value — the valid procedure when the copula parameters were estimated on the same sample. multivariate::fit::fit_vine selects a D-vine or C-vine structure on Kendall's τ (Dißmann et al. 2013) and the family of every edge by AIC or BIC among Independence, Gaussian, Student-t, Clayton, Frank, BB1 and BB7, with the h-functions of the fitted pairs producing the pseudo-observations of the next tree. Feed every fit and test normalised ranks (gof::pseudo_observations, srs.pseudo_observations in Python): the statistics are rank statistics, and a family's fit rejects margins that fail its uniformity check.

tests/doctest_copulas_bb1_vine.rs
// docs: copulas#bb1-bb7-two-parameter-tail-asymmetry
//! Backs the BB1 / vine-fitting example on the copulas catalog page.

use stochastic_rs::copulas::bivariate::bb1::Bb1;
use stochastic_rs::copulas::gof::gof_cramer_von_mises;
use stochastic_rs::copulas::gof::pseudo_observations;
use stochastic_rs::copulas::multivariate::fit::PairFamily;
use stochastic_rs::copulas::multivariate::fit::SelectionCriterion;
use stochastic_rs::copulas::multivariate::fit::VineStructure;
use stochastic_rs::copulas::multivariate::fit::fit_vine;
use stochastic_rs::traits::BivariateExt;

#[test]
fn bb1_fit_gof_and_vine_selection() {
  // BB1 with lower tail 2^{-1/(θδ)} and upper tail 2 - 2^{1/δ}: asymmetric tails.
  let truth = Bb1::new(Some(0.8), Some(1.6), None);
  let tails = truth.tail_dependence();
  assert!(tails.lower > 0.5 && tails.upper > 0.4 && tails.lower != tails.upper);
  // Every fit and test runs on pseudo-observations (normalised ranks), as with real data.
  let uv = pseudo_observations(&truth.sample_with_seed(2_000, 42).unwrap()); // shape (2_000, 2)

  // Maximum-likelihood fit of (θ, δ) recovers the generating pair.
  let mut fitted = Bb1::default();
  fitted.fit(&uv).unwrap();
  assert!((fitted.theta.unwrap() - 0.8).abs() < 0.25 && (fitted.delta - 1.6).abs() < 0.25);

  // Parametric-bootstrap Cramér–von Mises test does not reject the true family.
  let gof = gof_cramer_von_mises(&fitted, &uv, 20, 7, |c, x| c.fit(x)).unwrap();
  assert!(gof.p_value > 0.05, "p = {}", gof.p_value);

  // A two-column D-vine fit selects a family on the single edge by AIC.
  let fit = fit_vine(
    &uv,
    VineStructure::DVine,
    &PairFamily::ALL,
    SelectionCriterion::Aic,
  )
  .unwrap();
  assert_eq!(fit.families.len(), 1);
  assert!(fit.aic.is_finite() && fit.log_likelihood > 0.0);
}
import numpy as np
import stochastic_rs as srs

truth = srs.Bb1(theta=0.8, delta=1.6)
uv = srs.pseudo_observations(truth.sample(2_000, seed=42))
lower, upper = truth.tail_dependence()      # asymmetric: ≈ 0.58 vs ≈ 0.46

fitted = srs.Bb1()
fitted.fit(uv)                              # MLE of (theta, delta)
stat, p = srs.copula_gof("bb1", uv, replications=50, seed=7)
print(fitted.theta(), fitted.delta(), p)    # p well above 0.05

# 3-column data: structure + family selection with BIC
u3 = srs.pseudo_observations(np.column_stack([uv, np.random.default_rng(1).uniform(size=2_000)]))
fit = srs.fit_vine(u3, structure="dvine", criterion="bic")
print(fit["order"], fit["families"], fit["aic"])

Gaussian copula — multivariate sampling

tests/doctest_copulas_gaussian_multivariate.rs
// docs: copulas#gaussian-copula-multivariate-sampling
//! Backs the Gaussian multivariate copula example on the copulas catalog
//! page. The Cholesky factorisation runs on the pure-Rust `faer`, so the
//! file is gated on that feature.

use ndarray::array;
use stochastic_rs::copulas::multivariate::gaussian::GaussianMultivariate;
use stochastic_rs::traits::MultivariateExt;

#[test]
fn gaussian_multivariate_sample() {
  let corr = array![[1.0, 0.7, 0.3], [0.7, 1.0, 0.5], [0.3, 0.5, 1.0]];
  let cop = GaussianMultivariate::new_with_corr(corr).unwrap();
  let samples = cop.sample(10_000).unwrap(); // Array2<f64>, shape (10_000, 3)
  assert_eq!(samples.dim(), (10_000, 3));
}

The multivariate Gaussian copula is not wrapped for Python — use the Rust API directly.

Adding a copula

See the copula-bivariate SKILL — file-by-file recipe for Clayton / Frank / Gumbel / Joe / Plackett / FGM / extreme-value families.

On this page