Transforms and the Kendall function¶
rcopula.transforms ¶
Conditional copula distributions and the Rosenblatt transform.
The central object is the h-function, the conditional distribution of one coordinate given the others:
.. math:: h(u_1 \mid u_2) = P(U_1 \le u_1 \mid U_2 = u_2) = \frac{\partial C(u_1, u_2)}{\partial u_2}.
It does a surprising amount of work. It is the sampler for every copula without
a closed-form generator (draw w, invert h); it is the building block of
vine copulas; it is the Rosenblatt transform that turns dependent data into
independent uniforms for goodness-of-fit; and in pairs trading it is the
signal -- :math:h(u_1 \mid u_2) near zero says asset 1 is unusually cheap
given where asset 2 sits, which is a statement no correlation can make.
R exposes this as cCopula and supports Archimedean and elliptical families
only. Analytic forms are used here for those, and numerical differentiation
elsewhere, so every family in the package is covered.
References
Rosenblatt, M. (1952). Remarks on a multivariate transformation. Annals of Mathematical Statistics 23(3), 470-472. 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 h-function as the workhorse of vine copulas. Joe, H. (2014). Dependence Modeling with Copulas. Chapman & Hall/CRC.
conditional_cdf ¶
conditional_cdf(copula: Copula, u: ArrayLike, given: int = 1) -> NDArray[np.float64]
Probability that one variable is at most its value, given the other variable's value.
This is the conditional distribution of a bivariate copula, often called
the h-function: :math:P(U_j \le u_j \mid U_{given} = u_{given}). It
answers "given where variable B is, how unusual is variable A?" -- a value
near 0 means A is unusually low for that B, near 1 unusually high. It is
also the building block of sampling, vine copulas and the Rosenblatt
transform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
A bivariate ( |
required |
u
|
array_like of float, shape (n, 2)
|
Points in the unit square, one per row. A single point may be passed
as a 1-D sequence of length 2. Values are clipped to
|
required |
given
|
(0, 1)
|
Index of the column to condition on. |
0
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (n,)
|
Values in |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Under independence, conditioning changes nothing:
>>> import numpy as np
>>> from rcopula import IndependenceCopula
>>> from rcopula.transforms import conditional_cdf
>>> u = np.array([[0.3, 0.8], [0.7, 0.2]])
>>> conditional_cdf(IndependenceCopula(2), u)
array([0.3, 0.7])
Under positive dependence, a low value of the conditioning asset makes the other look high by comparison:
>>> from rcopula import GaussianCopula
>>> float(round(conditional_cdf(GaussianCopula(0.8), [[0.5, 0.05]])[0], 6))
0.985851
The output is uniform when the copula is right -- the property the Rosenblatt transform and every h-function-based test rely on:
>>> from scipy import stats
>>> from rcopula import ClaytonCopula
>>> cop = ClaytonCopula(3.0)
>>> h = conditional_cdf(cop, cop.rvs(20_000, random_state=0))
>>> bool(stats.kstest(h, "uniform").pvalue > 0.01)
True
conditional_ppf ¶
conditional_ppf(copula: Copula, w: ArrayLike, cond: ArrayLike, given: int = 1) -> NDArray[np.float64]
Find the value of one variable that has a given conditional probability, given the other.
The inverse of :func:conditional_cdf (the inverse h-function): returns
x with :math:h(x \mid \text{cond}) = w. Its main use is simulation --
draw w uniform, invert, and the pair (x, cond) follows the copula --
including conditional scenarios where cond is fixed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
A bivariate ( |
required |
w
|
float or array_like of float, shape (n,)
|
Target conditional probabilities, each in :math: |
required |
cond
|
float or array_like of float, shape (n,)
|
Values of the conditioning variable, each in :math: |
required |
given
|
(0, 1)
|
Which coordinate |
0
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (n,)
|
The solved values |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any |
Notes
Solved by 60 steps of vectorised bisection on [1e-12, 1 - 1e-12], which
handles every family uniformly and takes the bracket below double precision.
Out-of-range targets are rejected rather than quietly converging to the
nearest end of the bracket, which used to return a plausible-looking but
meaningless value. NaN in w or cond gives NaN in that position.
Examples:
Round-trips against :func:conditional_cdf:
>>> import numpy as np
>>> from rcopula import ClaytonCopula
>>> from rcopula.transforms import conditional_cdf, conditional_ppf
>>> cop = ClaytonCopula(2.5)
>>> w = np.array([0.1, 0.5, 0.9])
>>> cond = np.array([0.3, 0.6, 0.8])
>>> x = conditional_ppf(cop, w, cond)
>>> recovered = conditional_cdf(cop, np.column_stack([x, cond]))
>>> bool(np.allclose(recovered, w, atol=1e-8))
True
rosenblatt ¶
rosenblatt(copula: Copula, u: ArrayLike) -> NDArray[np.float64]
Turn dependent copula data into independent uniform numbers, if the copula is right.
This is the Rosenblatt transform (R's cCopula). Each column is replaced
by its conditional probability given all the columns before it. If the
data really came from copula, the output columns are independent and
uniform on :math:[0, 1]; if not, they are visibly not. That turns a
goodness-of-fit question into a simpler test of independence and
uniformity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The copula to test against, with its parameters set. Archimedean,
Gaussian and Student-t families work in any dimension; other families
only for |
required |
u
|
array_like of float, shape (n, d)
|
Observations on the copula scale (pseudo-observations), one row each.
|
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (n, d)
|
Transformed values in |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the number of columns of |
NotImplementedError
|
If |
Notes
Maps dependent uniforms to independent uniforms by conditioning successively:
.. math:: Z_1 = U_1,\quad Z_2 = C(U_2 \mid U_1),\quad Z_3 = C(U_3 \mid U_1, U_2),\ \dots
If the copula is correct the result is a sample of independent standard
uniforms, which turns any goodness-of-fit question into a test of
independence and uniformity -- the basis of the SnB and SnC
statistics.
Implemented analytically for Archimedean and elliptical families in any dimension, and for arbitrary bivariate copulas.
Examples:
The right copula gives independent uniforms; the wrong one does not:
>>> import numpy as np
>>> from scipy import stats
>>> from rcopula import ClaytonCopula
>>> from rcopula.transforms import rosenblatt
>>> cop = ClaytonCopula(3.0, dim=3)
>>> z = rosenblatt(cop, cop.rvs(5000, random_state=0))
>>> bool(all(stats.kstest(z[:, j], "uniform").pvalue > 0.01 for j in range(3)))
True
>>> bool(abs(np.corrcoef(z[:, 0], z[:, 1])[0, 1]) < 0.05)
True
Transformed with the wrong parameter, the output is visibly non-uniform:
>>> bad = rosenblatt(ClaytonCopula(0.2, dim=3), cop.rvs(5000, random_state=0))
>>> bool(stats.kstest(bad[:, 1], "uniform").pvalue < 1e-6)
True
inverse_rosenblatt ¶
inverse_rosenblatt(copula: Copula, z: ArrayLike) -> NDArray[np.float64]
Turn independent uniform numbers into draws from a copula.
The inverse Rosenblatt transform, the reverse of :func:rosenblatt. Unlike
copula.rvs, which makes its own randomness, this takes randomness you
supply. That lets you push a quasi-random point set through a copula
(see :mod:rcopula.sampling) or do conditional simulation: fix the first
coordinates and vary the rest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The target copula, with its parameters set. Any family for which
:func: |
required |
z
|
array_like of float, shape (n, d)
|
Independent uniforms on :math: |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (n, d)
|
Draws from |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the number of columns of |
NotImplementedError
|
If :func: |
Notes
Elliptical copulas invert analytically. Everything else is inverted by
bisection against :func:rosenblatt itself, so the two are exact
inverses by construction rather than by two derivations agreeing -- which is
the failure mode this arrangement removes. The conditional CDF is monotone
in its own argument, so bisection cannot fail; 60 steps takes the bracket
below double precision.
Examples:
Round trip, in both directions:
>>> import numpy as np, rcopula as rc
>>> from rcopula.transforms import inverse_rosenblatt, rosenblatt
>>> cop = rc.ClaytonCopula(3.0, dim=3)
>>> u = cop.rvs(500, random_state=0)
>>> bool(np.max(np.abs(inverse_rosenblatt(cop, rosenblatt(cop, u)) - u)) < 1e-9)
True
Independent uniforms in, correctly dependent draws out:
>>> rng = np.random.default_rng(0)
>>> z = rng.uniform(size=(20000, 2))
>>> u = inverse_rosenblatt(rc.GumbelCopula(2.0), z)
>>> bool(abs(rc.cor_kendall(u)[0, 1] - 0.5) < 0.02)
True
radial_simplex ¶
radial_simplex(copula: ArchimedeanCopula, u: ArrayLike) -> tuple[NDArray, NDArray]
Split Archimedean copula data into a "size" part and a "direction" part.
Every Archimedean copula sample can be written as a radial variable
:math:R (one number per row, how far out the point is) times a direction
:math:S that is uniform on the unit simplex. The direction part has the
same distribution for every Archimedean copula, so if it does not look
uniform, no Archimedean family fits the data. This is the decomposition of
McNeil and Neslehova (2009).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
ArchimedeanCopula
|
Supplies the generator :math: |
required |
u
|
array_like of float, shape (n, d)
|
Observations on the copula scale, strictly inside :math: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
radial |
numpy.ndarray of float, shape (n,)
|
:math: |
angular |
numpy.ndarray of float, shape (n, d)
|
:math: |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If the number of columns of |
Notes
McNeil and Neslehova showed that :math:U \sim C for an Archimedean copula
with generator :math:\psi if and only if
.. math:: \bigl(\psi^{-1}(U_1), \dots, \psi^{-1}(U_d)\bigr) = R\,S,
with :math:S uniform on the unit simplex and independent of the radial
variable :math:R = \sum_k \psi^{-1}(U_k). All the family-specific
information sits in the distribution of :math:R; the angular part is the
same for every Archimedean copula in every dimension.
That is the whole basis for testing Archimedeanity rather than testing one particular family: if the angular part is not uniform on the simplex, no Archimedean copula fits, whatever generator is tried.
Examples:
The angular part is uniform on the simplex, so each of its coordinates has
mean :math:1/d -- and this does not depend on the family or the parameter:
>>> import numpy as np, rcopula as rc
>>> from rcopula.transforms import radial_simplex
>>> for copula in (rc.ClaytonCopula(3.0, dim=4), rc.GumbelCopula(2.0, dim=4)):
... u = copula.rvs(20000, random_state=0)
... angular = radial_simplex(copula, u)[1]
... print(bool(np.all(np.abs(angular.mean(axis=0) - 0.25) < 0.01)))
True
True
htrafo ¶
htrafo(copula: ArchimedeanCopula, u: ArrayLike) -> NDArray[np.float64]
Turn Archimedean copula data into independent uniforms, even in high dimensions.
This is the Hering-Hofert transform (R's htrafo). It does the same job
as :func:rosenblatt -- if the data came from copula, the output is
independent and uniform -- but it avoids high-order derivatives of the
generator, so it stays accurate at :math:d = 100 where the Rosenblatt
transform breaks down. Use it for goodness-of-fit testing of Archimedean
models with many variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
ArchimedeanCopula
|
The copula to test against, with its parameter set. |
required |
u
|
array_like of float, shape (n, d)
|
Observations on the copula scale, strictly inside :math: |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (n, d)
|
Transformed values in |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If the number of columns of |
See Also
rosenblatt : the conditioning-based alternative, exact but derivative-hungry. radial_simplex : the decomposition this is built on.
Notes
The Rosenblatt transform conditions one coordinate at a time, which needs
:math:d-1 derivatives of the generator and loses accuracy fast as :math:d
grows. This transform goes through the simplex decomposition instead, so it
needs no high-order derivatives at all -- which is why it exists.
Writing :math:S_j = \sum_{k \le j} \psi^{-1}(U_k),
.. math::
Y_j = \left(\frac{S_j}{S_{j+1}}\right)^{j}, \quad j = 1, \dots, d-1,
\qquad Y_d = K\bigl(\psi(S_d)\bigr),
with :math:K the Kendall distribution function. Under the null that u
came from this copula the :math:Y_j are independent and uniform, so any
test of multivariate uniformity becomes a goodness-of-fit test.
The first :math:d-1 components come from the angular part and the last
from the radial part; since those are independent, so are the two groups.
Examples:
Data from the copula transforms to uniforms; data from a different copula does not:
>>> import numpy as np, rcopula as rc
>>> from scipy import stats
>>> from rcopula.transforms import htrafo
>>> copula = rc.ClaytonCopula(2.0, dim=5)
>>> y = htrafo(copula, copula.rvs(4000, random_state=0))
>>> bool(stats.kstest(y.ravel(), "uniform").pvalue > 0.01)
True
>>> wrong = htrafo(copula, rc.GumbelCopula(3.0, dim=5).rvs(4000, random_state=0))
>>> bool(stats.kstest(wrong.ravel(), "uniform").pvalue < 1e-6)
True
It still works where the Rosenblatt transform's derivatives would not:
>>> big = rc.GumbelCopula(2.0, dim=50)
>>> y = htrafo(big, big.rvs(500, random_state=0))
>>> bool(np.all(np.isfinite(y)) and y.shape == (500, 50))
True
radial_cdf ¶
radial_cdf(copula: ArchimedeanCopula, x: ArrayLike) -> NDArray[np.float64]
Probability that the radial ("size") part of an Archimedean sample is at most x.
The distribution function of :math:R from :func:radial_simplex (R's
pacR). Since the direction part is the same for every Archimedean
copula, this curve carries all the family-specific information, which
makes it the thing to compare between families or against data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
ArchimedeanCopula
|
The copula, with its parameter set. |
required |
x
|
float or array_like of float, any shape
|
Radii at which to evaluate, each non-negative. |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
:math: |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If any element of |
Notes
:func:radial_simplex splits an Archimedean sample into a radial variable
:math:R = \sum_j \psi^{-1}(U_j) and a point uniform on the simplex. The
angular half is the same for every Archimedean copula in every dimension,
so all the family-specific information lives in the distribution of
:math:R -- which is this.
It needs no new machinery, because it is the Kendall distribution function
in disguise. Since :math:C(\mathbf U) = \psi(R) and :math:\psi is
decreasing,
.. math::
K(t) = P\{\psi(R) \le t\} = P\{R \ge \psi^{-1}(t)\},
\qquad\text{so}\qquad
F_R(x) = 1 - K(\psi(x)).
Examples:
>>> import numpy as np, rcopula as rc
>>> from rcopula.transforms import radial_cdf
>>> cop = rc.ClaytonCopula(2.0, dim=3)
>>> values = radial_cdf(cop, [1.5861, 11.6107, 346.2882])
>>> np.round(values, 3)
array([0.1 , 0.499, 0.9 ])
Which is what the sampled radial part actually does:
>>> radii = np.sum(cop.generator.ipsi(cop.rvs(50000, random_state=0), 2.0), axis=1)
>>> bool(np.all(np.abs(np.mean(radii[:, None] <= [1.5861, 11.6107], axis=0)
... - values[:2]) < 0.01))
True
radial_ppf ¶
radial_ppf(copula: ArchimedeanCopula, q: ArrayLike) -> NDArray[np.float64]
Find the radius below which a given fraction of an Archimedean sample's radial part falls.
The quantile (inverse) function of :func:radial_cdf (R's qacR):
returns x with :math:F_R(x) = q.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
ArchimedeanCopula
|
The copula, with its parameter set. |
required |
q
|
float or array_like of float, any shape
|
Probabilities, each in :math: |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
Non-negative radii, one per element of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If any element of |
Notes
By the same identity as :func:radial_cdf: since
:math:F_R(x) = 1 - K(\psi(x)), the quantile is
:math:\psi^{-1}(K^{-1}(1 - q)), so it reduces to the Kendall function's
own quantile and needs no root-finding of its own.
Examples:
>>> import numpy as np, rcopula as rc
>>> from rcopula.transforms import radial_cdf, radial_ppf
>>> cop = rc.GumbelCopula(2.0, dim=4)
>>> levels = np.array([0.1, 0.25, 0.5, 0.75, 0.9])
>>> bool(np.max(np.abs(radial_cdf(cop, radial_ppf(cop, levels)) - levels)) < 1e-6)
True
rcopula.kendall ¶
The Kendall distribution function.
For a copula :math:C and :math:\mathbf U \sim C, the Kendall distribution
function is the law of the copula evaluated at its own draw:
.. math:: K(t) = P\bigl(C(\mathbf U) \le t\bigr).
It is the univariate summary of a multivariate dependence structure: a single
curve on :math:[0, 1] that determines Kendall's tau
(:math:\tau = 3 - 4\int_0^1 K), identifies the generator of an Archimedean
copula uniquely, and answers the question a joint quantile cannot.
That last point is what makes it useful rather than merely elegant. Ask how
often is a flood this bad, and there is no single answer from the margins: the
set :math:\{C(\mathbf u) > t\} is a region, not a point, and every point on its
boundary is "equally extreme". :math:K measures that region. The Kendall
return period :math:1/(1 - K(t)) is therefore the honest multivariate
analogue of a return period, and it is systematically longer than the
univariate one -- treating a compound event as if only its worst component
mattered understates how rare it is.
============================= ===============================================
:func:kendall_cdf :math:K(t); R's pK.
:func:kendall_pdf Its density; R's dK.
:func:kendall_ppf Its quantile function; R's qK.
:func:kendall_rvs Draws of :math:C(\mathbf U); R's rK.
:func:kendall_empirical The nonparametric estimator; R's Kn.
:func:kendall_return_period :math:1/(1 - K(t)), and its inverse.
============================= ===============================================
For Archimedean copulas everything is closed form in any dimension. For other
bivariate copulas :math:K is obtained by integrating the conditional
distribution along the level curve of :math:C, which needs only the CDF.
References
Genest, C. and Rivest, L.-P. (1993). Statistical inference procedures for bivariate Archimedean copulas. JASA 88(423), 1034-1043. The bivariate form and the nonparametric estimator. Barbe, P., Genest, C., Ghoudi, K. and Remillard, B. (1996). On Kendall's process. Journal of Multivariate Analysis 58(2), 197-229. The multivariate generalisation used here. Salvadori, G. and De Michele, C. (2004). Frequency analysis via copulas. Water Resources Research 40, W12511. Kendall return periods, and why the univariate one is the wrong number. Nappo, G. and Spizzichino, F. (2009). Kendall distributions and level sets in bivariate exchangeable survival models. Information Sciences 179(17), 2878-2890.
kendall_cdf ¶
kendall_cdf(copula: Copula, t: ArrayLike) -> NDArray[np.float64]
Probability that the copula, evaluated at a random draw from itself, is at most t.
This is the Kendall distribution function
:math:K(t) = P(C(\mathbf U) \le t) (R's pK). It squeezes a whole
multivariate dependence structure into one curve on :math:[0, 1]: the
value :math:C(\mathbf u) says how "jointly low" a point is, and :math:K
says how often points are at least that jointly low. Use it to compute joint
(Kendall) return periods, to compare a fitted family with data via
:func:kendall_empirical, or to recover Kendall's tau.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The dependence model, with its parameters set. Archimedean families
(Clayton, Gumbel, Frank, Joe, AMH, ...), :class: |
required |
t
|
float or array_like of float, any shape
|
Levels at which to evaluate :math: |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
:math: |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If |
Notes
For an Archimedean copula in :math:d dimensions (Barbe et al. 1996),
.. math:: K(t) = \sum_{k=0}^{d-1} \frac{(-1)^k}{k!}\, s^k\,\psi^{(k)}(s), \qquad s = \psi^{-1}(t),
which is a sum of non-negative terms once the alternating sign is
absorbed into :math:|\psi^{(k)}|, so it never cancels. In two dimensions it
reduces to the familiar :math:K(t) = t - s\,\psi'(s).
For any other bivariate copula, integrating along the level curve gives
.. math:: K(t) = t + \int_t^1 \frac{\partial C}{\partial u}\bigl(u, v_t(u)\bigr)\,du,
where :math:v_t(u) solves :math:C(u, v_t) = t. Below :math:u = t the
event is certain, which is where the leading :math:t comes from.
Examples:
Independence has the closed form :math:K(t) = t(1 - \log t):
>>> import numpy as np
>>> from rcopula import IndependenceCopula
>>> from rcopula.kendall import kendall_cdf
>>> t = np.array([0.1, 0.3, 0.7])
>>> bool(np.allclose(kendall_cdf(IndependenceCopula(2), t), t * (1 - np.log(t))))
True
Comonotonicity leaves nothing to summarise -- :math:C(\mathbf U) = U, so
:math:K is uniform:
>>> from rcopula import FrechetUpperCopula
>>> bool(np.allclose(kendall_cdf(FrechetUpperCopula(2), t), t))
True
K dominates the uniform for every copula, which is why the Kendall
return period always exceeds the univariate one:
>>> from rcopula import ClaytonCopula
>>> bool(np.all(kendall_cdf(ClaytonCopula(3.0), t) >= t))
True
It recovers Kendall's tau through :math:\tau = 3 - 4\int_0^1 K:
>>> from scipy import integrate
>>> cop = ClaytonCopula(3.0)
>>> area = integrate.quad(lambda x: kendall_cdf(cop, x)[0], 0, 1)[0]
>>> bool(abs((3 - 4 * area) - cop.tau()) < 1e-6)
True
kendall_pdf ¶
kendall_pdf(copula: Copula, t: ArrayLike) -> NDArray[np.float64]
Probability density of the Kendall distribution function (R's dK).
The slope of :func:kendall_cdf: where it is large, many draws of the
copula land at that joint level. Useful for plotting the Kendall
distribution or building likelihoods on it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The dependence model, with its parameters set. Same support as
:func: |
required |
t
|
float or array_like of float, any shape
|
Levels at which to evaluate the density. Points outside the open
interval :math: |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
Non-negative density values, one per element of |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If |
Notes
Differentiating the Archimedean sum telescopes -- every term cancels against the next -- and only the last survives:
.. math:: K'(t) = \frac{s^{d-1}\,\bigl|\psi^{(d)}(s)\bigr|} {(d-1)!\,\bigl|\psi'(s)\bigr|}, \qquad s = \psi^{-1}(t),
manifestly non-negative, as a density must be. Elsewhere it is a central
difference of :func:kendall_cdf with step 1e-6.
Examples:
Integrates to one, and matches the derivative of K:
>>> import numpy as np
>>> from scipy import integrate
>>> from rcopula import ClaytonCopula
>>> from rcopula.kendall import kendall_cdf, kendall_pdf
>>> cop = ClaytonCopula(3.0)
>>> total = integrate.quad(lambda x: kendall_pdf(cop, x)[0], 0, 1)[0]
>>> bool(abs(total - 1.0) < 1e-8)
True
>>> h = 1e-6
>>> slope = (kendall_cdf(cop, 0.4 + h)[0] - kendall_cdf(cop, 0.4 - h)[0]) / (2 * h)
>>> bool(abs(kendall_pdf(cop, 0.4)[0] - slope) < 1e-6)
True
kendall_ppf ¶
kendall_ppf(copula: Copula, p: ArrayLike) -> NDArray[np.float64]
Find the joint level t below which a given fraction of copula draws fall.
The quantile (inverse) function of the Kendall distribution, R's qK:
returns t with :math:K(t) = p. Solved by 60 steps of bisection, so it
works for every family :func:kendall_cdf supports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The dependence model, with its parameters set (see :func: |
required |
p
|
float or array_like of float, any shape
|
Probabilities, each in :math: |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
Levels |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any element of |
NotImplementedError
|
If :func: |
Examples:
>>> import numpy as np
>>> from rcopula import GumbelCopula
>>> from rcopula.kendall import kendall_cdf, kendall_ppf
>>> cop = GumbelCopula(2.5)
>>> p = np.array([0.05, 0.5, 0.95])
>>> bool(np.allclose(kendall_cdf(cop, kendall_ppf(cop, p)), p, atol=1e-9))
True
kendall_rvs ¶
kendall_rvs(copula: Copula, size: int = 1, random_state: Generator | int | None = None) -> NDArray[np.float64]
Draw random values from the Kendall distribution of a copula (R's rK).
It simulates points from the copula and evaluates the copula's CDF at each
one, so the output is a sample of :math:C(\mathbf U).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The dependence model, with its parameters set. Must support |
required |
size
|
int
|
Number of draws; a positive integer. |
1
|
random_state
|
(int, Generator or None)
|
Seed or generator for reproducibility. |
None
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (size,)
|
Independent draws, each in :math: |
Notes
Exact by construction -- :math:K is the law of :math:C(\mathbf U) --
and cheaper than inverting :math:K.
Examples:
The draws reproduce the analytic K:
>>> import numpy as np
>>> from scipy import stats
>>> from rcopula import ClaytonCopula
>>> from rcopula.kendall import kendall_cdf, kendall_rvs
>>> cop = ClaytonCopula(3.0)
>>> w = kendall_rvs(cop, 20_000, random_state=0)
>>> p = stats.kstest(w, lambda x: kendall_cdf(cop, x)).pvalue
>>> bool(p > 0.01)
True
kendall_empirical ¶
kendall_empirical(data: ArrayLike, t: ArrayLike | None = None) -> NDArray[np.float64]
Estimate the Kendall distribution function straight from data, with no model.
This is the nonparametric estimator of Genest and Rivest (1993), R's
Kn. For each observation it counts the fraction of the other
observations that are smaller in every coordinate, then takes the empirical
distribution of those fractions. It uses only ranks, so the margins do not
matter and no copula has to be fitted. Compare it with :func:kendall_cdf
of a fitted family to see whether the family's joint behaviour matches the
data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
array_like of float, shape (n, d)
|
Observations, one row per observation, on any scale (raw data or
pseudo-observations). Needs |
required |
t
|
float, array_like of float or None
|
Levels at which to evaluate the estimate. |
None
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float
|
If |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Genest & Rivest (1993) replace :math:C(\mathbf X_i) by the fraction of the
sample it dominates,
.. math:: W_i = \frac{1}{n-1}\,#{j \ne i : \mathbf X_j < \mathbf X_i},
and take the empirical distribution of the :math:W_i. Being rank-based it
needs no margins and no fitted copula, which makes it the natural check on
whether a family's :math:K matches the data -- and the basis of the
Sn^K goodness-of-fit statistics. Memory and time are :math:O(n^2).
Examples:
It tracks the true K without knowing the family:
>>> import numpy as np
>>> from rcopula import ClaytonCopula
>>> from rcopula.kendall import kendall_cdf, kendall_empirical
>>> cop = ClaytonCopula(3.0)
>>> x = cop.rvs(4000, random_state=0)
>>> grid = np.array([0.1, 0.3, 0.5, 0.8])
>>> gap = np.abs(kendall_empirical(x, grid) - kendall_cdf(cop, grid))
>>> bool(gap.max() < 0.03)
True
kendall_return_period ¶
kendall_return_period(copula: Copula, t: ArrayLike, interval: float = 1.0) -> NDArray[np.float64]
How long, on average, until a joint event at least this severe happens again.
This is the Kendall return period :math:\text{interval}/(1 - K(t)): the
multivariate counterpart of "the 100-year flood". Use it when an event is
dangerous because several variables are extreme together (for example
river flow and rainfall, or losses on several assets).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The fitted dependence structure, with its parameters set (see
:func: |
required |
t
|
float or array_like of float, any shape
|
Critical level, in copula units: the value of :math: |
required |
interval
|
float
|
Mean time between observations, in whatever unit the answer should be in -- 1 for annual maxima. Must be positive. |
1.0
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
Return periods in the units of |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If :func: |
Notes
A univariate return period asks how often one variable is exceeded; the
joint event :math:\{C(\mathbf U) > t\} is a region, and every point on its
boundary is equally extreme, so no single margin can express it.
Because :math:K(t) \ge t for every copula, the Kendall return period is
always longer than the univariate one at the same level. Reporting the
univariate number for a compound event therefore overstates how often it
happens.
Examples:
Comonotone variables move together exactly, so the joint event is the univariate one and the two return periods coincide. Everything else waits longer, and independence longest of all:
>>> from rcopula import FrechetUpperCopula, IndependenceCopula
>>> from rcopula.kendall import kendall_return_period
>>> float(round(kendall_return_period(FrechetUpperCopula(2), 0.99)[0], 1))
100.0
>>> float(round(kendall_return_period(IndependenceCopula(2), 0.99)[0], 1))
19933.2
The number that should give a practitioner pause: two copulas with the same Kendall's tau of 0.5 give 199 years and 6689 years. Gumbel has upper tail dependence and Clayton has none, and the critical layer is an upper-corner event -- so a rank correlation, however carefully estimated, does not determine the design life.
>>> from rcopula import ClaytonCopula, GumbelCopula
>>> float(round(kendall_return_period(GumbelCopula(2.0), 0.99)[0], 1))
199.0
>>> float(round(kendall_return_period(ClaytonCopula(2.0), 0.99)[0], 1))
6689.0
return_period_level ¶
return_period_level(copula: Copula, period: ArrayLike, interval: float = 1.0) -> NDArray[np.float64]
Find the joint severity level that corresponds to a chosen return period.
The inverse of :func:kendall_return_period: given "the 100-year event",
return the critical level :math:t (in copula units) whose Kendall return
period is period. This is the quantity a design standard actually
specifies -- the return period is fixed and the engineer needs the level
that goes with it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The fitted dependence structure, with its parameters set (see
:func: |
required |
period
|
float or array_like of float, any shape
|
Target return periods, in the units of |
required |
interval
|
float
|
Mean time between observations -- 1 for annual maxima. Must be positive. |
1.0
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (m,)
|
Critical levels |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from rcopula import GumbelCopula
>>> from rcopula.kendall import kendall_return_period, return_period_level
>>> cop = GumbelCopula(2.0)
>>> t = return_period_level(cop, 100.0)
>>> bool(abs(kendall_return_period(cop, t)[0] - 100.0) < 1e-6)
True
rcopula.core.measures ¶
Dependence measures by quadrature, for copulas with no closed form.
Structural copulas -- rotations, mixtures, Khoudraji devices -- generally have no analytic Kendall's tau. Both measures are integrals of the copula itself,
.. math:: \rho = 12\int_0^1!!\int_0^1 C(u,v)\,du\,dv - 3, \qquad \tau = 4\int_0^1!!\int_0^1 C(u,v)\,dC(u,v) - 1,
so a tensor-product rule evaluates them directly.
The rule is tanh-sinh (double exponential) rather than Gauss-Legendre, and
the reason is specific: a copula with tail dependence has a density that blows
up at a corner of the unit square. Gauss-Legendre assumes an analytic integrand
and degrades to algebraic convergence when that fails -- for a Gumbel copula it
reaches only 4e-4 with 128 nodes per axis and improves like :math:n^{-2}.
The tanh-sinh substitution
:math:u = \tfrac{1}{2}(1 + \tanh(\tfrac{\pi}{2}\sinh t)) sends the node
spacing to zero doubly exponentially at each end, which absorbs endpoint
singularities; the same Gumbel case reaches 5e-10 with 159 nodes.
References
Nelsen, R. B. (2006). An Introduction to Copulas, 2nd ed. Springer, Theorems 5.1.1 and 5.1.3 -- the integral representations used here. Takahasi, H. and Mori, M. (1974). Double exponential formulas for numerical integration. Publications of RIMS, Kyoto University 9(3), 721-741.
rho_by_quadrature ¶
rho_by_quadrature(copula: Copula, level: int = _LEVEL) -> float
Compute Spearman's rho of a two-variable copula by numerical integration.
Spearman's rho is a rank correlation between -1 and 1: 0 means no monotone association, positive values mean the two variables tend to be large together. Use this for copulas that have no closed-form rho, such as rotations, mixtures and Khoudraji constructions.
Technically, it evaluates :math:\rho = 12\int\int C\,du\,dv - 3 with a
tanh-sinh rule. It needs only the CDF, so it works for every bivariate
copula -- including those with no density at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
A fully specified copula with |
required |
level
|
int
|
Half-width of the tanh-sinh node index; larger means more nodes per axis and higher accuracy at more cost. The default gives roughly 150 usable nodes per axis and about 1e-9 accuracy. |
140
|
Returns:
| Type | Description |
|---|---|
float
|
Spearman's rho, clipped to |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Reproduces the analytic value for families that have one:
>>> from rcopula import ClaytonCopula, GumbelCopula
>>> from rcopula.core.measures import rho_by_quadrature
>>> for cop in (ClaytonCopula(2.0), GumbelCopula(2.5)):
... print(bool(abs(rho_by_quadrature(cop) - cop.rho()) < 1e-8))
True
True
tau_by_quadrature ¶
tau_by_quadrature(copula: Copula, level: int = _LEVEL) -> float
Compute Kendall's tau of a two-variable copula by numerical integration.
Kendall's tau is a rank correlation between -1 and 1 based on how often
pairs of observations move in the same direction. Use this for copulas
with a density but no closed-form tau. If the copula has a singular part
or very strong dependence, prefer :func:tau_by_partials.
Technically, it evaluates
:math:\tau = 4\int\int C\,dC - 1 = 4\int\int C\,c\,du\,dv - 1 with a
tanh-sinh rule. It needs the density as well as the CDF. Unlike Spearman's rho, tau is not
linear in the copula -- which is exactly why mixtures and rotations cannot
simply average their components' values and have to come here instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
A fully specified copula with |
required |
level
|
int
|
Half-width of the tanh-sinh node index; larger means more nodes per axis and higher accuracy at more cost. |
140
|
Returns:
| Type | Description |
|---|---|
float
|
Kendall's tau, clipped to |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from rcopula import GumbelCopula, JoeCopula
>>> from rcopula.core.measures import tau_by_quadrature
>>> for cop in (GumbelCopula(2.5), JoeCopula(3.0)):
... print(bool(abs(tau_by_quadrature(cop) - cop.tau()) < 1e-7))
True
True
tau_by_partials ¶
tau_by_partials(copula: Copula, nodes: int = _PARTIAL_NODES) -> float
Compute Kendall's tau of a two-variable copula using only its CDF.
Kendall's tau is a rank correlation between -1 and 1. This is the most robust of the quadrature routines: use it when the copula has a singular part (no density everywhere) or very strong dependence.
Technically, it evaluates
:math:\tau = 1 - 4\int\int \partial_1 C\,\partial_2 C\,du\,dv, with the
partial derivatives taken by central differences and a Gauss-Legendre rule.
The same quantity as :func:tau_by_quadrature, integrated a different way,
and better in two situations that matter.
- It needs only the CDF. The :math:
\int\int C\,dCform silently assumes the copula is absolutely continuous: a copula with a singular component -- Marshall-Olkin, or Clayton below zero -- carries mass that no density represents, and integrating the density alone misses it. Here the partial derivatives are conditional distribution functions, which exist regardless. - The integrand is bounded in :math:
[0, 1]. Under strong dependence the density concentrates into a near-delta on the diagonal and no quadrature can resolve it: at Placketttheta = 1e4the density form returns 1.0 against a true 0.9756. The bounded form gets 0.97572.
The trade is a numerical derivative, so it carries about 4e-8 rather than 1e-8 -- immaterial next to what it buys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
A fully specified copula with |
required |
nodes
|
int
|
Gauss-Legendre nodes per axis. The cost grows with |
400
|
Returns:
| Type | Description |
|---|---|
float
|
Kendall's tau, clipped to |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Agrees with the closed forms:
>>> from rcopula import ClaytonCopula, GumbelCopula
>>> from rcopula.core.measures import tau_by_partials
>>> for cop in (ClaytonCopula(2.0), GumbelCopula(3.0)):
... print(bool(abs(tau_by_partials(cop) - cop.tau()) < 1e-7))
True
True
And works where the density form cannot -- Marshall-Olkin puts mass on a curve, so its density integrates to less than one:
>>> from rcopula import MarshallOlkinCopula
>>> cop = MarshallOlkinCopula(0.7, 0.4)
>>> bool(abs(tau_by_partials(cop) - cop.tau()) < 1e-3)
True
rcopula.sampling ¶
Quasi-random and variance-reduced copula sampling.
Every Monte Carlo answer in this package -- a CDO tranche spread, a basket option, a portfolio's expected shortfall -- has a standard error, and the default way to halve it is to quadruple the sample. These are the cheaper ways.
Quasi-random. A Sobol or Halton point set fills the unit cube more evenly
than independent uniforms do, because it is designed to rather than merely
tending to. Pushing such a set through the inverse Rosenblatt transform gives a
sample from the copula whose integration error falls like :math:(\log n)^d / n
rather than :math:n^{-1/2}. For a smooth payoff in a few dimensions that is
the difference between four digits and two.
The catch, stated plainly because it is usually not: the improvement depends on
the integrand being smooth and of low effective dimension. A payoff with a kink
(any option), a high dimension, or a discontinuity (a default indicator) erodes
it, and the rate can fall back to :math:n^{-1/2}. :func:variance_ratio
measures what you actually got instead of assuming it.
Antithetic. Pair every draw :math:z with :math:1 - z. Free, and helps
exactly when the integrand is monotone in the uniforms -- which a European call
on a single asset is, and a straddle is not. It can increase variance for a
symmetric payoff, so it is not a default.
Latin hypercube. Stratifies each margin into :math:n equal bins and
permutes. Guarantees marginal coverage, does nothing about the joint structure,
and is a good default when the answer depends mostly on the margins.
============================ ================================================
:func:quasi_rvs Sobol or Halton draws from a copula.
:func:antithetic_rvs Draws paired with their reflections.
:func:latin_hypercube_rvs Stratified draws.
:func:variance_ratio What the variance reduction actually was.
============================ ================================================
Examples:
>>> import numpy as np, rcopula as rc
>>> from rcopula.sampling import quasi_rvs
>>> u = quasi_rvs(rc.ClaytonCopula(2.0), 1024, random_state=0)
>>> u.shape
(1024, 2)
>>> bool(abs(rc.cor_kendall(u)[0, 1] - 0.5) < 0.02)
True
References
Cambou, M., Hofert, M. and Lemieux, C. (2017). Quasi-random numbers for copula models. Statistics and Computing 27(5), 1307-1329. The paper this implements: low-discrepancy sequences through the inverse Rosenblatt transform. Sobol', I. M. (1967). On the distribution of points in a cube and the approximate evaluation of integrals. USSR Computational Mathematics and Mathematical Physics 7(4), 86-112. Owen, A. B. (1997). Scrambled net variance for integrals of smooth functions. Annals of Statistics 25(4), 1541-1562. Why scrambling is what makes an error estimate possible at all. McKay, M. D., Beckman, R. J. and Conover, W. J. (1979). A comparison of three methods for selecting values of input variables in the analysis of output from a computer code. Technometrics 21(2), 239-245. Latin hypercube sampling. Glasserman, P. (2003). Monte Carlo Methods in Financial Engineering. Springer. Chapter 4 on variance reduction, chapter 5 on quasi-Monte Carlo.
quasi_rvs ¶
quasi_rvs(copula: Copula, size: int, *, sequence: Sequence = 'sobol', scramble: bool = True, random_state: Any = None) -> NDArray[np.float64]
Draw a sample from a copula whose points are spread more evenly than random ones.
This is quasi-random (quasi-Monte Carlo) sampling. Use it in place of
copula.rvs when you are averaging something over the sample -- a price,
a risk measure -- and want a smaller error for the same number of draws.
It generates a Sobol or Halton set in :math:[0,1]^d and pushes it through
the copula's inverse Rosenblatt transform (Cambou, Hofert and Lemieux 2017).
The result has the right distribution and covers the space more evenly than
independent draws.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
Any fitted or constructed copula of dimension |
required |
size
|
int
|
Number of points, at least 1. For Sobol, a power of two (512, 1024, ...) preserves the balance properties the construction is built on; scipy warns otherwise, and so does the note below. |
required |
sequence
|
('sobol', 'halton')
|
Which point set to use. Sobol is the usual choice and better in moderate dimensions; Halton is simpler and degrades faster past about ten. |
"sobol"
|
scramble
|
bool
|
Owen scrambling. Keep it on: an unscrambled set is deterministic, so it gives one answer with no way to estimate its error, and its first point is the corner of the cube. Scrambling preserves the low discrepancy and restores the ability to replicate. |
True
|
random_state
|
(int, Generator or None)
|
Seeds the scrambling. Pass an int for reproducible output. |
None
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (size, d)
|
Draws from the copula, every value strictly between 0 and 1. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
The gain depends on the integrand. Smooth and low-dimensional: large. Kinked
(any option payoff), high-dimensional, or discontinuous (a default
indicator): smaller, sometimes none. Measure it with :func:variance_ratio
rather than assuming.
Examples:
The margins are uniform and the dependence is right, as for rvs:
>>> import numpy as np, rcopula as rc
>>> from rcopula.sampling import quasi_rvs
>>> u = quasi_rvs(rc.GumbelCopula(2.0, dim=3), 4096, random_state=0)
>>> bool(np.all(np.abs(u.mean(axis=0) - 0.5) < 0.01))
True
>>> bool(abs(rc.cor_kendall(u)[0, 1] - 0.5) < 0.02)
True
Coverage is visibly more even -- the largest gap between consecutive sorted values in a margin is smaller than for independent draws:
>>> plain = rc.GumbelCopula(2.0, dim=3).rvs(4096, random_state=0)
>>> gap = lambda a: np.max(np.diff(np.sort(a[:, 0])))
>>> bool(gap(u) < gap(plain))
True
antithetic_rvs ¶
antithetic_rvs(copula: Copula, size: int, *, random_state: Any = None) -> NDArray[np.float64]
Draw a sample from a copula in mirror-image pairs, to reduce Monte Carlo error.
This is antithetic sampling. It is free and helps when the quantity you average goes up (or down) steadily as the draws go up, such as a call option payoff; it can hurt for symmetric payoffs (see the warning below).
Each independent uniform vector :math:z is used twice, as :math:z and
:math:1-z, before the inverse Rosenblatt transform. The two resulting
draws are negatively associated, so a Monte Carlo average over them has
lower variance when the integrand is monotone in the uniforms.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
Any copula of dimension |
required |
size
|
int
|
Total number of draws, at least 1. Rounded up to an even number, since
they come in pairs; the first half and second half are the partners of
each other, so with |
required |
random_state
|
(int, Generator or None)
|
Seed for the underlying uniforms. Pass an int for reproducible output. |
None
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (2 * ceil(size / 2), d)
|
Draws from the copula: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Warnings
For a payoff symmetric about the middle of the distribution -- a straddle, an
absolute deviation, a variance -- antithetic pairing can raise variance
rather than lower it, because the two halves of a pair move the same way.
Check with :func:variance_ratio before adopting it.
Examples:
>>> import numpy as np, rcopula as rc
>>> from rcopula.sampling import antithetic_rvs
>>> u = antithetic_rvs(rc.ClaytonCopula(2.0), 2000, random_state=0)
>>> u.shape
(2000, 2)
The pairing is exact on the first coordinate, which the transform leaves alone:
>>> half = 1000
>>> bool(np.allclose(u[:half, 0] + u[half:, 0], 1.0))
True
latin_hypercube_rvs ¶
latin_hypercube_rvs(copula: Copula, size: int, *, random_state: Any = None) -> NDArray[np.float64]
Draw a sample from a copula whose margins are spread evenly by construction.
This is Latin hypercube sampling. Use it when the answer you are computing depends mostly on the individual margins rather than on how they move together.
Each of the :math:d uniform coordinates is stratified into size equal
bins with exactly one point per bin, then the columns are permuted
independently. The marginal coverage is therefore guaranteed rather than
random, which is worth having when the answer depends mostly on the margins.
It says nothing about the joint structure -- that comes from the inverse Rosenblatt transform, as usual.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
Any copula of dimension |
required |
size
|
int
|
Number of draws, at least 1. Also the number of bins per margin. |
required |
random_state
|
(int, Generator or None)
|
Seed for the design. Pass an int for reproducible output. |
None
|
Returns:
| Type | Description |
|---|---|
numpy.ndarray of float, shape (size, d)
|
Draws from the copula, every value strictly between 0 and 1. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Every margin covers its range by construction, so the sample mean of a uniform coordinate is almost exactly one half:
>>> import numpy as np, rcopula as rc
>>> from rcopula.sampling import latin_hypercube_rvs
>>> u = latin_hypercube_rvs(rc.FrankCopula(5.0, dim=3), 2000, random_state=0)
>>> bool(abs(u[:, 0].mean() - 0.5) < 0.005)
True
>>> bool(abs(rc.cor_kendall(u)[0, 1] - rc.FrankCopula(5.0).tau()) < 0.03)
True
variance_ratio ¶
variance_ratio(copula: Copula, payoff: Callable[[NDArray[float64]], ArrayLike], size: int, *, method: str = 'sobol', replicates: int = 20, random_state: Any = None) -> dict[str, float]
Measure how much a variance-reduction sampler shrinks the Monte Carlo error for your payoff.
Use it before adopting :func:quasi_rvs, :func:antithetic_rvs or
:func:latin_hypercube_rvs: it tells you, for your copula and your
payoff, whether the method helps and by how much.
Runs the estimator replicates times under plain Monte Carlo and again
under method, and compares the spread of the answers. This is the honest
way to report a quasi-Monte Carlo gain: the theory gives a rate, not a
number, and the number depends on the payoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The copula to sample from, of dimension |
required |
payoff
|
callable
|
Function |
required |
size
|
int
|
Draws per replicate, at least 1. |
required |
method
|
('sobol', 'halton', 'antithetic', 'lhs')
|
The sampler to compare against plain |
"sobol"
|
replicates
|
int
|
Independent repetitions, at least 2. Each uses a different scramble or seed, which is what makes a spread measurable at all -- an unscrambled quasi-random set would give the same answer every time and no error estimate. |
20
|
random_state
|
(int, Generator or None)
|
Seed from which every replicate's seed is drawn. |
None
|
Returns:
| Type | Description |
|---|---|
dict of str to float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
A smooth payoff, where quasi-random sampling does well:
>>> import numpy as np, rcopula as rc
>>> from rcopula.sampling import variance_ratio
>>> smooth = lambda u: u[:, 0] * u[:, 1]
>>> out = variance_ratio(rc.ClaytonCopula(2.0), smooth, 1024, replicates=12,
... random_state=0)
>>> bool(out["ratio"] > 2.0)
True
>>> bool(abs(out["plain_mean"] - out["reduced_mean"]) < 0.01)
True