Source code for merton.portfolio.copulas.gaussian

"""Gaussian copula sampler."""

from __future__ import annotations

import numpy as np
from scipy.stats import norm

from ..._typing import FloatArray
from ...exceptions import MertonInputError


[docs] class GaussianCopula: r"""Multivariate Gaussian copula. Samples ``u_i ∈ [0, 1]`` from a multivariate Gaussian with the supplied correlation matrix, then maps to uniform marginals via the normal CDF. These uniforms are then used to drive obligor default indicators. Parameters ---------- correlation Either a scalar (constant pairwise correlation) or a ``(n_firms, n_firms)`` matrix. n_firms When ``correlation`` is a scalar, this sets the dimension. """ def __init__(self, correlation: float | FloatArray, n_firms: int | None = None) -> None: if np.isscalar(correlation): if n_firms is None: raise MertonInputError("n_firms required when correlation is scalar") rho = float(correlation) if not -1.0 < rho < 1.0: raise MertonInputError("scalar correlation must lie in (-1, 1)") cov = np.full((n_firms, n_firms), rho) np.fill_diagonal(cov, 1.0) self.corr = cov else: corr = np.asarray(correlation, dtype=np.float64) if corr.ndim != 2 or corr.shape[0] != corr.shape[1]: raise MertonInputError("correlation matrix must be square 2-D") self.corr = corr # Cholesky for fast sampling. try: self._chol = np.linalg.cholesky(self.corr) except np.linalg.LinAlgError as err: # pragma: no cover - degenerate raise MertonInputError( "correlation matrix is not positive-definite", suggested_fix="Apply nearest-correlation regularisation before passing in.", ) from err
[docs] self.n = self.corr.shape[0]
[docs] def sample(self, n: int, *, rng: np.random.Generator | None = None) -> FloatArray: """Draw ``n`` independent samples; returns shape ``(n, n_firms)``.""" if rng is None: rng = np.random.default_rng() z = rng.standard_normal(size=(n, self.n)) x = z @ self._chol.T # Marginals: each column is N(0, 1); CDF gives uniforms. return norm.cdf(x)
[docs] def sample_normal(self, n: int, *, rng: np.random.Generator | None = None) -> FloatArray: """Draw the *latent normals* directly (skip the CDF transform).""" if rng is None: rng = np.random.default_rng() z = rng.standard_normal(size=(n, self.n)) return z @ self._chol.T
__all__ = ["GaussianCopula"]