1use crate::error::FdarError;
20use crate::matrix::FdMatrix;
21use crate::regression::FpcaResult;
22
23mod bootstrap;
24mod cv;
25mod fregre_lm;
26mod logistic;
27mod nonparametric;
28mod robust;
29#[cfg(test)]
30mod tests;
31
32pub use bootstrap::{bootstrap_ci_fregre_lm, bootstrap_ci_functional_logistic};
34pub use cv::{fregre_basis_cv, fregre_np_cv};
35pub use fregre_lm::{fregre_cv, fregre_lm, model_selection_ncomp, predict_fregre_lm};
36pub use logistic::{functional_logistic, predict_functional_logistic};
37pub use nonparametric::{fregre_np_mixed, predict_fregre_np};
38pub use robust::{fregre_huber, fregre_l1, predict_fregre_robust};
39
40#[derive(Debug, Clone, PartialEq)]
46pub struct FregreLmResult {
47 pub intercept: f64,
49 pub beta_t: Vec<f64>,
51 pub beta_se: Vec<f64>,
53 pub gamma: Vec<f64>,
55 pub fitted_values: Vec<f64>,
57 pub residuals: Vec<f64>,
59 pub r_squared: f64,
61 pub r_squared_adj: f64,
63 pub std_errors: Vec<f64>,
65 pub ncomp: usize,
67 pub fpca: FpcaResult,
69 pub coefficients: Vec<f64>,
71 pub residual_se: f64,
73 pub gcv: f64,
75 pub aic: f64,
77 pub bic: f64,
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub struct FregreNpResult {
84 pub fitted_values: Vec<f64>,
86 pub residuals: Vec<f64>,
88 pub r_squared: f64,
90 pub h_func: f64,
92 pub h_scalar: f64,
94 pub cv_error: f64,
96}
97
98#[derive(Debug, Clone, PartialEq)]
100#[non_exhaustive]
101pub struct FregreRobustResult {
102 pub intercept: f64,
104 pub beta_t: Vec<f64>,
106 pub fitted_values: Vec<f64>,
108 pub residuals: Vec<f64>,
110 pub coefficients: Vec<f64>,
112 pub ncomp: usize,
114 pub fpca: FpcaResult,
116 pub iterations: usize,
118 pub converged: bool,
120 pub weights: Vec<f64>,
122 pub r_squared: f64,
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct FunctionalLogisticResult {
129 pub intercept: f64,
131 pub beta_t: Vec<f64>,
133 pub beta_se: Vec<f64>,
135 pub gamma: Vec<f64>,
137 pub probabilities: Vec<f64>,
139 pub predicted_classes: Vec<usize>,
141 pub ncomp: usize,
143 pub accuracy: f64,
145 pub std_errors: Vec<f64>,
147 pub coefficients: Vec<f64>,
149 pub log_likelihood: f64,
151 pub iterations: usize,
153 pub fpca: FpcaResult,
155 pub aic: f64,
157 pub bic: f64,
159}
160
161#[derive(Debug, Clone, PartialEq)]
163pub struct FregreCvResult {
164 pub k_values: Vec<usize>,
166 pub cv_errors: Vec<f64>,
168 pub optimal_k: usize,
170 pub min_cv_error: f64,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq)]
176pub enum SelectionCriterion {
177 Aic,
179 Bic,
181 Gcv,
183}
184
185#[derive(Debug, Clone, PartialEq)]
187pub struct ModelSelectionResult {
188 pub best_ncomp: usize,
190 pub criteria: Vec<(usize, f64, f64, f64)>,
192}
193
194#[derive(Debug, Clone, PartialEq)]
196pub struct BootstrapCiResult {
197 pub lower: Vec<f64>,
199 pub upper: Vec<f64>,
201 pub center: Vec<f64>,
203 pub sim_lower: Vec<f64>,
205 pub sim_upper: Vec<f64>,
207 pub n_boot_success: usize,
209}
210
211#[derive(Debug, Clone, PartialEq)]
213pub struct FregreBasisCvResult {
214 pub optimal_lambda: f64,
216 pub cv_errors: Vec<f64>,
218 pub cv_se: Vec<f64>,
220 pub lambda_values: Vec<f64>,
222 pub min_cv_error: f64,
224}
225
226#[derive(Debug, Clone, PartialEq)]
228pub struct FregreNpCvResult {
229 pub optimal_h: f64,
231 pub cv_errors: Vec<f64>,
233 pub cv_se: Vec<f64>,
235 pub h_values: Vec<f64>,
237 pub min_cv_error: f64,
239}
240
241pub(crate) fn compute_xtx(x: &FdMatrix) -> Vec<f64> {
247 let (n, p) = x.shape();
248 let mut xtx = vec![0.0; p * p];
249 for k in 0..p {
250 for j in k..p {
251 let mut s = 0.0;
252 for i in 0..n {
253 s += x[(i, k)] * x[(i, j)];
254 }
255 xtx[k * p + j] = s;
256 xtx[j * p + k] = s;
257 }
258 }
259 xtx
260}
261
262fn compute_xty(x: &FdMatrix, y: &[f64]) -> Vec<f64> {
264 let (n, p) = x.shape();
265 (0..p)
266 .map(|k| {
267 let mut s = 0.0;
268 for i in 0..n {
269 s += x[(i, k)] * y[i];
270 }
271 s
272 })
273 .collect()
274}
275
276pub(crate) fn cholesky_factor(a: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
278 let mut l = vec![0.0; p * p];
279 for j in 0..p {
280 let mut diag = a[j * p + j];
281 for k in 0..j {
282 diag -= l[j * p + k] * l[j * p + k];
283 }
284 if diag <= 1e-12 {
285 return Err(FdarError::ComputationFailed {
286 operation: "Cholesky factorization",
287 detail: "matrix is singular or near-singular; try reducing ncomp or check for collinear FPC scores".into(),
288 });
289 }
290 l[j * p + j] = diag.sqrt();
291 for i in (j + 1)..p {
292 let mut s = a[i * p + j];
293 for k in 0..j {
294 s -= l[i * p + k] * l[j * p + k];
295 }
296 l[i * p + j] = s / l[j * p + j];
297 }
298 }
299 Ok(l)
300}
301
302pub(crate) fn cholesky_forward_back(l: &[f64], b: &[f64], p: usize) -> Vec<f64> {
304 let mut z = b.to_vec();
305 for j in 0..p {
306 for k in 0..j {
307 z[j] -= l[j * p + k] * z[k];
308 }
309 z[j] /= l[j * p + j];
310 }
311 for j in (0..p).rev() {
312 for k in (j + 1)..p {
313 z[j] -= l[k * p + j] * z[k];
314 }
315 z[j] /= l[j * p + j];
316 }
317 z
318}
319
320pub(super) fn cholesky_solve(a: &[f64], b: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
322 let l = cholesky_factor(a, p)?;
323 Ok(cholesky_forward_back(&l, b, p))
324}
325
326pub(crate) fn compute_hat_diagonal(x: &FdMatrix, l: &[f64]) -> Vec<f64> {
328 let (n, p) = x.shape();
329 let mut hat_diag = vec![0.0; n];
330 for i in 0..n {
331 let mut v = vec![0.0; p];
332 for j in 0..p {
333 v[j] = x[(i, j)];
334 for k in 0..j {
335 v[j] -= l[j * p + k] * v[k];
336 }
337 v[j] /= l[j * p + j];
338 }
339 hat_diag[i] = v.iter().map(|vi| vi * vi).sum();
340 }
341 hat_diag
342}
343
344fn compute_ols_std_errors(l: &[f64], p: usize, sigma2: f64) -> Vec<f64> {
346 let mut se = vec![0.0; p];
347 for j in 0..p {
348 let mut v = vec![0.0; p];
349 v[j] = 1.0;
350 for k in 0..p {
351 for kk in 0..k {
352 v[k] -= l[k * p + kk] * v[kk];
353 }
354 v[k] /= l[k * p + k];
355 }
356 se[j] = (sigma2 * v.iter().map(|vi| vi * vi).sum::<f64>()).sqrt();
357 }
358 se
359}
360
361fn validate_fregre_inputs(
368 n: usize,
369 m: usize,
370 y: &[f64],
371 scalar_covariates: Option<&FdMatrix>,
372) -> Result<(), FdarError> {
373 if n < 3 {
374 return Err(FdarError::InvalidDimension {
375 parameter: "data",
376 expected: "at least 3 rows".to_string(),
377 actual: format!("{n}"),
378 });
379 }
380 if m == 0 {
381 return Err(FdarError::InvalidDimension {
382 parameter: "data",
383 expected: "at least 1 column".to_string(),
384 actual: "0".to_string(),
385 });
386 }
387 if y.len() != n {
388 return Err(FdarError::InvalidDimension {
389 parameter: "y",
390 expected: format!("{n}"),
391 actual: format!("{}", y.len()),
392 });
393 }
394 if let Some(sc) = scalar_covariates {
395 if sc.nrows() != n {
396 return Err(FdarError::InvalidDimension {
397 parameter: "scalar_covariates",
398 expected: format!("{n} rows"),
399 actual: format!("{} rows", sc.nrows()),
400 });
401 }
402 }
403 Ok(())
404}
405
406fn resolve_ncomp(
408 ncomp: usize,
409 data: &FdMatrix,
410 y: &[f64],
411 scalar_covariates: Option<&FdMatrix>,
412 n: usize,
413 m: usize,
414) -> Result<usize, FdarError> {
415 if ncomp == 0 {
416 let cv = fregre_cv(data, y, scalar_covariates, 1, m.min(n - 1).min(20), 5)?;
417 Ok(cv.optimal_k)
418 } else {
419 Ok(ncomp.min(n - 1).min(m))
420 }
421}
422
423pub(crate) fn build_design_matrix(
424 fpca_scores: &FdMatrix,
425 ncomp: usize,
426 scalar_covariates: Option<&FdMatrix>,
427 n: usize,
428) -> FdMatrix {
429 let p_scalar = scalar_covariates.map_or(0, super::matrix::FdMatrix::ncols);
430 let p_total = 1 + ncomp + p_scalar;
431 let mut design = FdMatrix::zeros(n, p_total);
432 for i in 0..n {
433 design[(i, 0)] = 1.0;
434 for k in 0..ncomp {
435 design[(i, 1 + k)] = fpca_scores[(i, k)];
436 }
437 if let Some(sc) = scalar_covariates {
438 for j in 0..p_scalar {
439 design[(i, 1 + ncomp + j)] = sc[(i, j)];
440 }
441 }
442 }
443 design
444}
445
446fn recover_beta_t(fpc_coeffs: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
448 let ncomp = fpc_coeffs.len();
449 let mut beta_t = vec![0.0; m];
450 for k in 0..ncomp {
451 for j in 0..m {
452 beta_t[j] += fpc_coeffs[k] * rotation[(j, k)];
453 }
454 }
455 beta_t
456}
457
458fn compute_beta_se(gamma_se: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
462 let ncomp = gamma_se.len();
463 let mut beta_se = vec![0.0; m];
464 for j in 0..m {
465 let mut var_j = 0.0;
466 for k in 0..ncomp {
467 var_j += rotation[(j, k)].powi(2) * gamma_se[k].powi(2);
468 }
469 beta_se[j] = var_j.sqrt();
470 }
471 beta_se
472}
473
474fn compute_fitted(design: &FdMatrix, coeffs: &[f64]) -> Vec<f64> {
476 let (n, p) = design.shape();
477 (0..n)
478 .map(|i| {
479 let mut yhat = 0.0;
480 for j in 0..p {
481 yhat += design[(i, j)] * coeffs[j];
482 }
483 yhat
484 })
485 .collect()
486}
487
488fn compute_r_squared(y: &[f64], residuals: &[f64], p_total: usize) -> (f64, f64) {
490 let n = y.len();
491 let y_mean = y.iter().sum::<f64>() / n as f64;
492 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
493 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
494 let r_squared = if ss_tot > 0.0 {
495 1.0 - ss_res / ss_tot
496 } else {
497 0.0
498 };
499 let df_model = (p_total - 1) as f64;
500 let r_squared_adj = if n as f64 - df_model - 1.0 > 0.0 {
501 1.0 - (1.0 - r_squared) * (n as f64 - 1.0) / (n as f64 - df_model - 1.0)
502 } else {
503 r_squared
504 };
505 (r_squared, r_squared_adj)
506}
507
508fn ols_solve(x: &FdMatrix, y: &[f64]) -> Result<(Vec<f64>, Vec<f64>), FdarError> {
515 let (n, p) = x.shape();
516 if n < p || p == 0 {
517 return Err(FdarError::InvalidDimension {
518 parameter: "design matrix",
519 expected: format!("n >= p and p > 0 (p={p})"),
520 actual: format!("n={n}, p={p}"),
521 });
522 }
523 let xtx = compute_xtx(x);
524 let xty = compute_xty(x, y);
525 let l = cholesky_factor(&xtx, p)?;
526 let b = cholesky_forward_back(&l, &xty, p);
527 let hat_diag = compute_hat_diagonal(x, &l);
528 Ok((b, hat_diag))
529}
530
531pub(crate) fn sigmoid(x: f64) -> f64 {
533 if x >= 0.0 {
534 1.0 / (1.0 + (-x).exp())
535 } else {
536 let ex = x.exp();
537 ex / (1.0 + ex)
538 }
539}
540
541impl FregreLmResult {
546 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
548 predict_fregre_lm(self, new_data, new_scalar)
549 }
550}
551
552impl FregreRobustResult {
553 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
555 predict_fregre_robust(self, new_data, new_scalar)
556 }
557}
558
559impl FunctionalLogisticResult {
560 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
562 predict_functional_logistic(self, new_data, new_scalar)
563 }
564}