Skip to content

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 (matrix + matrix.T) / 2.

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 copula.param_names.

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 -inf; "raise" re-raises the underlying error.

"-inf"

Returns:

Type Description
float

The log-likelihood, or -inf for inadmissible parameters when error="-inf".

Raises:

Type Description
(ValueError, LinAlgError or NotImplementedError)

Only with error="raise", when the parameters are inadmissible or the family has no density.

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. ClaytonCopula() with unspecified parameters. Parameters pinned with fix_params are held fixed.

required
data array_like of float or pandas.DataFrame, shape (n, d)

Observations, one row per observation and one column per variable; d must equal copula.dim. If every value is strictly inside (0, 1) the data are taken to be pseudo-observations as they are; otherwise each column is rank-transformed first. A 1-D input is treated as a single column.

required
method ('mpl', 'ml', 'itau', 'irho', 'itau.mpl')

Estimation method:

  • "mpl" -- maximum pseudo-likelihood; the usual choice.
  • "ml" -- same estimate, standard errors that assume the margins are known.
  • "itau" / "irho" -- invert Kendall's tau / Spearman's rho; fast, no optimiser.
  • "itau.mpl" -- Student-t copula only: correlations from tau, degrees of freedom by pseudo-likelihood.

See the module docstring for details.

"mpl"
start array_like of float, shape (p,), or None

Starting parameters (full vector, in the order of copula.param_names; entries of fixed parameters are ignored). For "mpl" and "ml", None uses the inversion-of-tau estimate, as in R. For "itau.mpl" only the last element -- the starting df -- is used, since the correlations come from tau (a scalar is accepted too). Ignored by "itau" and "irho", which have nothing to start.

None
optim_method str or None

A scipy.optimize.minimize method name, e.g. "BFGS". None uses L-BFGS-B for multi-parameter problems and Nelder-Mead for one. Used by "mpl" and "ml", and by "itau.mpl" for its search over df (where None together with start=None means R's bounded Brent search on (0.2, 200); giving either one switches to minimize with those bounds, Nelder-Mead by default). Ignored by "itau" and "irho".

None
estimate_variance bool

Whether to compute the asymptotic covariance matrix (and therefore standard errors). Set to False to save time. For "itau.mpl" the correlations get their inversion covariance and the df entries are nan (see Notes).

True
ties_method ('average', 'min', 'max', 'dense', 'ordinal', 'random')

How tied values are ranked; passed to :func:~rcopula.dependence.pseudo_obs when transforming raw data.

"average"

Returns:

Type Description
CopulaFitResult

Fitted copula, estimates of the free parameters, standard errors (bse), log-likelihood, AIC/BIC and a printable summary(). Whatever the method, params and param_names (plain str) list the estimated parameters only -- those free in copula.free -- as R's fitCopula does; parameters pinned with fix_params appear only in result.copula.params, which is always the full vector.

Raises:

Type Description
ValueError

If method is not one of the five listed; if the number of data columns differs from copula.dim; if "itau"/"irho" cannot invert the sample measure for this family (e.g. negative tau for a Gumbel copula), or the parameter tau/rho determines is fixed while another one is free; if correlations pinned with fix_params cannot be completed to a valid correlation matrix; or if "itau.mpl" is used with anything other than a Student-t copula with dispstr="un".

Warns:

Type Description
UserWarning

For "itau"/"irho" when a free parameter does not enter tau/rho at all -- a t copula's df -- and so cannot be estimated by inversion. It is held at its current value (df=4 by default) and left out of params. Pin it with fix_params (or df_fixed=True) to silence the warning, or use "itau.mpl".

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 scipy order.

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 True for "ifm").

required
message str

Optimiser message (empty for "ifm").

""
marginal_loglik float

The margins' share of loglik.

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 margin_params were pinned through margin_kwargs (floc=0 and the like) rather than estimated. None means nothing was pinned.

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 rvs or pdf.

copula Copula

The dependence part alone.

margin_params list of tuple of float, length d

The fitted parameters of each margin, in scipy order (shape parameters first, then loc and scale).

loglik float

Joint log-likelihood, margins included -- so it is not comparable with a copula-only log-likelihood from :func:~rcopula.fit.

method str

"ifm" or "ml".

n_obs int

Number of observations.

converged bool

Whether the optimiser reported success.

message str

Optimiser message, empty for "ifm".

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 (0, 1) before the copula density is evaluated, and this count reports how many were patched.

margin_fixed list of tuple of bool or None

Per margin, True where the parameter was pinned rather than estimated; None when nothing was pinned.

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 margin_params minus any pinned through margin_kwargs (see :attr:margin_fixed) -- plus the number of free copula parameters. Pinned values were not estimated, so counting them would overstate AIC and BIC.

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

loglik - marginal_loglik.

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 print.

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 scipy.stats distribution, so it can be refitted.

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

Data on the original scale, not pseudo-observations; d must equal distribution.dim. A 1-D input is treated as a single row.

required
method ('ifm', 'ml')

"ifm" fits each margin first, then the copula (two steps, the standard choice). "ml" then optimises everything jointly, starting from the IFM answer. See the module docstring.

"ifm"
margin_kwargs list of dict or None

One dict per margin (length d) of extra arguments for scipy's fit -- most usefully {"floc": 0} to pin a location that the family requires to be zero. Use {} for margins that need nothing. Parameters pinned this way (f0, f<shape>, fix_<shape>, floc, fscale) stay pinned under method="ml" too, and are not counted in :attr:JointFitResult.n_params (so not in AIC/BIC). Getting this wrong is the commonest cause of an implausible fit: a Gamma fitted with a free location will happily slide it to just below the sample minimum.

None
copula_method ('mpl', 'ml', 'itau', 'irho', 'itau.mpl')

Passed to :func:~rcopula.fit for the copula step.

"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 x does not have distribution.dim columns, method is not "ifm" or "ml", or margin_kwargs has the wrong length.

TypeError

If a margin has no underlying scipy distribution to refit.

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 params, in the same order; converted to plain str.

required
loglik float

Maximised log-likelihood (the pseudo-log-likelihood for "mpl").

required
n_obs int

Number of observations (rows) the copula was fitted to.

required
method str

Estimation method used, e.g. "mpl", "ml", "itau", "irho" or "itau.mpl".

required
cov_params ndarray of float, shape (k, k), or None

Asymptotic covariance matrix of params; None when it was not computed.

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 params.

cov_params ndarray of float, shape (k, k), or None

Asymptotic covariance matrix, or None when it was not computed.

loglik float

Maximised log-likelihood (pseudo-likelihood for "mpl").

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 param_names and params differ in length.

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, k (fixed parameters are not counted).

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 None if the covariance matrix was not computed.

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

params / bse, or None without standard errors.

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 [0, 1], or None without standard errors.

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 gives 95% intervals.

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 None without a covariance matrix.

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 print.

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 logpdf(theta) -> ndarray of float, shape (n,) mapping a parameter vector to the per-observation log densities.

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 theta, or None when the negative Hessian is not positive definite or the result is not finite.

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 logpdf_at(u, theta) -> ndarray of float, shape (n,) giving per-observation log densities at data u and parameters theta.

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 nan when more than 2% of observations had to be discarded because their derivatives were not finite (typically a family with a moving support boundary).

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 logpdf_at(u, theta) -> ndarray of float, shape (n,) giving per-observation log densities.

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 theta, or None when the negative Hessian is not positive definite, too many observations sit on a support boundary, or the result is not finite.

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). n must be at least 2.

required

Returns:

Type Description
ndarray of float, shape (n,)

Values in [0, 1]. 2 * mean - 1 approximately equals the sample Kendall's tau.

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 (0, 1).

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, d >= 2.

required
jacobian ndarray of float, shape (p, p)

Derivative of the parameter vector with respect to the statistic vector, where p = d * (d - 1) / 2 is the number of column pairs (ordered column by column down the lower triangle). Diagonal for elliptical copulas, where each correlation depends only on its own pair.

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 None if it is not finite or has a negative diagonal entry.

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 d > 2 the estimate is assumed to come from the average of the pairwise statistics, as :func:fit does.

required
dtheta_dmeasure float

:math:g', the derivative of the inverse map (parameter as a function of tau or rho) at the estimate.

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 None if d < 2 or the value is not finite.

Raises:

Type Description
ValueError

If measure is neither "tau" nor "rho".

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 (or gof="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_validate is 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:FAMILIES, e.g. "clayton".

required
factory callable

Function factory(dim: int) -> Copula returning an unfitted instance of the family in dimension dim.

required
max_dim int or None

Largest supported dimension; None means any dimension.

required
groups frozenset of str

Group labels accepted by select_copula(families=...).

required

Attributes:

Name Type Description
name str

Key in :data:FAMILIES.

factory callable

dim -> Copula, an unfitted instance.

max_dim int or None

Largest supported dimension; None means any.

groups frozenset of str

Group labels accepted by select_copula(families=...).

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

True if max_dim is None or dim <= max_dim.

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: n_params, loglik, aic, bic, tau, lambda_lower, lambda_upper, converged, n_obs, message, plus xv and the goodness-of-fit columns (gof_statistic, gof_pvalue) when those were requested.

results dict of str to CopulaFitResult

Family name to :class:~rcopula.fit.results.CopulaFitResult, including the ones that did not converge (absent if the fit raised).

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 print.

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; d must equal copula.dim. Rank-transformed internally, so raw data is fine.

required
k int

Number of folds, between 2 and n.

10
method ('mpl', 'ml', 'itau', 'irho', 'itau.mpl')

Estimation method for each training fit; see :func:~rcopula.fit.

"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 data is turned into pseudo-observations.

"average"

Returns:

Type Description
float

Cross-validated log-likelihood, rescaled to a full-sample scale; higher is better.

Raises:

Type Description
ValueError

If k is not between 2 and the number of observations, or if a training fit fails (for example, the data have the wrong number of columns).

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", "elliptical", "archimedean", "extreme", "other", "baseline", or "rotated", "bb" and "vine" for the rotations and BB1/BB7, which "all" leaves out), a list of family names from :data:FAMILIES (e.g. ["clayton", "clayton270", "bb1"]), or a list of unfitted :class:~rcopula.core.base.Copula instances when you want full control (a fixed df, a particular dispstr, ...). Families limited to two dimensions are skipped automatically for a group name.

"all"
criterion ('aic', 'bic', 'loglik', 'xv')

Ranking column. "aic" and "bic" rank lowest first; "loglik" and "xv" rank highest first. "xv" triggers a k-fold cross-validation per family and is k times slower.

"aic"
method ('mpl', 'ml', 'itau', 'irho', 'itau.mpl')

Estimation method passed to :func:~rcopula.fit.fit.

"mpl"
gof bool or {'pb', 'mult'}

True or "pb" runs the parametric-bootstrap goodness-of-fit test on each family; "mult" uses the multiplier bootstrap, which is much faster. Adds gof_statistic and gof_pvalue columns.

False
k int

Number of folds (2 to n), used only when criterion="xv".

10
n_rep int

Bootstrap replicates (positive), used only when gof is requested.

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) is reduced to a single int seed drawn once (one draw from the Generator, and only when criterion="xv" or gof is requested). Pass an int for reproducible results.

None
ties_method ('average', 'min', 'max', 'dense', 'ordinal', 'random')

How tied values are ranked when data is turned into pseudo-observations.

"average"

Returns:

Type Description
SelectionResult

The ranked table (.table), the winner (.best, .best_name) and every fit (.results).

Raises:

Type Description
ValueError

If criterion or gof is not one of the listed values, a family name or group is unknown, a named family does not support the data's dimension, families is empty, or a supplied copula's dim does not match the data.

TypeError

If families mixes names and copula objects, or contains something that is neither.

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 ((k,) for a vector, (d, d) for a matrix, ...).

confidence_interval tuple of (float, float) or tuple of (numpy.ndarray, numpy.ndarray)

(lower, upper). Scalars for a scalar statistic, arrays shaped like estimate otherwise.

standard_error float or numpy.ndarray of float

Standard deviation of the replicates, shaped like estimate. Note this is a bootstrap estimate of the standard error, not the asymptotic one.

replicates numpy.ndarray of float, shape (n_kept,) or (n_kept, *estimate.shape)

Every replicate that succeeded (n_kept = n_resamples - n_failed), kept so the distribution can be plotted -- which is usually more informative than the interval.

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 statistic(sample) taking a numpy.ndarray of shape (n, d) and returning a float or an array of floats of any fixed shape (a vector, a (d, d) matrix, ...). The estimate, interval limits, standard error and replicates keep that shape. Raising, or returning a value of a different shape, is allowed and is treated as refusing that resample.

required
n_resamples int

Number of bootstrap replicates, at least 2. 999 rather than 1000 by convention: the percentile of B replicates is exact when (B+1)*alpha is an integer.

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 n-point jackknife, so it costs one extra pass over the data.

"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. 1 stays in this process; a negative value (conventionally -1) or None uses all cores. 0 is rejected. Anything other than 1 needs statistic to be picklable, which rules out lambdas and closures -- use a module-level function or :func:functools.partial.

The seeds are drawn up front, so results do not depend on n_jobs: a parallel run returns replicate-for-replicate what a serial one does.

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 n_jobs=4, and 29s at n_jobs=8. Half the cores is a reasonable default, and for a statistic as cheap as Kendall's tau the process startup costs more than it saves.

1

Returns:

Type Description
BootstrapResult

Estimate, interval, standard error and the replicates themselves.

Raises:

Type Description
ValueError

If x has fewer than 2 rows, level is not strictly between 0 and 1, n_resamples is below 2, method is not one of the three names, n_jobs is 0 or not an integer, or statistic fails on the original data.

RuntimeError

If more than 10% of the resamples are refused by statistic.

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:bootstrap.

"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 stays in this process; a negative value or None uses all cores; 0 is rejected. See :func:bootstrap for when parallelism pays.

1

Returns:

Type Description
BootstrapResult

With a float estimate and a (lower, upper) tuple of floats.

Raises:

Type Description
ValueError

If measure is unknown, x does not have exactly 2 columns, or for any reason listed in :func:bootstrap.

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:~rcopula.fit ("mpl" is maximum pseudo-likelihood, "itau" inverts Kendall's tau).

"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:bootstrap.

"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 stays in this process; a negative value or None uses all cores; 0 is rejected. See :func:bootstrap for when parallelism pays.

1

Returns:

Type Description
BootstrapResult

One entry per free parameter of copula: a float estimate for a one-parameter family, arrays of shape (k,) otherwise.

Raises:

Type Description
ValueError

For any reason listed in :func:bootstrap, including the fit failing on the original data.

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:~rcopula.core.empirical.EmpiricalCopula.

A fit result -- any object whose .copula attribute is a :class:~rcopula.core.base.Copula, such as the result of :func:rcopula.fit or :func:rcopula.discrete.fit_discrete -- is accepted too: its fitted copula is saved, and its summary numbers are added under "fit". Only the copula is restored by :func:from_dict; the "fit" entry is a record, not a way to rebuild the result object.

required

Returns:

Type Description
dict of str to Any

"rcopula" : str Version of rcopula that wrote the document. "schema" : int Layout version of the document (currently 2). "copula" : dict The copula itself. Every node has a "kind" (the class name). A plain family also has "dim" (int), "params" (list of float) and "free" (list of bool), plus "dispstr" for the elliptical families and "df_fixed" for the Student t. A structural copula instead holds its components as nested nodes. "fit" : dict, only when a fit result was passed "result" (the result's class name) and whichever of "params", "param_names", "loglik", "n_obs", "method", "converged" and "cov_params" the result has. Non-finite numbers are stored as None.

Raises:

Type Description
TypeError

If the copula is (or contains) an :class:~rcopula.core.empirical.EmpiricalCopula, which is deliberately not serialisable, or if copula is neither a copula nor an object with a .copula attribute holding one.

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:to_dict for what is accepted and what is kept.

required
indent int or None

Spaces of indentation, passed to :func:json.dumps. None gives the compact single-line form.

2

Returns:

Type Description
str

The JSON document.

Raises:

Type Description
TypeError

If the copula is (or contains) an :class:~rcopula.core.empirical.EmpiricalCopula, or copula is neither a copula nor a fit result.

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:to_dict, with at least the keys "schema" and "copula".

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 "schema" or "copula" field, was written by a newer schema, or names a copula this version does not know. Refusing is deliberate: silently mis-reading a model is worse than not reading it.

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:to_json.

required

Returns:

Type Description
Copula

The rebuilt copula.

Raises:

Type Description
JSONDecodeError

If text is not valid JSON.

ValueError

For the same reasons as :func:from_dict.

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