Source code for copul.schur_order.cis_verifier

import logging
import numpy as np

log = logging.getLogger(__name__)


[docs] class CISVerifier: """ Verifier for Comprehensive Increasing/Decreasing Stochasticity (CIS) property of copulas. Also known as stochastically increasing/decreasing copulas (CI/CD). """ def __init__(self, cond_distr=1): """ Initialize CISVerifier with the specified conditional distribution to check. Parameters: ----------- cond_distr : int Which conditional distribution to check (1 or 2) """ self.cond_distr = cond_distr
[docs] def is_cis(self, copul, range_min=None, range_max=None): """ Check if the copula satisfies CIS property. Parameters: ----------- copul : Copula Copula to check range_min : float, optional Minimum value for parameter range range_max : float, optional Maximum value for parameter range Returns: -------- tuple (is_ci, is_cd) - whether the copula is CI/CD """ log.info(f"Checking if {type(self).__name__} copula is CI") range_min = -10 if range_min is None else range_min n_interpolate = 20 linspace = np.linspace(0.001, 0.999, 20) # If no parameters, check the copula directly try: param = str(copul.params[0]) except IndexError: is_ci, is_cd = self._is_copula_cis(copul, linspace) if is_ci: log.debug("CI True for param: None") elif is_cd: log.debug("CD True for param: None") else: log.debug("False for param: None") return is_ci, is_cd # Handle parameters and their ranges interval = copul.intervals[param] range_min = float(max(interval.inf, range_min)) if interval.left_open: range_min += 0.01 param_range_max = 10 if range_max is None else range_max param_range_max = float(min(interval.end, param_range_max)) if interval.right_open: param_range_max -= 0.01 param_range = np.linspace(range_min, param_range_max, n_interpolate) points = linspace # Initialize result variables final_is_ci = None final_is_cd = None for param_value in param_range: param_dict = {param: param_value} my_copul = copul(**param_dict) is_ci, is_cd = self._is_copula_cis(my_copul, points) # Store the latest result final_is_ci, final_is_cd = is_ci, is_cd if is_ci: log.debug(f"CI True for param: {param_value}") elif is_cd: log.debug(f"CD True for param: {param_value}") else: log.debug(f"False for param: {param_value}") # Return the last result return final_is_ci, final_is_cd
def _is_copula_cis(self, my_copul, points): """ Check if a specific copula instance satisfies CIS property. Parameters: ----------- my_copul : Copula Copula instance to check points : numpy.ndarray Points to evaluate Returns: -------- tuple (is_ci, is_cd) - whether the copula is CI/CD """ is_cis = True is_cds = True # Get the right conditional distribution method if self.cond_distr == 1: cond_method = my_copul.cond_distr_1 elif self.cond_distr == 2: cond_method = my_copul.cond_distr_2 else: raise ValueError("cond_distr must be 1 or 2") try: # Try to get the symbolic function cond_method = cond_method().func except (TypeError, ValueError): # Method-based approach for v in points: for u, next_u in zip(points[:-1], points[1:]): if self.cond_distr == 1: val1 = cond_method(u, v) val2 = cond_method(next_u, v) else: val1 = cond_method(v, u) val2 = cond_method(v, next_u) # CI: decreasing in u, CD: increasing in u if val1 < val2 - 1e-10: is_cis = False if val1 > val2 + 1e-10: is_cds = False if not is_cis and not is_cds: break if not is_cis and not is_cds: break else: # Symbolic function approach for v in points: cond_distr_eval_u = cond_method.subs(my_copul.v, v) for u, next_u in zip(points[:-1], points[1:]): eval_u = cond_distr_eval_u.subs(my_copul.u, u) eval_next_u = cond_distr_eval_u.subs(my_copul.u, next_u) # CI: decreasing in u, CD: increasing in u if eval_u < eval_next_u: is_cis = False if eval_u > eval_next_u: is_cds = False if not is_cis and not is_cds: break if not is_cis and not is_cds: break return is_cis, is_cds