Conditional sampling and conditioning-aware vines

Beyond joint density evaluation and unconditional sampling, a fitted Vinecop can draw from the conditional distribution of a subset of variables given the rest, select a structure whose order tail is a chosen conditioning set, and be relabelled to an equivalent vine with a given tail. This notebook covers simulate_conditional, FitControlsVinecop.conditioning_set, reorient, and the list-of-trees round-trip (get_trees / RVineStructure.from_trees).

For fully non-simplified / conditional pair copulas on a custom (e.g. neural) backend, see notebook 11_extending_pyvinecopulib instead — this notebook is about the simplified vine’s exact conditional.

[ ]:
import copy

import numpy as np

import pyvinecopulib as pv

rng = np.random.default_rng(1234)
n, d = 2000, 4
# Correlated Gaussian data -> pseudo-observations -> fitted vine.
a = rng.normal(size=(d, d))
x = rng.multivariate_normal(np.zeros(d), a @ a.T, size=n)
u = pv.to_pseudo_obs(x)
cop = pv.Vinecop.from_data(
  u, controls=pv.FitControlsVinecop(family_set=[pv.families.gaussian])
)
print(cop)

Sampling from a conditional distribution

simulate_conditional(u_cond) conditions on the last ``k`` variables of the vine order (cop.order). Each row of u_cond is one conditioning point (on the copula scale); the returned n x d matrix draws the remaining variables from their conditional distribution and reproduces the conditioning values in those variables’ columns.

[ ]:
order = list(cop.order)
k = 2
cond_vars = order[-k:]  # 1-based labels of the conditioning variables
print("vine order:", order, "-> conditioning on variables", cond_vars)

# Draw 5 samples at a single conditioning point (passed repeated).
u0 = np.array([[0.2, 0.8]])
u_cond = np.repeat(u0, 5, axis=0)
sim = cop.simulate_conditional(u_cond, seeds=[1, 2, 3])
print("output shape:", sim.shape)

# The conditioning variables land in their natural columns and reproduce u_cond.
cond_cols = [v - 1 for v in cond_vars]
print(
  "conditioning columns reproduced:", np.allclose(sim[:, cond_cols], u_cond)
)
sim

Selecting a structure conditioned on a given set

If you know in advance which variables you will condition on, fit a vine whose order ends with them via FitControlsVinecop.conditioning_set (1-based labels). The conditioning set’s own optimal sub-vine is fit first and placed at the tail, so subsequent simulate_conditional calls are efficient.

[ ]:
controls = pv.FitControlsVinecop(family_set=[pv.families.gaussian])
controls.conditioning_set = [1, 2]  # place variables 1 and 2 at the order tail
cop_c = pv.Vinecop.from_data(u, controls=controls)
sel_order = list(cop_c.structure.order)
print("selected order:", sel_order)
print("order tail is the conditioning set:", set(sel_order[-2:]) == {1, 2})

Reorienting a fitted vine

reorient(conditioning_set) relabels an already-fitted vine to an equivalent one whose order tail equals the given set — no refitting. It is value-preserving: the density and log-likelihood are unchanged. (The set must be an admissible tail of the current structure; a suffix of the current order always is.)

[ ]:
cop_r = copy.deepcopy(cop)
tail = list(cop.order)[-2:]  # a current-order suffix is always admissible
ll_before = cop_r.loglik(u)
cop_r.reorient(tail)
print("order after reorient:", list(cop_r.order))
print("log-likelihood unchanged:", np.isclose(ll_before, cop_r.loglik(u)))

Inspecting and round-tripping the structure

Vinecop.get_trees() returns the fitted vine as nested lists [tree][edge], each edge a dict with its conditioned pair, conditioning set, and fitted pair-copula. The bare structure round-trips exactly through RVineStructure.get_trees() / RVineStructure.from_trees().

[ ]:
edge = cop.get_trees()[0][0]
print(
  "tree 0, edge 0: conditioned",
  edge["conditioned"],
  "| conditioning",
  edge["conditioning"],
  "| family",
  edge["pair_copula"].family,
)

s = cop.structure
s2 = pv.RVineStructure.from_trees(s.dim, s.get_trees())
print("structure round-trip equal:", s2 == s)