import copy
import sympy
from copul.exceptions import PropertyUnavailableException
from copul.family.core.biv_copula import BivCopula
from copul.wrapper.sympy_wrapper import SymPyFuncWrapper
[docs]
class Mardia(BivCopula):
"""
Mardia Copula.
A convex mixture of the Fréchet bounds and the independence copula.
C(u,v) = theta^2 * (1 + theta) / 2 * min(u,v) +
(1 - theta^2) * u*v +
theta^2 * (1 - theta) / 2 * max(u+v-1, 0)
Parameters:
-----------
theta : float, -1 ≤ theta ≤ 1
Dependence parameter
"""
@property
def is_symmetric(self) -> bool:
return True
theta = sympy.symbols("theta")
params = [theta]
intervals = {"theta": sympy.Interval(-1, 1, left_open=False, right_open=False)}
def __init__(self, *args, **kwargs):
"""Initialize the Mardia copula with parameter validation."""
if args and len(args) == 1:
kwargs["theta"] = args[0]
if "theta" in kwargs:
# Validate theta parameter
theta_val = kwargs["theta"]
if theta_val < -1 or theta_val > 1:
raise ValueError(
f"Parameter theta must be between -1 and 1, got {theta_val}"
)
self.theta = kwargs["theta"]
self.params = [param for param in self.params if str(param) != "theta"]
del kwargs["theta"]
super().__init__(**kwargs)
def __call__(self, *args, **kwargs):
"""Handle parameter updates when calling the instance."""
if args and len(args) == 1:
kwargs["theta"] = args[0]
if "theta" in kwargs:
# Validate theta parameter
theta_val = kwargs["theta"]
if theta_val < -1 or theta_val > 1:
raise ValueError(
f"Parameter theta must be between -1 and 1, got {theta_val}"
)
new_copula = copy.deepcopy(self)
new_copula.theta = kwargs["theta"]
new_copula.params = [
param for param in new_copula.params if str(param) != "theta"
]
del kwargs["theta"]
return new_copula.__call__(**kwargs)
return super().__call__(**kwargs)
@property
def is_absolutely_continuous(self) -> bool:
return self.theta == 0 or self.theta == -1
@property
def cdf(self):
"""
Cumulative distribution function of the copula.
C(u,v) = theta^2 * (1 + theta) / 2 * min(u,v) +
(1 - theta^2) * u*v +
theta^2 * (1 - theta) / 2 * max(u+v-1, 0)
"""
frechet_upper = sympy.Min(self.u, self.v)
frechet_lower = sympy.Max(self.u + self.v - 1, 0)
theta = self.theta
theta_sq = theta**2
# Handle special cases
if theta == -1:
# For theta = -1, the formula simplifies to (u*v + max(u+v-1,0))/2
cdf = (self.u * self.v + frechet_lower) / 2
return SymPyFuncWrapper(cdf)
if theta == 0:
# For theta = 0, it's the independence copula
return SymPyFuncWrapper(self.u * self.v)
if theta == 1:
# For theta = 1, it's the upper Fréchet bound
return SymPyFuncWrapper(frechet_upper)
# General case
cdf = (
theta_sq * (1 + theta) / 2 * frechet_upper
+ (1 - theta_sq) * self.u * self.v
+ theta_sq * (1 - theta) / 2 * frechet_lower
)
return SymPyFuncWrapper(cdf)
@property
def lambda_L(self):
"""
Lower tail dependence coefficient.
For Mardia, lambda_L = theta^2 * (1 + theta) / 2
When theta = 1, this equals 1
When theta = -1, this equals 0
"""
theta = self.theta
# Special case for theta = 1
if theta == 1:
return 1
# Regular formula for 0 <= theta < 1
return theta**2 * (1 + theta) / 2
@property
def lambda_U(self):
"""
Upper tail dependence coefficient.
For Mardia, lambda_U = theta^2 * (1 + theta) / 2
When theta = 1, this equals 1
When theta = -1, this equals 0
"""
theta = self.theta
# Special case for theta = 1
if theta == 1:
return 1
# Regular formula for 0 <= theta < 1
return theta**2 * (1 + theta) / 2
[docs]
def chatterjees_xi(self, *args, **kwargs):
"""
Calculate Chatterjee's xi for the Mardia copula.
For Mardia, xi = theta^4 * (3*theta^2 + 1) / 4
"""
self._set_params(args, kwargs)
return self.theta**4 * (3 * self.theta**2 + 1) / 4
[docs]
def spearmans_rho(self, *args, **kwargs):
"""
Calculate Spearman's rho for the Mardia copula.
For Mardia, rho = theta^3
"""
self._set_params(args, kwargs)
return self.theta**3
[docs]
def kendalls_tau(self, *args, **kwargs):
"""
Calculate Kendall's tau for the Mardia copula.
For Mardia, tau = theta^3 * (theta^2 + 2) / 3
"""
self._set_params(args, kwargs)
return self.theta**3 * (self.theta**2 + 2) / 3
@property
def pdf(self):
"""
Probability density function of the copula.
The Mardia copula does not have a PDF due to its singular components.
"""
raise PropertyUnavailableException("Mardia copula does not have a pdf")