1use crate::error::FdarError;
20use crate::linalg::cholesky_solve as linalg_cholesky_solve;
21use crate::matrix::FdMatrix;
22use crate::regression::{FpcaResult, PlsResult};
23
24mod bootstrap;
25mod cv;
26mod fregre_lm;
27mod logistic;
28mod nonparametric;
29mod pls;
30mod robust;
31#[cfg(test)]
32mod tests;
33
34pub use bootstrap::{bootstrap_ci_fregre_lm, bootstrap_ci_functional_logistic};
36pub use cv::{fregre_basis_cv, fregre_np_cv};
37pub use fregre_lm::{fregre_cv, fregre_lm, model_selection_ncomp, predict_fregre_lm};
38pub use logistic::{functional_logistic, predict_functional_logistic};
39pub use nonparametric::{
40 fregre_np_from_distances, fregre_np_mixed, predict_fregre_np, predict_fregre_np_from_distances,
41};
42pub use pls::{fregre_pls, predict_fregre_pls};
43pub use robust::{fregre_huber, fregre_l1, predict_fregre_robust};
44
45#[derive(Debug, Clone, PartialEq)]
51#[non_exhaustive]
52pub struct FregreLmResult {
53 pub intercept: f64,
55 pub beta_t: Vec<f64>,
57 pub beta_se: Vec<f64>,
59 pub gamma: Vec<f64>,
61 pub fitted_values: Vec<f64>,
63 pub residuals: Vec<f64>,
65 pub r_squared: f64,
67 pub r_squared_adj: f64,
69 pub std_errors: Vec<f64>,
71 pub ncomp: usize,
73 pub fpca: FpcaResult,
75 pub coefficients: Vec<f64>,
77 pub residual_se: f64,
79 pub gcv: f64,
81 pub aic: f64,
83 pub bic: f64,
85}
86
87#[derive(Debug, Clone, PartialEq)]
89#[non_exhaustive]
90pub struct FregreNpResult {
91 pub fitted_values: Vec<f64>,
93 pub residuals: Vec<f64>,
95 pub r_squared: f64,
97 pub h_func: f64,
99 pub h_scalar: f64,
101 pub cv_error: f64,
103}
104
105#[derive(Debug, Clone, PartialEq)]
107#[non_exhaustive]
108pub struct FregreRobustResult {
109 pub intercept: f64,
111 pub beta_t: Vec<f64>,
113 pub fitted_values: Vec<f64>,
115 pub residuals: Vec<f64>,
117 pub coefficients: Vec<f64>,
119 pub ncomp: usize,
121 pub fpca: FpcaResult,
123 pub iterations: usize,
125 pub converged: bool,
127 pub weights: Vec<f64>,
129 pub r_squared: f64,
131}
132
133#[derive(Debug, Clone, PartialEq)]
135#[non_exhaustive]
136pub struct FunctionalLogisticResult {
137 pub intercept: f64,
139 pub beta_t: Vec<f64>,
141 pub beta_se: Vec<f64>,
143 pub gamma: Vec<f64>,
145 pub probabilities: Vec<f64>,
147 pub predicted_classes: Vec<usize>,
149 pub ncomp: usize,
151 pub accuracy: f64,
153 pub std_errors: Vec<f64>,
155 pub coefficients: Vec<f64>,
157 pub log_likelihood: f64,
159 pub iterations: usize,
161 pub fpca: FpcaResult,
163 pub aic: f64,
165 pub bic: f64,
167}
168
169#[derive(Debug, Clone, PartialEq)]
171#[non_exhaustive]
172pub struct FregreCvResult {
173 pub k_values: Vec<usize>,
175 pub cv_errors: Vec<f64>,
177 pub optimal_k: usize,
179 pub min_cv_error: f64,
181 pub oof_predictions: Vec<f64>,
183 pub fold_assignments: Vec<usize>,
185 pub fold_errors: Vec<f64>,
187}
188
189#[derive(Debug, Clone, PartialEq)]
191#[non_exhaustive]
192pub struct PlsRegressionResult {
193 pub intercept: f64,
195 pub beta_t: Vec<f64>,
197 pub gamma: Vec<f64>,
199 pub fitted_values: Vec<f64>,
201 pub residuals: Vec<f64>,
203 pub r_squared: f64,
205 pub r_squared_adj: f64,
207 pub ncomp: usize,
209 pub pls: PlsResult,
211 pub coefficients: Vec<f64>,
213 pub residual_se: f64,
215 pub aic: f64,
217 pub bic: f64,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq)]
223pub enum SelectionCriterion {
224 Aic,
226 Bic,
228 Gcv,
230}
231
232#[derive(Debug, Clone, PartialEq)]
234#[non_exhaustive]
235pub struct ModelSelectionResult {
236 pub best_ncomp: usize,
238 pub criteria: Vec<(usize, f64, f64, f64)>,
240}
241
242#[derive(Debug, Clone, PartialEq)]
244#[non_exhaustive]
245pub struct BootstrapCiResult {
246 pub lower: Vec<f64>,
248 pub upper: Vec<f64>,
250 pub center: Vec<f64>,
252 pub sim_lower: Vec<f64>,
254 pub sim_upper: Vec<f64>,
256 pub n_boot_success: usize,
258}
259
260#[derive(Debug, Clone, PartialEq)]
262#[non_exhaustive]
263pub struct FregreBasisCvResult {
264 pub optimal_lambda: f64,
266 pub cv_errors: Vec<f64>,
268 pub cv_se: Vec<f64>,
270 pub lambda_values: Vec<f64>,
272 pub min_cv_error: f64,
274}
275
276#[derive(Debug, Clone, PartialEq)]
278#[non_exhaustive]
279pub struct FregreNpCvResult {
280 pub optimal_h: f64,
282 pub cv_errors: Vec<f64>,
284 pub cv_se: Vec<f64>,
286 pub h_values: Vec<f64>,
288 pub min_cv_error: f64,
290}
291
292pub(crate) use crate::linalg::cholesky_factor;
299pub(crate) use crate::linalg::cholesky_forward_back;
300pub(crate) use crate::linalg::compute_xtx;
301
302fn compute_xty(x: &FdMatrix, y: &[f64]) -> Vec<f64> {
304 let (n, p) = x.shape();
305 (0..p)
306 .map(|k| {
307 let mut s = 0.0;
308 for i in 0..n {
309 s += x[(i, k)] * y[i];
310 }
311 s
312 })
313 .collect()
314}
315
316pub(super) fn cholesky_solve(a: &[f64], b: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
318 linalg_cholesky_solve(a, b, p)
319}
320
321pub(crate) fn compute_hat_diagonal(x: &FdMatrix, l: &[f64]) -> Vec<f64> {
323 let (n, p) = x.shape();
324 let mut hat_diag = vec![0.0; n];
325 for i in 0..n {
326 let mut v = vec![0.0; p];
327 for j in 0..p {
328 v[j] = x[(i, j)];
329 for k in 0..j {
330 v[j] -= l[j * p + k] * v[k];
331 }
332 v[j] /= l[j * p + j];
333 }
334 hat_diag[i] = v.iter().map(|vi| vi * vi).sum();
335 }
336 hat_diag
337}
338
339fn compute_ols_std_errors(l: &[f64], p: usize, sigma2: f64) -> Vec<f64> {
341 let mut se = vec![0.0; p];
342 for j in 0..p {
343 let mut v = vec![0.0; p];
344 v[j] = 1.0;
345 for k in 0..p {
346 for kk in 0..k {
347 v[k] -= l[k * p + kk] * v[kk];
348 }
349 v[k] /= l[k * p + k];
350 }
351 se[j] = (sigma2 * v.iter().map(|vi| vi * vi).sum::<f64>()).sqrt();
352 }
353 se
354}
355
356fn validate_fregre_inputs(
363 n: usize,
364 m: usize,
365 y: &[f64],
366 scalar_covariates: Option<&FdMatrix>,
367) -> Result<(), FdarError> {
368 if n < 3 {
369 return Err(FdarError::InvalidDimension {
370 parameter: "data",
371 expected: "at least 3 rows".to_string(),
372 actual: format!("{n}"),
373 });
374 }
375 if m == 0 {
376 return Err(FdarError::InvalidDimension {
377 parameter: "data",
378 expected: "at least 1 column".to_string(),
379 actual: "0".to_string(),
380 });
381 }
382 if y.len() != n {
383 return Err(FdarError::InvalidDimension {
384 parameter: "y",
385 expected: format!("{n}"),
386 actual: format!("{}", y.len()),
387 });
388 }
389 if let Some(sc) = scalar_covariates {
390 if sc.nrows() != n {
391 return Err(FdarError::InvalidDimension {
392 parameter: "scalar_covariates",
393 expected: format!("{n} rows"),
394 actual: format!("{} rows", sc.nrows()),
395 });
396 }
397 }
398 Ok(())
399}
400
401fn resolve_ncomp(
403 ncomp: usize,
404 data: &FdMatrix,
405 y: &[f64],
406 scalar_covariates: Option<&FdMatrix>,
407 n: usize,
408 m: usize,
409) -> Result<usize, FdarError> {
410 if ncomp == 0 {
411 let cv = fregre_cv(data, y, scalar_covariates, 1, m.min(n - 1).min(20), 5)?;
412 Ok(cv.optimal_k)
413 } else {
414 Ok(ncomp.min(n - 1).min(m))
415 }
416}
417
418pub(crate) fn build_design_matrix(
419 fpca_scores: &FdMatrix,
420 ncomp: usize,
421 scalar_covariates: Option<&FdMatrix>,
422 n: usize,
423) -> FdMatrix {
424 let p_scalar = scalar_covariates.map_or(0, super::matrix::FdMatrix::ncols);
425 let p_total = 1 + ncomp + p_scalar;
426 let mut design = FdMatrix::zeros(n, p_total);
427 for i in 0..n {
428 design[(i, 0)] = 1.0;
429 for k in 0..ncomp {
430 design[(i, 1 + k)] = fpca_scores[(i, k)];
431 }
432 if let Some(sc) = scalar_covariates {
433 for j in 0..p_scalar {
434 design[(i, 1 + ncomp + j)] = sc[(i, j)];
435 }
436 }
437 }
438 design
439}
440
441fn recover_beta_t(fpc_coeffs: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
443 let ncomp = fpc_coeffs.len();
444 let mut beta_t = vec![0.0; m];
445 for k in 0..ncomp {
446 for j in 0..m {
447 beta_t[j] += fpc_coeffs[k] * rotation[(j, k)];
448 }
449 }
450 beta_t
451}
452
453fn compute_beta_se(gamma_se: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
457 let ncomp = gamma_se.len();
458 let mut beta_se = vec![0.0; m];
459 for j in 0..m {
460 let mut var_j = 0.0;
461 for k in 0..ncomp {
462 var_j += rotation[(j, k)].powi(2) * gamma_se[k].powi(2);
463 }
464 beta_se[j] = var_j.sqrt();
465 }
466 beta_se
467}
468
469fn compute_fitted(design: &FdMatrix, coeffs: &[f64]) -> Vec<f64> {
471 let (n, p) = design.shape();
472 (0..n)
473 .map(|i| {
474 let mut yhat = 0.0;
475 for j in 0..p {
476 yhat += design[(i, j)] * coeffs[j];
477 }
478 yhat
479 })
480 .collect()
481}
482
483fn compute_r_squared(y: &[f64], residuals: &[f64], p_total: usize) -> (f64, f64) {
485 let n = y.len();
486 let y_mean = y.iter().sum::<f64>() / n as f64;
487 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
488 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
489 let r_squared = if ss_tot > 0.0 {
490 1.0 - ss_res / ss_tot
491 } else {
492 0.0
493 };
494 let df_model = (p_total - 1) as f64;
495 let r_squared_adj = if n as f64 - df_model - 1.0 > 0.0 {
496 1.0 - (1.0 - r_squared) * (n as f64 - 1.0) / (n as f64 - df_model - 1.0)
497 } else {
498 r_squared
499 };
500 (r_squared, r_squared_adj)
501}
502
503fn ols_solve(x: &FdMatrix, y: &[f64]) -> Result<(Vec<f64>, Vec<f64>), FdarError> {
510 let (n, p) = x.shape();
511 if n < p || p == 0 {
512 return Err(FdarError::InvalidDimension {
513 parameter: "design matrix",
514 expected: format!("n >= p and p > 0 (p={p})"),
515 actual: format!("n={n}, p={p}"),
516 });
517 }
518 let xtx = compute_xtx(x);
519 let xty = compute_xty(x, y);
520 let l = cholesky_factor(&xtx, p)?;
521 let b = cholesky_forward_back(&l, &xty, p);
522 let hat_diag = compute_hat_diagonal(x, &l);
523 Ok((b, hat_diag))
524}
525
526pub(crate) fn sigmoid(x: f64) -> f64 {
528 if x >= 0.0 {
529 1.0 / (1.0 + (-x).exp())
530 } else {
531 let ex = x.exp();
532 ex / (1.0 + ex)
533 }
534}
535
536impl FregreLmResult {
541 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
543 predict_fregre_lm(self, new_data, new_scalar)
544 }
545}
546
547impl FregreRobustResult {
548 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
550 predict_fregre_robust(self, new_data, new_scalar)
551 }
552}
553
554impl FunctionalLogisticResult {
555 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
557 predict_functional_logistic(self, new_data, new_scalar)
558 }
559}