Skip to content

Structural constructions

Copulas built from other copulas. Each result is a genuine copula carrying the base families' parameters, so fit, select_copula and the goodness-of-fit tests all work on them unchanged.

rcopula.structural.rotated

Rotated and survival copulas.

Every Archimedean family is asymmetric in a fixed direction: Clayton binds in the lower tail, Gumbel in the upper, and neither can be told to do the opposite. Reflecting one or more coordinates fixes that, and costs nothing -- the reflected object is still a copula, with the same number of parameters.

Reflecting coordinate :math:j replaces :math:U_j by :math:1 - U_j. Two cases have names:

  • All coordinates reflected gives the survival copula :math:\hat C, the copula of :math:(1-U_1,\dots,1-U_d). In two dimensions this is the 180-degree rotation. It swaps the tails: survival Clayton has upper tail dependence, which is the usual way to get an upper-tail-dependent alternative to Gumbel with a different shape.
  • One coordinate reflected, in two dimensions, gives the 90-degree (or 270-degree) rotation, which turns positive dependence into negative. Clayton and Gumbel admit no negative dependence at all; rotating them is the standard way to model it, and it is what vine libraries mean by "rotation 90".

The CDF follows from inclusion-exclusion over the reflected coordinates,

.. math:: C_S(\mathbf u) = \sum_{T \subseteq S} (-1)^{|T|}\, C(\mathbf x^T), \qquad x^T_j = \begin{cases} u_j & j \notin S\ 1-u_j & j \in T\ 1 & j \in S\setminus T,\end{cases}

while the density is simply the base density at the reflected point -- the Jacobian of a reflection is 1.

.. warning::

Rotation is why "Clayton for losses" is usually backwards. Aggregate loss risk is driven by the upper tail, and plain Clayton has none: at :math:\tau = 0.5 the 99% expected shortfall of a two-line loss portfolio comes out below the Gaussian answer. It is the survival Clayton that belongs there.

References

Nelsen, R. B. (2006). An Introduction to Copulas, 2nd ed. Springer, section 2.6 -- the survival copula and the reflection identities. Brechmann, E. C. and Schepsmeier, U. (2013). Modeling dependence with C- and D-vine copulas: the R package CDVine. Journal of Statistical Software 52(3), 1-27. The 0/90/180/270 rotation convention used by vine libraries.

RotatedCopula

RotatedCopula(base: Copula, flip: ArrayLike | int | bool = True, *, free: ArrayLike | None = None)

Bases: Copula

Turn an existing copula around so its strong dependence sits in a different corner.

Reflecting coordinate j means replacing each value u_j by 1 - u_j (a small value becomes a large one). Use this when a family has the right shape but the wrong orientation: for example Clayton ties variables together when they are all small (lower tail), and reflecting every coordinate gives a copula that ties them together when they are all large (upper tail) -- usually what you want for joint losses. In two dimensions, reflecting one coordinate turns positive dependence into negative, which Clayton and Gumbel cannot otherwise express.

The result has exactly the base copula's parameters, so it can be fitted, evaluated and sampled like any other copula.

Parameters:

Name Type Description Default
base Copula

The copula being reflected, with any dimension d >= 2. Its parameters may be nan (to be fitted).

required
flip bool, array_like of bool with shape (d,), or int

Which coordinates to reflect. A single bool applies to all of them, so flip=True (the default) is the survival copula. A sequence of d booleans picks coordinates individually. In two dimensions an int in {0, 90, 180, 270} selects a rotation by that many degrees (90 reflects the second coordinate, 270 the first, 180 both).

True
free array_like of bool, shape (n_params,), or None

Keyword-only. Which parameters are estimated when fitting; None keeps the base copula's mask.

None

Attributes:

Name Type Description
base Copula

The copula being reflected. Never itself a :class:RotatedCopula -- nested rotations are collapsed on construction.

flip numpy.ndarray of bool, shape (d,)

Read-only mask of reflected coordinates.

name str

"Rotated <base name>".

param_names tuple of str

The base copula's parameter names.

Raises:

Type Description
ValueError

If flip is a sequence whose length is not d, if it is an int but d != 2, or if the int is not one of 0, 90, 180, 270.

Notes

Rotations compose: RotatedCopula(RotatedCopula(c, S1), S2) collapses to a single rotation by the symmetric difference of S1 and S2, so the class is closed under composition and a double rotation returns to the base.

That symmetric difference is worth reading carefully. The operation is a coordinate reflection, which is an involution, so composition follows the Klein four-group rather than the cyclic one the degree labels suggest: applying 90 twice gives 0, not 180, and reaching 180 takes a 90 and a 270. The names match the vine convention and denote the right copulas; only their composition differs from what "rotate twice" would imply. (For a non-exchangeable base a true geometric rotation would also transpose the coordinates; for the exchangeable families this wraps, the two coincide.)

Parameters are the base copula's, so :func:~rcopula.fit.fit, :func:~rcopula.select_copula and the goodness-of-fit machinery all work unchanged.

Examples:

The survival Clayton has upper tail dependence where Clayton has lower:

>>> from rcopula import ClaytonCopula
>>> from rcopula.structural import RotatedCopula
>>> base = ClaytonCopula(3.0)
>>> base.lambda_()
TailDependence(lower=0.7937005259840998, upper=0.0)
>>> RotatedCopula(base, True).lambda_()
TailDependence(lower=0.0, upper=0.7937005259840998)

A 90-degree rotation turns positive dependence into negative:

>>> cop = RotatedCopula(ClaytonCopula(3.0), 90)
>>> float(round(cop.tau(), 12))
-0.6
>>> float(round(base.tau(), 12))
0.6

Rotating twice returns the original:

>>> RotatedCopula(RotatedCopula(base, True), True).flip.tolist()
[False, False]

flip property

flip: NDArray[bool_]

Which coordinates are reflected.

Returns:

Type Description
numpy.ndarray of bool, shape (d,)

Read-only mask; True where u_j is replaced by 1 - u_j.

degrees property

degrees: int

The rotation expressed in degrees, as vine libraries label it (two dimensions only).

Returns:

Type Description
int

One of 0, 90, 180, 270. 90 means the second coordinate is reflected, 270 the first, 180 both.

Raises:

Type Description
ValueError

If the copula is not bivariate.

Examples:

>>> from rcopula import ClaytonCopula
>>> from rcopula.structural import RotatedCopula
>>> RotatedCopula(ClaytonCopula(2.0), [True, False]).degrees
270

is_survival property

is_survival: bool

Whether this is the survival copula, i.e. every coordinate is reflected.

Returns:

Type Description
bool

True when every coordinate is reflected.

param_bounds property

param_bounds: list[tuple[float, float]]

Allowed range of each parameter -- the same as the base copula's.

Returns:

Type Description
list of tuple of (float, float)

One (lower, upper) pair per parameter.

tau

tau() -> float

Return Kendall's tau, a rank correlation between -1 and 1, after reflection.

Exact in d = 2; the base's value when d > 2 and all or none of the coordinates are reflected.

In two dimensions concordance simply flips sign with each reflection. Beyond two dimensions the scalar tau of an exchangeable family survives only when all or no coordinates are reflected; a partial reflection makes the pairwise taus differ, so there is no single number to report.

Returns:

Type Description
float

Kendall's tau of the rotated copula.

Raises:

Type Description
NotImplementedError

For a partial reflection with d > 2.

rho

rho() -> float

Return Spearman's rho, a rank correlation between -1 and 1, after reflection.

Same conventions as :meth:tau: in two dimensions the sign flips with each reflected coordinate.

Returns:

Type Description
float

Spearman's rho of the rotated copula.

Raises:

Type Description
NotImplementedError

For a partial reflection with d > 2.

lambda_

lambda_() -> TailDependence

Return how strongly the variables move together in extremes, after reflection.

The lower (upper) tail-dependence coefficient is the limiting chance that one variable is extremely small (large) given that another is.

Reflecting every coordinate swaps the two coefficients: the survival copula's lower tail is the base's upper tail. Reflecting none leaves them alone.

A partial reflection is different in kind. In two dimensions the rotated copula's lower-tail coefficient is the base's cross coefficient :math:\lim_{u\to0} P(U_1 > 1-u,\ U_2 \le u)/u, which measures mass at an off-diagonal corner. That is zero for any positively quadrant dependent copula, hence for every parametric family in this package. The one exception is the Frechet lower bound :math:W, whose entire mass sits on that anti-diagonal -- and reflecting it gives :math:M, which is comonotone -- so it is handled explicitly.

Returns:

Type Description
TailDependence

Named tuple (lower, upper) of floats in [0, 1].

calibrated

calibrated(measure: str, value: float) -> Copula

Return a new rotated copula whose tau or rho equals a target value.

The wrapped family is calibrated so that the rotated copula hits the target (the sign is flipped for you where the rotation reverses concordance). The rotation itself is kept.

This is why the instance-level hook exists: a classmethod cannot know which family is being rotated. It is what makes method="itau" and "irho" work on rotated copulas, and what supplies the starting value for "mpl".

Parameters:

Name Type Description Default
measure ('tau', 'rho')

Which rank correlation to match.

"tau"
value float

Target value of that measure for the rotated copula.

required

Returns:

Type Description
Copula

A :class:RotatedCopula with the same flip and new parameters.

Raises:

Type Description
ValueError

If measure is not "tau" or "rho", or if the base family cannot reach the (sign-adjusted) value.

Examples:

>>> from rcopula import ClaytonCopula
>>> from rcopula.structural import RotatedCopula
>>> template = RotatedCopula(ClaytonCopula(1.0), 90)
>>> float(round(template.calibrated("tau", -0.4).tau(), 12))
-0.4

from_tau classmethod

from_tau(tau: float, dim: int = 2, **kwargs: Any) -> Copula

Build a rotated copula with a given Kendall's tau.

The base family is calibrated so that the rotated copula has this tau; for a 90- or 270-degree rotation that means a negative tau gives a valid positive-dependence base.

Parameters:

Name Type Description Default
tau float

Target Kendall's tau of the rotated copula, in (-1, 1).

required
dim int

Number of variables.

2
**kwargs Any

base (required): the copula class to rotate, e.g. ClaytonCopula. flip (bool, array_like of bool or int, default True): as in the constructor. Anything else is passed to base.from_tau.

{}

Returns:

Type Description
Copula

A :class:RotatedCopula.

Raises:

Type Description
TypeError

If base is not given.

ValueError

If flip is invalid or the base family cannot reach the value.

Examples:

>>> from rcopula import ClaytonCopula
>>> from rcopula.structural import RotatedCopula
>>> cop = RotatedCopula.from_tau(-0.5, base=ClaytonCopula, flip=90)
>>> float(round(cop.tau(), 12))
-0.5

from_rho classmethod

from_rho(rho: float, dim: int = 2, **kwargs: Any) -> Copula

Build a rotated copula with a given Spearman's rho.

Exactly like :meth:from_tau, but matching Spearman's rho.

Parameters:

Name Type Description Default
rho float

Target Spearman's rho of the rotated copula, in (-1, 1).

required
dim int

Number of variables.

2
**kwargs Any

base (required): the copula class to rotate. flip (default True): as in the constructor. Anything else is passed to base.from_rho.

{}

Returns:

Type Description
Copula

A :class:RotatedCopula.

Raises:

Type Description
TypeError

If base is not given.

ValueError

If flip is invalid or the base family cannot reach the value.

describe

describe() -> str

Return a one-line, human-readable description of the rotated copula.

Returns:

Type Description
str

For example "90-degree rotated Clayton ..." in two dimensions, or "survival ..." / "reflected on [0, 2] ..." otherwise, followed by the base copula's own description.

survival

survival(copula: Copula) -> RotatedCopula

Flip a copula so its lower-tail behaviour becomes upper-tail behaviour.

Returns the survival copula :math:\hat C: the copula with every coordinate reflected (u -> 1 - u), and lower and upper tails swapped. It is shorthand for RotatedCopula(copula, True).

:math:\hat C(\mathbf u) is the copula of :math:(1-U_1,\dots,1-U_d), and it is what you want whenever the risk lives in the upper tail: joint large losses, joint large claims, joint large flows.

Parameters:

Name Type Description Default
copula Copula

Any copula, of any dimension.

required

Returns:

Type Description
RotatedCopula

The survival copula, with the same parameters as copula. Applying :func:survival twice gives back the original copula's behaviour.

Examples:

Survival Clayton is the upper-tail-dependent version of Clayton. On a portfolio of two exponential losses at identical Kendall's tau, plain Clayton comes out below the Gaussian answer and survival Clayton above -- a 29% difference in expected shortfall between two copulas with the same rank correlation:

>>> import numpy as np
>>> from rcopula import ClaytonCopula, GaussianCopula
>>> from rcopula.structural import survival
>>> from rcopula.risk import expected_shortfall
>>> def es(cop):
...     losses = -np.log(1 - cop.rvs(200_000, random_state=0)).sum(axis=1)
...     return expected_shortfall(losses, 0.99)
>>> plain = ClaytonCopula.from_tau(0.5)
>>> bool(es(plain) < es(GaussianCopula.from_tau(0.5)) < es(survival(plain)))
True

Kendall's tau is unchanged, so no rank-correlation-based summary would show the difference:

>>> bool(abs(survival(plain).tau() - plain.tau()) < 1e-12)
True

rcopula.structural.khoudraji

Khoudraji's device: making an asymmetric copula out of symmetric ones.

Every Archimedean copula is exchangeable -- :math:C(u,v) = C(v,u) -- and so is every elliptical one with equal correlations. That is a real restriction. Exchangeability says the dependence between two variables looks the same viewed from either side, and plenty of data disagrees: an equity index and its volatility, a river's peak and its volume, a claim and its settlement cost.

Khoudraji (1995) gives a construction that breaks it, using two copulas and a shape parameter per coordinate:

.. math::

C(\mathbf u) = C_1\bigl(u_1^{1-a_1}, \dots, u_d^{1-a_d}\bigr)\;
               C_2\bigl(u_1^{a_1},   \dots, u_d^{a_d}\bigr),
\qquad a_j \in [0, 1].

Equal shapes give back an exchangeable copula; unequal ones tilt the dependence towards one coordinate. At :math:\mathbf a = 0 it is :math:C_1, at :math:\mathbf a = 1 it is :math:C_2, and in between it interpolates in a way that is genuinely asymmetric rather than a mixture.

The usual recipe takes :math:C_1 to be the independence copula and :math:C_2 something with tail dependence, which produces a family whose dependence concentrates in one corner and one direction. That is what the example below does.

Two facts make the construction practical rather than merely definable:

  • Sampling is exact and trivial. Draw :math:\mathbf U \sim C_1 and :math:\mathbf V \sim C_2 independently and set :math:W_j = \max(U_j^{1/(1-a_j)},\, V_j^{1/a_j}). The maximum of two independent events gives the product of their probabilities, which is the definition above -- no inversion, no rejection.
  • The extreme-value class is closed under it. If both components are extreme-value then so is the result, with a Pickands function assembled from theirs (see :meth:KhoudrajiCopula.pickands). This is the standard way to build asymmetric extreme-value copulas, and it is what gives the tail dependence coefficient in closed form.
References

Khoudraji, A. (1995). Contributions a l'etude des copules et a la modelisation de valeurs extremes bivariees. PhD thesis, Universite Laval. Genest, C., Ghoudi, K. and Rivest, L.-P. (1998). Discussion of "Understanding relationships using copulas" by Frees and Valdez. North American Actuarial Journal 2(3), 143-149. Where the device reached a wider audience. Liebscher, E. (2008). Construction of asymmetric multivariate copulas. Journal of Multivariate Analysis 99(10), 2234-2250. The general form, of which this is the two-component case.

KhoudrajiCopula

KhoudrajiCopula(copula1: Copula, copula2: Copula, shapes: ArrayLike, *, free: ArrayLike | None = None)

Bases: Copula

Combine two copulas into one whose dependence is lopsided (not symmetric in the variables).

Most families are exchangeable: swapping two variables changes nothing, C(u, v) = C(v, u). Real data often disagree -- the link between an index and its volatility, or a flood peak and its volume, looks different from each side. Khoudraji's device takes two copulas and one "shape" number per variable and returns C(u) = C1(u_1**(1 - a_1), ...) * C2(u_1**a_1, ...), which is asymmetric whenever the shapes differ. The usual recipe is copula1 = independence and copula2 = a tail-dependent family such as Gumbel.

Parameters:

Name Type Description Default
copula1 Copula

The two components, of the same dimension d. copula1 is raised to the 1 - a powers and copula2 to the a powers, so shapes near zero recovers copula1 and near one recovers copula2. Their parameters may be nan (to be fitted).

required
copula2 Copula

The two components, of the same dimension d. copula1 is raised to the 1 - a powers and copula2 to the a powers, so shapes near zero recovers copula1 and near one recovers copula2. Their parameters may be nan (to be fitted).

required
shapes float or array_like of float, shape (d,)

One shape in [0, 1] per coordinate; a single number is used for every coordinate. Equal shapes leave the result exchangeable, so asymmetry requires them to differ.

required
free array_like of bool, shape (n_params,), or None

Keyword-only. Which parameters are estimated when fitting; None means all of them.

None

Attributes:

Name Type Description
copula1, copula2 Copula

The two components, as given.

shapes numpy.ndarray of float64, shape (d,)

The per-coordinate shape parameters (see :attr:shapes).

name str

"Khoudraji(<name 1>, <name 2>)".

param_names tuple of str

"c1.*" names, then "c2.*" names, then "shape1", ....

Raises:

Type Description
ValueError

If the components differ in dimension, if shapes has the wrong length, or if a shape lies outside [0, 1].

Notes

Parameters are the two components' parameters followed by the shapes, so :func:~rcopula.fit.fit and :func:~rcopula.select_copula work on the whole object. The density is available in two dimensions; above that only the CDF and the sampler are, which is the same trichotomy R applies.

Examples:

The canonical construction -- independence tilted by a Gumbel:

>>> import numpy as np
>>> from rcopula import GumbelCopula, IndependenceCopula
>>> from rcopula.structural import KhoudrajiCopula
>>> cop = KhoudrajiCopula(IndependenceCopula(2), GumbelCopula(3.0), [0.4, 0.95])

It is genuinely asymmetric: swapping the arguments changes the value, which no Archimedean or exchangeable elliptical copula can do.

>>> a = float(cop.cdf([[0.3, 0.7]])[0])
>>> b = float(cop.cdf([[0.7, 0.3]])[0])
>>> bool(abs(a - b) > 0.01)
True

Equal shapes hand back exchangeability:

>>> even = KhoudrajiCopula(IndependenceCopula(2), GumbelCopula(3.0), [0.6, 0.6])
>>> bool(abs(even.cdf([[0.3, 0.7]])[0] - even.cdf([[0.7, 0.3]])[0]) < 1e-12)
True

Both components extreme-value makes the result extreme-value too, so the tail dependence is exact rather than estimated:

>>> float(round(cop.lambda_().upper, 10))
0.3769268825

shapes property

shapes: NDArray[float64]

How far each coordinate leans towards copula2 (1) rather than copula1 (0).

Returns:

Type Description
numpy.ndarray of float64, shape (d,)

One shape in [0, 1] per coordinate.

is_exchangeable property

is_exchangeable: bool

Whether all shapes are equal, which makes the result symmetric after all.

Equal shapes leave the construction symmetric, defeating its purpose (assuming the components are themselves exchangeable).

Returns:

Type Description
bool

True when every shape equals the first (to numpy.allclose tolerance).

param_bounds property

param_bounds: list[tuple[float, float]]

Allowed range of each parameter: both components' bounds, then [0, 1] per shape.

Returns:

Type Description
list of tuple of (float, float)

One (lower, upper) pair per parameter.

is_extreme_value property

is_extreme_value: bool

Whether the result is an extreme-value copula (needed by :meth:pickands).

Extreme-value status also makes :meth:lambda_ available in closed form. The construction lands back in the extreme-value class exactly when both components do -- and the Gumbel copula counts, being the one family that is both Archimedean and extreme-value, as does the independence copula. Only bivariate constructions are considered.

Returns:

Type Description
bool

True when dim == 2 and both components are extreme-value (an :class:~rcopula.core.extreme_value.ExtremeValueCopula, a Gumbel copula, or the independence copula).

pickands

pickands(w: ArrayLike) -> NDArray[np.float64]

Evaluate the Pickands function A(t), the curve that defines an extreme-value copula.

Available only when both components are extreme-value (see :attr:is_extreme_value). A maps [0, 1] to [1/2, 1]; the copula is recovered as C(u, v) = (uv) ** A(log v / log(uv)).

Substituting :math:C_i(u,v) = (uv)^{A_i(t_i)} into the definition and collecting the exponent of :math:\log(uv) gives

.. math:: A(t) = \bigl[(1-a_1)(1-t) + (1-a_2)t\bigr] A_1(t') + \bigl[a_1(1-t) + a_2 t\bigr] A_2(t''),

a convex combination of the components' Pickands functions evaluated at reweighted arguments. So the extreme-value class is closed under the device -- which is the whole reason it is the standard route to an asymmetric extreme-value copula.

Parameters:

Name Type Description Default
w float or array_like of float, any shape

Points t in [0, 1] at which to evaluate A.

required

Returns:

Type Description
numpy.ndarray of float64, same shape as ``w``

A(t), elementwise.

Raises:

Type Description
NotImplementedError

If the copula is not extreme-value.

Examples:

Reproduces the copula's own CDF, which is the point:

>>> import numpy as np
>>> from rcopula import GumbelCopula, IndependenceCopula
>>> from rcopula.structural import KhoudrajiCopula
>>> cop = KhoudrajiCopula(IndependenceCopula(2), GumbelCopula(2.5), [0.3, 0.9])
>>> u, v = 0.4, 0.7
>>> t = np.log(v) / (np.log(u) + np.log(v))
>>> direct = float(cop.cdf([[u, v]])[0])
>>> viaA = float(np.exp((np.log(u) + np.log(v)) * cop.pickands(t)))
>>> bool(abs(direct - viaA) < 1e-12)
True

tau

tau() -> float

Return Kendall's tau, a rank correlation between -1 and 1, computed numerically.

By quadrature -- the device admits no closed form.

Returns:

Type Description
float

Kendall's tau.

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

NotImplementedError

If dim != 2.

rho

rho() -> float

Return Spearman's rho, a rank correlation between -1 and 1, computed numerically.

By quadrature on the CDF.

Returns:

Type Description
float

Spearman's rho.

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

NotImplementedError

If dim != 2.

lambda_

lambda_() -> TailDependence

Return the chance of joint extremes in each tail (tail-dependence coefficients).

Exact when both components are extreme-value, where :math:\lambda_U = 2(1 - A(1/2)) and :math:\lambda_L = 0. Otherwise the limit depends on how each component behaves along a direction the shapes choose, which no single coefficient of the components records -- so it is refused rather than guessed.

Returns:

Type Description
TailDependence

Named tuple (lower, upper); lower is always 0.0.

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

NotImplementedError

If the copula is not extreme-value.

from_tau classmethod

from_tau(tau: float, dim: int = 2, **kwargs: Any) -> Copula

Not available: one tau value cannot pin down both components and the shapes.

Raises:

Type Description
NotImplementedError

Always. Fit the copula to data instead, or calibrate a component and choose the shapes yourself.

describe

describe() -> str

Return a multi-line, human-readable summary: the shapes and both components.

Returns:

Type Description
str

A header line with the shapes, then one line per component.

rcopula.structural.mixture

Mixtures of copulas.

A convex combination of copulas is a copula:

.. math:: C(\mathbf u) = \sum_{i} w_i\, C_i(\mathbf u), \qquad w_i \ge 0,\ \sum_i w_i = 1.

Nothing needs checking -- the defining properties are all preserved by convex combination -- which makes this the cheapest way to build a family that no single parametric form can express. The usual reason is both tails at once: Clayton binds below and Gumbel above, and a mixture of the two has genuine asymmetric dependence in both corners, which neither component and no elliptical copula can produce.

Three quantities are exactly linear in the copula and so are exact weighted averages of the components', with no integration at all:

  • Spearman's rho, because :math:\rho = 12\int\int C - 3;
  • both tail-dependence coefficients, because each is a limit of :math:C(u,u)/u or its survival counterpart;
  • Blomqvist's beta, which depends on :math:C only at the centre.

Kendall's tau is not, since :math:\tau = 4\int\int C\,dC - 1 is quadratic in :math:C -- a mixture of two copulas with the same tau generally has a different one. It is computed by quadrature.

Weights are carried on an unconstrained log-odds scale internally, as R does, so that an optimiser sees a box rather than a simplex; :attr:MixtureCopula.weights converts back.

References

Nelsen, R. B. (2006). An Introduction to Copulas, 2nd ed. Springer, section 3.2.4 -- convex sums of copulas. Hu, L. (2006). Dependence patterns across financial markets: a mixed copula approach. Applied Financial Economics 16(10), 717-729. The both-tails mixture, and why a single family cannot do it.

MixtureCopula

MixtureCopula(copulas: Sequence[Copula], weights: ArrayLike | None = None, *, free: ArrayLike | None = None)

Bases: Copula

Blend several copulas with fixed proportions, e.g. 40% Clayton and 60% Gumbel.

A mixture behaves like "pick component i with probability w_i, then draw from it". Its CDF and density are the weighted averages of the components'. Use it when no single family has the shape you need -- most often to get dependence in both the lower and the upper tail at once, which neither Clayton nor Gumbel (nor any elliptical copula) can give alone.

The weights are parameters too, so :func:~rcopula.fit.fit can estimate them together with the components' parameters.

Parameters:

Name Type Description Default
copulas sequence of Copula

Two or more components, all of the same dimension d. Their parameters may be nan (to be fitted).

required
weights array_like of float, shape (k,), or None

Mixing weights for the k components, non-negative and summing to one. None gives equal weights 1/k.

None
free array_like of bool, shape (n_params,), or None

Keyword-only. Which parameters are estimated when fitting; None means all of them.

None

Attributes:

Name Type Description
copulas list of Copula

The components, in the order given.

weights numpy.ndarray of float64, shape (k,)

The mixing weights (see :attr:weights).

n_components int

Number of components k.

name str

"Mixture(<name 1>, <name 2>, ...)".

param_names tuple of str

"c1.theta"-style names for each component's parameters, then "logodds1", ..., "logodds{k-1}".

Raises:

Type Description
ValueError

If fewer than two components are given, the components differ in dimension, the number of weights differs from the number of components, or the weights are negative or do not sum to one.

Notes

Parameters are the components' parameters followed by k - 1 log-odds against the last component. That parameterisation is what makes the object fittable: the weights live on a simplex, which a box-constrained optimiser cannot represent, while the log-odds are unconstrained.

Examples:

Clayton binds in the lower tail, Gumbel in the upper, and the mixture has both -- which neither component, and no elliptical copula, can manage:

>>> from rcopula import ClaytonCopula, GumbelCopula
>>> from rcopula.structural import MixtureCopula
>>> mix = MixtureCopula([ClaytonCopula(3.0), GumbelCopula(2.5)], [0.4, 0.6])
>>> lam = mix.lambda_()
>>> bool(lam.lower > 0.3 and lam.upper > 0.3)
True

Tail dependence is an exact weighted average, so it needs no integration:

>>> a, b = ClaytonCopula(3.0).lambda_(), GumbelCopula(2.5).lambda_()
>>> bool(abs(lam.lower - 0.4 * a.lower) < 1e-12)
True
>>> bool(abs(lam.upper - 0.6 * b.upper) < 1e-12)
True

So is Spearman's rho:

>>> expected = 0.4 * ClaytonCopula(3.0).rho() + 0.6 * GumbelCopula(2.5).rho()
>>> bool(abs(mix.rho() - expected) < 1e-12)
True

Kendall's tau is not, and the clearest case is half comonotonicity and half independence. Spearman's rho is exactly 0.5, as averaging says it must be; Kendall's tau is 0.416, not 0.5:

>>> from rcopula import FrechetUpperCopula, IndependenceCopula
>>> half = MixtureCopula([FrechetUpperCopula(2), IndependenceCopula(2)], [0.5, 0.5])
>>> float(round(half.rho(), 10))
0.5
>>> float(round(half.tau(), 4))
0.4159

n_components property

n_components: int

Number of copulas being mixed.

Returns:

Type Description
int

len(self.copulas), at least 2.

weights property

weights: NDArray[float64]

The share of each component in the mixture.

Recovered from the internal log-odds parameters, so it always reflects the current (e.g. fitted) values.

Returns:

Type Description
numpy.ndarray of float64, shape (k,)

Non-negative weights summing to one, in component order.

Examples:

>>> from rcopula import ClaytonCopula, GumbelCopula
>>> from rcopula.structural import MixtureCopula
>>> mix = MixtureCopula([ClaytonCopula(3.0), GumbelCopula(2.5)], [0.4, 0.6])
>>> mix.weights.round(12).tolist()
[0.4, 0.6]

param_bounds property

param_bounds: list[tuple[float, float]]

Allowed range of each parameter: the components' bounds, then the log-odds.

Returns:

Type Description
list of tuple of (float, float)

One (lower, upper) pair per parameter. Each log-odds is bounded to [-30, 30], beyond which a weight is 0 or 1 to double precision.

tau

tau() -> float

Return Kendall's tau of the mixture, a rank correlation between -1 and 1.

Computed by quadrature -- it is not the weighted average of the components' taus. Tau is quadratic in the copula, so mixing two families with the same tau generally changes it. Averaging the components' values is the natural guess and it is wrong; see the class docstring.

Returns:

Type Description
float

Kendall's tau.

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

NotImplementedError

If dim != 2.

rho

rho() -> float

Return Spearman's rho of the mixture: exactly the weighted average of the components'.

:math:\rho = 12\int\int C - 3 is affine in :math:C, so mixing the copulas mixes their rhos. No integration needed.

Returns:

Type Description
float

sum(w_i * rho_i).

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

lambda_

lambda_() -> TailDependence

Return the chance of joint extremes in each tail, averaged over the components.

Each coefficient is a limit of a quantity affine in :math:C, so it mixes. This is what lets a Clayton-Gumbel mixture have dependence in both corners while each component has it in only one.

Returns:

Type Description
TailDependence

Named tuple (lower, upper) of floats in [0, 1].

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

beta

beta() -> float

Return Blomqvist's beta (medial correlation): the weighted average of the components'.

Blomqvist's beta depends on C only at the centre, C(1/2, 1/2), so it mixes linearly.

Returns:

Type Description
float

sum(w_i * beta_i).

Raises:

Type Description
ValueError

If any parameter is unspecified (nan).

from_tau classmethod

from_tau(tau: float, dim: int = 2, **kwargs: Any) -> Copula

Not available: a mixture has more parameters than one tau value can identify.

Raises:

Type Description
NotImplementedError

Always. Fit the mixture to data instead, or calibrate each component separately and choose the weights yourself.

describe

describe() -> str

Return a multi-line, human-readable summary: one line per component with its weight.

Returns:

Type Description
str

A header line followed by " <weight> x <component description>" rows.

rcopula.structural.nested

Nested (hierarchical) Archimedean copulas.

An Archimedean copula gives every pair of variables the same dependence. That is often wrong in an obvious way: three equity indices and two bond yields do not form one exchangeable block, and forcing them to does not merely lose detail, it puts the wrong number on every cross-asset pair.

Nesting fixes it while keeping the Archimedean machinery. Replace an argument of one generator by a whole copula built from another:

.. math::

C(\mathbf u) = \psi_0\Bigl(
    \sum_{j \in \text{direct}} \psi_0^{-1}(u_j)
    + \sum_{\text{children}} \psi_0^{-1}\bigl(C_{\text{child}}\bigr)\Bigr).

The result is a tree. Leaves sharing a parent are more dependent than leaves that meet only at the root, and the dependence between any two variables is governed by their lowest common ancestor -- which makes Kendall's tau between them exact and immediate, with no integration at all.

For the result to be a copula the generators must nest compatibly. For two copulas of the same family the condition is simply that the inner parameter is at least the outer one: dependence may increase as you descend, never decrease. That is checked on construction rather than left to the user.

============================ ================================================ :class:NestedArchimedean The tree, its CDF, sampler and tau structure. :func:fit_nested Estimate every node by inverting Kendall's tau. ============================ ================================================

.. note::

Sampling is exact for Gumbel and Clayton. Gumbel's inner frailty is an ordinary positive stable law; Clayton's is an exponentially tilted stable, which is why nested Archimedean copulas have not been available in Python before -- see :func:~rcopula.special.stable.retstable. Frank and Joe need their own conditional frailties and are refused explicitly rather than approximated.

The density is not implemented. It requires high-order derivatives of a composition of generators, and every quantity this module does provide -- the CDF, exact sampling, exact pairwise tau and tail dependence, and estimation by tau inversion -- is available without it.

References

Joe, H. (1997). Multivariate Models and Dependence Concepts. Chapman & Hall. The nesting condition. McNeil, A. J. (2008). Sampling nested Archimedean copulas. Journal of Statistical Computation and Simulation 78(6), 567-581. The frailty algorithm implemented here. Hofert, M. (2011). Efficiently sampling nested Archimedean copulas. Computational Statistics & Data Analysis 55(1), 57-70. The tilted-stable inner frailty that nested Clayton needs. Okhrin, O., Okhrin, Y. and Schmid, W. (2013). On the structure and estimation of hierarchical Archimedean copulas. Journal of Econometrics 173(2), 189-204. Estimation by inverting pairwise Kendall's tau.

NestedArchimedean

NestedArchimedean(generator: Copula, components: Sequence[int] | None = None, children: Sequence[NestedArchimedean] | None = None, *, free: ArrayLike | None = None, _validate: bool = True)

Bases: Copula

Model groups of variables that are more tightly linked within a group than across groups.

A plain Archimedean copula gives every pair of variables the same dependence. A nested (hierarchical) Archimedean copula is a tree: each node holds an Archimedean copula, variables hang off the nodes, and two variables are linked by the copula at the node where their branches meet. For example, three equities strongly linked to each other, two bonds strongly linked to each other, and a weaker link between the two blocks.

You build the tree bottom-up: create a node for each group, then a parent node whose children are those groups. Each node object is itself a :class:NestedArchimedean; only the root, which covers variables 0 .. d-1, can be evaluated as a copula.

Parameters:

Name Type Description Default
generator Copula

A one-parameter Archimedean copula (e.g. GumbelCopula(2.0)) supplying this node's generator and parameter. Its dim is ignored -- the tree determines dimension. Every node in a tree must use the same family, and a child's parameter must be at least its parent's.

required
components sequence of int or None

Zero-based indices of the variables attached directly to this node. None means none.

None
children sequence of NestedArchimedean or None

Sub-trees nested inside this node. None means none.

None
free array_like of bool or None

Keyword-only. Accepted for interface compatibility; the tree has no flat parameter vector (estimate it with :func:fit_nested).

None

Attributes:

Name Type Description
generator_copula Copula

The Archimedean copula at this node.

components tuple of int

Variables attached directly to this node.

children tuple of NestedArchimedean

Sub-trees directly below this node.

theta float

This node's parameter (see :attr:theta).

dim int

Number of variables below this node (at least 2).

Raises:

Type Description
TypeError

If generator is not a one-parameter Archimedean copula.

ValueError

If the node has no variables below it, a variable appears twice, a child uses a different family, or a child's parameter is smaller than this node's (the nesting condition).

Notes

Every variable must appear exactly once across the whole tree, and the indices must be 0 .. d-1. Both are checked, because a mis-specified tree otherwise produces a plausible-looking object that is not a copula. Contiguity is checked when the root is used (CDF, sampling, tau matrix), since a sub-tree legitimately covers indices like [3, 4].

What is available: :meth:cdf, :meth:rvs (Gumbel and Clayton only), :meth:tau_matrix and :meth:lambda_matrix. The density is not implemented, so likelihood-based fitting is not possible; use :func:fit_nested.

Examples:

Five variables in two blocks -- the shape a single Archimedean copula cannot express:

>>> import rcopula as rc
>>> from rcopula.structural import NestedArchimedean
>>> equities = NestedArchimedean(rc.GumbelCopula(4.0), [0, 1, 2])
>>> bonds = NestedArchimedean(rc.GumbelCopula(3.0), [3, 4])
>>> cop = NestedArchimedean(rc.GumbelCopula(1.5), children=[equities, bonds])
>>> cop.dim
5
>>> print(cop.describe())
Nested Archimedean copula, dim 5
Gumbel(theta=1.5)
  Gumbel(theta=4) on [0, 1, 2]
  Gumbel(theta=3) on [3, 4]

Kendall's tau depends on where two variables meet, and is exact:

>>> tau = cop.tau_matrix()
>>> float(round(tau[0, 1], 6))     # both inside the equity block
0.75
>>> float(round(tau[3, 4], 6))     # both inside the bond block
0.666667
>>> float(round(tau[0, 3], 6))     # they meet only at the root
0.333333

No single Archimedean copula can produce three different pairwise taus.

theta property

theta: float

This node's dependence parameter (the generator copula's theta).

Returns:

Type Description
float

The parameter; nan if not yet specified.

depth property

depth: int

How many levels the tree has below and including this node.

Returns:

Type Description
int

1 for a node with no children (a flat Archimedean copula), 2 for one level of nesting, and so on.

param_bounds property

param_bounds: list[tuple[float, float]]

Bounds of the flat parameter vector: empty, as the tree keeps parameters per node.

Returns:

Type Description
list

Always [].

leaves

leaves() -> tuple[int, ...]

List every variable that sits somewhere below this node.

Returns:

Type Description
tuple of int

The node's own components, then each child's leaves, recursively (tree order, not sorted).

Examples:

>>> import rcopula as rc
>>> from rcopula.structural import NestedArchimedean
>>> inner = NestedArchimedean(rc.GumbelCopula(3.0), [2, 0])
>>> NestedArchimedean(rc.GumbelCopula(1.5), [1], [inner]).leaves()
(1, 2, 0)

nodes

nodes() -> list[NestedArchimedean]

List this node and every node beneath it.

This order (depth-first, root first) is the one :meth:with_thetas expects.

Returns:

Type Description
list of NestedArchimedean

This node first, then each child's nodes in turn.

with_thetas

with_thetas(thetas: Sequence[float]) -> NestedArchimedean

Return a copy of the tree with new parameters, one per node.

The shape of the tree is kept; only the theta at each node changes. The nesting condition is checked again on the result.

Parameters:

Name Type Description Default
thetas sequence of float

One parameter per node, in :meth:nodes order (root first, depth-first).

required

Returns:

Type Description
NestedArchimedean

The re-parameterised tree.

Raises:

Type Description
ValueError

If the number of values differs from the number of nodes, or if the new values break the nesting condition.

Examples:

>>> import rcopula as rc
>>> from rcopula.structural import NestedArchimedean
>>> inner = NestedArchimedean(rc.GumbelCopula(4.0), [0, 1])
>>> tree = NestedArchimedean(rc.GumbelCopula(1.5), [2], [inner])
>>> [node.theta for node in tree.with_thetas([2.0, 5.0]).nodes()]
[2.0, 5.0]

lowest_common_ancestor

lowest_common_ancestor(i: int, j: int) -> NestedArchimedean

Find the node where the branches of variables i and j meet.

That node's generator is the bivariate copula of the pair, so its parameter alone sets their dependence.

Parameters:

Name Type Description Default
i int

Two different variable indices below this node.

required
j int

Two different variable indices below this node.

required

Returns:

Type Description
NestedArchimedean

The deepest node whose leaves include both i and j.

Raises:

Type Description
ValueError

If i == j, or if i or j is not a variable of this (sub-)tree.

Examples:

>>> import rcopula as rc
>>> from rcopula.structural import NestedArchimedean
>>> inner = NestedArchimedean(rc.GumbelCopula(3.0), [0, 1])
>>> tree = NestedArchimedean(rc.GumbelCopula(1.5), [2], [inner])
>>> tree.lowest_common_ancestor(0, 1) is tree.children[0]
True
>>> tree.lowest_common_ancestor(0, 2) is tree
True

tau_matrix

tau_matrix() -> NDArray[np.float64]

Return Kendall's tau (a rank correlation) for every pair of variables, exactly.

Two variables meet at exactly one node, and that node's generator is the bivariate copula of the pair -- so the tau is the generator's own, with no integration. A flat Archimedean copula would give one number for every pair; this gives one per branch of the tree.

Returns:

Type Description
numpy.ndarray of float64, shape (d, d)

Symmetric matrix with ones on the diagonal; entry [i, j] is Kendall's tau between variables i and j.

Raises:

Type Description
ValueError

If this node is not a root covering 0 .. d-1, or if any node's parameter is unspecified.

lambda_matrix

lambda_matrix() -> tuple[NDArray[np.float64], NDArray[np.float64]]

Return the tail-dependence coefficients for every pair of variables.

By the same argument as :meth:tau_matrix: each pair takes the coefficients of the generator at the node where it meets.

Returns:

Name Type Description
lower numpy.ndarray of float64, shape (d, d)

Lower tail-dependence coefficients; ones on the diagonal.

upper numpy.ndarray of float64, shape (d, d)

Upper tail-dependence coefficients; ones on the diagonal.

Raises:

Type Description
ValueError

If this node is not a root covering 0 .. d-1, or if any node's parameter is unspecified.

tau

tau() -> float

Not available: a nested copula has no single tau, which is the point of it.

Raises:

Type Description
NotImplementedError

Always. Use :meth:tau_matrix for the pairwise values.

rho

rho() -> float

Not available: Spearman's rho differs from pair to pair in a nested copula.

Raises:

Type Description
NotImplementedError

Always. Estimate it pairwise from a sample instead.

lambda_

lambda_() -> TailDependence

Not available: tail dependence differs from pair to pair in a nested copula.

Raises:

Type Description
NotImplementedError

Always. Use :meth:lambda_matrix for the pairwise values.

describe

describe() -> str

Return a multi-line, indented picture of the tree with each node's parameter.

Returns:

Type Description
str

A header line, then one indented line per node such as " Gumbel(theta=4) on [0, 1, 2]".

from_tau classmethod

from_tau(tau: float, dim: int = 2, **kwargs: Any) -> Copula

Not available: one tau value cannot set a parameter at every node.

Raises:

Type Description
NotImplementedError

Always. Use :func:fit_nested, or set each node's parameter.

fit_nested

fit_nested(structure: NestedArchimedean, data: ArrayLike) -> NestedArchimedean

Fit a nested Archimedean copula to data, given the tree shape you want.

You supply the structure (which variables are grouped under which node); this estimates the parameter at every node from the sample's pairwise Kendall's tau. No density is needed.

Each node governs the pairs whose lowest common ancestor it is, so the natural estimator averages the sample tau over exactly those pairs and inverts the generator's own tau function. That is Okhrin, Okhrin & Schmid's estimator and R's etau, and it needs no density -- which matters here, because the nested density is not available.

Two constraints are then imposed, root first:

  • The family's attainable range. A node's averaged tau is clipped to the Kendall's tau its family can reach. For a genuinely nested tree (depth at least 2) that is [0, 1) for every family -- Gumbel and Joe cannot go below independence at all, and Clayton, Frank and AMH reach negative tau only as bivariate copulas, while the nesting condition is only known to hold for generators that are completely monotone -- capped at 1/3 for AMH. A flat tree (one node) gets the family's range in structure.dim dimensions, so a bivariate Clayton or Frank can still be negatively dependent. Near +-1 the clip is +-0.999 for families whose parameter is unbounded there. A weakly dependent block whose sample tau is slightly negative therefore fits as (near-)independence instead of raising.
  • The nesting condition. A child's parameter is raised to its parent's if sampling noise left it below (a running maximum down the tree); otherwise the result would not be a copula.

Either adjustment is silent when it moves a node's tau by at most 0.05 -- ordinary sampling noise -- and emits a :class:UserWarning naming the node when it moves it further, since the tree shape or the family is then probably wrong for the data.

Parameters:

Name Type Description Default
structure NestedArchimedean

The tree shape and family (a root covering 0 .. d-1). Its parameters are ignored and replaced.

required
data array_like of float, shape (n, d)

Observations, on any scale -- only ranks are used.

required

Returns:

Type Description
NestedArchimedean

A tree of the same shape with an estimated parameter at every node, satisfying the nesting condition.

Raises:

Type Description
ValueError

If data does not have d columns.

Warns:

Type Description
UserWarning

If clipping to the family's range or enforcing the nesting condition changes a node's Kendall's tau by more than 0.05.

Examples:

>>> import rcopula as rc
>>> from rcopula.structural import NestedArchimedean, fit_nested
>>> inner = NestedArchimedean(rc.GumbelCopula(4.0), [0, 1, 2])
>>> truth = NestedArchimedean(rc.GumbelCopula(1.6), [3], [inner])
>>> u = truth.rvs(4000, random_state=0)
>>> fitted = fit_nested(truth, u)
>>> bool(abs(fitted.theta - 1.6) < 0.15)
True
>>> bool(abs(fitted.children[0].theta - 4.0) < 0.4)
True

A block that is independent of the rest fits as independence, even when its sample tau comes out slightly negative:

>>> import numpy as np
>>> block = rc.GumbelCopula(3.0).rvs(500, random_state=1)
>>> noise = np.random.default_rng(7).uniform(size=(500, 1))
>>> shape = NestedArchimedean(
...     rc.GumbelCopula(1.5), [2], [NestedArchimedean(rc.GumbelCopula(3.0), [0, 1])]
... )
>>> fit_nested(shape, np.hstack([block, noise])).theta
1.0

rcopula.structural.opower

The outer power transformation (R's opower).

Every Archimedean family here is one-parameter, and one parameter buys one shape: Clayton is always lower-tail dependent and never upper, Gumbel always the reverse. The outer power transformation adds a second parameter that acts on the tail behaviour while leaving the family's character intact.

Given a generator :math:\psi and :math:\alpha \ge 1, the transformed generator is

.. math:: \psi_\alpha(t) = \psi\bigl(t^{1/\alpha}\bigr),

whose inverse is :math:\psi_\alpha^{-1}(u) = \{\psi^{-1}(u)\}^{\alpha}, so

.. math::

C_\alpha(u_1, \dots, u_d)
  = \psi\Bigl(\bigl\{\textstyle\sum_j (\psi^{-1}(u_j))^{\alpha}\bigr\}^{1/\alpha}\Bigr).

Two facts make it worth having. Kendall's tau moves by a closed form,

.. math:: \tau_\alpha = 1 - \frac{1 - \tau_\psi}{\alpha},

so the transformation is a dial from the base family's dependence up towards comonotonicity. And it creates upper tail dependence where there was none: applied to the independence generator :math:\psi(t) = e^{-t} it produces exactly the Gumbel copula with parameter :math:\alpha, which is the cleanest possible statement of what it does.

============================ ================================================ :class:OuterPowerCopula The transformed family. :func:opower Convenience constructor. ============================ ================================================

.. warning::

The density is implemented for :math:d = 2 only. In higher dimensions it needs the :math:d-th derivative of a composition of generators, which is the same open problem that leaves :class:~rcopula.structural.nested.NestedArchimedean without one. The CDF, Kendall's tau and tail dependence work in any dimension.

Examples:

>>> import rcopula as rc
>>> from rcopula.structural.opower import opower
>>> base = rc.ClaytonCopula(2.0)
>>> lifted = opower(base, 1.5)
>>> round(base.tau(), 4), round(lifted.tau(), 4)
(0.5, 0.6667)

Clayton has no upper tail dependence; the transformation supplies some:

>>> round(base.lambda_().upper, 4), round(lifted.lambda_().upper, 4)
(0.0, 0.4126)
References

Nelsen, R. B. (2006). An Introduction to Copulas, 2nd ed. Springer, Theorem 4.5.1 and exercise 4.23 -- the transformation and its tau. Hofert, M. (2010). Sampling Nested Archimedean Copulas. PhD thesis, Ulm University. The outer power transformation and its frailty. Hofert, M. and Mächler, M. (2011). Nested Archimedean copulas meet R: the nacopula package. J. Statistical Software 39(9), 1-20.

OuterPowerCopula

OuterPowerCopula(base: ArchimedeanCopula, alpha: float = np.nan, *, free: ArrayLike | None = None)

Bases: Copula

Give an Archimedean copula a second parameter that adds dependence and an upper tail.

One-parameter Archimedean families have one fixed shape (Clayton: strong lower tail, no upper tail). The outer power transformation replaces the generator :math:\psi(t) by :math:\psi(t^{1/\alpha}) with :math:\alpha \ge 1. Raising alpha moves Kendall's tau towards 1 by the closed form :math:\tau_\alpha = 1 - (1 - \tau_\psi)/\alpha and creates upper tail dependence, so you get a two-parameter family to fit. :func:opower is a shorter way to build one.

Parameters:

Name Type Description Default
base ArchimedeanCopula

Supplies :math:\psi, e.g. ClaytonCopula(2.0). Its parameter becomes the first parameter here, and its dimension is used.

required
alpha float

The outer power, :math:\alpha \ge 1. At :math:\alpha = 1 the transformation is the identity and this is the base copula. nan means "to be estimated by fitting".

nan
free array_like of bool, shape (2,), or None

Keyword-only. Which of (theta, alpha) are estimated when fitting; None means both.

None

Attributes:

Name Type Description
base ArchimedeanCopula

The copula whose generator is transformed.

alpha float

The outer power (see :attr:alpha).

params numpy.ndarray of float64, shape (2,)

(theta, alpha).

param_names tuple of str

The base generator's parameter name, then "alpha".

Raises:

Type Description
TypeError

If base is not an Archimedean copula.

ValueError

If alpha < 1 or theta lies outside the base family's range.

Notes

Parameters are ordered (theta, alpha), so fit estimates both. The density and sampler are available for d = 2 only; the CDF, Kendall's tau and tail dependence work in any dimension.

Examples:

>>> import numpy as np, rcopula as rc
>>> from rcopula.structural.opower import OuterPowerCopula
>>> cop = OuterPowerCopula(rc.ClaytonCopula(1.5), 2.0)
>>> cop.dim, [round(float(p), 3) for p in cop.params]
(2, [1.5, 2.0])

At alpha = 1 it reduces to the base copula exactly:

>>> identity = OuterPowerCopula(rc.ClaytonCopula(1.5), 1.0)
>>> u = np.array([[0.3, 0.7], [0.5, 0.5], [0.9, 0.2]])
>>> bool(np.allclose(identity.cdf(u), rc.ClaytonCopula(1.5).cdf(u)))
True

param_bounds property

param_bounds: list[tuple[float, float]]

Allowed range of (theta, alpha): the base family's range, then [1, inf).

Returns:

Type Description
list of tuple of (float, float)

Two (lower, upper) pairs.

alpha property

alpha: float

The outer power :math:lpha (1 means no transformation).

Returns:

Type Description
float

The current value; nan if not yet specified.

tau

tau() -> float

Return Kendall's tau, a rank correlation, from its closed form.

:math:\tau = 1 - (1 - \tau_\psi)/\alpha, where :math:\tau_\psi is the base copula's tau.

Returns:

Type Description
float

Kendall's tau.

Raises:

Type Description
ValueError

If theta or alpha is unspecified (nan).

Examples:

>>> import rcopula as rc
>>> from rcopula.structural.opower import opower
>>> round(opower(rc.ClaytonCopula(2.0), 2.0).tau(), 4)
0.75

rho

rho() -> float

Return Spearman's rho, a rank correlation, computed numerically from the CDF.

Returns:

Type Description
float

Spearman's rho, by quadrature.

Raises:

Type Description
ValueError

If theta or alpha is unspecified (nan).

lambda_

lambda_() -> TailDependence

Return the chance of joint extremes in each tail, estimated close to the corners.

The coefficients are the diagonal limits :math:\lambda_L = \lim C(u, u)/u and :math:\lambda_U = \lim (1 - 2u + C(u, u))/(1 - u), evaluated at u = 1e-6 and u = 1 - 1e-6.

Evaluated numerically rather than from a closed form: the transformation changes the regular variation of :math:\psi' at each end differently for each base family, and a formula asserted for all of them would be wrong for some.

Returns:

Type Description
TailDependence

Named tuple (lower, upper) of floats clipped to [0, 1].

Raises:

Type Description
ValueError

If theta or alpha is unspecified (nan).

Examples:

The transformation creates upper tail dependence in a family that has none:

>>> import rcopula as rc
>>> from rcopula.structural.opower import opower
>>> rc.ClaytonCopula(2.0).lambda_().upper
0.0
>>> bool(opower(rc.ClaytonCopula(2.0), 2.0).lambda_().upper > 0.5)
True

describe

describe() -> str

Return a one-line, human-readable summary with the family and both parameters.

Returns:

Type Description
str

For example "Outer-power Clayton copula, dim 2, theta=2, alpha=1.5".

opower

opower(base: ArchimedeanCopula, alpha: float, **kwargs: Any) -> OuterPowerCopula

Add a second "dial" to an Archimedean copula via the outer power transformation.

Shorthand for OuterPowerCopula(base, alpha, **kwargs) (R's opower). The result has parameters (theta, alpha): larger alpha means stronger dependence and more upper-tail dependence.

Parameters:

Name Type Description Default
base ArchimedeanCopula

The copula whose generator is transformed, e.g. ClaytonCopula(2.0).

required
alpha float

The power, at least 1 (nan to leave it for fitting).

required
**kwargs Any

Passed to :class:OuterPowerCopula; currently only free.

{}

Returns:

Type Description
OuterPowerCopula

The transformed copula, of the same dimension as base.

Raises:

Type Description
TypeError

If base is not an Archimedean copula.

ValueError

If alpha < 1.

Examples:

The cleanest statement of what the transformation does: applied to the independence generator it is the Gumbel copula.

>>> import numpy as np, rcopula as rc
>>> from rcopula.structural.opower import opower
>>> nearly_independent = rc.ClaytonCopula(1e-10)
>>> lifted = opower(nearly_independent, 2.5)
>>> u = np.array([[0.3, 0.7], [0.5, 0.5], [0.9, 0.2]])
>>> bool(np.max(np.abs(lifted.cdf(u) - rc.GumbelCopula(2.5).cdf(u))) < 1e-6)
True

And it moves Kendall's tau by a closed form:

>>> base = rc.FrankCopula(4.0)
>>> round(base.tau(), 4), round(opower(base, 3.0).tau(), 4)
(0.3881, 0.796)

rcopula.structural.marginal

Lower-dimensional margins of a copula (R's margCopula).

A :math:d-dimensional copula contains every lower-dimensional copula inside it: fix a subset of coordinates and let the rest go to one, and what is left is itself a copula. Extracting it is the natural way to ask "what does my five-asset model say about these two?", and to check a fitted model against a bivariate diagnostic that only makes sense in two dimensions -- a Pickands plot, a K-plot, a tail concentration function.

For most families the answer is immediate: an Archimedean copula's margin is the same generator in fewer dimensions, and an elliptical copula's is the corresponding sub-matrix of its correlation. R supports exactly those two classes and refuses everything else. Here the structural constructions come too, because a rotation, a mixture and a Khoudraji device all commute with taking a margin.

============================ ================================================ :func:marginal_copula The margin on a chosen subset of coordinates. ============================ ================================================

Examples:

>>> import rcopula as rc
>>> from rcopula.structural.marginal import marginal_copula
>>> five = rc.GaussianCopula(
...     [0.72, 0.63, 0.54, 0.45, 0.56, 0.48, 0.40, 0.42, 0.35, 0.30],
...     dim=5, dispstr="un",
... )
>>> pair = marginal_copula(five, [0, 3])
>>> pair.dim
2
>>> float(pair.params[0]) == float(five.sigma()[0, 3])
True
References

Nelsen, R. B. (2006). An Introduction to Copulas, 2nd ed. Springer, Theorem 2.10.13 -- margins of a copula are copulas. Joe, H. (2014). Dependence Modeling with Copulas. Chapman & Hall/CRC.

marginal_copula

marginal_copula(copula: Copula, indices: Sequence[int]) -> Copula

Extract the dependence between just some of the variables from a larger copula.

Given a d-dimensional copula, return the copula of a chosen subset of its coordinates (R's margCopula). Use it to ask what a five-asset model says about two of the assets, or to run a bivariate-only diagnostic (a Pickands plot, a K-plot, a tail-concentration plot) on a pair taken from a larger fitted model. Nothing is re-estimated: the result is read off the existing parameters.

Supported inputs: Archimedean families (same parameters -- all of them, for a multi-parameter subclass -- and the same class, fewer dimensions), Gaussian and Student-t copulas (the sub-matrix of the correlation), independence and comonotonicity, and the structural wrappers -- :class:~rcopula.structural.RotatedCopula, :class:~rcopula.structural.MixtureCopula, and for pairs only :class:~rcopula.structural.NestedArchimedean and :class:~rcopula.structural.KhoudrajiCopula.

Parameters:

Name Type Description Default
copula Copula

The d-dimensional copula to take the margin of.

required
indices sequence of int

Zero-based coordinates to keep, in the order they should appear. At least two, all distinct, all in [0, d). The order matters for an asymmetric copula: [1, 0] is not [0, 1] unless the copula is exchangeable. Passing every coordinate in increasing order returns copula itself.

required

Returns:

Type Description
Copula

A copula of dimension len(indices), usually of the same family. Elliptical margins always come back with dispstr="un".

Raises:

Type Description
ValueError

If fewer than two indices are given, if they repeat, or if any is out of range.

NotImplementedError

For families whose margins are not of the same family -- the empirical copula, and extreme-value families defined only in two dimensions -- and for nested or Khoudraji margins of more than two coordinates.

Notes

Exchangeable families (Archimedean, and elliptical with dispstr="ex") have the same margin whichever coordinates are chosen, so indices only fixes the dimension. For an unstructured correlation matrix it selects the sub-matrix, and for ar1 or toep the result is generally not of the same structure -- dropping a coordinate from an AR(1) chain leaves a gap -- so it comes back unstructured.

Examples:

An Archimedean margin is the same generator in fewer dimensions, so its Kendall's tau is unchanged:

>>> import rcopula as rc
>>> from rcopula.structural.marginal import marginal_copula
>>> five = rc.ClaytonCopula(2.0, dim=5)
>>> pair = marginal_copula(five, [1, 4])
>>> pair.dim, float(pair.params[0])
(2, 2.0)
>>> bool(abs(pair.tau() - five.tau()) < 1e-12)
True

And the margin agrees with the parent, which is the property that defines it -- setting the dropped coordinates to one:

>>> import numpy as np
>>> point = np.array([[0.3, 0.7]])
>>> full = np.array([[1.0, 0.3, 1.0, 1.0, 0.7]])
>>> bool(abs(pair.cdf(point)[0] - five.cdf(full)[0]) < 1e-12)
True

Dropping a coordinate from an AR(1) chain leaves a gap, so the result is unstructured rather than AR(1):

>>> chain = rc.GaussianCopula(0.8, dim=4, dispstr="ar1")
>>> gapped = marginal_copula(chain, [0, 2, 3])
>>> gapped.dispstr
'un'
>>> bool(np.allclose(gapped.sigma(), chain.sigma()[np.ix_([0, 2, 3], [0, 2, 3])]))
True

Vines

rcopula.vine

Vine copulas: a joint distribution built from bivariate pieces.

Archimedean copulas give every pair the same dependence; elliptical ones give every pair the same shape. A vine gives up neither flexibility nor tractability: it factorises a :math:d-dimensional density into :math:d(d-1)/2 bivariate copulas, each of which may be a different family with a different parameter.

.. math::

c(u_1,\dots,u_d) = \prod_{k=1}^{d-1}\prod_{i}
    c_{\,e_{k,i}}\bigl(F(u_a \mid \mathbf u_D),\, F(u_b \mid \mathbf u_D)\bigr)

The conditioning arguments are h-functions -- conditional distribution functions -- and every one of them is available in closed form for the Archimedean and elliptical families, which is what makes the construction practical rather than merely decomposable.

Three structures are implemented here:

  • a C-vine (canonical) puts one variable at the centre of each tree, which suits a market factor plus its satellites;
  • a D-vine (drawable) lays each tree out as a path, which suits an ordering -- a term structure, a spatial transect, a time series of maturities;
  • a regular vine (R-vine) allows any sequence of trees that satisfies the proximity condition -- two edges may be joined in the next tree only if they share a node. C- and D-vines are the two extreme special cases. An R-vine is written down as an R-vine matrix (Dissmann et al. 2013), the same convention as R's VineCopula package.

============================ ================================================ :class:VineCopula The construction: density, sampler, Rosenblatt. :func:fit_vine Sequential estimation, selecting each pair (and, for an R-vine, each tree). :data:EXTENDED_FAMILIES Pair-copula families with rotations and BB1/BB7. ============================ ================================================

Correctness has an unusually sharp check available. A vine whose pair-copulas are all Gaussian is itself a Gaussian copula, with a correlation matrix determined by the pair parameters through the partial-correlation recursion. So :meth:VineCopula.to_gaussian reconstructs that matrix and the vine's density can be compared against :class:~rcopula.core.elliptical.GaussianCopula directly -- an exact identity, not a tolerance.

References

Joe, H. (1996). Families of m-variate distributions with given margins and m(m-1)/2 bivariate dependence parameters. In Distributions with Fixed Marginals and Related Topics, IMS Lecture Notes 28, 120-141. Bedford, T. and Cooke, R. M. (2002). Vines -- a new graphical model for dependent random variables. Annals of Statistics 30(4), 1031-1068. Aas, K., Czado, C., Frigessi, A. and Bakken, H. (2009). Pair-copula constructions of multiple dependence. Insurance: Mathematics and Economics 44(2), 182-198. The likelihood and simulation algorithms implemented here. Dissmann, J., Brechmann, E. C., Czado, C. and Kurowicka, D. (2013). Selecting and estimating regular vine copulae and application to financial returns. Computational Statistics & Data Analysis 59, 52-69. The sequential selection used by :func:fit_vine, and the R-vine matrix. Joe, H. (2014). Dependence Modeling with Copulas. Chapman & Hall/CRC, section 6.17 -- the inverse h-functions used by the sampler.

VineCopula

VineCopula(pair_copulas: Sequence[Sequence[Copula]], structure: str = 'D', order: Sequence[int] | None = None, *, matrix: ArrayLike | None = None, free: ArrayLike | None = None)

Bases: Copula

A many-variable copula built by chaining together two-variable copulas.

Technically a pair-copula construction (vine copula). Each pair of variables -- and, in later "trees", each pair conditional on the variables between them -- gets its own bivariate copula, which may be a different family with a different parameter. Use it when one family cannot describe every pair: for example, crash-together (Clayton) dependence between two assets but symmetric (Frank) dependence elsewhere. To estimate one from data, use :func:fit_vine rather than building it by hand.

Parameters:

Name Type Description Default
pair_copulas sequence of sequence of Copula

The bivariate copulas, grouped by tree. pair_copulas[k] holds tree k's copulas, so for d variables there are d - 1 trees and tree k has d - 1 - k entries (d(d-1)/2 copulas in all). Every entry must have dim == 2. The dimension d is inferred as len(pair_copulas) + 1. For an R-vine, pair_copulas[k][i] is the copula of the edge in column i of matrix (see below).

required
structure ('C', 'D', 'R')

"C" (canonical): each tree is a star around one central variable, which suits one market factor plus its satellites. "D" (drawable): each tree is a path, which suits naturally ordered variables such as maturities. "R" (regular): any trees allowed by the proximity condition, given by matrix.

"C"
order sequence of int or None

A permutation of 0, 1, ..., d-1 saying which data column takes which position in the structure. For a C-vine the first entry is the root of tree 1; for a D-vine the sequence is the path. None means 0, 1, ..., d-1. Must be None for an R-vine, whose matrix already says where every variable sits.

None
matrix array_like of int, shape (d, d), or None

The R-vine matrix, required when structure="R" and refused otherwise. It follows R VineCopula's RVineMatrix convention with 0-based variable labels (subtract 1 from an R matrix): a lower-triangular matrix M whose diagonal is a permutation of the variables; column i, row r > i is the edge joining M[r, i] and M[i, i] given M[r+1:, i], in tree d - r (row d - 1 is tree 1). Its pair-copula is pair_copulas[d - 1 - r][i], evaluated at (u_{M[r,i] | given}, u_{M[i,i] | given}) -- the off-diagonal variable first, as in VineCopula. Entries above the diagonal are ignored.

None, keyword-only
free array_like of bool or None

Free/fixed mask passed to :class:~rcopula.core.base.Copula. A vine has no top-level parameters of its own (its parameters live in the pair-copulas), so this is accepted for interface compatibility and normally left as None.

None, keyword-only

Attributes:

Name Type Description
pair_copulas list of list of Copula

The bivariate copulas by tree, as given.

structure str

"C", "D" or "R".

order tuple of int

The variable ordering, length d: the order in which :meth:rvs draws the variables and :meth:rosenblatt conditions them. For an R-vine it is the matrix diagonal read from the bottom up.

matrix numpy.ndarray of int, shape (d, d)

The R-vine matrix (0-based labels). Computed for a C- or D-vine, so any vine can be passed to R or rebuilt as structure="R".

dim int

Number of variables d.

n_pairs int

Number of bivariate copulas, d(d-1)/2.

is_gaussian bool

Whether every pair-copula is Gaussian.

truncation_level int

Number of leading trees that carry dependence; every tree after it is all independence copulas.

Raises:

Type Description
ValueError

If structure is not "C", "D" or "R", if a tree has the wrong number of copulas for the dimension, if any pair-copula is not bivariate, if order is not a permutation of 0..d-1, if matrix is missing for an R-vine (or given for a C- or D-vine), or if matrix is not a valid R-vine matrix.

Notes

The density, the sampler (:meth:rvs) and the Rosenblatt transform are exact. All three stop at :attr:truncation_level: trees made entirely of independence copulas after it are skipped, so a vine truncated after one tree samples in time linear in d (an 801-variable C-vine with one Student-t tree draws 50,000 rows in well under a minute).

The sampler inverts each pair-copula's h-function in closed form where an exact inverse exists (Gaussian, Student t, Clayton, Frank and their rotations; BB1 and BB7 by a safeguarded Newton solve) and by bisection otherwise (Gumbel, Joe, ...). rcopula 0.4.0 and earlier used bisection everywhere on a full vine. Seeded draws agree with those to about 1e-13 where the old h-function was exact and to about 1e-7 where it was a numerical derivative (rotated families), and a ten-variable full vine now samples 15 to 40 times faster.

An R-vine matrix is checked for the proximity condition in every tree that carries dependence; trees past :attr:truncation_level hold only independence copulas, do not affect the distribution, and are checked only for the column condition (which keeps a 500-variable truncated R-vine cheap to build).

There is no closed-form distribution function, so :meth:cdf raises :class:NotImplementedError; neither are there single-number tau/rho/lambda_ summaries, since every pair has its own.

Examples:

A three-dimensional D-vine mixing three families -- something no single parametric copula can express:

>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> vine = VineCopula(
...     [[rc.ClaytonCopula(2.0), rc.GumbelCopula(2.5)], [rc.FrankCopula(3.0)]],
...     structure="D",
... )
>>> vine.dim
3
>>> u = vine.rvs(2000, random_state=0)
>>> u.shape
(2000, 3)

Tree 1 pairs adjacent variables, so their dependence is the pair-copula's:

>>> from scipy import stats
>>> tau = stats.kendalltau(u[:, 0], u[:, 1]).statistic
>>> bool(abs(tau - rc.ClaytonCopula(2.0).tau()) < 0.03)
True

An all-Gaussian vine is a Gaussian copula, which makes it checkable against one exactly:

>>> gaussian = VineCopula(
...     [[rc.GaussianCopula(0.6), rc.GaussianCopula(0.5)], [rc.GaussianCopula(0.3)]],
...     structure="D",
... )
>>> equivalent = gaussian.to_gaussian()
>>> import numpy as np
>>> pts = np.array([[0.3, 0.5, 0.7], [0.8, 0.2, 0.4]])
>>> bool(np.allclose(gaussian.logpdf(pts), equivalent.logpdf(pts), atol=1e-9))
True

A regular vine on five variables whose first tree is neither a star nor a path: variable 0 is joined to 1, 2 and 3, and variable 4 hangs off 3. Column i of the matrix lists, from the bottom up, the variables its diagonal entry is paired with in trees 1, 2, ... (this is the example matrix of R VineCopula's RVineMatrix help page, minus 1):

>>> m = np.array(
...     [[4, 0, 0, 0, 0], [1, 1, 0, 0, 0], [2, 2, 2, 0, 0], [0, 3, 3, 3, 0], [3, 0, 0, 0, 0]]
... )
>>> rvine = VineCopula(
...     [
...         [
...             rc.ClaytonCopula(2.0),
...             rc.GumbelCopula(1.5),
...             rc.FrankCopula(4.0),
...             rc.GaussianCopula(0.5),
...         ],
...         [
...             rc.GaussianCopula(0.3),
...             rc.RotatedCopula(rc.ClaytonCopula(1.0), 90),
...             rc.FrankCopula(-2.0),
...         ],
...         [rc.StudentCopula(0.2, df=6.0), rc.GumbelCopula(1.2)],
...         [rc.IndependenceCopula(2)],
...     ],
...     structure="R",
...     matrix=m,
... )
>>> print(rvine.describe())
R-vine copula, dim 5, order [0, 3, 2, 1, 4]
  tree 1  3,4            Clayton copula, dim 2, theta=2
  tree 1  0,1            Gumbel copula, dim 2, theta=1.5
  tree 1  0,2            Frank copula, dim 2, theta=4
  tree 1  0,3            Gaussian copula, dim 2, rho=0.5
  tree 2  0,4|3          Gaussian copula, dim 2, rho=0.3
  tree 2  3,1|0          90-degree rotated Clayton copula, dim 2, theta=1
  tree 2  3,2|0          Frank copula, dim 2, theta=-2
  tree 3  2,4|0,3        Student copula, dim 2, rho=0.2, df=6
  tree 3  2,1|3,0        Gumbel copula, dim 2, theta=1.2
  tree 4  1,4|2,0,3      Independence copula, dim 2
>>> rvine.truncation_level
3
>>> u = rvine.rvs(4000, random_state=0)
>>> w = rvine.rosenblatt(u)
>>> bool(np.all(np.abs(np.corrcoef(w, rowvar=False) - np.eye(5)) < 0.06))
True

n_pairs property

n_pairs: int

How many bivariate copulas the vine contains.

Returns:

Type Description
int

d(d-1)/2 for a d-dimensional vine.

param_bounds property

param_bounds: list[tuple[float, float]]

Bounds on the vine's own top-level parameters: always empty.

A vine's parameters belong to its pair-copulas, so there is nothing at this level to bound.

Returns:

Type Description
list of tuple of (float, float)

Always [].

matrix property

matrix: NDArray[int64]

The vine's R-vine matrix, with 0-based variable labels.

For an R-vine this is the matrix it was built from (upper triangle zeroed); for a C- or D-vine it is computed, so every vine has one. Add 1 to every entry on and below the diagonal to get R VineCopula's RVineMatrix. The edge in column i, row r has pair-copula pair_copulas[d - 1 - r][i] -- for a C- or D-vine that is the tree's copula list reversed (see :meth:to_rvine).

Returns:

Type Description
numpy.ndarray of int, shape (d, d)

Lower-triangular, read-only.

Examples:

>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> vine = VineCopula(
...     [[rc.ClaytonCopula(2.0), rc.GumbelCopula(2.5)], [rc.FrankCopula(3.0)]],
...     structure="D",
... )
>>> vine.matrix.tolist()
[[2, 0, 0], [0, 1, 0], [1, 0, 0]]

edges property

edges: list[tuple[int, int, int, tuple[int, ...], Copula]]

Every edge of the vine as (tree, first, second, conditioning, copula).

tree is 0-based (it indexes :attr:pair_copulas); first and second are the conditioned variables in the order the copula takes them, and conditioning the variables they are conditioned on, all in original column indices.

Returns:

Type Description
list of tuple of (int, int, int, tuple of int, Copula)

d(d-1)/2 entries, tree by tree.

Examples:

>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> vine = VineCopula(
...     [[rc.ClaytonCopula(2.0), rc.GumbelCopula(2.5)], [rc.FrankCopula(3.0)]],
...     structure="D",
... )
>>> [(t, a, b, given) for t, a, b, given, _ in vine.edges]
[(0, 0, 1, ()), (0, 1, 2, ()), (1, 0, 2, (1,))]

truncation_level property

truncation_level: int

How many trees carry dependence: the last tree with a non-independence pair-copula.

A vine is truncated at level t when every pair-copula in trees t + 1, ..., d - 1 is the independence copula, as :func:fit_vine produces with truncate=t. Those trees contribute nothing to the density and their conditional transforms are the identity, so the density, :meth:rvs and :meth:rosenblatt stop after tree t.

Returns:

Type Description
int

Between 0 (every pair-copula is independence) and d - 1 (the last tree carries dependence, i.e. the vine is not truncated).

is_gaussian property

is_gaussian: bool

Whether every pair-copula is Gaussian, which makes the whole vine Gaussian.

When True, :meth:to_gaussian can return the equivalent :class:~rcopula.core.elliptical.GaussianCopula.

Returns:

Type Description
bool

to_rvine

to_rvine() -> VineCopula

Re-express the vine as an R-vine (structure="R") with the same distribution.

A C- or D-vine is a special case of a regular vine, so this only rewrites the bookkeeping: the result has :attr:matrix as its matrix and each tree's pair-copulas in column order. Its density is the same function; its sampler draws the variables in the same :attr:order. An R-vine is returned unchanged.

Returns:

Type Description
VineCopula

A vine with structure == "R".

Examples:

>>> import numpy as np
>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> cvine = VineCopula(
...     [[rc.ClaytonCopula(2.0), rc.GumbelCopula(2.5)], [rc.FrankCopula(3.0)]],
...     structure="C",
... )
>>> rvine = cvine.to_rvine()
>>> rvine.structure
'R'
>>> pts = np.array([[0.3, 0.5, 0.7], [0.8, 0.2, 0.4]])
>>> bool(np.allclose(cvine.logpdf(pts), rvine.logpdf(pts), atol=1e-12))
True

loglik

loglik(data: ArrayLike) -> float

Sum the log-density over all rows: how well the vine explains the data.

Higher is better. Useful for comparing fitted vines on the same data, or for building an AIC/BIC by hand.

Parameters:

Name Type Description Default
data array_like of float, shape (n, d)

Observations. If every value already lies strictly inside (0, 1) they are used as-is (treated as copula-scale data); otherwise they are converted to pseudo-observations (ranks scaled into (0, 1)) first.

required

Returns:

Type Description
float

The total log-likelihood, sum(log c(u_i)).

rosenblatt

rosenblatt(u: ArrayLike) -> NDArray[np.float64]

Turn dependent copula data into independent uniform columns.

This is the Rosenblatt transform: each column is replaced by its conditional probability given the variables before it in the vine's :attr:order (the D-vine path, the C-vine roots, or the R-vine matrix diagonal read bottom-up). If the vine is the right model for the data, the output columns are independent and uniform on (0, 1), so it is the standard way to check a fitted vine (test the output for independence) and the inverse of how :meth:rvs samples.

Parameters:

Name Type Description Default
u array_like of float, shape (n, d)

Copula-scale data (values in (0, 1)), with columns in the original variable order; the vine's order is applied internally.

required

Returns:

Type Description
numpy.ndarray of float, shape (n, d)

The transformed values, in the original column order -- the same order as the input, :meth:rvs and :meth:logpdf. Column order[i] holds :math:P(U_{order[i]} \le u_{order[i]} \mid U_{order[0]}, \dots, U_{order[i-1]}), so column order[0] is passed through unchanged and column order[-1] is conditioned on every other variable. (rcopula 0.2.0 and earlier returned the columns in path order; with the default order the two agree.)

Raises:

Type Description
ValueError

If u does not have d columns.

Notes

The forward direction of the sampler, and the sharpest available check on both: under the true vine the output is independent :math:\mathrm{Unif}(0,1), so any error in either recursion shows up as dependence that should not be there.

A C-vine or R-vine is transformed through its R-vine form (see :meth:to_rvine); a D-vine uses its own recursion. (rcopula 0.4.0 and earlier raised NotImplementedError for a C-vine.)

Examples:

>>> import numpy as np
>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> vine = VineCopula(
...     [[rc.ClaytonCopula(2.0), rc.FrankCopula(4.0)], [rc.GumbelCopula(1.5)]],
...     structure="D",
... )
>>> w = vine.rosenblatt(vine.rvs(4000, random_state=1))
>>> w.shape
(4000, 3)
>>> bool(np.all(np.abs(np.corrcoef(w, rowvar=False) - np.eye(3)) < 0.06))
True

to_gaussian

to_gaussian() -> GaussianCopula

Convert an all-Gaussian vine into the single Gaussian copula it equals.

Only works when every pair-copula is Gaussian (see :attr:is_gaussian). Useful for reading off the implied full correlation matrix, or as an exact correctness check on a vine.

Returns:

Type Description
GaussianCopula

A d-dimensional Gaussian copula with an unstructured (dispstr="un") correlation matrix, indexed by the original variable order.

Raises:

Type Description
ValueError

If any pair-copula is not Gaussian.

Notes

A vine's tree-:math:k parameters are partial correlations given the conditioning set, and the partial-correlation recursion

.. math:: \rho_{ab\mid D} = \frac{\rho_{ab\mid D'} - \rho_{ac\mid D'}\rho_{bc\mid D'}} {\sqrt{(1-\rho_{ac\mid D'}^2)(1-\rho_{bc\mid D'}^2)}}

runs backwards to recover the ordinary correlations. So an all-Gaussian vine is a Gaussian copula, and its density can be checked against one exactly rather than statistically -- which is the strongest test available on the tree recursions.

Examples:

>>> import numpy as np
>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> vine = VineCopula(
...     [[rc.GaussianCopula(0.7), rc.GaussianCopula(0.4)], [rc.GaussianCopula(0.2)]],
...     structure="C",
... )
>>> sigma = vine.to_gaussian().sigma()
>>> float(round(sigma[0, 1], 6)), float(round(sigma[0, 2], 6))
(0.7, 0.4)

The 1-2 correlation is implied rather than given, since tree 2 supplies only the partial one:

>>> float(round(sigma[1, 2], 6))
0.410905

tau

tau() -> float

Not available for a vine: every pair has its own Kendall's tau.

Raises:

Type Description
NotImplementedError

Always. Estimate pairwise taus from :meth:rvs output, or read them from tree 1's pair-copulas.

rho

rho() -> float

Not available for a vine: every pair has its own Spearman's rho.

Raises:

Type Description
NotImplementedError

Always.

lambda_

lambda_() -> TailDependence

Not available for a vine: tail dependence differs by pair.

Raises:

Type Description
NotImplementedError

Always. Tree 1's pair-copulas give it for the pairs they join.

from_tau classmethod

from_tau(tau: float, dim: int = 2, **kwargs: Any) -> Copula

Not available for a vine: one tau cannot pin down d(d-1)/2 parameters.

Parameters:

Name Type Description Default
tau float

Ignored.

required
dim int

Ignored.

2
**kwargs Any

Ignored.

{}

Raises:

Type Description
NotImplementedError

Always. Use :func:fit_vine to estimate a vine from data.

describe

describe() -> str

Return a readable, multi-line summary of the vine, one line per pair-copula.

The first line gives the structure, dimension and order. Each further line names the tree, the edge as a,b|conditioning in original variable indices, and that pair-copula's own one-line description.

Returns:

Type Description
str

1 + d(d-1)/2 lines joined by newlines.

Examples:

>>> import rcopula as rc
>>> from rcopula.vine import VineCopula
>>> vine = VineCopula(
...     [[rc.ClaytonCopula(2.0), rc.GumbelCopula(2.5)], [rc.FrankCopula(3.0)]],
...     structure="D",
... )
>>> print(vine.describe().splitlines()[0])
D-vine copula, dim 3, order [0, 1, 2]
>>> len(vine.describe().splitlines())
4

fit_vine

fit_vine(data: ArrayLike, structure: str = 'D', families: str | Sequence[str] = DEFAULT_FAMILIES, order: Sequence[int] | None = None, criterion: str = 'aic', truncate: int | None = None) -> VineCopula

Fit a vine copula to data, picking the best family for each pair automatically.

Give it a table of observations (one column per variable) and it returns a :class:VineCopula whose every pair-copula family and parameter has been chosen from the data. Use it when you want flexible, pair-by-pair dependence across three or more variables without choosing families by hand. With structure="R" it also chooses the trees themselves.

Parameters:

Name Type Description Default
data array_like or pandas.DataFrame of float, shape (n, d)

Observations, one row per observation and one column per variable, with d >= 2. Raw data are fine: they are converted to pseudo-observations (ranks scaled into (0, 1)) internally.

required
structure ('C', 'D', 'R')

"C" builds star-shaped trees around a central variable; "D" builds path-shaped trees; "R" lets the data choose any regular vine, tree by tree, with Dissmann et al.'s algorithm (see Notes). See :class:VineCopula.

"C"
families str or sequence of str

Candidate families tried on every edge; names as in :data:~rcopula.select.FAMILIES, or one group name from there (such as "vine"). The default is ("independence", "gaussian", "student", "clayton", "gumbel", "frank"), which spans no tail dependence, lower-only, upper-only and both. :data:EXTENDED_FAMILIES adds Joe, the 90/180/270-degree rotations ("clayton270", "gumbel90", ...) and BB1/BB7 ("bb1", "bb7_180", ...), which can capture negative dependence and asymmetric tails.

DEFAULT_FAMILIES
order sequence of int or None

A permutation of 0..d-1 giving the variable ordering of a C- or D-vine. None orders variables by total absolute Kendall's tau with the others, strongest first. For a C-vine that puts the most connected variable at the root -- which is what makes a C-vine worth choosing; for a D-vine the same rule is used for reproducibility. Must be None for an R-vine, whose structure is selected.

None
criterion ('aic', 'bic', 'loglik', 'xv')

How each edge's winning family is chosen, passed to :func:~rcopula.select.select_copula: lowest AIC or BIC, highest log-likelihood, or cross-validated likelihood.

"aic"
truncate int or None

Fit only the first truncate trees and set every pair-copula in the higher trees to independence. None fits all d - 1 trees. Higher trees usually carry little, and truncating is the standard way to stop a vine from spending parameters on noise. It also keeps the cost linear in d: neither the fit nor the resulting vine's density, sampler or Rosenblatt transform touches the independence trees (see :attr:VineCopula.truncation_level).

None

Returns:

Type Description
VineCopula

The fitted vine. Read fitted.order for the ordering actually used, fitted.matrix for the R-vine matrix and fitted.describe() for the chosen families.

Raises:

Type Description
ValueError

If data has fewer than two columns, structure is not "C", "D" or "R", order is given for an R-vine, or a family name or group is unknown.

Notes

Tree by tree, each edge's family is selected by :func:~rcopula.select.select_copula on the h-transformed data that edge actually sees, and the h-functions for the next tree are built from the winner. That is Dissmann et al.'s sequential procedure, and it is what makes a :math:d(d-1)/2-parameter model estimable at all.

For an R-vine the trees are chosen the same way. Tree 1 is the maximum spanning tree of the complete graph on the variables, weighted by absolute empirical Kendall's tau; each later tree is the maximum spanning tree, weighted by the absolute tau of the h-transformed data, over the pairs of previous edges that share a node (the proximity condition). Trees past truncate are completed with any valid structure, since they hold only independence copulas.

Families that can describe only one sign of dependence are tried only on edges whose empirical tau has that sign: the 90/270-degree rotations only where tau is negative, and the 0/180-degree versions of BB1 and BB7 and the 180-degree rotations only where it is positive, as R's VineCopula does. (Plain Clayton, Gumbel and Joe are always tried, as before.)

Examples:

>>> import rcopula as rc
>>> from rcopula.vine import VineCopula, fit_vine
>>> truth = VineCopula(
...     [[rc.ClaytonCopula(3.0), rc.GumbelCopula(2.5)], [rc.FrankCopula(4.0)]],
...     structure="D",
... )
>>> u = truth.rvs(3000, random_state=0)
>>> fitted = fit_vine(
...     u, structure="D", order=[0, 1, 2], families=["clayton", "gumbel", "frank"]
... )
>>> [cop.name for cop in fitted.pair_copulas[0]]
['Clayton', 'Gumbel']
>>> bool(fitted.loglik(u) > 0)
True

Left to itself it reorders the variables by dependence strength, which is what makes a C-vine's root the right one -- so read the structure from fitted.order rather than assuming it:

>>> fitted = fit_vine(u, structure="D", families=["clayton", "gumbel", "frank"])
>>> len(fitted.order) == 3
True

An R-vine finds the trees as well; negative dependence is picked up by a rotated family:

>>> import numpy as np
>>> v = np.column_stack([u, 1.0 - u[:, 0] ** 0.5 * 0.5 - 0.25 * u[:, 2]])
>>> rfit = fit_vine(v, structure="R", families=["gaussian", "clayton", "clayton90"])
>>> rfit.structure, rfit.dim
('R', 4)

Two-parameter pair-copulas

BB1 and BB7 set the lower and upper tails separately. They are mainly vine pair-copulas, but they work anywhere a bivariate copula does.

rcopula.core.bb

Two-parameter Archimedean copulas: BB1 and BB7.

A one-parameter family ties the strength of dependence to its tail behaviour: fix Clayton's :math:\theta and you have fixed both its Kendall's tau and its lower-tail coefficient. The BB families of Joe and Hu (1996) add a second parameter so the two tails can be set separately, which is what a pair-copula in a vine often needs -- equity pairs, for example, usually crash together more than they rally together, but rarely not at all in either tail.

Both are Archimedean, :math:C(u, v) = \psi\bigl(\psi^{-1}(u) + \psi^{-1}(v)\bigr), with a generator built by composing two familiar ones:

  • BB1 (Clayton-Gumbel), :math:\psi(s) = (1 + s^{1/\delta})^{-1/\theta} with :math:\theta > 0,\ \delta \ge 1. Lower-tail dependence :math:2^{-1/(\theta\delta)}, upper-tail :math:2 - 2^{1/\delta}. :math:\delta = 1 is Clayton; :math:\theta \to 0 is Gumbel.
  • BB7 (Joe-Clayton), :math:\psi(s) = 1 - \bigl(1 - (1+s)^{-1/\delta}\bigr)^{1/\theta} with :math:\theta \ge 1,\ \delta > 0. Lower-tail dependence :math:2^{-1/\delta}, upper-tail :math:2 - 2^{1/\theta}. :math:\theta = 1 is Clayton; :math:\delta \to 0 is Joe.

Everything a vine needs is in closed form: the density, the distribution function and both conditional distributions (h-functions). The inverse h-function is solved numerically by a safeguarded Newton iteration, since no closed form exists. Sampling uses the frailty (mixture) representation of each generator, so it is exact and vectorised.

============================ ================================================ :class:BB1Copula Clayton-Gumbel: both tails, set separately. :class:BB7Copula Joe-Clayton: both tails, set separately. ============================ ================================================

Parameter names and ranges follow R's VineCopula package (families 7 and 9), so fitted values can be compared directly.

References

Joe, H. and Hu, T. (1996). Multivariate distributions from mixtures of max-infinitely divisible distributions. Journal of Multivariate Analysis 57(2), 240-265. Joe, H. (2014). Dependence Modeling with Copulas. Chapman & Hall/CRC, sections 4.17 (BB1) and 4.23 (BB7) -- the formulas implemented here. Nelsen, R. B. (2006). An Introduction to Copulas, 2nd ed. Springer, Theorem 5.1.6 -- Kendall's tau of an Archimedean copula from its generator.

BB1Copula

BB1Copula(theta: float = np.nan, delta: float = np.nan, dim: int = 2, *, free: ArrayLike | None = None)

Bases: _BBCopula

Clayton-Gumbel copula: two parameters that set the lower and upper tails separately.

A two-variable Archimedean copula with tail dependence in both tails, each controlled separately -- which neither Clayton (lower only) nor Gumbel (upper only) can do, and which the Student t can do only symmetrically. A common choice for equity pairs in a vine. Positive dependence only; rotate it (:class:~rcopula.RotatedCopula) for negative.

.. math::

C(u, v) = \Bigl(1 + \bigl[(u^{-\theta} - 1)^{\delta}
          + (v^{-\theta} - 1)^{\delta}\bigr]^{1/\delta}\Bigr)^{-1/\theta}

Parameters:

Name Type Description Default
theta float

Lower-tail shape, theta > 0. nan leaves it to be fitted.

nan
delta float

Upper-tail shape, delta >= 1. nan leaves it to be fitted.

nan
dim int

Number of variables; must be exactly 2.

2
free array_like of bool, shape (2,), or None

Keyword-only. Which of (theta, delta) are estimated by fitting; None means both.

None

Attributes:

Name Type Description
theta float

Lower-tail shape parameter.

delta float

Upper-tail shape parameter.

Raises:

Type Description
ValueError

If dim != 2, theta <= 0 or delta < 1.

Notes

Kendall's tau is :math:1 - 2/(\delta(\theta + 2)); the tail coefficients are :math:\lambda_L = 2^{-1/(\theta\delta)} and :math:\lambda_U = 2 - 2^{1/\delta} (Joe 2014, section 4.17). With delta = 1 it is exactly the Clayton copula with the same theta. Parameters match R VineCopula's family 7 (par = theta, par2 = delta).

Examples:

>>> from rcopula import BB1Copula, ClaytonCopula
>>> cop = BB1Copula(0.5, 1.5)
>>> round(cop.tau(), 6)
0.466667
>>> lam = cop.lambda_()
>>> round(lam.lower, 4), round(lam.upper, 4)
(0.3969, 0.4126)

With delta = 1 it reduces to Clayton:

>>> import numpy as np
>>> pts = np.array([[0.2, 0.7], [0.6, 0.4]])
>>> bool(np.allclose(BB1Copula(2.0, 1.0).pdf(pts), ClaytonCopula(2.0).pdf(pts)))
True

param_bounds property

param_bounds: list[tuple[float, float]]

Admissible ranges of (theta, delta).

Returns:

Type Description
list of tuple of (float, float)

[(0.0, inf), (1.0, inf)]; theta = 0 itself is excluded.

tau

tau() -> float

Return Kendall's tau, a rank correlation in [0, 1).

Closed form: :math:1 - 2/(\delta(\theta + 2)).

Returns:

Type Description
float

Kendall's tau.

Examples:

>>> from rcopula import BB1Copula
>>> round(BB1Copula(2.0, 2.0).tau(), 6)
0.75

lambda_

lambda_() -> TailDependence

Return the lower and upper tail-dependence coefficients.

:math:\lambda_L = 2^{-1/(\theta\delta)} and :math:\lambda_U = 2 - 2^{1/\delta}.

Returns:

Type Description
TailDependence

Named tuple (lower, upper) of floats in [0, 1).

BB7Copula

BB7Copula(theta: float = np.nan, delta: float = np.nan, dim: int = 2, *, free: ArrayLike | None = None)

Bases: _BBCopula

Joe-Clayton copula: two parameters that set the upper and lower tails separately.

A two-variable Archimedean copula with tail dependence in both tails, the upper governed by theta (Joe-like) and the lower by delta (Clayton-like). An alternative to :class:BB1Copula with a different shape in between the tails. Positive dependence only; rotate it (:class:~rcopula.RotatedCopula) for negative.

.. math::

C(u, v) = 1 - \Bigl(1 - \bigl[(1 - \bar u^{\theta})^{-\delta}
          + (1 - \bar v^{\theta})^{-\delta} - 1\bigr]^{-1/\delta}\Bigr)^{1/\theta},
\qquad \bar u = 1 - u.

Parameters:

Name Type Description Default
theta float

Upper-tail shape, theta >= 1. nan leaves it to be fitted.

nan
delta float

Lower-tail shape, delta > 0. nan leaves it to be fitted.

nan
dim int

Number of variables; must be exactly 2.

2
free array_like of bool, shape (2,), or None

Keyword-only. Which of (theta, delta) are estimated by fitting; None means both.

None

Attributes:

Name Type Description
theta float

Upper-tail shape parameter.

delta float

Lower-tail shape parameter.

Raises:

Type Description
ValueError

If dim != 2, theta < 1 or delta <= 0.

Notes

Tail coefficients :math:\lambda_L = 2^{-1/\delta} and :math:\lambda_U = 2 - 2^{1/\theta} (Joe 2014, section 4.23). Kendall's tau has no closed form and is computed from the generator by one-dimensional quadrature. With theta = 1 it is exactly the Clayton copula with parameter delta. Parameters match R VineCopula's family 9 (par = theta, par2 = delta).

Examples:

>>> from rcopula import BB7Copula, ClaytonCopula
>>> cop = BB7Copula(1.5, 2.0)
>>> lam = cop.lambda_()
>>> round(lam.lower, 4), round(lam.upper, 4)
(0.7071, 0.4126)
>>> round(cop.tau(), 4)
0.5429

With theta = 1 it reduces to Clayton:

>>> import numpy as np
>>> pts = np.array([[0.2, 0.7], [0.6, 0.4]])
>>> bool(np.allclose(BB7Copula(1.0, 2.0).pdf(pts), ClaytonCopula(2.0).pdf(pts)))
True

param_bounds property

param_bounds: list[tuple[float, float]]

Admissible ranges of (theta, delta).

Returns:

Type Description
list of tuple of (float, float)

[(1.0, inf), (0.0, inf)]; delta = 0 itself is excluded.

tau

tau() -> float

Return Kendall's tau, a rank correlation in [0, 1).

No closed form exists; it is computed as :math:1 + 4\int_0^1 \varphi(t)/\varphi'(t)\,dt with the generator inverse :math:\varphi(t) = (1 - (1-t)^\theta)^{-\delta} - 1, by adaptive quadrature.

Returns:

Type Description
float

Kendall's tau.

Examples:

>>> from rcopula import BB7Copula
>>> round(BB7Copula(1.0, 2.0).tau(), 8)  # theta = 1 is Clayton(2): tau = 0.5
0.5

lambda_

lambda_() -> TailDependence

Return the lower and upper tail-dependence coefficients.

:math:\lambda_L = 2^{-1/\delta} and :math:\lambda_U = 2 - 2^{1/\theta}.

Returns:

Type Description
TailDependence

Named tuple (lower, upper) of floats in [0, 1).