Fitting C-vine, D-vine, and R-vine Copulas
Vine copulas decompose high-dimensional dependence into bivariate pair copulas in a tree sequence. Learn when to use C-, D-, and R-vines and how to fit them.
Vine copulas let you model complex, high-dimensional dependence structures by breaking them down into a sequence of bivariate building blocks called pair copulas. Each tree in the vine connects variables (or pseudo-variables) through edges, and each edge carries its own pair copula family, parameters, and optional rotation. This decomposition gives you far more flexibility than a single multivariate copula family, and rscopulas fits the structure and pair families jointly from your pseudo-observations.
When to use each vine type
Rscopulas supports three vine structures:
- C-vine — Use when one variable drives the dependence on all others. The root variable appears in every first-tree edge (star structure). Fit with
VineCopula.fit_c. - D-vine — Use when variables have a natural chain ordering, such as time series or spatially ordered measurements. Each variable connects to its two neighbors in the first tree. Fit with
VineCopula.fit_d. - R-vine — The general case. No ordering is assumed; the algorithm selects the structure during fitting using a maximum spanning tree criterion. Fit with
VineCopula.fit_r.
When in doubt, start with fit_r and inspect fit.model.structure_kind afterward to see what structure was selected.
Fitting an R-vine in Python
- Prepare your pseudo-observations
Vine copulas operate on pseudo-observations — values strictly inside
(0, 1). Transform your raw data to uniforms before calling any fit method. Each row is one observation, each column is one variable.import numpy as np from rscopulas import VineCopula data = np.array( [ [0.10, 0.14, 0.18, 0.22], [0.18, 0.21, 0.24, 0.27], [0.24, 0.29, 0.33, 0.31], [0.33, 0.30, 0.38, 0.41], [0.47, 0.45, 0.43, 0.49], [0.52, 0.58, 0.55, 0.53], [0.69, 0.63, 0.67, 0.71], [0.81, 0.78, 0.74, 0.76], ], dtype=np.float64, ) - Fit the vine
Call
VineCopula.fit_rwith afamily_setand any options you need. The fitter selects both the vine structure and the pair copula family on each edge.fit = VineCopula.fit_r( data, family_set=["independence", "gaussian", "clayton", "frank", "gumbel"], truncation_level=2, max_iter=200, ) - Inspect the fitted model
The returned
FitResultcarries amodelanddiagnostics. Use the model's properties to understand the selected structure and pair families.print("structure kind:", fit.model.structure_kind) print("order:", fit.model.order) print("first-tree families:", [edge.family for edge in fit.model.trees[0].edges]) print("sample:\n", fit.model.sample(4, seed=13))
Fitting options
family_set
A list of string family names to consider on each vine edge. The fitter evaluates all candidates and picks the best according to criterion.
| String | Family |
|---|---|
"independence" | Independence (no dependence) |
"gaussian" | Gaussian |
"student_t" | Student t |
"clayton" | Clayton |
"frank" | Frank |
"gumbel" | Gumbel |
"khoudraji" | Khoudraji (asymmetric composition) |
Include "independence" in your family set to allow the fitter to drop edges where there is no detectable dependence, which keeps the model parsimonious.
criterion
Controls which information criterion the fitter minimizes when selecting pair families on each edge. Accepts "aic" (default) or "bic". BIC applies a stronger penalty for model complexity and tends to select simpler pair families.
truncation_level
Limits fitting to only the first k trees. Higher trees in a vine model tend to have weaker conditional dependence, so truncating at a low level (e.g. truncation_level=2) often produces a good approximation while reducing computation significantly. Edges in truncated trees are set to the independence copula.
For large d, always set truncation_level. Fitting all d - 1 trees grows quadratically in the number of pair copulas that must be estimated.
independence_threshold
A float in (0, 1). When set, the fitter skips edges where a statistical test fails to reject independence at this significance level, treating those edges as independence copulas without running full family selection. This is a fast way to sparsify the vine.
include_rotations
Boolean, defaults to True. When enabled, the fitter also considers 90°, 180°, and 270° rotations of asymmetric families (Clayton, Gumbel) to capture lower tail, upper tail, and reflected dependence patterns.
Fitting C-vine and D-vine
fit_c and fit_d accept the same keyword arguments as fit_r:
fit_c = VineCopula.fit_c(
data,
family_set=["independence", "gaussian", "clayton", "gumbel"],
criterion="bic",
)
print("structure:", fit_c.model.structure_kind)
fit_d = VineCopula.fit_d(
data,
family_set=["independence", "gaussian", "clayton", "gumbel"],
criterion="bic",
)
print("structure:", fit_d.model.structure_kind)
Inspecting a fitted vine
A VineCopula model exposes the following properties:
| Property | Type | Description |
|---|---|---|
structure_kind | str | "c", "d", or "r" (same values as structure_info.kind) |
order | list[int] | Variable ordering of the vine |
pair_parameters | ndarray | Flat array of all pair copula parameters |
trees | list[VineTreeInfo] | Tree-by-tree edge information (family, rotation, parameters) |
structure_info | VineStructureInfo | Structure matrix and truncation level |
Each VineTreeInfo contains a list of VineEdgeInfo objects with family, rotation, parameters, conditioned, and conditioning fields.
For every vine edge, rotation is always one of "R0", "R90", "R180", or "R270".
Constructing a Gaussian vine from a correlation matrix
If you already have a correlation matrix, you can build a Gaussian C-vine or D-vine directly without fitting:
import numpy as np
from rscopulas import VineCopula
correlation = np.array([
[1.0, 0.60, 0.35],
[0.60, 1.0, 0.25],
[0.35, 0.25, 1.0],
], dtype=np.float64)
model = VineCopula.gaussian_c_vine(order=[0, 1, 2], correlation=correlation)
print("order:", model.order)
model_d = VineCopula.gaussian_d_vine(order=[0, 1, 2], correlation=correlation)
gaussian_c_vine and gaussian_d_vine return a VineCopula model directly, not a FitResult. There are no diagnostics because no data is fitted.
Fitting a vine in Rust
The Rust API mirrors the Python interface. Use VineCopula::fit_r_vine with VineFitOptions to control families, criterion, and truncation:
use ndarray::array;
use rscopulas_core::{
PairCopulaFamily, PseudoObs, SelectionCriterion, VineCopula, VineFitOptions,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let data = PseudoObs::new(array![
[0.12, 0.18, 0.21],
[0.21, 0.25, 0.29],
[0.27, 0.22, 0.31],
[0.35, 0.42, 0.39],
[0.48, 0.51, 0.46],
[0.56, 0.49, 0.58],
[0.68, 0.73, 0.69],
[0.82, 0.79, 0.76],
])?;
let options = VineFitOptions {
family_set: vec![
PairCopulaFamily::Independence,
PairCopulaFamily::Gaussian,
PairCopulaFamily::Clayton,
PairCopulaFamily::Frank,
PairCopulaFamily::Gumbel,
PairCopulaFamily::Khoudraji,
],
include_rotations: true,
criterion: SelectionCriterion::Aic,
truncation_level: Some(1),
..VineFitOptions::default()
};
let fit = VineCopula::fit_r_vine(&data, &options)?;
println!("structure = {:?}", fit.model.structure());
println!("order = {:?}", fit.model.order());
println!("pair parameters = {:?}", fit.model.pair_parameters());
Ok(())
}
Use VineCopula::fit_c_vine and VineCopula::fit_d_vine for the other vine types. The Rust VineFitOptions::default() uses AIC, all classical families with rotations, and no truncation.
Conditional sampling
A fitted vine is built from pair-copula h-functions and their inverses, which is exactly the machinery needed for conditional simulation. Given observed values on a subset of columns, rscopulas can draw the remaining columns from the vine's conditional distribution without importance reweighting or any approximation — the tail dependence encoded in each pair copula propagates through exactly.
The primitives are the two Rosenblatt transforms: rosenblatt(V) = U turns vine-distributed data into independent uniforms, and inverse_rosenblatt(U) = V does the reverse. Unconditional sampling is just inverse_rosenblatt of a uniform matrix, and conditional sampling pins a prefix of the uniforms to match the known values.
The variable_order convention
Every fitted vine has a diagonal order variable_order giving the sequence in which the Rosenblatt chain emits variables. variable_order[0] is the Rosenblatt anchor: its input uniform passes through unchanged, so V[:, variable_order[0]] equals the anchor uniform bit-for-bit.
For canonical C- and D-vines the relationship to the user-supplied order is the same:
variable_order[0] == order[-1]
So to pin column X at the anchor, place it at the end of order:
fit = VineCopula.fit_c(u, order=[*other_cols, X])
vine = fit.model
assert vine.variable_order[0] == X
For a D-vine variable_order is exactly list(reversed(order)); for a C-vine the remaining positions are permuted by the structure-matrix construction, so inspect vine.variable_order after fitting if you care about positions beyond the anchor.
R-vines (fit_r) pick their own structure and do not accept an order= argument. If you need conditional sampling, fit a C- or D-vine with an explicit order.
sample_conditional
from rscopulas import VineCopula
fit = VineCopula.fit_c(u, order=[*others, us10y_idx])
vine = fit.model
assert vine.variable_order[0] == us10y_idx
scenarios = vine.sample_conditional(
known={us10y_idx: yield_uniforms}, # 1D array of length n, values in (0, 1)
n=10_000,
seed=2026,
)
The returned (n, dim) matrix is in original variable-label order, with the conditioned column set to the supplied values. For the single-variable fast path (k == 1, matching the Rosenblatt anchor), the output column for the known variable is bit-exact. For k >= 2 the returned values for the known columns match the input up to ~1e-8 drift.
known must supply a diagonal prefix of variable_order: its keys must equal set(variable_order[:k]) for some k. Otherwise sample_conditional raises rscopulas.NonPrefixConditioningError. The error message points at the fit_c(order=...) / fit_d(order=...) pattern needed to set up the right prefix.
Rosenblatt transforms directly
Both rosenblatt and inverse_rosenblatt take and return (n, dim) arrays indexed by the original variable label. They round-trip up to ~1e-8:
V = vine.sample(1_000, seed=42)
U = vine.rosenblatt(V) # independent uniforms, same column layout
V_back = vine.inverse_rosenblatt(U) # exact inverse
Use rosenblatt for goodness-of-fit diagnostics, copula-based PIT transforms, or any workflow that needs to turn vine-distributed data into IID uniforms.
Conditional sampling in Rust
The same primitives are available on VineCopula in Rust:
use rscopulas::{VineCopula, VineFitOptions, SampleOptions};
let target = 2usize; // column to condition on
let order = vec![0, 1, target];
let vine = VineCopula::fit_c_vine_with_order(
&data, &order, &VineFitOptions::default(),
)?.model;
assert_eq!(vine.variable_order()[0], target);
// Build a U matrix with the known column pinned and the rest freshly drawn,
// then call inverse_rosenblatt. The Python sample_conditional is a thin
// convenience over this loop.
let samples = vine.inverse_rosenblatt(u.view(), &SampleOptions::default())?;
rosenblatt, rosenblatt_prefix, and variable_order are the matching accessors.