regression_diagnostics/glm/
fit.rs1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
2use statrs::distribution::{ContinuousCDF, Normal, StudentsT};
3
4use super::family::Family;
5use crate::error::{RegressionError, Result};
6use crate::linalg::dmatrix_from_rows;
7
8#[derive(Debug, Clone)]
22pub struct GlmFit<F: Family> {
23 family: F,
24 x: Array2<f64>,
25 y: Array1<f64>,
26 coefficients: Array1<f64>,
27 eta: Array1<f64>,
29 mu: Array1<f64>,
31 weights: Array1<f64>,
33 cov_unscaled: Array2<f64>,
35 dispersion: f64,
37 log_likelihood: f64,
38 intercept_col: Option<usize>,
39 iterations: usize,
40 n: usize,
41 p: usize,
42}
43
44impl<F: Family> GlmFit<F> {
45 pub fn new(family: F, x: Array2<f64>, y: Array1<f64>) -> Result<Self> {
59 Self::with_options(family, x, y, 100, 1e-10)
60 }
61
62 pub fn with_options(
64 family: F,
65 x: Array2<f64>,
66 y: Array1<f64>,
67 max_iter: usize,
68 tol: f64,
69 ) -> Result<Self> {
70 let n = x.nrows();
71 let p = x.ncols();
72 if n == 0 || p == 0 {
73 return Err(RegressionError::EmptyInput { what: "X" });
74 }
75 if y.len() != n {
76 return Err(RegressionError::ShapeMismatch {
77 what: "y length vs X rows",
78 expected: n,
79 got: y.len(),
80 });
81 }
82 let y_std = y.as_standard_layout();
83 family.validate(y_std.as_slice().expect("standard layout is contiguous"))?;
84
85 let intercept_col = detect_constant_column(&x);
86
87 let mut eta = Array1::from_shape_fn(n, |i| family.link(family.init_mu(y[i])));
90 let mut beta = Array1::<f64>::zeros(p);
91 let mut mu = Array1::<f64>::zeros(n);
92 let mut weights = Array1::<f64>::zeros(n);
93 let mut cov_unscaled = Array2::<f64>::zeros((p, p));
94 let mut iterations = 0usize;
95 let mut converged = false;
96
97 while iterations < max_iter {
98 iterations += 1;
99
100 let mut z = Array1::<f64>::zeros(n);
102 for i in 0..n {
103 let mui = family.inverse_link(eta[i]);
104 let dmu = family.dmu_deta(eta[i]);
105 let v = family.variance(mui);
106 mu[i] = mui;
107 weights[i] = if v > 0.0 { dmu * dmu / v } else { 0.0 };
108 z[i] = eta[i] + (y[i] - mui) / dmu;
109 }
110
111 let mut xtwx = Array2::<f64>::zeros((p, p));
113 let mut xtwz = Array1::<f64>::zeros(p);
114 for a in 0..p {
115 let mut sz = 0.0;
116 for i in 0..n {
117 sz += x[(i, a)] * weights[i] * z[i];
118 }
119 xtwz[a] = sz;
120 for b in a..p {
121 let mut s = 0.0;
122 for i in 0..n {
123 s += x[(i, a)] * weights[i] * x[(i, b)];
124 }
125 xtwx[(a, b)] = s;
126 xtwx[(b, a)] = s;
127 }
128 }
129
130 let xtwx_dm = dmatrix_from_rows(p, p, xtwx.as_standard_layout().as_slice().unwrap());
131 let inv = xtwx_dm
132 .try_inverse()
133 .ok_or(RegressionError::RankDeficient)?;
134 let inv_arr = Array2::from_shape_fn((p, p), |(i, j)| inv[(i, j)]);
135
136 let new_beta = inv_arr.dot(&xtwz);
137 let step = (&new_beta - &beta)
138 .iter()
139 .fold(0.0_f64, |m, v| m.max(v.abs()));
140 beta = new_beta;
141 cov_unscaled = inv_arr;
142 eta = x.dot(&beta);
143
144 if !beta.iter().all(|v| v.is_finite()) || beta.iter().any(|v| v.abs() > 1e8) {
145 return Err(RegressionError::NotConverged {
146 iterations,
147 msg: "coefficients diverging (perfect fit or unstable link)".into(),
148 });
149 }
150 if step < tol {
151 converged = true;
152 break;
153 }
154 }
155
156 if !converged {
157 return Err(RegressionError::NotConverged {
158 iterations,
159 msg: "IRLS did not reach tolerance".into(),
160 });
161 }
162
163 for i in 0..n {
165 let mui = family.inverse_link(eta[i]);
166 let dmu = family.dmu_deta(eta[i]);
167 let v = family.variance(mui);
168 mu[i] = mui;
169 weights[i] = if v > 0.0 { dmu * dmu / v } else { 0.0 };
170 }
171
172 let dispersion = if family.dispersion_known() {
173 1.0
174 } else if n > p {
175 let pearson_chi2: f64 = (0..n)
177 .map(|i| {
178 let v = family.variance(mu[i]);
179 if v > 0.0 {
180 let r = y[i] - mu[i];
181 r * r / v
182 } else {
183 0.0
184 }
185 })
186 .sum();
187 pearson_chi2 / (n - p) as f64
188 } else {
189 f64::NAN
190 };
191
192 let log_likelihood = (0..n)
193 .map(|i| family.loglik(y[i], mu[i], dispersion))
194 .sum();
195
196 Ok(Self {
197 family,
198 x,
199 y,
200 coefficients: beta,
201 eta,
202 mu,
203 weights,
204 cov_unscaled,
205 dispersion,
206 log_likelihood,
207 intercept_col,
208 iterations,
209 n,
210 p,
211 })
212 }
213
214 pub fn family(&self) -> &F {
216 &self.family
217 }
218
219 pub fn n_observations(&self) -> usize {
221 self.n
222 }
223
224 pub fn n_parameters(&self) -> usize {
226 self.p
227 }
228
229 pub fn has_intercept(&self) -> bool {
231 self.intercept_col.is_some()
232 }
233
234 pub fn iterations(&self) -> usize {
236 self.iterations
237 }
238
239 pub fn design_matrix(&self) -> ArrayView2<'_, f64> {
241 self.x.view()
242 }
243
244 pub fn response(&self) -> ArrayView1<'_, f64> {
246 self.y.view()
247 }
248
249 pub fn coefficients(&self) -> ArrayView1<'_, f64> {
251 self.coefficients.view()
252 }
253
254 pub fn linear_predictor(&self) -> ArrayView1<'_, f64> {
256 self.eta.view()
257 }
258
259 pub fn fitted_means(&self) -> ArrayView1<'_, f64> {
261 self.mu.view()
262 }
263
264 pub fn weights(&self) -> ArrayView1<'_, f64> {
266 self.weights.view()
267 }
268
269 pub fn dispersion(&self) -> f64 {
272 self.dispersion
273 }
274
275 pub fn log_likelihood(&self) -> f64 {
278 self.log_likelihood
279 }
280
281 pub fn covariance(&self) -> Array2<f64> {
283 &self.cov_unscaled * self.dispersion
284 }
285
286 pub(crate) fn cov_unscaled(&self) -> ArrayView2<'_, f64> {
289 self.cov_unscaled.view()
290 }
291
292 pub fn coefficient_standard_errors(&self) -> Array1<f64> {
294 Array1::from_shape_fn(self.p, |j| {
295 (self.cov_unscaled[(j, j)] * self.dispersion).max(0.0).sqrt()
296 })
297 }
298
299 pub fn wald_statistics(&self) -> Array1<f64> {
305 let se = self.coefficient_standard_errors();
306 Array1::from_shape_fn(self.p, |j| {
307 if se[j] > 0.0 {
308 self.coefficients[j] / se[j]
309 } else {
310 f64::NAN
311 }
312 })
313 }
314
315 pub fn p_values(&self) -> Array1<f64> {
321 let stat = self.wald_statistics();
322 if self.family.dispersion_known() {
323 let normal = Normal::new(0.0, 1.0).expect("standard normal");
324 Array1::from_shape_fn(self.p, |j| {
325 if stat[j].is_finite() {
326 2.0 * (1.0 - normal.cdf(stat[j].abs()))
327 } else {
328 f64::NAN
329 }
330 })
331 } else {
332 let df = (self.n.saturating_sub(self.p)) as f64;
333 let t = StudentsT::new(0.0, 1.0, df.max(1.0)).expect("t distribution");
334 Array1::from_shape_fn(self.p, |j| {
335 if stat[j].is_finite() {
336 2.0 * (1.0 - t.cdf(stat[j].abs()))
337 } else {
338 f64::NAN
339 }
340 })
341 }
342 }
343
344 pub fn predict_mean(&self, x: ArrayView2<'_, f64>) -> Array1<f64> {
347 x.dot(&self.coefficients)
348 .mapv(|eta| self.family.inverse_link(eta))
349 }
350}
351
352fn detect_constant_column(x: &Array2<f64>) -> Option<usize> {
355 for (j, col) in x.columns().into_iter().enumerate() {
356 let first = col[0];
357 let scale = first.abs().max(1.0);
358 if col.iter().all(|&v| (v - first).abs() <= 1e-12 * scale) {
359 return Some(j);
360 }
361 }
362 None
363}