Inference¶
rcopula.fit.api ¶
Copula estimation.
Five methods, using R's exact option strings so code ports across unchanged:
========== ====================================================================
"mpl" Maximum pseudo-likelihood. The default and the usual choice:
maximise the copula likelihood at rank-based pseudo-observations,
making no assumption about the margins.
"ml" Same estimate, different standard errors. Use only when the data
really are copula observations (margins known, not estimated).
"itau" Invert Kendall's tau. Closed form for most families, robust, and a
good starting value even when it is not the final answer.
"irho" Invert Spearman's rho. Same idea, usually slightly less efficient.
"itau.mpl" Mashal-Zeevi: correlations from inverted tau, degrees of
freedom by pseudo-likelihood. For t copulas in higher dimensions,
where a joint optimisation over both is badly conditioned.
========== ====================================================================
"mpl" and "ml" produce the same point estimate and differ only in the
variance -- an easy thing to misread in R's documentation, and the reason
:func:fit reports the method it used in the summary.
References
Genest, C., Ghoudi, K. and Rivest, L.-P. (1995). A semiparametric estimation procedure of dependence parameters in multivariate families of distributions. Biometrika 82(3), 543-552. Mashal, R. and Zeevi, A. (2002). Beyond correlation: extreme co-movements between financial assets. Columbia University working paper. Higham, N. J. (2002). Computing the nearest correlation matrix -- a problem from finance. IMA Journal of Numerical Analysis 22(3), 329-343.
nearest_correlation ¶
nearest_correlation(matrix: ArrayLike, tol: float = 1e-10, max_iter: int = 200) -> NDArray[np.float64]
Repair a "correlation matrix" that is not a valid one, changing it as little as possible.
Returns the nearest positive-definite matrix with ones on the diagonal, found by alternating projections (Higham, 2002). Use it when a matrix assembled entry by entry -- for example from pairwise Kendall's tau -- cannot be used as a correlation matrix because it has a negative eigenvalue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
array_like of float, shape (d, d)
|
Square matrix to repair. It is symmetrised first, as
|
required |
tol
|
float
|
Small positive number: the smallest eigenvalue allowed in the result, and the convergence tolerance. |
1e-10
|
max_iter
|
int
|
Maximum number of projection rounds (a positive integer). |
200
|
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (d, d)
|
Symmetric, positive-definite matrix with unit diagonal. |
Notes
Inverting pairwise dependence measures gives a symmetric matrix with unit
diagonal, but nothing guarantees it is positive definite -- each entry is
estimated separately. R applies the same repair (Matrix::nearPD) after
itau and irho fits.
Examples:
>>> import numpy as np
>>> from rcopula.fit import nearest_correlation
>>> bad = np.array([[1.0, 0.9, -0.9], [0.9, 1.0, 0.9], [-0.9, 0.9, 1.0]])
>>> bool(np.linalg.eigvalsh(bad).min() < 0)
True
>>> fixed = nearest_correlation(bad)
>>> bool(np.linalg.eigvalsh(fixed).min() > 0)
True
>>> bool(np.allclose(np.diag(fixed), 1.0))
True
loglik_copula ¶
loglik_copula(params: ArrayLike, u: ArrayLike, copula: Copula, error: str = '-inf') -> float
Score how well a copula with the given parameters explains the data (higher is better).
Returns the copula log-likelihood: the sum of the log-density over all
rows of u. This is R's loglikCopula. Use it to compare parameter
values by hand or to build your own optimiser; :func:fit uses it
internally.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
array_like of float, shape (p,)
|
Full parameter vector to evaluate at, in the order of
|
required |
u
|
array_like of float, shape (n, d)
|
Observations strictly inside the unit cube (pseudo-observations or copula samples). A 1-D input is treated as a single row. |
required |
copula
|
Copula
|
Supplies the family; its own parameter values are ignored. |
required |
error
|
('-inf', 'raise')
|
What to do when the parameters are inadmissible (outside the family's
range, or giving a non-finite density): |
"-inf"
|
Returns:
| Type | Description |
|---|---|
float
|
The log-likelihood, or |
Raises:
| Type | Description |
|---|---|
(ValueError, LinAlgError or NotImplementedError)
|
Only with |
Examples:
>>> import numpy as np
>>> from rcopula import ClaytonCopula, loglik_copula
>>> u = ClaytonCopula(2.0).rvs(500, random_state=0)
>>> at_truth = loglik_copula([2.0], u, ClaytonCopula())
>>> at_wrong = loglik_copula([0.5], u, ClaytonCopula())
>>> bool(at_truth > at_wrong)
True
Inadmissible parameters give -inf rather than an error, so optimisers
can walk into them safely:
>>> bool(np.isneginf(loglik_copula([-5.0], u, ClaytonCopula())))
True
fit ¶
fit(copula: Copula, data: ArrayLike, method: str = 'mpl', *, start: ArrayLike | None = None, optim_method: str | None = None, estimate_variance: bool = True, ties_method: str = 'average') -> CopulaFitResult
Estimate a copula's parameters from data.
Give it a copula family (for example ClaytonCopula()) and a table of
observations; it returns the best-fitting parameters together with
standard errors, a log-likelihood and AIC/BIC. Your data can be raw
values or already-uniform pseudo-observations -- raw values are turned
into ranks first, so the margins do not matter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
Family to fit. Its parameter values are used only as a starting point;
pass e.g. |
required |
data
|
array_like of float or pandas.DataFrame, shape (n, d)
|
Observations, one row per observation and one column per variable;
|
required |
method
|
('mpl', 'ml', 'itau', 'irho', 'itau.mpl')
|
Estimation method:
See the module docstring for details. |
"mpl"
|
start
|
array_like of float, shape (p,), or None
|
Starting parameters (full vector, in the order of
|
None
|
optim_method
|
str or None
|
A |
None
|
estimate_variance
|
bool
|
Whether to compute the asymptotic covariance matrix (and therefore
standard errors). Set to |
True
|
ties_method
|
('average', 'min', 'max', 'dense', 'ordinal', 'random')
|
How tied values are ranked; passed to
:func: |
"average"
|
Returns:
| Type | Description |
|---|---|
CopulaFitResult
|
Fitted copula, estimates of the free parameters, standard errors
( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Warns:
| Type | Description |
|---|---|
UserWarning
|
For |
See Also
rcopula.fit_joint : Fit the margins and the copula together. rcopula.select_copula : Fit several families and rank them.
Notes
"mpl" and "ml" produce the same point estimate and differ only
in the variance. Every method respects parameters pinned with
fix_params: inversion keeps pinned correlations (repairing the matrix
around them when needed) and "itau.mpl" skips the df search when
df is pinned. For "itau.mpl" the correlations' standard errors are
those of the inversion estimator -- the correlations are the "itau"
estimates -- while the df row and column of cov_params are
nan: a variance for the second stage would have to account for the
first, which is not standard, and R reports no variance for this method
at all. A copula with no free parameters is not optimised; its
log-likelihood is still reported so it can take part in AIC/BIC
comparisons.
Examples:
>>> import numpy as np
>>> from rcopula import ClaytonCopula, fit
>>> u = ClaytonCopula(2.0).rvs(2000, random_state=0)
>>> res = fit(ClaytonCopula(), u, method="mpl")
>>> bool(abs(res.params[0] - 2.0) < 0.2)
True
>>> bool(res.bse[0] > 0) # standard errors, unlike elsewhere
True
"mpl" and "ml" agree on the estimate and differ on the uncertainty:
>>> a = fit(ClaytonCopula(), u, method="mpl")
>>> b = fit(ClaytonCopula(), u, method="ml")
>>> bool(np.allclose(a.params, b.params, rtol=1e-6))
True
>>> bool(a.bse[0] != b.bse[0])
True
Inversion of Kendall's tau needs no optimiser at all:
>>> res = fit(ClaytonCopula(), u, method="itau")
>>> bool(abs(res.params[0] - 2.0) < 0.2)
True
rcopula.fit.mvdc ¶
Fitting margins and copula together (R's fitMvdc).
Everywhere else in this package the margins are removed first, by
:func:~rcopula.pseudo_obs, and only the copula is estimated. That is the
semiparametric route and usually the right one: it makes no claim about the
marginal shapes, so a wrong guess there cannot contaminate the dependence.
Sometimes you want the whole distribution anyway -- to simulate from it, to price something, to report a fitted model rather than a fitted copula. That means committing to parametric margins, and there are two ways to do it.
"ifm" -- inference functions for margins (Joe and Xu 1996). Fit each
margin by maximum likelihood, then fit the copula to
:math:(\hat F_1(x_1), \dots, \hat F_d(x_d)). Two steps, each small, and the
standard choice. Note what changed: the copula now sees parametric probability
integral transforms, not ranks, so a misspecified margin distorts the estimated
dependence -- which the rank-based route would have been immune to.
"ml" -- full maximum likelihood. Optimise every parameter at once.
Asymptotically efficient, and in practice often not worth it: the surface has
:math:\sum_j p_j + q dimensions, it is not concave, and the gain over IFM is
usually in the third decimal. Started from the IFM estimate, because started
anywhere else it frequently does not arrive.
============================ ================================================
:func:fit_joint Estimate margins and copula from data.
:class:JointFitResult The fitted distribution, with both parts.
============================ ================================================
Examples:
>>> import numpy as np, rcopula as rc
>>> from scipy import stats
>>> from rcopula.fit.mvdc import fit_joint
>>> truth = rc.CopulaDistribution(
... rc.ClaytonCopula(2.0), [stats.norm(1.0, 2.0), stats.expon(scale=3.0)]
... )
>>> x = truth.rvs(3000, random_state=0)
>>> template = rc.CopulaDistribution(rc.ClaytonCopula(1.0), [stats.norm(), stats.expon()])
>>> result = fit_joint(template, x)
>>> bool(abs(result.copula.params[0] - 2.0) < 0.25)
True
References
Joe, H. and Xu, J. J. (1996). The estimation method of inference functions for margins for multivariate models. Technical Report 166, Department of Statistics, University of British Columbia. The IFM estimator. Joe, H. (2005). Asymptotic efficiency of the two-stage estimation method for copula-based models. J. Multivariate Analysis 94(2), 401-419. How much IFM gives up against full maximum likelihood, which is usually very little. Genest, C., Ghoudi, K. and Rivest, L.-P. (1995). A semiparametric estimation procedure of dependence parameters in multivariate families of distributions. Biometrika 82(3), 543-552. The rank-based alternative, and why it is the safer default.
JointFitResult
dataclass
¶
JointFitResult(distribution: CopulaDistribution, copula: Copula, margin_params: list[tuple[float, ...]], loglik: float, method: str, n_obs: int, converged: bool, message: str = '', marginal_loglik: float = 0.0, n_at_boundary: int = 0, margin_fixed: list[tuple[bool, ...]] | None = None, _x: NDArray[float64] = (lambda: np.empty((0, 0)))())
The result of fitting both the margins and the copula: a complete, usable distribution.
Returned by :func:fit_joint; you do not normally build one yourself.
The fitted :attr:distribution can be sampled or evaluated straight
away, and :meth:summary prints a report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distribution
|
CopulaDistribution
|
Margins and copula, both fitted. |
required |
copula
|
Copula
|
The fitted dependence part alone. |
required |
margin_params
|
list of tuple of float, length d
|
Fitted parameters of each margin, in |
required |
loglik
|
float
|
Joint log-likelihood, margins included. |
required |
method
|
('ifm', 'ml')
|
Estimation method used. |
"ifm"
|
n_obs
|
int
|
Number of observations (rows) fitted. |
required |
converged
|
bool
|
Whether the optimiser reported success (always |
required |
message
|
str
|
Optimiser message (empty for |
""
|
marginal_loglik
|
float
|
The margins' share of |
0.0
|
n_at_boundary
|
int
|
Number of observations whose fitted probability integral transform landed on 0 or 1. |
0
|
margin_fixed
|
list of tuple of bool or None
|
For each margin, which entries of |
None
|
_x
|
ndarray of float, shape (n, d)
|
The data that were fitted. Internal; leave at its default. |
(lambda: empty((0, 0)))()
|
Attributes:
| Name | Type | Description |
|---|---|---|
distribution |
CopulaDistribution
|
Margins and copula, both fitted. Ready to |
copula |
Copula
|
The dependence part alone. |
margin_params |
list of tuple of float, length d
|
The fitted parameters of each margin, in |
loglik |
float
|
Joint log-likelihood, margins included -- so it is not comparable
with a copula-only log-likelihood from :func: |
method |
str
|
|
n_obs |
int
|
Number of observations. |
converged |
bool
|
Whether the optimiser reported success. |
message |
str
|
Optimiser message, empty for |
marginal_loglik |
float
|
Sum of the margins' log-densities at the fitted parameters. |
n_at_boundary |
int
|
Observations whose fitted probability integral transform landed exactly
on 0 or 1. Almost always the sample extremes, because maximum likelihood
puts a margin's support boundary there; those values are nudged just
inside |
margin_fixed |
list of tuple of bool or None
|
Per margin, |
n_params
property
¶
n_params: int
How many parameters were estimated in total, margins plus copula.
Returns:
| Type | Description |
|---|---|
int
|
The number of estimated margin parameters -- the lengths of
|
aic
property
¶
aic: float
A fit score for the whole model that penalises extra parameters; lower is better (AIC).
Akaike information criterion, 2 k - 2 loglik with k =
:attr:n_params.
Returns:
| Type | Description |
|---|---|
float
|
|
bic
property
¶
bic: float
A fit score for the whole model with a stronger penalty than AIC; lower is better (BIC).
Bayesian information criterion, k log n - 2 loglik with k =
:attr:n_params.
Returns:
| Type | Description |
|---|---|
float
|
|
dependence_loglik
property
¶
dependence_loglik: float
The part of the log-likelihood due to the copula alone (joint minus margins).
Use this, not :attr:loglik, to compare copula families.
Returns:
| Type | Description |
|---|---|
float
|
|
Notes
This is comparable across copula families fitted to the same margins, which the joint figure is not.
summary ¶
summary() -> str
A human-readable text report of the fit, ready to print.
Lists each margin's fitted parameters, the copula, the joint, marginal and dependence log-likelihoods, AIC/BIC, the parameter count and the number of boundary observations, plus a warning if the optimiser did not converge.
Returns:
| Type | Description |
|---|---|
str
|
Multi-line text; pass it to |
Examples:
>>> import rcopula as rc
>>> from scipy import stats
>>> from rcopula.fit.mvdc import fit_joint
>>> truth = rc.CopulaDistribution(rc.GumbelCopula(2.0), [stats.norm()] * 2)
>>> x = truth.rvs(500, random_state=0)
>>> template = rc.CopulaDistribution(rc.GumbelCopula(1.5), [stats.norm()] * 2)
>>> print(fit_joint(template, x).summary().splitlines()[0])
Joint fit by IFM, 500 observations
fit_joint ¶
fit_joint(distribution: CopulaDistribution, x: ArrayLike, *, method: Method = 'ifm', margin_kwargs: list[dict[str, Any]] | None = None, copula_method: str = 'mpl') -> JointFitResult
Fit a full joint distribution: each variable's own distribution plus the copula linking them.
Use this when you want a complete model on the original scale -- to
simulate realistic data, compute probabilities, or report fitted
margins -- rather than the dependence alone. This is R's fitMvdc.
If you only care about the dependence, :func:~rcopula.fit on ranks is
safer, because it makes no assumption about the margins.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distribution
|
CopulaDistribution
|
Supplies the shapes: which copula family and which marginal families.
Its current parameter values are only a starting point. Each margin
must be a frozen |
required |
x
|
array_like of float, shape (n, d)
|
Data on the original scale, not pseudo-observations; |
required |
method
|
('ifm', 'ml')
|
|
"ifm"
|
margin_kwargs
|
list of dict or None
|
One dict per margin (length |
None
|
copula_method
|
('mpl', 'ml', 'itau', 'irho', 'itau.mpl')
|
Passed to :func: |
"mpl"
|
Returns:
| Type | Description |
|---|---|
JointFitResult
|
The fitted distribution, the copula, each margin's parameters, the log-likelihoods and AIC/BIC. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If a margin has no underlying |
Notes
"ml" starts from the "ifm" estimate. That is not a convenience: the
joint surface is not concave, and from a cold start the optimiser often ends
somewhere that is not the maximum at all.
Examples:
>>> import numpy as np, rcopula as rc
>>> from scipy import stats
>>> from rcopula.fit.mvdc import fit_joint
>>> truth = rc.CopulaDistribution(
... rc.GumbelCopula(2.5), [stats.norm(1.0, 2.0), stats.norm(-1.0, 0.5)]
... )
>>> x = truth.rvs(4000, random_state=0)
>>> template = rc.CopulaDistribution(rc.GumbelCopula(1.5), [stats.norm()] * 2)
>>> result = fit_joint(template, x)
>>> bool(abs(result.margin_params[0][0] - 1.0) < 0.1)
True
>>> bool(abs(result.copula.params[0] - 2.5) < 0.2)
True
The fitted object is a distribution, so it can be sampled straight away:
>>> result.distribution.rvs(5, random_state=0).shape
(5, 2)
rcopula.fit.results ¶
The object returned by :func:rcopula.fit.
Follows the statsmodels Model/Results split rather than scikit-learn's
"return self": copula inference needs standard errors, a covariance matrix,
a log-likelihood and a printable summary, and hanging those off a mutated
copula makes fitted and unfitted objects indistinguishable.
Notably, bse (standard errors) is the piece no other Python copula package
provides at all.
CopulaFitResult ¶
CopulaFitResult(copula: Copula, params: NDArray[float64], param_names: tuple[str, ...], loglik: float, n_obs: int, method: str, cov_params: NDArray[float64] | None = None, converged: bool = True, message: str = '')
Everything you get back after fitting a copula: estimates, uncertainty and fit quality.
You normally do not build this yourself -- :func:rcopula.fit returns
one. It holds the fitted copula, the estimated parameters, their standard
errors and confidence intervals, the log-likelihood with AIC/BIC for
comparing models, and a printable :meth:summary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
The fitted copula, with the estimated parameters already set. |
required |
params
|
array_like of float, shape (k,)
|
Estimated free (non-fixed) parameters. A scalar is turned into a length-1 array. |
required |
param_names
|
sequence of str, length k
|
Names matching |
required |
loglik
|
float
|
Maximised log-likelihood (the pseudo-log-likelihood for |
required |
n_obs
|
int
|
Number of observations (rows) the copula was fitted to. |
required |
method
|
str
|
Estimation method used, e.g. |
required |
cov_params
|
ndarray of float, shape (k, k), or None
|
Asymptotic covariance matrix of |
None
|
converged
|
bool
|
Whether the optimiser reported success. |
True
|
message
|
str
|
Optimiser message, or a note for closed-form estimators. |
""
|
Attributes:
| Name | Type | Description |
|---|---|---|
copula |
Copula
|
The fitted copula, with estimated parameters in place. |
params |
ndarray of float, shape (k,)
|
Estimated free parameters. |
param_names |
tuple of str, length k
|
Names matching |
cov_params |
ndarray of float, shape (k, k), or None
|
Asymptotic covariance matrix, or |
loglik |
float
|
Maximised log-likelihood (pseudo-likelihood for |
n_obs |
int
|
Number of observations. |
method |
str
|
Estimation method used. |
converged |
bool
|
Whether the optimiser reported success. |
message |
str
|
Optimiser message, or a note for closed-form estimators. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
See Also
rcopula.fit : The function that produces this object.
Notes
Follows the statsmodels Model/Results split: the copula you passed in
is not modified, and the fitted one lives in :attr:copula.
Examples:
>>> from rcopula import ClaytonCopula, fit
>>> u = ClaytonCopula(2.0).rvs(1000, random_state=0)
>>> res = fit(ClaytonCopula(), u)
>>> res.n_params
1
>>> res.conf_int().shape
(1, 2)
n_params
property
¶
n_params: int
How many parameters were estimated.
Returns:
| Type | Description |
|---|---|
int
|
Number of free parameters, |
bse
property
¶
bse: NDArray[float64] | None
Standard error of each estimate: roughly how far it could be from the true value.
Computed as the square root of the diagonal of :attr:cov_params.
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (k,), or None
|
Asymptotic standard errors, or |
Notes
This is what no other Python copula package offers.
tvalues
property
¶
tvalues: NDArray[float64] | None
Each estimate divided by its standard error (the z-statistic).
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (k,), or None
|
|
pvalues
property
¶
pvalues: NDArray[float64] | None
P-value per parameter: how surprising its estimate would be if the true value were zero.
Two-sided p-values from the normal approximation, 2 * P(Z > |z|).
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (k,), or None
|
Values in |
Notes
Interpret with care: for most families zero is not a meaningful null (Gumbel's parameter space starts at 1), so these are informative only where zero really means independence.
aic
property
¶
aic: float
A fit score that penalises extra parameters; lower is better (AIC).
Akaike information criterion, -2 loglik + 2 k. Use it to compare
models fitted to the same data.
Returns:
| Type | Description |
|---|---|
float
|
|
bic
property
¶
bic: float
A fit score that penalises extra parameters more than AIC does; lower is better (BIC).
Bayesian information criterion, -2 loglik + k log n. Use it to
compare models fitted to the same data.
Returns:
| Type | Description |
|---|---|
float
|
|
conf_int ¶
conf_int(alpha: float = 0.05) -> NDArray[np.float64] | None
A plausible range (lower, upper) for each parameter.
Wald confidence intervals at level 1 - alpha: estimate plus or
minus a normal quantile times the standard error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
float
|
One minus the confidence level, strictly between 0 and 1;
|
0.05
|
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (k, 2), or None
|
Column 0 holds the lower bounds and column 1 the upper bounds, or
|
summary ¶
summary() -> str
A human-readable text table of the fit, ready to print.
Shows the family, method, sample size, log-likelihood, AIC/BIC and a
row per parameter (estimate, standard error, z and p-value), in the
spirit of R's summary.fitCopula. A warning line is added when the
optimiser did not converge.
Returns:
| Type | Description |
|---|---|
str
|
Multi-line text; pass it to |
rcopula.fit.variance ¶
Asymptotic variance estimators for fitted copulas.
This module is the reason rcopula exists in the form it does: no other
Python copula package reports a standard error for a fitted copula parameter,
so a fitted value arrives with no indication of how much to trust it.
Three estimators live here, matching R's three cases.
Maximum likelihood (var_ml). If the data really are copula
observations, the information equality holds and the covariance is the inverse
observed information :math:H^{-1}/n.
Maximum pseudo-likelihood (var_mpl). The usual case, and the subtle
one. Because the margins are replaced by ranks, the score is evaluated at
estimated pseudo-observations rather than the true uniforms, and that adds a
term. Ignoring it -- as one is tempted to, since the point estimate is
unaffected -- understates the standard error, sometimes badly. Genest, Ghoudi &
Rivest (1995) give the correction: expanding around the true margins,
.. math::
\sqrt{n}(\hat\theta - \theta) \approx H^{-1}\,\frac{1}{\sqrt n}\sum_i W_i,
\qquad
W_i = \dot\ell(U_i) + \sum_{k=1}^{d} W_k(U_{ik}),
where :math:W_k(t) = \mathbb{E}\bigl[\dot\ell_{,k}(U)\,(\mathbf{1}\{t \le U_k\}
- U_k)\bigr] and :math:\dot\ell_{,k} = \partial^2 \log c/\partial\theta
\partial u_k. Each :math:W_k is estimated by its empirical average.
Inversion of a dependence measure (var_itau, var_irho). The
estimator is a smooth function of a rank statistic, so the delta method applies
once the statistic's own asymptotic variance is known. Both Kendall's tau and
Spearman's rho have known influence functions, estimated here empirically.
References
Genest, C., Ghoudi, K. and Rivest, L.-P. (1995). A semiparametric estimation procedure of dependence parameters in multivariate families of distributions. Biometrika 82(3), 543-552. The maximum-pseudo-likelihood variance. Kojadinovic, I. and Yan, J. (2010). Comparison of three semiparametric methods for estimating dependence parameters in copula models. Insurance: Mathematics and Economics 47(1), 52-63. The inversion estimators and their relative efficiency. Hoeffding, W. (1948). A class of statistics with asymptotically normal distribution. Annals of Mathematical Statistics 19(3), 293-325. The projection underlying the Kendall influence function. Borkowf, C. B. (2002). Computing the nonnull asymptotic variance and the asymptotic relative efficiency of Spearman's rank correlation. Computational Statistics & Data Analysis 39(3), 271-286.
var_ml ¶
var_ml(logpdf: Callable[[NDArray[float64]], NDArray[float64]], theta: NDArray[float64], n: int) -> NDArray[np.float64] | None
Estimate how uncertain maximum-likelihood copula parameters are (their covariance matrix).
This is the observed-information covariance used by
fit(..., method="ml"). Most users never call it directly: read
CopulaFitResult.cov_params or .bse instead. Call it yourself only
when you have written your own likelihood maximiser.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logpdf
|
callable
|
Function |
required |
theta
|
ndarray of float, shape (p,)
|
The estimate to evaluate at (ideally the maximiser). |
required |
n
|
int
|
Number of observations (positive). |
required |
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (p, p), or None
|
Estimated covariance matrix of |
Notes
Assumes the supplied data are copula observations -- margins known rather
than estimated -- so the information equality holds and
:math:\mathrm{Cov}(\hat\theta) = H^{-1}/n with :math:H the averaged
negative Hessian of the log-density.
The robust sandwich :math:H^{-1}\Sigma H^{-1}/n was tried first and
rejected. It is valid under misspecification, but here it merely adds noise:
on a Clayton sample of 1000 it gave 0.0932 against the information form's
0.089144, which reproduces R to six decimal places, while a Monte-Carlo
sampling SD put the truth near 0.090. Correct specification is exactly the
assumption method="ml" already makes.
Examples:
>>> import numpy as np
>>> from rcopula import ClaytonCopula
>>> from rcopula.fit import var_ml
>>> u = ClaytonCopula(2.0).rvs(1000, random_state=0)
>>> cov = var_ml(lambda t: ClaytonCopula(t[0]).logpdf(u), np.array([2.0]), 1000)
>>> cov.shape
(1, 1)
>>> bool(cov[0, 0] > 0)
True
mpl_influence ¶
mpl_influence(logpdf_at: Callable[[NDArray[float64], NDArray[float64]], NDArray[float64]], u: NDArray[float64], theta: NDArray[float64]) -> tuple[NDArray[np.float64], NDArray[np.float64]]
Measure how much each observation pushes a pseudo-likelihood estimate around.
Returns the per-observation influence :math:W_i (score plus the
rank-estimation correction) and the averaged negative Hessian. These are
the building blocks of :func:var_mpl; you only need them directly to
build a bootstrap or your own variance estimate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logpdf_at
|
callable
|
Function |
required |
u
|
ndarray of float, shape (n, d)
|
Pseudo-observations strictly inside the unit cube. |
required |
theta
|
ndarray of float, shape (p,)
|
The estimate to evaluate at. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
w |
ndarray of float, shape (n, p)
|
Influence contributions, one row per observation. All |
hessian |
ndarray of float, shape (p, p)
|
Averaged negative Hessian of the log-density. |
Notes
Split out from :func:var_mpl because the multiplier goodness-of-fit
bootstrap needs exactly the same quantity: it replicates
:math:\sqrt n(\hat\theta - \theta) as
:math:H^{-1} n^{-1/2}\sum_i Z_i W_i for random multipliers :math:Z_i,
which is what lets it avoid refitting the copula on every bootstrap draw.
var_mpl ¶
var_mpl(logpdf_at: Callable[[NDArray[float64], NDArray[float64]], NDArray[float64]], u: NDArray[float64], theta: NDArray[float64]) -> NDArray[np.float64] | None
Estimate how uncertain pseudo-likelihood copula parameters are (their covariance matrix).
This is the Genest-Ghoudi-Rivest covariance used by
fit(..., method="mpl"), the default. It accounts for the margins
having been replaced by ranks, which plain likelihood standard errors
ignore. Most users read CopulaFitResult.bse instead of calling this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logpdf_at
|
callable
|
Function |
required |
u
|
ndarray of float, shape (n, d)
|
The pseudo-observations the fit used, strictly inside the unit cube. |
required |
theta
|
ndarray of float, shape (p,)
|
The estimate. |
required |
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (p, p), or None
|
Estimated covariance matrix of |
Notes
Adds the rank-estimation correction that plain maximum-likelihood standard
errors omit. Concretely, alongside the score :math:\dot\ell(U_i) each
observation contributes
.. math:: W_k(U_{ik}) = \frac{1}{n}\sum_j \dot\ell_{,k}(U_j) \bigl(\mathbf{1}{U_{ik} \le U_{jk}} - U_{jk}\bigr)
for every margin k, and the covariance is the sandwich built from
:math:W_i = \dot\ell(U_i) + \sum_k W_k(U_{ik}).
Calibrated against its own sampling distribution: over 250 Clayton samples of size 1000 the mean estimate was 0.1238 against an empirical SD of 0.1234, a ratio of 1.004. Against R on 40 shared datasets the mean ratio is 1.013 (paired range 0.91-1.12), the spread reflecting that R has analytic derivatives for Clayton while these are numerical.
Examples:
>>> import numpy as np
>>> from rcopula import ClaytonCopula, pseudo_obs
>>> from rcopula.fit import var_mpl
>>> u = pseudo_obs(ClaytonCopula(2.0).rvs(500, random_state=0))
>>> cov = var_mpl(lambda x, t: ClaytonCopula(t[0]).logpdf(x), u, np.array([2.0]))
>>> bool(cov[0, 0] > 0)
True
kendall_influence ¶
kendall_influence(u: NDArray[float64]) -> NDArray[np.float64]
For each observation, the share of other observations that move in the same direction.
This is the empirical influence function of Kendall's tau for a bivariate
sample: the fraction of other points that are concordant with point
i. It is used to compute the standard error of tau-based estimates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
ndarray of float, shape (n, 2)
|
Bivariate sample (pseudo-observations or raw values; only the
ordering matters). |
required |
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (n,)
|
Values in |
Notes
Kendall's tau is a U-statistic of degree two, so by Hoeffding's projection its first-order behaviour is governed by
.. math:: h(u, v) = \Pr(U < u, V < v) + \Pr(U > u, V > v),
the probability of concordance with an independent copy. Since concordance
and discordance exhaust the possibilities, :math:\tau = 2\,\mathbb{E}[h] - 1.
The projection of a degree-two U-statistic carries a further factor of two,
giving :math:\mathrm{Var}(\hat\tau) \approx 16\,\mathrm{Var}(h)/n.
(Measured against 800 Gaussian samples of size 1500, the implied constant is 17.9 rather than 16 -- the usual finite-sample gap in a first-order projection, and small next to the delta-method step that follows.)
Examples:
The scaled mean reproduces the sample tau:
>>> import numpy as np
>>> from scipy import stats
>>> from rcopula import ClaytonCopula
>>> from rcopula.fit.variance import kendall_influence
>>> u = ClaytonCopula(2.0).rvs(1000, random_state=0)
>>> h = kendall_influence(u)
>>> bool(abs((2 * h.mean() - 1) - stats.kendalltau(u[:, 0], u[:, 1]).statistic) < 0.01)
True
spearman_influence ¶
spearman_influence(u: NDArray[float64]) -> NDArray[np.float64]
For each observation, how much it contributes to Spearman's rank correlation.
This is the empirical influence function of Spearman's rho for a bivariate sample, used to compute the standard error of rho-based estimates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
ndarray of float, shape (n, 2)
|
Bivariate pseudo-observations in |
required |
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (n,)
|
Influence contribution of each observation. |
Notes
With :math:\hat\rho \approx 12\,\overline{U_1 U_2} - 3, the influence
contribution of observation i is
.. math:: 12\Bigl(U_{i1}U_{i2} + \overline{U_{j2}\mathbf{1}{U_{j1} \ge U_{i1}}} + \overline{U_{j1}\mathbf{1}{U_{j2} \ge U_{i2}}}\Bigr) - 9,
the two averages accounting for the ranks being estimated rather than known.
Examples:
>>> from rcopula import ClaytonCopula, pseudo_obs
>>> from rcopula.fit.variance import spearman_influence
>>> u = pseudo_obs(ClaytonCopula(2.0).rvs(200, random_state=0))
>>> spearman_influence(u).shape
(200,)
var_inversion_multi ¶
var_inversion_multi(u: NDArray[float64], jacobian: NDArray[float64], measure: str = 'tau') -> NDArray[np.float64] | None
Estimate the uncertainty of several correlations, each obtained from a pairwise tau or rho.
Delta-method covariance for a multi-parameter inversion estimator, as
used by fit(..., method="itau") and "irho" for elliptical copulas
with an unstructured correlation matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
ndarray of float, shape (n, d)
|
The pseudo-observations, |
required |
jacobian
|
ndarray of float, shape (p, p)
|
Derivative of the parameter vector with respect to the statistic
vector, where |
required |
measure
|
('tau', 'rho')
|
Which pairwise statistic was inverted: Kendall's tau or Spearman's rho. |
"tau"
|
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (p, p), or None
|
Estimated covariance matrix, or |
Notes
Each correlation is inverted from its own pairwise statistic, so the
covariance follows from the joint covariance of those statistics:
:math:\mathrm{Cov}(\hat\theta) = J\,\mathrm{Cov}(\hat{\boldsymbol\tau})\,J^{\top}.
The pairwise statistics are not independent -- they share observations --
which is why the full covariance is estimated from the joint influence
vectors rather than pair by pair.
var_itau ¶
var_itau(u: NDArray[float64], dtheta_dmeasure: float, measure: str = 'tau') -> NDArray[np.float64] | None
Estimate the uncertainty of a one-parameter estimate obtained by inverting tau or rho.
Delta-method variance for a one-parameter inversion estimator, as used
by fit(..., method="itau") and "irho". Most users read
CopulaFitResult.bse instead of calling this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
ndarray of float, shape (n, d)
|
The pseudo-observations. For |
required |
dtheta_dmeasure
|
float
|
:math: |
required |
measure
|
('tau', 'rho')
|
Which dependence measure was inverted. |
"tau"
|
Returns:
| Type | Description |
|---|---|
ndarray of float, shape (1, 1), or None
|
The estimated variance, or |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
:math:\hat\theta = g(\hat\tau) gives
:math:\mathrm{Var}(\hat\theta) \approx g'(\tau)^2\,\mathrm{Var}(\hat\tau),
with the variance of the rank statistic taken from its influence function.
Examples:
For Clayton, :math:\theta = 2\tau/(1-\tau), so
:math:g'(\tau) = 2/(1-\tau)^2:
>>> import numpy as np
>>> from scipy import stats
>>> from rcopula import ClaytonCopula
>>> from rcopula.fit import var_itau
>>> u = ClaytonCopula(2.0).rvs(1000, random_state=0)
>>> tau = stats.kendalltau(u[:, 0], u[:, 1]).statistic
>>> var = var_itau(u, 2.0 / (1.0 - tau) ** 2)
>>> var.shape
(1, 1)
rcopula.select ¶
Automatic copula family selection.
Choosing a family is the step everyone has to do and nobody enjoys. R makes you
loop by hand: construct each candidate, call fitCopula, collect the
log-likelihoods, remember which families are bivariate-only, and handle the ones
that fail to converge. This module does that once, properly.
.. code-block:: python
ranking = rc.select_copula(u)
print(ranking) # a ranked table
best = ranking.best # the fitted copula, ready to use
The comparison is more than a beauty contest, because the candidates differ in
what they can express. Gaussian and Frank have no tail dependence at all;
Clayton has it only below, Gumbel only above; Student-t has it in both tails and
buys that with one extra parameter. So the ranking is really a statement about
which asymmetries the data insists on -- and the table reports
:math:\lambda_L, :math:\lambda_U and :math:\tau next to each score so that
statement is visible rather than buried.
Two cautions, both stated in the table rather than hidden:
- AIC and BIC only compare fit against complexity. The best of a bad set is
still bad. Pass
gof=True(orgof="mult"for the fast version) to test the winner against the data rather than against the other candidates. - Selection is itself an estimate. With a few hundred observations the top
few families are usually within noise of each other; a difference in AIC of
less than about 2 is not evidence. :func:
cross_validateis the more honest comparison when you can afford it.
============================ ================================================
:func:select_copula Fit every admissible family and rank them.
:func:cross_validate k-fold cross-validated log-likelihood.
:class:SelectionResult The ranked table, plus the winning copula.
:data:FAMILIES The candidate registry, by name.
============================ ================================================
References
Akaike, H. (1974). A new look at the statistical model identification. IEEE Transactions on Automatic Control 19(6), 716-723. Schwarz, G. (1978). Estimating the dimension of a model. Annals of Statistics 6(2), 461-464. Gronneberg, S. and Hjort, N. L. (2014). The copula information criteria. Scandinavian Journal of Statistics 41(2), 436-459. Why the naive AIC is biased for copulas fitted to pseudo-observations, and why cross-validation avoids the problem. Genest, C., Remillard, B. and Beaudoin, D. (2009). Goodness-of-fit tests for copulas: a review and a power study. Insurance: Mathematics and Economics 44(2), 199-213.
FamilySpec
dataclass
¶
FamilySpec(name: str, factory: Callable[[int], Copula], max_dim: int | None, groups: frozenset[str])
A registry entry describing one candidate copula family for :func:select_copula.
Each entry knows how to build an unfitted copula of a given dimension,
the largest dimension the family supports, and which groups (such as
"archimedean") it belongs to. The built-in entries live in
:data:FAMILIES; you rarely need to create one yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Key in :data: |
required |
factory
|
callable
|
Function |
required |
max_dim
|
int or None
|
Largest supported dimension; |
required |
groups
|
frozenset of str
|
Group labels accepted by |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Key in :data: |
factory |
callable
|
|
max_dim |
int or None
|
Largest supported dimension; |
groups |
frozenset of str
|
Group labels accepted by |
Examples:
>>> from rcopula.select import FAMILIES
>>> spec = FAMILIES["tawn"]
>>> spec.max_dim, spec.admissible(2), spec.admissible(3)
(2, True, False)
admissible ¶
admissible(dim: int) -> bool
Check whether this family can be used for data with dim columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dim
|
int
|
Number of variables (columns) in the data, at least 2. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
SelectionResult
dataclass
¶
SelectionResult(table: DataFrame, results: dict[str, CopulaFitResult], criterion: str)
The ranking from :func:select_copula: a comparison table plus the winning fitted copula.
Print it to see the table; use :attr:best to get the winning copula,
already fitted, and :attr:results for every candidate's full fit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table
|
DataFrame
|
Ranked comparison table (see Attributes). |
required |
results
|
dict of str to CopulaFitResult
|
Fit result per family name. |
required |
criterion
|
('aic', 'bic', 'loglik', 'xv')
|
Column that decided the ranking. |
"aic"
|
Attributes:
| Name | Type | Description |
|---|---|---|
table |
DataFrame
|
One row per candidate, indexed by family name and sorted best-first.
Columns: |
results |
dict of str to CopulaFitResult
|
Family name to :class: |
criterion |
str
|
Which column decided the ranking. |
best_name
property
¶
best_name: str
The name of the family that ranked first, e.g. "clayton".
Returns:
| Type | Description |
|---|---|
str
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the table is empty. |
best_result
property
¶
best_result: CopulaFitResult
The full fit result (estimates, log-likelihood, ...) of the winning family.
Returns:
| Type | Description |
|---|---|
CopulaFitResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the table is empty. |
KeyError
|
If the top-ranked family has no fit result because its fit raised (possible only when every candidate failed). |
best
property
¶
best: Copula
The winning copula, with its fitted parameters, ready to sample from or evaluate.
Returns:
| Type | Description |
|---|---|
Copula
|
|
summary ¶
summary() -> str
The ranking as a human-readable text table, ready to print.
Includes a header with the sample size and criterion; the n_obs
and message columns are left out for width. repr of the
result shows the same text.
Returns:
| Type | Description |
|---|---|
str
|
Multi-line text; pass it to |
cross_validate ¶
cross_validate(copula: Copula, data: ArrayLike, k: int = 10, method: str = 'mpl', random_state: Generator | int | None = None, ties_method: str = 'average') -> float
Score a copula family by how well it predicts data it was not fitted on (higher is better).
Splits the data into k parts, fits on all but one part and scores
the left-out part, rotating through every part. Use it to compare
families more honestly than with AIC, at the cost of k fits per
family. This is R's xvCopula.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula
|
Family to score; its parameter values are only a starting point. |
required |
data
|
array_like of float or pandas.DataFrame, shape (n, d)
|
Observations, one row per observation; |
required |
k
|
int
|
Number of folds, between 2 and |
10
|
method
|
('mpl', 'ml', 'itau', 'irho', 'itau.mpl')
|
Estimation method for each training fit; see :func: |
"mpl"
|
random_state
|
(int, Generator or None)
|
Controls the random fold assignment. Pass an int for reproducible results. |
None
|
ties_method
|
('average', 'min', 'max', 'dense', 'ordinal', 'random')
|
How tied values are ranked when |
"average"
|
Returns:
| Type | Description |
|---|---|
float
|
Cross-validated log-likelihood, rescaled to a full-sample scale; higher is better. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Fits on k-1 folds and scores the held-out one, summed over folds and
multiplied by :math:n/(n - n/k) so the result is on the scale of a
full-sample log-likelihood and comparable across families -- the same
convention R uses.
This is the honest answer to a real problem: the ordinary AIC is biased for
copulas, because the pseudo-observations are themselves estimated from the
data and the usual "one penalty unit per parameter" accounting no longer
holds (Gronneberg & Hjort 2014). Cross-validation sidesteps the bias by
scoring on data the fit never saw. It costs k fits per family.
Examples:
The generating family wins on data it generated:
>>> import rcopula as rc
>>> from rcopula.select import cross_validate
>>> u = rc.ClaytonCopula(3.0).rvs(600, random_state=0)
>>> right = cross_validate(rc.ClaytonCopula(), u, k=5, random_state=0)
>>> wrong = cross_validate(rc.GumbelCopula(), u, k=5, random_state=0)
>>> bool(right > wrong)
True
select_copula ¶
select_copula(data: ArrayLike, families: str | Sequence[str] | Sequence[Copula] = 'all', criterion: str = 'aic', method: str = 'mpl', gof: bool | str = False, k: int = 10, n_rep: int = 200, random_state: Generator | int | None = None, ties_method: str = 'average') -> SelectionResult
Try many copula families on your data and rank them from best to worst fit.
Fits every candidate family that supports your data's dimension, scores each one (by AIC unless you choose otherwise), and returns a sorted table along with the winning copula, already fitted. Use it when you do not know which family to pick.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
array_like of float or pandas.DataFrame, shape (n, d)
|
Observations, one row per observation and one column per variable. Rank-transformed internally, so raw data is fine. |
required |
families
|
str, sequence of str or sequence of Copula
|
Which candidates to try: a group name ( |
"all"
|
criterion
|
('aic', 'bic', 'loglik', 'xv')
|
Ranking column. |
"aic"
|
method
|
('mpl', 'ml', 'itau', 'irho', 'itau.mpl')
|
Estimation method passed to :func: |
"mpl"
|
gof
|
bool or {'pb', 'mult'}
|
|
False
|
k
|
int
|
Number of folds (2 to |
10
|
n_rep
|
int
|
Bootstrap replicates (positive), used only when |
200
|
random_state
|
(int, Generator or None)
|
Seed for the cross-validation folds and the goodness-of-fit bootstrap.
Every family gets the same folds and the same bootstrap stream, so
the comparison is not confounded by random draws: an int is used as
the seed for each family, and a Generator (or |
None
|
ties_method
|
('average', 'min', 'max', 'dense', 'ordinal', 'random')
|
How tied values are ranked when |
"average"
|
Returns:
| Type | Description |
|---|---|
SelectionResult
|
The ranked table ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If |
Notes
A family whose fit raises is reported, not raised: its row carries
the error message, NaN scores and converged=False, and it is
ranked last. A fit that runs but whose optimiser reports non-convergence
keeps its score and is ranked by it, with converged=False in its
row. Silently dropping either would misrepresent the comparison. If both
the cross-validation and the goodness-of-fit run fail for a family, the
message column carries both errors, separated by "; ".
Examples:
The generating family is recovered:
>>> import rcopula as rc
>>> u = rc.ClaytonCopula(3.0).rvs(1000, random_state=0)
>>> ranking = rc.select_copula(u, families="archimedean")
>>> ranking.best_name
'clayton'
The winner comes back fitted, so it can be used immediately:
>>> bool(abs(ranking.best.theta - 3.0) < 0.4)
True
The table shows why -- Clayton is the family with lower-tail dependence and no upper-tail dependence, which is what the data has:
>>> float(ranking.table.loc["clayton", "lambda_lower"]) > 0.5
True
>>> float(ranking.table.loc["clayton", "lambda_upper"])
0.0
Upper-tail data flips the answer to Gumbel:
>>> v = rc.GumbelCopula(2.5).rvs(1000, random_state=0)
>>> rc.select_copula(v, families="archimedean").best_name
'gumbel'
On independent data no candidate separates from the parameter-free baseline. Twice the log-likelihood is the likelihood-ratio statistic against independence, and for a one-parameter family it stays below the 0.1% point of a chi-squared with one degree of freedom. Which family tops the table there is a coin flip, and reading it as a result would be a mistake:
>>> w = rc.IndependenceCopula(2).rvs(2000, random_state=0)
>>> table = rc.select_copula(w, families=["independence", "gaussian", "frank"]).table
>>> bool((2 * table["loglik"] < 10.83).all())
True
Dependence of any real size settles it immediately:
>>> x = rc.FrankCopula(4.0).rvs(2000, random_state=0)
>>> table = rc.select_copula(x, families=["independence", "gaussian", "frank"]).table
>>> bool(table.loc["independence", "aic"] - table["aic"].min() > 100)
True
rcopula.bootstrap ¶
Bootstrap confidence intervals for dependence measures and fitted parameters.
R's copula reports asymptotic standard errors and nothing else. Those are
excellent when they apply and quietly wrong when they do not, and the cases where
they do not are the ones people care about:
- a statistic bounded at 1, near 1 -- a symmetric interval runs off the end of the parameter space;
- tail dependence, which is a limit estimated from a handful of corner points, so its sampling distribution is skewed at any realistic sample size;
- anything at n = 50, where the asymptotics have not arrived.
A bootstrap makes no distributional assumption and produces an interval that
respects the boundary. The cost is compute, which is why every function here
takes n_jobs.
Resampling rows, not columns. Copula data is multivariate and the dependence is the object of study, so a resample must take whole rows. Resampling each coordinate independently would destroy exactly what is being measured -- and would produce beautifully tight intervals around zero.
============================ ================================================
:func:bootstrap The general machine: any statistic of a matrix.
:func:bootstrap_measure tau, rho, beta or lambda, with one call.
:func:bootstrap_fit Intervals for a fitted copula's parameters.
:class:BootstrapResult Estimate, interval, standard error, replicates.
============================ ================================================
Three interval types:
percentile
The replicate quantiles. Simple, and biased when the statistic's
distribution is not centred on the estimate.
basic
Reflects the replicates through the estimate. Corrects location bias, and
can run outside the parameter space.
bca
Bias-corrected and accelerated (Efron 1987): adjusts for both median bias
and for the statistic's variance changing with its value. Second-order
accurate and transformation-respecting, so it stays inside the parameter
space. The default, and worth the extra jackknife pass.
Examples:
>>> import rcopula as rc
>>> from rcopula.bootstrap import bootstrap_measure
>>> u = rc.ClaytonCopula(2.0).rvs(400, random_state=0)
>>> result = bootstrap_measure(u, "tau", n_resamples=199, random_state=0)
>>> lower, upper = result.confidence_interval
>>> bool(lower < 0.5 < upper) # the true tau of Clayton(2)
True
References
Efron, B. (1987). Better bootstrap confidence intervals. J. American Statistical Association 82(397), 171-185. The BCa interval. Efron, B. and Tibshirani, R. J. (1993). An Introduction to the Bootstrap. Chapman & Hall. Davison, A. C. and Hinkley, D. V. (1997). Bootstrap Methods and their Application. Cambridge University Press. Genest, C., Ghoudi, K. and Rivest, L.-P. (1995). A semiparametric estimation procedure of dependence parameters in multivariate families of distributions. Biometrika 82(3), 543-552. The asymptotic variance this complements rather than replaces.
BootstrapResult
dataclass
¶
BootstrapResult(estimate: Any, confidence_interval: tuple[Any, Any], standard_error: Any, replicates: NDArray[float64], method: str, level: float, n_failed: int = 0)
The answer from a bootstrap: the estimate, its confidence interval and every replicate.
You normally get one of these from :func:bootstrap,
:func:bootstrap_measure or :func:bootstrap_fit rather than building it
yourself. Read confidence_interval for the interval, call
:meth:summary for a printable table, or plot replicates to see the
whole sampling distribution.
Attributes:
| Name | Type | Description |
|---|---|---|
estimate |
float or numpy.ndarray of float
|
The statistic on the original data. A float for a scalar statistic,
otherwise an array with the statistic's own shape ( |
confidence_interval |
tuple of (float, float) or tuple of (numpy.ndarray, numpy.ndarray)
|
|
standard_error |
float or numpy.ndarray of float
|
Standard deviation of the replicates, shaped like |
replicates |
numpy.ndarray of float, shape (n_kept,) or (n_kept, *estimate.shape)
|
Every replicate that succeeded ( |
method |
{'bca', 'percentile', 'basic'}
|
The interval type used. |
level |
float
|
Coverage of the interval, between 0 and 1 (e.g. 0.95). |
n_failed |
int, default 0
|
Resamples the statistic refused (raised an error or returned a non-finite value). |
bias
property
¶
bias: Any
How far the resampled values sit from the original estimate, on average.
Returns mean(replicates) - estimate: the bootstrap estimate of
bias, a float for a scalar statistic or an array shaped like
estimate.
A bias comparable to the standard error is a warning that the statistic is not well behaved at this sample size, whatever the interval says.
summary ¶
summary() -> str
Return a printable table of the estimate, standard error, bias and interval.
Returns:
| Type | Description |
|---|---|
str
|
One row per component of the statistic (flattened in C order for a matrix-valued statistic), with columns estimate, SE, bias and the lower and upper interval limits, followed by a note of how many resamples were refused, if any. |
bootstrap ¶
bootstrap(x: ArrayLike, statistic: Callable[[NDArray], Any], *, n_resamples: int = 999, level: float = 0.95, method: Method = 'bca', random_state: Any = None, n_jobs: int | None = 1) -> BootstrapResult
Get a confidence interval for any number computed from a data table, by resampling rows.
The bootstrap redraws the rows of x at random (with replacement) many
times, recomputes statistic on each redraw, and uses the spread of
those values to build an interval. It makes no assumption about the
statistic's distribution. Whole rows are resampled so the dependence
between columns is kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like of float, shape (n, d)
|
The data. Rows are observations; at least 2 rows. |
required |
statistic
|
callable
|
Function |
required |
n_resamples
|
int
|
Number of bootstrap replicates, at least 2. 999 rather than 1000 by
convention: the percentile of |
999
|
level
|
float
|
Coverage, strictly between 0 and 1, e.g. 0.95 for a 95% interval. |
0.95
|
method
|
('bca', 'percentile', 'basic')
|
Interval type; see the module docstring. BCa additionally runs an
|
"bca"
|
random_state
|
(int, Generator or None)
|
Seed for the resampling. Pass an int for reproducible output. |
None
|
n_jobs
|
int or None
|
Processes to spread the resamples over. The seeds are drawn up front, so results do not depend on Expect sublinear speedup, and a peak well below the core count.
NumPy already runs several BLAS threads inside each process, so the
workers compete with each other: on an 8-core machine, 200 t-copula
fits took 39s serially, 21s at |
1
|
Returns:
| Type | Description |
|---|---|
BootstrapResult
|
Estimate, interval, standard error and the replicates themselves. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If more than 10% of the resamples are refused by |
Examples:
>>> import numpy as np, rcopula as rc
>>> from rcopula.bootstrap import bootstrap
>>> u = rc.GumbelCopula(2.0).rvs(300, random_state=0)
>>> result = bootstrap(u, lambda d: rc.cor_kendall(d)[0, 1], n_resamples=199,
... random_state=0)
>>> lower, upper = result.confidence_interval
>>> bool(lower < 0.5 < upper)
True
>>> result.replicates.shape
(199,)
bootstrap_measure ¶
bootstrap_measure(x: ArrayLike, measure: str = 'tau', *, n_resamples: int = 999, level: float = 0.95, method: Method = 'bca', random_state: Any = None, n_jobs: int | None = 1) -> BootstrapResult
Get a confidence interval for how strongly two variables move together.
A one-call wrapper around :func:bootstrap for the standard rank-based
dependence measures of a two-column dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like of float, shape (n, 2)
|
The data: two columns, at least 2 rows. Raw data or pseudo-observations both work, since every measure uses ranks. |
required |
measure
|
('tau', 'rho', 'beta', 'lambda_upper', 'lambda_lower')
|
Which measure: Kendall's tau, Spearman's rho, Blomqvist's beta, or a nonparametric upper / lower tail-dependence estimate (the share of points with both ranks above 0.95, resp. below 0.05, divided by 0.05). |
"tau"
|
n_resamples
|
int
|
Number of bootstrap replicates, at least 2. |
999
|
level
|
float
|
Coverage, strictly between 0 and 1. |
0.95
|
method
|
('bca', 'percentile', 'basic')
|
Interval type; see :func: |
"bca"
|
random_state
|
(int, Generator or None)
|
Seed for the resampling. Pass an int for reproducible output. |
None
|
n_jobs
|
int or None
|
Number of worker processes. |
1
|
Returns:
| Type | Description |
|---|---|
BootstrapResult
|
With a float |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Tail dependence is where the bootstrap earns its keep: the estimate is a ratio of small counts, so the interval is wide and asymmetric in a way no symmetric asymptotic interval would show.
>>> import rcopula as rc
>>> from rcopula.bootstrap import bootstrap_measure
>>> u = rc.ClaytonCopula(2.0).rvs(500, random_state=0)
>>> result = bootstrap_measure(u, "lambda_lower", n_resamples=299, random_state=0)
>>> lower, upper = result.confidence_interval
>>> bool(0.0 <= lower < result.estimate < upper)
True
bootstrap_fit ¶
bootstrap_fit(x: ArrayLike, copula: Copula, *, fit_method: str = 'mpl', n_resamples: int = 499, level: float = 0.95, method: Method = 'bca', random_state: Any = None, n_jobs: int | None = 1) -> BootstrapResult
Get confidence intervals for a copula's fitted parameters by refitting it on resampled data.
The nonparametric complement to the asymptotic standard errors from
:func:~rcopula.fit: the whole estimation is redone on each resample, so
the interval reflects the estimator actually used rather than its limiting
distribution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
(array_like, shape(n, d))
|
Data. Converted to pseudo-observations inside each resample, which is the right order -- ranks must be recomputed on the resample, not carried over from the original sample. |
required |
copula
|
Copula
|
The family to fit; its current parameters are only a starting point. |
required |
fit_method
|
('mpl', 'ml', 'itau', 'irho', 'itau.mpl')
|
Estimation method passed to :func: |
"mpl"
|
n_resamples
|
int
|
Number of bootstrap replicates, at least 2. Fewer than for a cheap statistic, because each one is a full fit. |
499
|
level
|
float
|
Coverage, strictly between 0 and 1. |
0.95
|
method
|
('bca', 'percentile', 'basic')
|
Interval type; see :func: |
"bca"
|
random_state
|
(int, Generator or None)
|
Seed for the resampling. Pass an int for reproducible output. |
None
|
n_jobs
|
int or None
|
Number of worker processes. |
1
|
Returns:
| Type | Description |
|---|---|
BootstrapResult
|
One entry per free parameter of |
Raises:
| Type | Description |
|---|---|
ValueError
|
For any reason listed in :func: |
RuntimeError
|
If more than 10% of the refits fail. |
Notes
Costs n_resamples fits, plus n more for the BCa jackknife. At
n = 500 that is a thousand fits; use method="percentile" or
n_jobs > 1 when that is too slow.
Examples:
>>> import rcopula as rc
>>> from rcopula.bootstrap import bootstrap_fit
>>> u = rc.ClaytonCopula(2.0).rvs(300, random_state=0)
>>> result = bootstrap_fit(u, rc.ClaytonCopula(1.0), n_resamples=99,
... method="percentile", random_state=0)
>>> lower, upper = result.confidence_interval
>>> bool(lower < 2.0 < upper)
True
rcopula.serialize ¶
Save a fitted copula and load it back.
R's copula has no equivalent: an S4 object can go through saveRDS, but
that is an opaque binary blob tied to the R session that wrote it. Here a copula
becomes a small, readable JSON document that can be committed to a repository,
sent over a wire, diffed in a pull request, or read by something that is not
Python at all.
The round trip is exact. Parameters go through Python's shortest round-tripping float representation, which JSON preserves, so a reloaded copula returns bit-identical densities -- not "close enough". That is the property worth having: a model checked into version control today must price the same book next year.
Structural constructions nest, because they nest in the objects too. A Khoudraji copula whose second component is a rotated mixture of two Archimedeans serialises to exactly that shape, and comes back as the same tree.
============================ ================================================
:func:to_json Copula (or fit result) to a JSON string.
:func:from_json ...and back.
:func:to_dict The intermediate form, if JSON is not wanted.
:func:from_dict
============================ ================================================
Examples:
>>> import rcopula as rc
>>> from rcopula.serialize import from_json, to_json
>>> text = to_json(rc.ClaytonCopula(2.5, dim=3))
>>> reloaded = from_json(text)
>>> reloaded == rc.ClaytonCopula(2.5, dim=3)
True
Nested constructions survive intact:
>>> original = rc.KhoudrajiCopula(
... rc.IndependenceCopula(2),
... rc.RotatedCopula(rc.GumbelCopula(3.0), 180),
... shapes=(0.4, 0.95),
... )
>>> from_json(to_json(original)).describe() == original.describe()
True
A fit result (anything with a .copula attribute, such as the
:class:~rcopula.fit.results.CopulaFitResult from :func:rcopula.fit) can be
passed directly. The fitted copula is what gets saved and what comes back;
the fit's summary numbers ride along under a "fit" key for the record:
>>> u = rc.ClaytonCopula(2.0).rvs(200, random_state=0)
>>> result = rc.fit(rc.ClaytonCopula(), u)
>>> document = to_dict(result)
>>> sorted(document["fit"])
['converged', 'cov_params', 'loglik', 'method', 'n_obs', 'param_names', 'params', 'result']
>>> from_dict(document) == result.copula
True
Notes
A document records the version of rcopula that wrote it. Loading a document
from a newer version raises rather than guessing, because silently
mis-reading a model is worse than refusing to read it.
:class:~rcopula.core.empirical.EmpiricalCopula is deliberately not
serialisable: it is its data, and writing the data into the document would
turn a model file into a dataset -- with whatever licence and privacy
consequences that carries. Save the data yourself and rebuild it.
to_dict ¶
to_dict(copula: Any) -> dict[str, Any]
Convert a copula into a plain Python dictionary you can store or send anywhere.
The dictionary holds only strings, numbers, booleans, lists and nested
dictionaries, so it can go into JSON, YAML or a database as-is. Use
:func:to_json if you just want a JSON string, and :func:from_dict to
turn the dictionary back into a copula.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula or fit result
|
The copula to save: any parametric family, or a structural
construction (rotated, outer power, Khoudraji, mixture, nested
Archimedean, vine) built from them. Not an
:class: A fit result -- any object whose |
required |
Returns:
| Type | Description |
|---|---|
dict of str to Any
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If the copula is (or contains) an
:class: |
Examples:
>>> import rcopula as rc
>>> from rcopula.serialize import to_dict
>>> document = to_dict(rc.GumbelCopula(2.0))
>>> document["copula"]["kind"], document["copula"]["params"]
('GumbelCopula', [2.0])
to_json ¶
to_json(copula: Any, *, indent: int | None = 2) -> str
Save a copula as a JSON text string, so it can be written to a file and reloaded exactly.
The string is the JSON form of :func:to_dict's output. Read it back
with :func:from_json; the reloaded copula is bit-identical.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copula
|
Copula or fit result
|
The copula to save, or a fit result whose fitted copula is saved;
see :func: |
required |
indent
|
int or None
|
Spaces of indentation, passed to :func: |
2
|
Returns:
| Type | Description |
|---|---|
str
|
The JSON document. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the copula is (or contains) an
:class: |
Examples:
>>> import rcopula as rc
>>> from rcopula.serialize import to_json
>>> print(to_json(rc.FrankCopula(4.0), indent=None))
{"rcopula": ..., "schema": 2, "copula": {"kind": "FrankCopula", ...}}
from_dict ¶
from_dict(document: dict[str, Any]) -> Copula
Turn a dictionary made by :func:to_dict back into a working copula.
Documents written by older versions of rcopula (schema 1) are upgraded automatically on the way in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document
|
dict of str to Any
|
A dictionary as returned by :func: |
required |
Returns:
| Type | Description |
|---|---|
Copula
|
The rebuilt copula, of the same class and with the same parameters (and fixed/free flags) as the one that was saved. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dictionary has no |
Examples:
>>> import rcopula as rc
>>> from rcopula.serialize import from_dict, to_dict
>>> from_dict(to_dict(rc.JoeCopula(3.0))) == rc.JoeCopula(3.0)
True
from_json ¶
from_json(text: str) -> Copula
Load a copula back from a JSON string made by :func:to_json.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The JSON document, for example the contents of a file written from
:func: |
required |
Returns:
| Type | Description |
|---|---|
Copula
|
The rebuilt copula. |
Raises:
| Type | Description |
|---|---|
JSONDecodeError
|
If |
ValueError
|
For the same reasons as :func: |
Examples:
The round trip is exact, not approximate -- the reloaded copula gives bit-identical densities:
>>> import numpy as np, rcopula as rc
>>> from rcopula.serialize import from_json, to_json
>>> original = rc.ClaytonCopula(2.3456789012345678, dim=4)
>>> reloaded = from_json(to_json(original))
>>> u = original.rvs(50, random_state=0)
>>> bool(np.array_equal(original.logpdf(u), reloaded.logpdf(u)))
True