qualia_core_db/solvers/learning/survival/
cox.rs1use crate::solvers::learning::LearningError;
7use crate::solvers::linear_algebra::cholesky::{cholesky_factor, cholesky_solve};
8use crate::solvers::statistics::distributions::normal;
9
10#[derive(Debug, Clone)]
12pub struct CoxModel {
13 pub coefficients: Vec<f64>,
14 pub std_errors: Vec<f64>,
15 pub z_values: Vec<f64>,
18 pub p_values: Vec<f64>,
19 pub log_partial_likelihood: f64,
20 pub n_iter: usize,
21 pub converged: bool,
22}
23
24const MAX_ITER: usize = 100;
25const TOL: f64 = 1e-8;
26
27pub fn fit(
31 x: &[f64],
32 times: &[f64],
33 event: &[bool],
34 n: usize,
35 p: usize,
36) -> Result<CoxModel, LearningError> {
37 if n == 0 || p == 0 || x.len() != n * p || times.len() != n || event.len() != n {
38 return Err(LearningError::InvalidDimension);
39 }
40 if event.iter().filter(|&&e| e).count() == 0 {
41 return Err(LearningError::InsufficientData); }
43
44 let mut beta = vec![0.0; p];
45 let mut grad = vec![0.0; p];
46 let mut info = vec![0.0; p * p];
47 let mut converged = false;
48 let mut iters = 0;
49 let mut log_pl = 0.0;
50
51 for it in 1..=MAX_ITER {
52 iters = it;
53 grad.iter_mut().for_each(|v| *v = 0.0);
54 info.iter_mut().for_each(|v| *v = 0.0);
55 log_pl = 0.0;
56
57 let mut w = vec![0.0; n];
59 for j in 0..n {
60 let eta: f64 = (0..p).map(|c| beta[c] * x[j * p + c]).sum();
61 w[j] = eta.exp();
62 }
63
64 for i in 0..n {
65 if !event[i] {
66 continue;
67 }
68 let ti = times[i];
70 let mut sum_w = 0.0;
71 let mut sum_wx = vec![0.0; p];
72 let mut sum_wxx = vec![0.0; p * p];
73 for j in 0..n {
74 if times[j] >= ti {
75 let wj = w[j];
76 sum_w += wj;
77 for a in 0..p {
78 let xa = x[j * p + a];
79 sum_wx[a] += wj * xa;
80 for b in 0..p {
81 sum_wxx[a * p + b] += wj * xa * x[j * p + b];
82 }
83 }
84 }
85 }
86 if sum_w <= 0.0 {
87 continue;
88 }
89 let eta_i: f64 = (0..p).map(|c| beta[c] * x[i * p + c]).sum();
90 log_pl += eta_i - sum_w.ln();
91 for a in 0..p {
93 let mean_a = sum_wx[a] / sum_w;
94 grad[a] += x[i * p + a] - mean_a;
95 for b in 0..p {
96 let mean_b = sum_wx[b] / sum_w;
97 info[a * p + b] += sum_wxx[a * p + b] / sum_w - mean_a * mean_b;
98 }
99 }
100 }
101
102 let mut l = vec![0.0; p * p];
105 cholesky_factor(p, &info, &mut l).map_err(|_| LearningError::Singular)?;
106 let mut delta = vec![0.0; p];
107 cholesky_solve(p, &l, &grad, &mut delta)?;
108 let mut max_step = 0.0_f64;
109 for c in 0..p {
110 beta[c] += delta[c];
111 max_step = max_step.max(delta[c].abs());
112 }
113 if max_step < TOL {
114 converged = true;
115 break;
116 }
117 }
118
119 let mut l = vec![0.0; p * p];
121 cholesky_factor(p, &info, &mut l).map_err(|_| LearningError::Singular)?;
122
123 let mut std_errors = vec![0.0; p];
124 let mut z_values = vec![0.0; p];
125 let mut p_values = vec![0.0; p];
126 let mut ej = vec![0.0; p];
127 let mut cj = vec![0.0; p];
128 for a in 0..p {
129 ej.iter_mut().for_each(|v| *v = 0.0);
130 ej[a] = 1.0;
131 cholesky_solve(p, &l, &ej, &mut cj)?;
132 let se = if cj[a] > 0.0 { cj[a].sqrt() } else { 0.0 };
133 std_errors[a] = se;
134 if se > 0.0 {
135 let z = beta[a] / se;
136 z_values[a] = z;
137 p_values[a] = normal::two_sided_p(z);
138 }
139 }
140
141 Ok(CoxModel {
142 coefficients: beta,
143 std_errors,
144 z_values,
145 p_values,
146 log_partial_likelihood: log_pl,
147 n_iter: iters,
148 converged,
149 })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn higher_covariate_increases_hazard() {
158 let x = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
161 let times = [8.0, 7.0, 5.0, 6.0, 3.0, 4.0, 2.0, 1.0];
162 let event = [true, true, true, true, true, true, true, true];
163 let m = fit(&x, ×, &event, 8, 1).unwrap();
164 assert!(m.converged);
165 assert!(m.coefficients[0] > 0.0, "coef {}", m.coefficients[0]);
166 assert!(m.std_errors[0] > 0.0 && m.std_errors[0].is_finite());
167 assert!(m.log_partial_likelihood.is_finite());
168 }
169
170 #[test]
171 fn protective_covariate_is_negative() {
172 let x = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
174 let times = [1.0, 2.0, 4.0, 3.0, 6.0, 5.0, 7.0, 8.0];
175 let event = [true, true, true, true, true, true, true, true];
176 let m = fit(&x, ×, &event, 8, 1).unwrap();
177 assert!(m.coefficients[0] < 0.0, "coef {}", m.coefficients[0]);
178 }
179
180 #[test]
181 fn guards() {
182 assert_eq!(
183 fit(&[1.0, 2.0], &[1.0, 2.0], &[false, false], 2, 1).unwrap_err(),
184 LearningError::InsufficientData
185 );
186 assert_eq!(
187 fit(&[1.0], &[1.0], &[true], 1, 2).unwrap_err(),
188 LearningError::InvalidDimension
189 );
190 }
191}