use std::fmt::Display;
use ndarray::Array1;
use crate::traits::FloatExt;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HazardInterpolation {
#[default]
PiecewiseConstantHazard,
LinearSurvival,
}
impl Display for HazardInterpolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PiecewiseConstantHazard => write!(f, "Piecewise-constant hazard"),
Self::LinearSurvival => write!(f, "Linear survival"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SurvivalPoint<T: FloatExt> {
pub time: T,
pub survival_probability: T,
}
#[derive(Debug, Clone)]
pub struct SurvivalCurve<T: FloatExt> {
points: Vec<SurvivalPoint<T>>,
method: HazardInterpolation,
}
impl<T: FloatExt> SurvivalCurve<T> {
pub fn new(points: Vec<SurvivalPoint<T>>, method: HazardInterpolation) -> Self {
let mut pts = points;
pts.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
if pts.is_empty() || pts[0].time > T::zero() {
pts.insert(
0,
SurvivalPoint {
time: T::zero(),
survival_probability: T::one(),
},
);
}
Self {
points: pts,
method,
}
}
pub fn from_survival_probs(
times: &Array1<T>,
survivals: &Array1<T>,
method: HazardInterpolation,
) -> Self {
let points = times
.iter()
.zip(survivals.iter())
.map(|(&t, &q)| SurvivalPoint {
time: t,
survival_probability: q,
})
.collect();
Self::new(points, method)
}
pub fn from_default_probs(
times: &Array1<T>,
defaults: &Array1<T>,
method: HazardInterpolation,
) -> Self {
let survivals: Array1<T> = defaults.mapv(|p| T::one() - p);
Self::from_survival_probs(times, &survivals, method)
}
pub fn from_hazard_rates(
times: &Array1<T>,
hazards: &Array1<T>,
method: HazardInterpolation,
) -> Self {
assert_eq!(
times.len(),
hazards.len(),
"times and hazards must have matching lengths"
);
let mut survivals = Array1::zeros(times.len());
let mut cum_log = T::zero();
let mut prev_t = T::zero();
for i in 0..times.len() {
let dt = times[i] - prev_t;
cum_log -= hazards[i] * dt;
survivals[i] = cum_log.exp();
prev_t = times[i];
}
Self::from_survival_probs(times, &survivals, method)
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.len() <= 1
}
pub fn points(&self) -> &[SurvivalPoint<T>] {
&self.points
}
pub fn method(&self) -> HazardInterpolation {
self.method
}
pub fn survival_probability(&self, t: T) -> T {
if t <= T::zero() {
return T::one();
}
let last = self.points.last().expect("survival curve has an anchor");
if t >= last.time {
return extrapolate_flat_hazard(last.time, last.survival_probability, t);
}
let idx = self
.points
.partition_point(|p| p.time < t)
.saturating_sub(1);
let p0 = &self.points[idx];
let p1 = &self.points[idx + 1];
match self.method {
HazardInterpolation::PiecewiseConstantHazard => {
let ln0 = p0.survival_probability.ln();
let ln1 = p1.survival_probability.ln();
let w = (t - p0.time) / (p1.time - p0.time);
(ln0 * (T::one() - w) + ln1 * w).exp()
}
HazardInterpolation::LinearSurvival => {
let w = (t - p0.time) / (p1.time - p0.time);
p0.survival_probability * (T::one() - w) + p1.survival_probability * w
}
}
}
pub fn default_probability(&self, t: T) -> T {
T::one() - self.survival_probability(t)
}
pub fn conditional_default_probability(&self, t1: T, t2: T) -> T {
if t2 <= t1 {
return T::zero();
}
let q1 = self.survival_probability(t1);
if q1 <= T::min_positive_val() {
return T::zero();
}
T::one() - self.survival_probability(t2) / q1
}
pub fn forward_hazard(&self, t1: T, t2: T) -> T {
if t2 <= t1 {
return T::zero();
}
let q1 = self.survival_probability(t1);
let q2 = self.survival_probability(t2);
if q1 <= T::min_positive_val() || q2 <= T::min_positive_val() {
return T::zero();
}
-(q2.ln() - q1.ln()) / (t2 - t1)
}
pub fn average_hazard(&self, t: T) -> T {
if t <= T::zero() {
return T::zero();
}
let q = self.survival_probability(t);
if q <= T::min_positive_val() {
return T::zero();
}
-q.ln() / t
}
pub fn survival_probabilities(&self, maturities: &Array1<T>) -> Array1<T> {
Array1::from_iter(maturities.iter().map(|&t| self.survival_probability(t)))
}
pub fn default_probabilities(&self, maturities: &Array1<T>) -> Array1<T> {
Array1::from_iter(maturities.iter().map(|&t| self.default_probability(t)))
}
}
#[derive(Debug, Clone)]
pub struct HazardRateCurve<T: FloatExt> {
inner: SurvivalCurve<T>,
}
impl<T: FloatExt> HazardRateCurve<T> {
pub fn new(survival: SurvivalCurve<T>) -> Self {
Self { inner: survival }
}
pub fn from_hazard_rates(
times: &Array1<T>,
hazards: &Array1<T>,
method: HazardInterpolation,
) -> Self {
Self::new(SurvivalCurve::from_hazard_rates(times, hazards, method))
}
pub fn survival_curve(&self) -> &SurvivalCurve<T> {
&self.inner
}
pub fn forward_hazard(&self, t1: T, t2: T) -> T {
self.inner.forward_hazard(t1, t2)
}
pub fn average_hazard(&self, t: T) -> T {
self.inner.average_hazard(t)
}
pub fn survival_probability(&self, t: T) -> T {
self.inner.survival_probability(t)
}
}
fn extrapolate_flat_hazard<T: FloatExt>(last_time: T, last_survival: T, t: T) -> T {
if last_time <= T::zero() {
return last_survival;
}
let h = -last_survival.ln() / last_time;
(-h * t).exp()
}