Source code for core.copulas.domain.models.elliptical.gaussian

"""
Gaussian Copula implementation following the project coding standard:

Norms:
 1. Use private attribute `_parameters` with public `@property parameters` and validation in the setter.
 2. All methods accept `param: np.ndarray = None` defaulting to `self.parameters`.
 3. Docstrings must include **Parameters** and **Returns** sections with types.
 4. Parameter bounds are defined in `bounds_param`; setter enforces them.
 5. Consistent boundary handling with `eps = 1e-12` and `np.clip`.
"""
import numpy as np
from scipy.special import erfinv
from scipy.stats import norm, multivariate_normal

from CopulaFurtif.core.copulas.domain.models.interfaces import CopulaModel
from CopulaFurtif.core.copulas.domain.models.mixins import SupportsTailDependence, ModelSelectionMixin

[docs] class GaussianCopula(CopulaModel, SupportsTailDependence, ModelSelectionMixin): """ Gaussian Copula class. Attributes ---------- family : str Identifier for the copula family, "gaussian". name : str Human-readable name for output/logging. bounds_param : list of tuple Bounds for the copula parameter rho, in (-1,1). parameters : np.ndarray Copula parameter [rho]. """ def __init__(self): super().__init__() self.type = "gaussian" self.name = "Gaussian Copula" self.bounds_param = [(-0.999, 0.999)] self._parameters = np.array([0.0]) self.default_optim_method = "SLSQP" @property def parameters(self) -> np.ndarray: """ Get the copula parameters. Returns ------- np.ndarray Current copula parameter [rho]. """ return self._parameters @parameters.setter def parameters(self, param: np.ndarray): """ Set and validate copula parameters against bounds_param. Parameters ---------- param : array-like New copula parameters [rho]. Raises ------ ValueError If parameter is outside its specified bound. """ param = np.asarray(param) for idx, (lower, upper) in enumerate(self.bounds_param): val = param[idx] if lower is not None and val <= lower: raise ValueError(f"Parameter at index {idx} must be > {lower}, got {val}") if upper is not None and val >= upper: raise ValueError(f"Parameter at index {idx} must be < {upper}, got {val}") self._parameters = param
[docs] def get_cdf(self, u, v, param: np.ndarray = None): """ Compute the Gaussian copula CDF C(u,v). Parameters ---------- u : float or array-like Pseudo-observations in (0,1). v : float or array-like Pseudo-observations in (0,1). param : ndarray, optional Copula parameter [rho]. If None, uses self.parameters. Returns ------- float or np.ndarray CDF value(s) at (u, v). """ if param is None: param = self.parameters rho = param[0] eps = 1e-12 u = np.clip(u, eps, 1 - eps) v = np.clip(v, eps, 1 - eps) y1 = norm.ppf(u) y2 = norm.ppf(v) cov = [[1.0, rho], [rho, 1.0]] if np.isscalar(y1) and np.isscalar(y2): return multivariate_normal.cdf([y1, y2], mean=[0.0, 0.0], cov=cov) else: return np.array([ multivariate_normal.cdf([a, b], mean=[0.0, 0.0], cov=cov) for a, b in zip(y1, y2) ])
[docs] def get_pdf(self, u, v, param: np.ndarray = None): """ Compute the Gaussian copula PDF c(u,v). Parameters ---------- u : float or array-like Pseudo-observations in (0,1). v : float or array-like Pseudo-observations in (0,1). param : ndarray, optional Copula parameter [rho]. If None, uses self.parameters. Returns ------- float or np.ndarray PDF value(s) at (u, v). """ if param is None: param = self.parameters rho = param[0] eps = 1e-12 u = np.clip(u, eps, 1 - eps) v = np.clip(v, eps, 1 - eps) x = np.sqrt(2) * erfinv(2 * u - 1) y = np.sqrt(2) * erfinv(2 * v - 1) det = 1 - rho**2 exponent = -((x**2 + y**2) * rho**2 - 2 * x * y * rho) / (2 * det) return (1.0 / np.sqrt(det)) * np.exp(exponent)
[docs] def kendall_tau(self, param: np.ndarray = None) -> float: """ Compute Kendall's tau = (2/π) · arcsin(rho). Parameters ---------- param : ndarray, optional Copula parameter [rho]. If None, uses self.parameters. Returns ------- float Kendall's tau. """ if param is None: param = self.parameters rho = param[0] return (2.0 / np.pi) * np.arcsin(rho)
[docs] def sample(self, n: int, param: np.ndarray = None) -> np.ndarray: """ Generate n samples from the Gaussian copula. Parameters ---------- n : int Number of samples. param : ndarray, optional Copula parameter [rho]. If None, uses self.parameters. Returns ------- np.ndarray Shape (n,2) array of pseudo-observations (u, v). """ if param is None: param = self.parameters rho = param[0] cov = np.array([[1.0, rho], [rho, 1.0]]) L = np.linalg.cholesky(cov) z = np.random.randn(n, 2) corr = z @ L.T u = norm.cdf(corr[:, 0]) v = norm.cdf(corr[:, 1]) return np.column_stack((u, v))
[docs] def LTDC(self, param: np.ndarray = None) -> float: """ Lower tail dependence coefficient (always 0 for Gaussian). Returns ------- float 0.0 """ return 0.0
[docs] def UTDC(self, param: np.ndarray = None) -> float: """ Upper tail dependence coefficient (always 0 for Gaussian). Returns ------- float 0.0 """ return 0.0
[docs] def IAD(self, data) -> float: """ Integrated absolute deviation (disabled for elliptical copulas). Returns ------- float np.nan """ print(f"[INFO] IAD is disabled for {self.name} due to performance limitations.") return np.nan
[docs] def AD(self, data) -> float: """ Anderson–Darling test statistic (disabled for elliptical copulas). Returns ------- float np.nan """ print(f"[INFO] AD is disabled for {self.name} due to performance limitations.") return np.nan
[docs] def partial_derivative_C_wrt_u(self, u, v, param: np.ndarray = None): """Compute ∂C(u,v)/∂u = P(V ≤ v | U = u)""" if param is None: param = self.parameters u = np.clip(u, 1e-12, 1 - 1e-12) v = np.clip(v, 1e-12, 1 - 1e-12) x, y = norm.ppf(u), norm.ppf(v) return norm.cdf((y - param[0] * x) / np.sqrt(1 - param[0]**2))
[docs] def partial_derivative_C_wrt_v(self, u, v, param: np.ndarray = None): """Compute ∂C(u,v)/∂v = P(U ≤ u | V = v) via symmetry""" # For a symmetric copula, ∂C/∂v(u,v) = ∂C/∂u(v,u) return self.partial_derivative_C_wrt_u(v, u, param)
[docs] def conditional_cdf_u_given_v(self, u, v, param: np.ndarray = None): """P(U ≤ u | V = v) = ∂C/∂v""" if param is None: param = self.parameters return self.partial_derivative_C_wrt_v(u, v, param)
[docs] def conditional_cdf_v_given_u(self, u, v, param: np.ndarray = None): """P(V ≤ v | U = u) = ∂C/∂u""" if param is None: param = self.parameters return self.partial_derivative_C_wrt_u(u, v, param)