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 |
required |
flip
|
bool, array_like of bool with shape (d,), or int
|
Which coordinates to reflect. A single |
True
|
free
|
array_like of bool, shape (n_params,), or None
|
Keyword-only. Which parameters are estimated when fitting; |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
base |
Copula
|
The copula being reflected. Never itself a :class: |
flip |
numpy.ndarray of bool, shape (d,)
|
Read-only mask of reflected coordinates. |
name |
str
|
|
param_names |
tuple of str
|
The base copula's parameter names. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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; |
degrees
property
¶
degrees: int
The rotation expressed in degrees, as vine libraries label it (two dimensions only).
Returns:
| Type | Description |
|---|---|
int
|
One of |
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
|
|
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 |
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 |
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 |
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 |
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: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
dim
|
int
|
Number of variables. |
2
|
**kwargs
|
Any
|
|
{}
|
Returns:
| Type | Description |
|---|---|
Copula
|
A :class: |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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 |
required |
dim
|
int
|
Number of variables. |
2
|
**kwargs
|
Any
|
|
{}
|
Returns:
| Type | Description |
|---|---|
Copula
|
A :class: |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
describe ¶
describe() -> str
Return a one-line, human-readable description of the rotated copula.
Returns:
| Type | Description |
|---|---|
str
|
For example |
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 |
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_1and :math:\mathbf V \sim C_2independently 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 |
required |
copula2
|
Copula
|
The two components, of the same dimension |
required |
shapes
|
float or array_like of float, shape (d,)
|
One shape in |
required |
free
|
array_like of bool, shape (n_params,), or None
|
Keyword-only. Which parameters are estimated when fitting; |
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: |
name |
str
|
|
param_names |
tuple of str
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the components differ in dimension, if |
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 |
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
|
|
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 |
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
|
|
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 |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float64, same shape as ``w``
|
|
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 ( |
NotImplementedError
|
If |
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 ( |
NotImplementedError
|
If |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any parameter is unspecified ( |
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)/uor its survival counterpart; - Blomqvist's beta, which depends on :math:
Conly 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 |
required |
weights
|
array_like of float, shape (k,), or None
|
Mixing weights for the |
None
|
free
|
array_like of bool, shape (n_params,), or None
|
Keyword-only. Which parameters are estimated when fitting; |
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: |
n_components |
int
|
Number of components |
name |
str
|
|
param_names |
tuple of str
|
|
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
|
|
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 |
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 ( |
NotImplementedError
|
If |
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
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If any parameter is unspecified ( |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any parameter is unspecified ( |
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
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If any parameter is unspecified ( |
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 |
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. |
required |
components
|
sequence of int or None
|
Zero-based indices of the variables attached directly to this
node. |
None
|
children
|
sequence of NestedArchimedean or None
|
Sub-trees nested inside this node. |
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: |
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: |
dim |
int
|
Number of variables below this node (at least 2). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
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; |
depth
property
¶
depth: int
How many levels the tree has below and including this node.
Returns:
| Type | Description |
|---|---|
int
|
|
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: |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If this node is not a root covering |
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 |
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: |
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: |
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
|
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 ¶
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 at1/3for AMH. A flat tree (one node) gets the family's range instructure.dimdimensions, so a bivariate Clayton or Frank can still be negatively dependent. Near+-1the clip is+-0.999for 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 |
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 |
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 |
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: |
required |
alpha
|
float
|
The outer power, :math: |
nan
|
free
|
array_like of bool, shape (2,), or None
|
Keyword-only. Which of |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
base |
ArchimedeanCopula
|
The copula whose generator is transformed. |
alpha |
float
|
The outer power (see :attr: |
params |
numpy.ndarray of float64, shape (2,)
|
|
param_names |
tuple of str
|
The base generator's parameter name, then |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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 |
alpha
property
¶
alpha: float
The outer power :math:lpha (1 means no transformation).
Returns:
| Type | Description |
|---|---|
float
|
The current value; |
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 |
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 |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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. |
required |
alpha
|
float
|
The power, at least 1 ( |
required |
**kwargs
|
Any
|
Passed to :class: |
{}
|
Returns:
| Type | Description |
|---|---|
OuterPowerCopula
|
The transformed copula, of the same dimension as |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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 |
required |
indices
|
sequence of int
|
Zero-based coordinates to keep, in the order they should appear. At
least two, all distinct, all in |
required |
Returns:
| Type | Description |
|---|---|
Copula
|
A copula of dimension |
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
VineCopulapackage.
============================ ================================================
: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. |
required |
structure
|
('C', 'D', 'R')
|
|
"C"
|
order
|
sequence of int or None
|
A permutation of |
None
|
matrix
|
array_like of int, shape (d, d), or None
|
The R-vine matrix, required when |
None, keyword-only
|
free
|
array_like of bool or None
|
Free/fixed mask passed to :class: |
None, keyword-only
|
Attributes:
| Name | Type | Description |
|---|---|---|
pair_copulas |
list of list of Copula
|
The bivariate copulas by tree, as given. |
structure |
str
|
|
order |
tuple of int
|
The variable ordering, length |
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 |
dim |
int
|
Number of variables |
n_pairs |
int
|
Number of bivariate copulas, |
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 |
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
|
|
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)
|
|
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 |
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 |
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
|
required |
Returns:
| Type | Description |
|---|---|
float
|
The total log-likelihood, |
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 |
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: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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: |
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: |
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
|
|
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 |
required |
structure
|
('C', 'D', 'R')
|
|
"C"
|
families
|
str or sequence of str
|
Candidate families tried on every edge; names as in
:data: |
DEFAULT_FAMILIES
|
order
|
sequence of int or None
|
A permutation of |
None
|
criterion
|
('aic', 'bic', 'loglik', 'xv')
|
How each edge's winning family is chosen, passed to
:func: |
"aic"
|
truncate
|
int or None
|
Fit only the first |
None
|
Returns:
| Type | Description |
|---|---|
VineCopula
|
The fitted vine. Read |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 = 1is Clayton; :math:\theta \to 0is 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 = 1is Clayton; :math:\delta \to 0is 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, |
nan
|
delta
|
float
|
Upper-tail shape, |
nan
|
dim
|
int
|
Number of variables; must be exactly 2. |
2
|
free
|
array_like of bool, shape (2,), or None
|
Keyword-only. Which of |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
theta |
float
|
Lower-tail shape parameter. |
delta |
float
|
Upper-tail shape parameter. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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)
|
|
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 |
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, |
nan
|
delta
|
float
|
Lower-tail shape, |
nan
|
dim
|
int
|
Number of variables; must be exactly 2. |
2
|
free
|
array_like of bool, shape (2,), or None
|
Keyword-only. Which of |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
theta |
float
|
Upper-tail shape parameter. |
delta |
float
|
Lower-tail shape parameter. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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)
|
|
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 |