regression_diagnostics/survival/
aft.rs1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
2use statrs::distribution::{ContinuousCDF, Normal};
3
4use crate::error::{RegressionError, Result};
5use crate::linalg::{dmatrix_from_rows, dvector_from_slice};
6use crate::optimize::{nelder_mead, numerical_hessian};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum AftDistribution {
12 Weibull,
15 Exponential,
18 LogNormal,
20 LogLogistic,
22}
23
24impl AftDistribution {
25 fn log_pdf(self, w: f64) -> f64 {
27 match self {
28 AftDistribution::Weibull | AftDistribution::Exponential => w - w.exp(),
29 AftDistribution::LogNormal => {
30 -0.5 * (2.0 * std::f64::consts::PI).ln() - 0.5 * w * w
31 }
32 AftDistribution::LogLogistic => {
33 w - 2.0 * log1p_exp(w)
35 }
36 }
37 }
38
39 fn log_surv(self, w: f64) -> f64 {
41 match self {
42 AftDistribution::Weibull | AftDistribution::Exponential => -w.exp(),
43 AftDistribution::LogNormal => {
44 let n = Normal::new(0.0, 1.0).unwrap();
45 (1.0 - n.cdf(w)).max(1e-300).ln()
46 }
47 AftDistribution::LogLogistic => -log1p_exp(w),
48 }
49 }
50
51 fn median_w(self) -> f64 {
53 match self {
54 AftDistribution::Weibull | AftDistribution::Exponential => (2.0_f64.ln()).ln(),
56 AftDistribution::LogNormal | AftDistribution::LogLogistic => 0.0,
57 }
58 }
59
60 fn scale_fixed(self) -> bool {
61 matches!(self, AftDistribution::Exponential)
62 }
63}
64
65fn log1p_exp(x: f64) -> f64 {
67 if x > 0.0 {
68 x + (-x).exp().ln_1p()
69 } else {
70 x.exp().ln_1p()
71 }
72}
73
74#[derive(Debug, Clone)]
85pub struct AftFit {
86 dist: AftDistribution,
87 coefficients: Array1<f64>,
88 scale: f64,
89 cov: Array2<f64>,
90 log_likelihood: f64,
91 n: usize,
92 p: usize,
93}
94
95impl AftFit {
96 pub fn new(
110 time: Array1<f64>,
111 event: Array1<f64>,
112 x: Array2<f64>,
113 dist: AftDistribution,
114 ) -> Result<Self> {
115 let n = x.nrows();
116 let p = x.ncols();
117 if n == 0 || p == 0 {
118 return Err(RegressionError::EmptyInput { what: "X" });
119 }
120 if time.len() != n || event.len() != n {
121 return Err(RegressionError::ShapeMismatch {
122 what: "time/event length vs X rows",
123 expected: n,
124 got: time.len().min(event.len()),
125 });
126 }
127 let mut n_events = 0usize;
128 for i in 0..n {
129 if !time[i].is_finite() || time[i] <= 0.0 {
130 return Err(RegressionError::InvalidResponse {
131 msg: format!("survival times must be positive, found {}", time[i]),
132 });
133 }
134 if event[i] == 1.0 {
135 n_events += 1;
136 } else if event[i] != 0.0 {
137 return Err(RegressionError::InvalidResponse {
138 msg: format!("event indicator must be 0 or 1, found {}", event[i]),
139 });
140 }
141 }
142 if n_events == 0 {
143 return Err(RegressionError::InvalidResponse {
144 msg: "no events observed".into(),
145 });
146 }
147
148 let logt: Vec<f64> = time.iter().map(|t| t.ln()).collect();
149 let scale_fixed = dist.scale_fixed();
150
151 let n_par = if scale_fixed { p } else { p + 1 };
153
154 let nll = |theta: &[f64]| -> f64 {
156 let sigma = if scale_fixed { 1.0 } else { theta[p].exp() };
157 if !sigma.is_finite() || sigma <= 0.0 {
158 return f64::INFINITY;
159 }
160 let mut s = 0.0;
161 for i in 0..n {
162 let mut eta = 0.0;
163 for j in 0..p {
164 eta += x[(i, j)] * theta[j];
165 }
166 let z = (logt[i] - eta) / sigma;
167 if event[i] == 1.0 {
168 s -= -sigma.ln() - logt[i] + dist.log_pdf(z);
169 } else {
170 s -= dist.log_surv(z);
171 }
172 }
173 if s.is_finite() {
174 s
175 } else {
176 f64::INFINITY
177 }
178 };
179
180 let xd = dmatrix_from_rows(n, p, x.as_standard_layout().as_slice().unwrap());
183 let ld = dvector_from_slice(&logt);
184 let beta0 = match (xd.transpose() * &xd).try_inverse() {
185 Some(inv) => inv * xd.transpose() * ld,
186 None => return Err(RegressionError::RankDeficient),
187 };
188 let mut theta0 = vec![0.0; n_par];
189 for j in 0..p {
190 theta0[j] = beta0[j];
191 }
192 if !scale_fixed {
193 let resid_var = (0..n)
194 .map(|i| {
195 let fit: f64 = (0..p).map(|j| x[(i, j)] * beta0[j]).sum();
196 (logt[i] - fit).powi(2)
197 })
198 .sum::<f64>()
199 / n as f64;
200 theta0[p] = (0.5 * resid_var.max(1e-6).ln()).max(-5.0);
201 }
202
203 let nll_ref = &nll;
204 let theta = nelder_mead(nll_ref, &theta0, 0.1, 1e-10, 5000);
205 let final_nll = nll(&theta);
206 if !final_nll.is_finite() {
207 return Err(RegressionError::NotConverged {
208 iterations: 5000,
209 msg: "AFT optimizer failed to find a finite optimum".into(),
210 });
211 }
212
213 let grad = |t: &[f64]| -> Vec<f64> {
215 let mut g = vec![0.0; n_par];
216 for j in 0..n_par {
217 let h = 1e-6 * t[j].abs().max(1.0);
218 let mut tp = t.to_vec();
219 let mut tm = t.to_vec();
220 tp[j] += h;
221 tm[j] -= h;
222 g[j] = (nll(&tp) - nll(&tm)) / (2.0 * h);
223 }
224 g
225 };
226 let hess = numerical_hessian(grad, &theta);
227 let flat: Vec<f64> = hess.iter().flat_map(|r| r.iter().copied()).collect();
228 let hd = dmatrix_from_rows(n_par, n_par, &flat);
229 let cov_full = hd.try_inverse().ok_or(RegressionError::RankDeficient)?;
230
231 let coefficients = Array1::from_shape_fn(p, |j| theta[j]);
232 let cov = Array2::from_shape_fn((p, p), |(i, j)| cov_full[(i, j)]);
233 let scale = if scale_fixed { 1.0 } else { theta[p].exp() };
234
235 Ok(Self {
236 dist,
237 coefficients,
238 scale,
239 cov,
240 log_likelihood: -final_nll,
241 n,
242 p,
243 })
244 }
245
246 pub fn distribution(&self) -> AftDistribution {
248 self.dist
249 }
250
251 pub fn n_observations(&self) -> usize {
253 self.n
254 }
255
256 pub fn n_parameters(&self) -> usize {
258 self.p
259 }
260
261 pub fn coefficients(&self) -> ArrayView1<'_, f64> {
264 self.coefficients.view()
265 }
266
267 pub fn scale(&self) -> f64 {
269 self.scale
270 }
271
272 pub fn covariance(&self) -> ArrayView2<'_, f64> {
274 self.cov.view()
275 }
276
277 pub fn log_likelihood(&self) -> f64 {
279 self.log_likelihood
280 }
281
282 pub fn aic(&self) -> f64 {
284 let k = self.p as f64 + if self.dist.scale_fixed() { 0.0 } else { 1.0 };
285 -2.0 * self.log_likelihood + 2.0 * k
286 }
287
288 pub fn coefficient_standard_errors(&self) -> Array1<f64> {
290 Array1::from_shape_fn(self.p, |j| self.cov[(j, j)].max(0.0).sqrt())
291 }
292
293 pub fn z_values(&self) -> Array1<f64> {
295 let se = self.coefficient_standard_errors();
296 Array1::from_shape_fn(self.p, |j| {
297 if se[j] > 0.0 {
298 self.coefficients[j] / se[j]
299 } else {
300 f64::NAN
301 }
302 })
303 }
304
305 pub fn p_values(&self) -> Array1<f64> {
307 let z = self.z_values();
308 let normal = Normal::new(0.0, 1.0).expect("standard normal");
309 Array1::from_shape_fn(self.p, |j| {
310 if z[j].is_finite() {
311 2.0 * (1.0 - normal.cdf(z[j].abs()))
312 } else {
313 f64::NAN
314 }
315 })
316 }
317
318 pub fn predict_median(&self, x: ArrayView2<'_, f64>) -> Array1<f64> {
321 let shift = self.scale * self.dist.median_w();
322 x.dot(&self.coefficients).mapv(|eta| (eta + shift).exp())
323 }
324}