1use ndarray::{Array1, Array2};
16use solow_core::error::{Error, Result};
17use solow_distributions::norm_sf;
18use solow_linalg::inv;
19use solow_optimize::newton_stationary;
20
21struct Surv {
26 ufailt_ix: Vec<Vec<usize>>,
29 risk_enter: Vec<Vec<usize>>,
32}
33
34#[derive(Clone, Debug)]
36pub struct PHReg {
37 exog_s: Array2<f64>,
39 k: usize,
41 surv_ufailt_ix: Vec<Vec<usize>>,
42 surv_risk_enter: Vec<Vec<usize>>,
43 maxiter: usize,
44 gtol: f64,
45}
46
47impl PHReg {
48 pub fn new(time: &[f64], exog: &Array2<f64>, status: &[f64]) -> Result<Self> {
55 let n = time.len();
56 if exog.nrows() != n || status.len() != n {
57 return Err(Error::Shape("time/exog/status length mismatch".into()));
58 }
59 if n == 0 {
60 return Err(Error::Shape("empty sample".into()));
61 }
62 let k = exog.ncols();
63
64 let has_event = (0..n).any(|i| status[i].round() as i64 == 1);
65 if !has_event {
66 return Err(Error::Convergence("no events in sample".into()));
67 }
68
69 let mut first_failure = f64::INFINITY;
74 for i in 0..n {
75 if status[i].round() as i64 == 1 && time[i] < first_failure {
76 first_failure = time[i];
77 }
78 }
79 let mut rows: Vec<usize> = (0..n).filter(|&i| time[i] >= first_failure).collect();
80
81 rows.sort_by(|&a, &b| time[a].total_cmp(&time[b]));
83
84 let m = rows.len();
86 let mut exog_s = Array2::<f64>::zeros((m, k));
87 let mut time_s = vec![0.0_f64; m];
88 let mut status_s = vec![0.0_f64; m];
89 for (new_i, &old_i) in rows.iter().enumerate() {
90 for j in 0..k {
91 exog_s[[new_i, j]] = exog[[old_i, j]];
92 }
93 time_s[new_i] = time[old_i];
94 status_s[new_i] = status[old_i];
95 }
96
97 let surv = build_surv(&time_s, &status_s);
98
99 Ok(PHReg {
100 exog_s,
101 k,
102 surv_ufailt_ix: surv.ufailt_ix,
103 surv_risk_enter: surv.risk_enter,
104 maxiter: 100,
105 gtol: 1e-10,
106 })
107 }
108
109 fn surv(&self) -> Surv {
110 Surv {
111 ufailt_ix: self.surv_ufailt_ix.clone(),
112 risk_enter: self.surv_risk_enter.clone(),
113 }
114 }
115
116 pub fn breslow_loglike(&self, params: &Array1<f64>) -> f64 {
118 let surv = self.surv();
119 let nuft = surv.ufailt_ix.len();
120
121 let mut linpred = self.exog_s.dot(params);
123 let lpmax = linpred.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
124 linpred.mapv_inplace(|v| v - lpmax);
125 let e_linpred: Vec<f64> = linpred.iter().map(|&v| v.exp()).collect();
126
127 let mut like = 0.0;
128 let mut xp0 = 0.0;
129 for i in (0..nuft).rev() {
130 for &ix in &surv.risk_enter[i] {
131 xp0 += e_linpred[ix];
132 }
133 for &ix in &surv.ufailt_ix[i] {
134 like += linpred[ix] - xp0.ln();
135 }
136 }
139 like
140 }
141
142 pub fn breslow_gradient(&self, params: &Array1<f64>) -> Array1<f64> {
144 let surv = self.surv();
145 let nuft = surv.ufailt_ix.len();
146
147 let mut linpred = self.exog_s.dot(params);
148 let lpmax = linpred.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
149 linpred.mapv_inplace(|v| v - lpmax);
150 let e_linpred: Vec<f64> = linpred.iter().map(|&v| v.exp()).collect();
151
152 let mut grad = Array1::<f64>::zeros(self.k);
153 let mut xp0 = 0.0;
154 let mut xp1 = Array1::<f64>::zeros(self.k);
155
156 for i in (0..nuft).rev() {
157 for &ix in &surv.risk_enter[i] {
158 xp0 += e_linpred[ix];
159 let row = self.exog_s.row(ix);
160 for j in 0..self.k {
161 xp1[j] += e_linpred[ix] * row[j];
162 }
163 }
164 for &ix in &surv.ufailt_ix[i] {
165 let row = self.exog_s.row(ix);
166 for j in 0..self.k {
167 grad[j] += row[j] - xp1[j] / xp0;
168 }
169 }
170 }
171 grad
172 }
173
174 pub fn breslow_hessian(&self, params: &Array1<f64>) -> Array2<f64> {
179 let surv = self.surv();
180 let nuft = surv.ufailt_ix.len();
181
182 let mut linpred = self.exog_s.dot(params);
183 let lpmax = linpred.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
184 linpred.mapv_inplace(|v| v - lpmax);
185 let e_linpred: Vec<f64> = linpred.iter().map(|&v| v.exp()).collect();
186
187 let mut hess = Array2::<f64>::zeros((self.k, self.k));
188 let mut xp0 = 0.0;
189 let mut xp1 = Array1::<f64>::zeros(self.k);
190 let mut xp2 = Array2::<f64>::zeros((self.k, self.k));
191
192 for i in (0..nuft).rev() {
193 for &ix in &surv.risk_enter[i] {
194 let el = e_linpred[ix];
195 xp0 += el;
196 let row = self.exog_s.row(ix);
197 for a in 0..self.k {
198 xp1[a] += el * row[a];
199 for b in 0..self.k {
200 xp2[[a, b]] += el * row[a] * row[b];
201 }
202 }
203 }
204 let mfail = surv.ufailt_ix[i].len() as f64;
205 for a in 0..self.k {
206 for b in 0..self.k {
207 let val = xp2[[a, b]] / xp0 - (xp1[a] * xp1[b]) / (xp0 * xp0);
208 hess[[a, b]] += mfail * val;
209 }
210 }
211 }
212 hess.mapv_inplace(|v| -v);
214 hess
215 }
216
217 pub fn fit(&self) -> Result<PHRegResults> {
219 let start = Array1::<f64>::zeros(self.k);
220 let opt = newton_stationary(
221 &start,
222 |b| {
223 let f = self.breslow_loglike(b);
224 let g = self.breslow_gradient(b);
225 let h = self.breslow_hessian(b);
226 (f, g, h)
227 },
228 self.maxiter,
229 self.gtol,
230 )?;
231
232 let params = opt.x;
233 let d2l = self.breslow_hessian(¶ms);
238 let info = d2l.mapv(|v| -v);
239 let cov = inv(&info)?;
240
241 let bse = Array1::from_iter((0..self.k).map(|i| cov[[i, i]].sqrt()));
242 let tvalues = Array1::from_iter((0..self.k).map(|i| params[i] / bse[i]));
243 let pvalues = Array1::from_iter((0..self.k).map(|i| 2.0 * norm_sf(tvalues[i].abs())));
244 let llf = self.breslow_loglike(¶ms);
245
246 Ok(PHRegResults {
247 params,
248 bse,
249 tvalues,
250 pvalues,
251 cov_params: cov,
252 llf,
253 converged: opt.converged,
254 })
255 }
256}
257
258fn build_surv(time_s: &[f64], status_s: &[f64]) -> Surv {
261 let m = time_s.len();
262
263 let mut ft: Vec<f64> = (0..m)
265 .filter(|&i| status_s[i].round() as i64 == 1)
266 .map(|i| time_s[i])
267 .collect();
268 ft.sort_by(|a, b| a.total_cmp(b));
269 let mut uft: Vec<f64> = Vec::new();
270 for &t in &ft {
271 if uft.is_empty() || t != *uft.last().unwrap() {
272 uft.push(t);
273 }
274 }
275 let nuft = uft.len();
276
277 let mut ufailt_ix: Vec<Vec<usize>> = vec![Vec::new(); nuft];
279 for (i, &t) in time_s.iter().enumerate().take(m) {
280 if status_s[i].round() as i64 == 1 {
281 let k = uft.iter().position(|&u| u == t).unwrap();
282 ufailt_ix[k].push(i);
283 }
284 }
285
286 let mut risk_enter: Vec<Vec<usize>> = vec![Vec::new(); nuft];
289 for (i, &t) in time_s.iter().enumerate().take(m) {
290 let cnt = uft.iter().filter(|&&u| u <= t).count();
292 if cnt >= 1 {
293 risk_enter[cnt - 1].push(i);
294 }
295 }
296
297 Surv {
298 ufailt_ix,
299 risk_enter,
300 }
301}
302
303#[derive(Clone, Debug)]
305pub struct PHRegResults {
306 pub params: Array1<f64>,
308 pub bse: Array1<f64>,
310 pub tvalues: Array1<f64>,
312 pub pvalues: Array1<f64>,
314 pub cov_params: Array2<f64>,
316 pub llf: f64,
318 pub converged: bool,
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use ndarray::array;
326
327 #[test]
328 fn gradient_zero_at_optimum() {
329 let time = [4.0, 3.0, 1.0, 1.0, 2.0, 2.0, 3.0];
331 let status = [1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0];
332 let exog = array![[0.5_f64], [1.2], [-0.3], [0.8], [0.1], [-1.0], [0.4]];
333 let model = PHReg::new(&time, &exog, &status).unwrap();
334 let res = model.fit().unwrap();
335 assert!(res.converged);
336 let g = model.breslow_gradient(&res.params);
337 assert!(g.iter().all(|&v| v.abs() < 1e-8));
338 }
339
340 #[test]
341 fn hessian_matches_numeric_gradient_diff() {
342 let time = [1.0, 2.0, 3.0, 4.0, 5.0];
345 let status = [1.0, 1.0, 0.0, 1.0, 1.0];
346 let exog = array![[0.2_f64], [-0.5], [1.0], [0.3], [-0.8]];
347 let model = PHReg::new(&time, &exog, &status).unwrap();
348 let b = array![0.1_f64];
349 let h = model.breslow_hessian(&b)[[0, 0]];
352 let eps = 1e-6;
353 let gp = model.breslow_gradient(&array![0.1 + eps])[0];
354 let gm = model.breslow_gradient(&array![0.1 - eps])[0];
355 let num_d2l = (gp - gm) / (2.0 * eps);
356 assert!((h - num_d2l).abs() < 1e-4, "h={h}, num={num_d2l}");
357 }
358}