Source code for risksim.results

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Sequence

import numpy as np

from . import metrics
from ._validation import as_1d_float_array


[docs] @dataclass(slots=True) class SimulationResult: """ Container for portfolio simulation outputs. If retained_losses is present, the primary `losses` view is retained/net loss. Otherwise, the primary `losses` view is gross loss. """ gross_losses: np.ndarray ceded_losses: np.ndarray | None = None retained_losses: np.ndarray | None = None component_losses: np.ndarray | None = None component_names: Sequence[str] | None = None layer_losses: np.ndarray | None = None layer_names: Sequence[str] | None = None contract_name: str | None = None def __post_init__(self) -> None: self.gross_losses = as_1d_float_array(self.gross_losses) if self.ceded_losses is not None: self.ceded_losses = as_1d_float_array(self.ceded_losses) if self.ceded_losses.shape != self.gross_losses.shape: raise ValueError("ceded_losses must match gross_losses shape") if self.retained_losses is not None: self.retained_losses = as_1d_float_array(self.retained_losses) if self.retained_losses.shape != self.gross_losses.shape: raise ValueError("retained_losses must match gross_losses shape") if self.component_losses is not None: self.component_losses = np.asarray(self.component_losses, dtype=float) if self.component_losses.ndim != 2: raise ValueError("component_losses must be a 2D array") if self.component_losses.shape[0] != self.gross_losses.shape[0]: raise ValueError("component_losses must have one row per simulation") if self.component_names is not None: if len(self.component_names) != self.component_losses.shape[1]: raise ValueError( "component_names length must match number of component columns" ) if self.layer_losses is not None: self.layer_losses = np.asarray(self.layer_losses, dtype=float) if self.layer_losses.ndim != 2: raise ValueError("layer_losses must be a 2D array") if self.layer_losses.shape[0] != self.gross_losses.shape[0]: raise ValueError("layer_losses must have one row per simulation") if self.layer_names is not None: if len(self.layer_names) != self.layer_losses.shape[1]: raise ValueError( "layer_names length must match number of layer columns" ) @property def n_sims(self) -> int: return int(self.gross_losses.size) @property def losses(self) -> np.ndarray: if self.retained_losses is not None: return self.retained_losses return self.gross_losses def mean(self) -> float: return metrics.mean(self.losses) def variance(self, ddof: int = 0) -> float: return metrics.variance(self.losses, ddof=ddof) def std(self, ddof: int = 0) -> float: return metrics.std(self.losses, ddof=ddof) def var(self, q: float) -> float: return metrics.var(self.losses, q) def tvar(self, q: float) -> float: return metrics.tvar(self.losses, q) def prob_exceeding(self, threshold: float) -> float: return metrics.prob_exceeding(self.losses, threshold) def gross_mean(self) -> float: return metrics.mean(self.gross_losses) def ceded_mean(self) -> float | None: if self.ceded_losses is None: return None return metrics.mean(self.ceded_losses) def retained_mean(self) -> float | None: if self.retained_losses is None: return None return metrics.mean(self.retained_losses) def component_means(self) -> dict[str, float]: if self.component_losses is None: return {} means = np.mean(self.component_losses, axis=0) if self.component_names is None: names = [f"component_{i}" for i in range(self.component_losses.shape[1])] else: names = list(self.component_names) return {name: float(value) for name, value in zip(names, means, strict=True)} def layer_means(self) -> dict[str, float]: if self.layer_losses is None: return {} means = np.mean(self.layer_losses, axis=0) if self.layer_names is None: names = [f"layer_{i}" for i in range(self.layer_losses.shape[1])] else: names = list(self.layer_names) return {name: float(value) for name, value in zip(names, means, strict=True)} def summary(self, quantiles: tuple[float, ...] = (0.95, 0.99)) -> dict[str, Any]: out = metrics.summary(self.losses, quantiles=quantiles) out["gross_mean"] = self.gross_mean() if self.ceded_losses is not None: out["ceded_mean"] = self.ceded_mean() if self.retained_losses is not None: out["retained_mean"] = self.retained_mean() component_means = self.component_means() if component_means: out["component_means"] = component_means layer_means = self.layer_means() if layer_means: out["layer_means"] = layer_means if self.contract_name is not None: out["contract_name"] = self.contract_name return out