import warnings
from typing import List, Union
import numpy as np
from copul.checkerboard.biv_check_pi import BivCheckPi
from copul.checkerboard.check_min import CheckMin
from copul.exceptions import PropertyUnavailableException
[docs]
class BivCheckMin(CheckMin, BivCheckPi):
"""Bivariate Checkerboard Minimum class.
A class that implements bivariate checkerboard minimum operations.
"""
def __new__(cls, matr, *args, **kwargs):
"""
Create a new BivCheckMin instance.
Parameters
----------
matr : array-like
Matrix of values that determine the copula's distribution.
*args, **kwargs
Additional arguments passed to the constructor.
Returns
-------
BivCheckMin
A BivCheckMin instance.
"""
# Skip intermediate classes and directly use Check.__new__
# This avoids Method Resolution Order (MRO) issues with multiple inheritance
from copul.checkerboard.check import Check
instance = Check.__new__(cls)
return instance
def __init__(self, matr: Union[List[List[float]], np.ndarray], **kwargs) -> None:
"""Initialize the BivCheckMin instance.
Args:
matr: Input matrix
**kwargs: Additional keyword arguments
"""
if isinstance(matr, BivCheckPi):
matr = matr.matr
CheckMin.__init__(self, matr, **kwargs)
BivCheckPi.__init__(self, matr, **kwargs)
def __str__(self) -> str:
"""Return string representation of the instance."""
return f"CheckMin(m={self.m}, n={self.n})"
def __repr__(self) -> str:
"""Return string representation of the instance."""
return f"CheckMin(m={self.m}, n={self.n})"
[docs]
def transpose(self):
"""
Transpose the checkerboard matrix.
"""
return BivCheckMin(self.matr.T)
@property
def is_symmetric(self) -> bool:
"""Check if the matrix is symmetric.
Returns:
bool: True if matrix is symmetric, False otherwise
"""
if self.matr.shape[0] != self.matr.shape[1]:
return False
return np.allclose(self.matr, self.matr.T)
@property
def is_absolutely_continuous(self) -> bool:
"""Check if the distribution is absolutely continuous.
Returns:
bool: Always returns False for checkerboard distributions
"""
return False
[docs]
@classmethod
def generate_randomly(cls, grid_size: int | list | None = None, n=1):
generated_copulas = BivCheckPi.generate_randomly(grid_size, n)
if n == 1:
return cls(generated_copulas)
else:
return [cls(copula) for copula in generated_copulas]
@property
def pdf(self):
"""PDF is not available for BivCheckMin.
Raises:
PropertyUnavailableException: Always raised, since PDF does not exist for BivCheckMin.
"""
raise PropertyUnavailableException("PDF does not exist for BivCheckMin.")
[docs]
def spearmans_rho(self) -> float:
return BivCheckPi.spearmans_rho(self) + 1 / (self.m * self.n)
[docs]
def kendalls_tau(self) -> float:
return BivCheckPi.kendalls_tau(self) + np.trace(self.matr.T @ self.matr)
[docs]
def chatterjees_xi(
self,
condition_on_y: bool = False,
) -> float:
m, n = (self.n, self.m) if condition_on_y else (self.m, self.n)
check_pi_xi = super().chatterjees_xi(condition_on_y)
add_on = m * np.trace(self.matr.T @ self.matr) / n
return check_pi_xi + add_on
[docs]
def blests_nu(self) -> float:
"""
Blest's measure (nu) for a BivCheckMin copula.
Returns:
float: Blest's nu.
Notes
-----
Decomposes as::
nu(CheckMin) = nu(CheckPi) + singular_add_on,
where the singular add-on arises from the minimum completion
placing a singular mass along the main diagonal segments of each
square cell (i,i). The add-on equals the diagonal mass weighted
by the average of (1-u) along the corresponding diagonal segment.
Closed forms::
nu(CheckPi) = (24 / (m^2 n)) * tr(Δ^T K) - 2,
with K as in BivCheckPi.blests_nu().
singular_add_on = (24 / m^2) * sum_{i=1}^m (m - i + 1/2) * Δ_{ii}.
"""
# Absolutely-continuous part (CheckPi)
nu_pi = super().blests_nu()
P = np.asarray(self.matr, dtype=float)
m, n = P.shape
# Singular add-on from the minimum completion along the main diagonal
# weight_i = average of (1 - u) on the diagonal segment of row i
# = (m - i + 1/2) / m, so total contribution scales as 24/m^2
i = np.arange(1, m + 1, dtype=float)
weight = m - i + 0.5 # row-wise weights before dividing by m
diagP = np.diag(P)
singular_add_on = (2 / (m**3)) * np.dot(weight, diagP)
return float(nu_pi + singular_add_on)
[docs]
def lambda_L(self):
return self.matr[0, 0] * np.min(self.m, self.n)
[docs]
def lambda_U(self):
return self.matr[-1, -1] * np.min(self.m, self.n)
[docs]
def ginis_gamma(self) -> float:
r"""
Compute Gini's Gamma for a BivCheckMin copula.
Within every cell BivCheckMin places the comonotone (M) copula instead
of the uniform one used by BivCheckPi. All cell margins stay uniform,
so the only contributions that differ from the parent value are the two
cells through which the global main- and anti-diagonals pass:
* main diagonal: the cell-local CDF is ``min(s, s) = s`` with
``\int_0^1 s\,ds = 1/2`` instead of ``\int_0^1 s^2\,ds = 1/3``;
* anti-diagonal: it is ``min(s, 1 - s)`` with
``\int_0^1 min(s, 1-s)\,ds = 1/4`` instead of
``\int_0^1 s(1-s)\,ds = 1/6``.
Hence, with ``gamma = 4 (D + A) - 2`` where ``D = \int C(u,u) du`` and
``A = \int C(u, 1-u) du``::
D_min = D_pi + (1/2 - 1/3)/n * trace(P) = D_pi + trace(P)/(6n)
A_min = A_pi + (1/4 - 1/6)/n * antidiag = A_pi + antidiag/(12n)
so ``gamma_min = gamma_pi + (2/(3n)) trace(P) + (1/(3n)) antidiag``.
Implemented for square checkerboard matrices.
Returns:
float: The value of Gini's Gamma.
"""
if self.m != self.n:
warnings.warn(
"Gini's Gamma analytical formula is implemented for square matrices only."
)
return np.nan
n = self.n
P = np.asarray(self.matr, dtype=float)
gamma_pi = super().ginis_gamma()
main_diag = np.trace(P)
anti_diag = np.trace(np.fliplr(P))
return gamma_pi + (2.0 / (3.0 * n)) * main_diag + (1.0 / (3.0 * n)) * anti_diag
if __name__ == "__main__":
matr1 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
matr2 = [[5, 1, 5, 1], [5, 1, 5, 1], [1, 5, 1, 5], [1, 5, 1, 5]]
matr = [[1, 0], [0, 1]]
matr = [[1, 1]]
ccop = BivCheckMin(matr).to_checkerboard()
footrule = ccop.spearmans_footrule()
rho = ccop.spearmans_rho()
ginis_gamma = ccop.ginis_gamma()
xi = ccop.chatterjees_xi()
# ccop.plot_cond_distr_1()
# ccop.transpose().plot_cond_distr_1()
is_cis, is_cds = ccop.is_cis()
transpose_is_cis, transpose_is_cds = ccop.transpose().is_cis()
print(f"Is cis: {is_cis}, Is cds: {is_cds}")
print(f"Is cis: {transpose_is_cis}, Is cds: {transpose_is_cds}")
print(f"Footrule: {footrule}, Gini's Gamma: {ginis_gamma}, xi: {xi}")