Skip to content

Extensions

extensions

Extensions module for advanced HeavyTails functionality.

This module contains advanced features and extensions that build upon the core library functionality.

ExtremeValueCopula

ExtremeValueCopula(copula_type, theta, marginals)

Bases: HeavyTailCopula

Extreme value copulas for modeling extremal dependence.

Supported types: - Gumbel copula (upper tail dependence) - Clayton copula (lower tail dependence) - Frank copula (symmetric dependence)

Source code in heavytails/extensions.py
def __init__(self, copula_type: str, theta: float, marginals: list[str]):
    super().__init__(marginals)

    # Validate copula type
    valid_types = ["gumbel", "clayton", "frank"]
    if copula_type.lower() not in valid_types:
        raise ValueError(
            f"Invalid copula type '{copula_type}'. Must be one of {valid_types}"
        )

    self.copula_type = copula_type.lower()
    self.theta = theta

    # Validate theta parameter based on copula type
    if self.copula_type == "gumbel" and theta < 1:
        raise ValueError("Gumbel copula requires theta >= 1")
    elif self.copula_type == "clayton" and theta <= 0:
        raise ValueError("Clayton copula requires theta > 0")
    elif self.copula_type == "frank" and theta == 0:
        raise ValueError("Frank copula requires theta != 0")

    # Currently only support bivariate copulas
    if len(marginals) != 2:
        raise NotImplementedError(
            "Extreme value copulas currently only support 2 dimensions"
        )

cdf

cdf(u)

Calculate the copula cumulative distribution function.

Formulas: - Gumbel: C(u,v) = exp(-[(-ln u)^θ + (-ln v)θ](1/θ)) - Clayton: C(u,v) = (u^(-θ) + v^(-θ) - 1)^(-1/θ) - Frank: C(u,v) = -1/θ * ln(1 + (e(-θu)-1)(e(-θv)-1)/(e^(-θ)-1))

Parameters:

Name Type Description Default
u list[float]

Vector of uniform margins in [0,1]^2

required

Returns:

Type Description
float

Copula CDF at point (u, v)

Source code in heavytails/extensions.py
def cdf(self, u: list[float]) -> float:
    """
    Calculate the copula cumulative distribution function.

    Formulas:
    - Gumbel: C(u,v) = exp(-[(-ln u)^θ + (-ln v)^θ]^(1/θ))
    - Clayton: C(u,v) = (u^(-θ) + v^(-θ) - 1)^(-1/θ)
    - Frank: C(u,v) = -1/θ * ln(1 + (e^(-θu)-1)(e^(-θv)-1)/(e^(-θ)-1))

    Args:
        u: Vector of uniform margins in [0,1]^2

    Returns:
        Copula CDF at point (u, v)
    """
    if len(u) != 2:
        raise ValueError("Expected 2-dimensional input")

    u_val, v_val = u[0], u[1]

    # Validate input
    if not (0 <= u_val <= 1 and 0 <= v_val <= 1):
        raise ValueError("All elements must be in [0, 1]")

    # Handle boundary cases
    if u_val == 0 or v_val == 0:
        return 0.0
    if u_val == 1 and v_val == 1:
        return 1.0

    if self.copula_type == "gumbel":
        # C(u,v) = exp(-[(-ln u)^θ + (-ln v)^θ]^(1/θ))
        term = (
            (-math.log(u_val)) ** self.theta + (-math.log(v_val)) ** self.theta
        ) ** (1.0 / self.theta)
        return math.exp(-term)

    elif self.copula_type == "clayton":
        # C(u,v) = (u^(-θ) + v^(-θ) - 1)^(-1/θ)
        term = u_val ** (-self.theta) + v_val ** (-self.theta) - 1
        if term <= 0:
            return 0.0
        return float(term ** (-1.0 / self.theta))

    elif self.copula_type == "frank":
        # C(u,v) = -1/θ * ln(1 + (e^(-θu)-1)(e^(-θv)-1)/(e^(-θ)-1))
        numerator = (math.exp(-self.theta * u_val) - 1) * (
            math.exp(-self.theta * v_val) - 1
        )
        denominator = math.exp(-self.theta) - 1
        return (-1.0 / self.theta) * math.log(1 + numerator / denominator)

    else:
        raise ValueError(f"Unknown copula type: {self.copula_type}")

pdf

pdf(u)

Calculate the copula probability density function.

The PDF is the second partial derivative of the CDF: c(u,v) = ∂²C(u,v) / (∂u ∂v)

Parameters:

Name Type Description Default
u list[float]

Vector of uniform margins in (0,1)^2

required

Returns:

Type Description
float

Copula density at point (u, v)

Source code in heavytails/extensions.py
def pdf(self, u: list[float]) -> float:
    """
    Calculate the copula probability density function.

    The PDF is the second partial derivative of the CDF:
    c(u,v) = ∂²C(u,v) / (∂u ∂v)

    Args:
        u: Vector of uniform margins in (0,1)^2

    Returns:
        Copula density at point (u, v)
    """
    if len(u) != 2:
        raise ValueError("Expected 2-dimensional input")

    u_val, v_val = u[0], u[1]

    # Validate input
    if not (0 < u_val < 1 and 0 < v_val < 1):
        raise ValueError("All elements must be in the open interval (0, 1)")

    theta = self.theta

    if self.copula_type == "gumbel":
        # PDF for Gumbel copula (derived from CDF)
        ln_u = -math.log(u_val)
        ln_v = -math.log(v_val)
        sum_term = ln_u**theta + ln_v**theta
        A = sum_term ** (1.0 / theta)

        C = math.exp(-A)  # CDF value

        # Complex derivative formula for Gumbel PDF
        term1 = A ** (1 - 2 * theta)
        term2 = (ln_u * ln_v) ** (theta - 1)
        term3 = A**theta + theta - 1

        pdf_val = C * term1 * term2 * term3 / (u_val * v_val)
        return float(pdf_val)

    elif self.copula_type == "clayton":
        # PDF for Clayton copula (derived from CDF)
        u_term = u_val ** (-theta)
        v_term = v_val ** (-theta)
        sum_term = u_term + v_term - 1

        if sum_term <= 0:
            return 0.0

        pdf_val = (
            (1 + theta)
            * (u_val * v_val) ** (-1 - theta)
            * sum_term ** (-1.0 / theta - 2)
        )
        return float(pdf_val)

    elif self.copula_type == "frank":
        # PDF for Frank copula (derived from CDF)
        exp_theta_u = math.exp(-theta * u_val)
        exp_theta_v = math.exp(-theta * v_val)
        exp_theta = math.exp(-theta)

        numerator = (
            -theta
            * (exp_theta - 1)
            * (
                1
                - exp_theta_u
                - exp_theta_v
                + exp_theta * exp_theta_u * exp_theta_v
            )
        )
        denominator_base = (exp_theta - 1) + (exp_theta_u - 1) * (exp_theta_v - 1)
        denominator = denominator_base**2

        if denominator == 0:
            return 0.0

        pdf_val = numerator / denominator
        return float(pdf_val)

    else:
        raise ValueError(f"Unknown copula type: {self.copula_type}")

tail_dependence_coefficient

tail_dependence_coefficient()

Calculate upper and lower tail dependence coefficients.

Tail dependence measures the probability of joint extreme events: - Upper tail dependence (λ_U): P(V > v | U > u) as u,v → 1 - Lower tail dependence (λ_L): P(V ≤ v | U ≤ u) as u,v → 0

Formulas: - Gumbel: λ_U = 2 - 2^(1/θ), λ_L = 0 - Clayton: λ_U = 0, λ_L = 2^(-1/θ) - Frank: λ_U = λ_L = 0

Returns:

Type Description
tuple[float, float]

Tuple of (upper_tail_dependence, lower_tail_dependence)

Source code in heavytails/extensions.py
def tail_dependence_coefficient(self) -> tuple[float, float]:
    """
    Calculate upper and lower tail dependence coefficients.

    Tail dependence measures the probability of joint extreme events:
    - Upper tail dependence (λ_U): P(V > v | U > u) as u,v → 1
    - Lower tail dependence (λ_L): P(V ≤ v | U ≤ u) as u,v → 0

    Formulas:
    - Gumbel: λ_U = 2 - 2^(1/θ), λ_L = 0
    - Clayton: λ_U = 0, λ_L = 2^(-1/θ)
    - Frank: λ_U = λ_L = 0

    Returns:
        Tuple of (upper_tail_dependence, lower_tail_dependence)
    """
    if self.copula_type == "gumbel":
        # Gumbel has upper tail dependence but no lower tail dependence
        lambda_upper = 2.0 - 2.0 ** (1.0 / self.theta)
        lambda_lower = 0.0

    elif self.copula_type == "clayton":
        # Clayton has lower tail dependence but no upper tail dependence
        lambda_upper = 0.0
        lambda_lower = 2.0 ** (-1.0 / self.theta)

    elif self.copula_type == "frank":
        # Frank copula has no tail dependence in either tail
        lambda_upper = 0.0
        lambda_lower = 0.0

    else:
        raise ValueError(f"Unknown copula type: {self.copula_type}")

    return (lambda_upper, lambda_lower)

HeavyTailCopula

HeavyTailCopula(marginals)

Bases: ABC

Abstract base class for copulas with heavy-tailed marginal distributions.

Applications: - Portfolio risk modeling - Insurance dependency modeling - Multivariate extreme value theory - Credit risk assessment

Source code in heavytails/extensions.py
def __init__(self, marginals: list[str]):
    self.marginals = marginals

cdf abstractmethod

cdf(u)

Compute the copula cumulative distribution function.

Source code in heavytails/extensions.py
@abstractmethod
def cdf(self, u: list[float]) -> float:
    """Compute the copula cumulative distribution function."""
    pass

pdf abstractmethod

pdf(u)

Compute the copula probability density function.

Source code in heavytails/extensions.py
@abstractmethod
def pdf(self, u: list[float]) -> float:
    """Compute the copula probability density function."""
    pass

StudentTCopula

StudentTCopula(nu, correlation_matrix, marginals)

Bases: HeavyTailCopula

Student-t copula for modeling tail dependence.

.. note:: The multivariate Student-t this needs now exists, in :mod:heavytails.multivariate, along with the closed-form coefficient of tail dependence. The copula itself is still open (#306); what happens to this module is open separately (#312).

Particularly useful for: - Financial asset correlations during crises - Insurance claims with common shocks - Environmental extremes

Source code in heavytails/extensions.py
def __init__(
    self, nu: float, correlation_matrix: list[list[float]], marginals: list[str]
):
    super().__init__(marginals)
    if nu <= 0:
        raise ValueError("Degrees of freedom nu must be positive")
    self.nu = nu

    if not SCIPY_AVAILABLE:
        raise ImportError("scipy is required for StudentTCopula")

    # Convert to numpy array and validate
    self.correlation = np.array(correlation_matrix)

    # Validate correlation matrix is square
    if (
        self.correlation.ndim != 2
        or self.correlation.shape[0] != self.correlation.shape[1]
    ):
        raise ValueError("Correlation matrix must be square")

    # Validate dimensions match marginals
    if len(marginals) != self.correlation.shape[0]:
        raise ValueError(
            "Number of marginals must match correlation matrix dimensions"
        )

    # Validate correlation matrix is symmetric
    if not np.allclose(self.correlation, self.correlation.T):
        raise ValueError("Correlation matrix must be symmetric")

    # Validate diagonal elements are 1
    if not np.allclose(np.diag(self.correlation), 1.0):
        raise ValueError("Diagonal elements of correlation matrix must be 1")

    # Validate positive definite by checking eigenvalues
    eigenvalues = np.linalg.eigvals(self.correlation)
    if np.any(eigenvalues <= 0):
        raise ValueError("Correlation matrix must be positive definite")

    # Store inverse and determinant for PDF calculation
    self.correlation_inv = inv(self.correlation)
    self.correlation_det = det(self.correlation)

cdf

cdf(u)

Calculate the t-copula cumulative distribution function.

This requires numerical integration and is computationally intensive for dimensions > 3.

Parameters:

Name Type Description Default
u list[float]

Vector of uniform margins in [0,1]^d

required

Returns:

Type Description
float

Copula CDF at point u

Source code in heavytails/extensions.py
def cdf(self, u: list[float]) -> float:
    """
    Calculate the t-copula cumulative distribution function.

    This requires numerical integration and is computationally intensive
    for dimensions > 3.

    Args:
        u: Vector of uniform margins in [0,1]^d

    Returns:
        Copula CDF at point u
    """
    if not SCIPY_AVAILABLE:
        raise ImportError("scipy is required for StudentTCopula")

    u_array = np.array(u)
    d = len(u_array)

    # Validate input
    if d != self.correlation.shape[0]:
        raise ValueError(
            f"Expected {self.correlation.shape[0]} dimensions, got {d}"
        )

    if np.any((u_array < 0) | (u_array > 1)):
        raise ValueError("All elements of u must be in [0, 1]")

    # Handle boundary cases
    if np.any(u_array == 0):
        return 0.0
    if np.all(u_array == 1):
        return 1.0

    # Convert uniform margins to t-distributed
    t_inv = stats.t.ppf(u_array, df=self.nu)

    # Use scipy's multivariate_t to compute CDF
    # Note: For high dimensions, this can be slow
    try:
        from scipy.stats import multivariate_t  # noqa: PLC0415

        cdf_value = multivariate_t.cdf(
            t_inv, loc=np.zeros(d), shape=self.correlation, df=self.nu
        )
        return float(cdf_value)
    except (ImportError, AttributeError):
        # Fallback for older scipy versions or if multivariate_t is not available
        raise NotImplementedError(
            "CDF calculation requires scipy.stats.multivariate_t, "
            "which may not be available in your scipy version"
        ) from None

pdf

pdf(u)

Calculate the t-copula probability density function.

Formula: c(u) = f_nu,R(t_nu^{-1}(u)) / prod_i f_nu(t_nu^{-1}(u_i))

Parameters:

Name Type Description Default
u list[float]

Vector of uniform margins in [0,1]^d

required

Returns:

Type Description
float

Copula density at point u

Source code in heavytails/extensions.py
def pdf(self, u: list[float]) -> float:
    """
    Calculate the t-copula probability density function.

    Formula: c(u) = f_nu,R(t_nu^{-1}(u)) / prod_i f_nu(t_nu^{-1}(u_i))

    Args:
        u: Vector of uniform margins in [0,1]^d

    Returns:
        Copula density at point u
    """
    if not SCIPY_AVAILABLE:
        raise ImportError("scipy is required for StudentTCopula")

    u_array = np.array(u)
    d = len(u_array)

    # Validate input
    if d != self.correlation.shape[0]:
        raise ValueError(
            f"Expected {self.correlation.shape[0]} dimensions, got {d}"
        )

    if np.any((u_array <= 0) | (u_array >= 1)):
        raise ValueError("All elements of u must be in the open interval (0, 1)")

    # Convert uniform margins to t-distributed using inverse CDF
    t_inv = stats.t.ppf(u_array, df=self.nu)

    # Calculate multivariate t density using the formula:
    # f(x) = Gamma((nu+d)/2) / (Gamma(nu/2) * (nu*pi)^(d/2) * |R|^(1/2))
    #        * (1 + (1/nu) * x^T * R^{-1} * x)^(-(nu+d)/2)  # noqa: ERA001
    quadratic_form = t_inv @ self.correlation_inv @ t_inv

    multivariate_t_density = (
        math.gamma((self.nu + d) / 2.0)
        / (
            math.gamma(self.nu / 2.0)
            * ((self.nu * math.pi) ** (d / 2.0))
            * (self.correlation_det**0.5)
        )
        * ((1.0 + quadratic_form / self.nu) ** (-(self.nu + d) / 2.0))
    )

    # Calculate product of univariate t densities
    univariate_t_densities = stats.t.pdf(t_inv, df=self.nu)
    product_univariate = np.prod(univariate_t_densities)

    # Copula density = multivariate density / product of marginal densities
    if product_univariate == 0:
        return 0.0

    return float(multivariate_t_density / product_univariate)