1use crate::cv::create_folds;
18use crate::error::FdarError;
19use crate::iter_maybe_parallel;
20use crate::matrix::FdMatrix;
21use crate::regression::{fdata_to_pc_1d, FpcaResult};
22use rand::prelude::*;
23
24mod bootstrap;
25mod cv;
26mod fregre_lm;
27mod logistic;
28mod nonparametric;
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};
38
39#[derive(Debug, Clone, PartialEq)]
45pub struct FregreLmResult {
46 pub intercept: f64,
48 pub beta_t: Vec<f64>,
50 pub beta_se: Vec<f64>,
52 pub gamma: Vec<f64>,
54 pub fitted_values: Vec<f64>,
56 pub residuals: Vec<f64>,
58 pub r_squared: f64,
60 pub r_squared_adj: f64,
62 pub std_errors: Vec<f64>,
64 pub ncomp: usize,
66 pub fpca: FpcaResult,
68 pub coefficients: Vec<f64>,
70 pub residual_se: f64,
72 pub gcv: f64,
74 pub aic: f64,
76 pub bic: f64,
78}
79
80#[derive(Debug, Clone, PartialEq)]
82pub struct FregreNpResult {
83 pub fitted_values: Vec<f64>,
85 pub residuals: Vec<f64>,
87 pub r_squared: f64,
89 pub h_func: f64,
91 pub h_scalar: f64,
93 pub cv_error: f64,
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub struct FunctionalLogisticResult {
100 pub intercept: f64,
102 pub beta_t: Vec<f64>,
104 pub beta_se: Vec<f64>,
106 pub gamma: Vec<f64>,
108 pub probabilities: Vec<f64>,
110 pub predicted_classes: Vec<usize>,
112 pub ncomp: usize,
114 pub accuracy: f64,
116 pub std_errors: Vec<f64>,
118 pub coefficients: Vec<f64>,
120 pub log_likelihood: f64,
122 pub iterations: usize,
124 pub fpca: FpcaResult,
126 pub aic: f64,
128 pub bic: f64,
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub struct FregreCvResult {
135 pub k_values: Vec<usize>,
137 pub cv_errors: Vec<f64>,
139 pub optimal_k: usize,
141 pub min_cv_error: f64,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq)]
147pub enum SelectionCriterion {
148 Aic,
150 Bic,
152 Gcv,
154}
155
156#[derive(Debug, Clone, PartialEq)]
158pub struct ModelSelectionResult {
159 pub best_ncomp: usize,
161 pub criteria: Vec<(usize, f64, f64, f64)>,
163}
164
165#[derive(Debug, Clone, PartialEq)]
167pub struct BootstrapCiResult {
168 pub lower: Vec<f64>,
170 pub upper: Vec<f64>,
172 pub center: Vec<f64>,
174 pub sim_lower: Vec<f64>,
176 pub sim_upper: Vec<f64>,
178 pub n_boot_success: usize,
180}
181
182#[derive(Debug, Clone, PartialEq)]
184pub struct FregreBasisCvResult {
185 pub optimal_lambda: f64,
187 pub cv_errors: Vec<f64>,
189 pub cv_se: Vec<f64>,
191 pub lambda_values: Vec<f64>,
193 pub min_cv_error: f64,
195}
196
197#[derive(Debug, Clone, PartialEq)]
199pub struct FregreNpCvResult {
200 pub optimal_h: f64,
202 pub cv_errors: Vec<f64>,
204 pub cv_se: Vec<f64>,
206 pub h_values: Vec<f64>,
208 pub min_cv_error: f64,
210}
211
212pub(crate) fn compute_xtx(x: &FdMatrix) -> Vec<f64> {
218 let (n, p) = x.shape();
219 let mut xtx = vec![0.0; p * p];
220 for k in 0..p {
221 for j in k..p {
222 let mut s = 0.0;
223 for i in 0..n {
224 s += x[(i, k)] * x[(i, j)];
225 }
226 xtx[k * p + j] = s;
227 xtx[j * p + k] = s;
228 }
229 }
230 xtx
231}
232
233fn compute_xty(x: &FdMatrix, y: &[f64]) -> Vec<f64> {
235 let (n, p) = x.shape();
236 (0..p)
237 .map(|k| {
238 let mut s = 0.0;
239 for i in 0..n {
240 s += x[(i, k)] * y[i];
241 }
242 s
243 })
244 .collect()
245}
246
247pub(crate) fn cholesky_factor(a: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
249 let mut l = vec![0.0; p * p];
250 for j in 0..p {
251 let mut diag = a[j * p + j];
252 for k in 0..j {
253 diag -= l[j * p + k] * l[j * p + k];
254 }
255 if diag <= 1e-12 {
256 return Err(FdarError::ComputationFailed {
257 operation: "Cholesky factorization",
258 detail: "matrix is singular or near-singular".into(),
259 });
260 }
261 l[j * p + j] = diag.sqrt();
262 for i in (j + 1)..p {
263 let mut s = a[i * p + j];
264 for k in 0..j {
265 s -= l[i * p + k] * l[j * p + k];
266 }
267 l[i * p + j] = s / l[j * p + j];
268 }
269 }
270 Ok(l)
271}
272
273pub(crate) fn cholesky_forward_back(l: &[f64], b: &[f64], p: usize) -> Vec<f64> {
275 let mut z = b.to_vec();
276 for j in 0..p {
277 for k in 0..j {
278 z[j] -= l[j * p + k] * z[k];
279 }
280 z[j] /= l[j * p + j];
281 }
282 for j in (0..p).rev() {
283 for k in (j + 1)..p {
284 z[j] -= l[k * p + j] * z[k];
285 }
286 z[j] /= l[j * p + j];
287 }
288 z
289}
290
291fn cholesky_solve(a: &[f64], b: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
293 let l = cholesky_factor(a, p)?;
294 Ok(cholesky_forward_back(&l, b, p))
295}
296
297pub(crate) fn compute_hat_diagonal(x: &FdMatrix, l: &[f64]) -> Vec<f64> {
299 let (n, p) = x.shape();
300 let mut hat_diag = vec![0.0; n];
301 for i in 0..n {
302 let mut v = vec![0.0; p];
303 for j in 0..p {
304 v[j] = x[(i, j)];
305 for k in 0..j {
306 v[j] -= l[j * p + k] * v[k];
307 }
308 v[j] /= l[j * p + j];
309 }
310 hat_diag[i] = v.iter().map(|vi| vi * vi).sum();
311 }
312 hat_diag
313}
314
315fn compute_ols_std_errors(l: &[f64], p: usize, sigma2: f64) -> Vec<f64> {
317 let mut se = vec![0.0; p];
318 for j in 0..p {
319 let mut v = vec![0.0; p];
320 v[j] = 1.0;
321 for k in 0..p {
322 for kk in 0..k {
323 v[k] -= l[k * p + kk] * v[kk];
324 }
325 v[k] /= l[k * p + k];
326 }
327 se[j] = (sigma2 * v.iter().map(|vi| vi * vi).sum::<f64>()).sqrt();
328 }
329 se
330}
331
332fn validate_fregre_inputs(
339 n: usize,
340 m: usize,
341 y: &[f64],
342 scalar_covariates: Option<&FdMatrix>,
343) -> Result<(), FdarError> {
344 if n < 3 {
345 return Err(FdarError::InvalidDimension {
346 parameter: "data",
347 expected: "at least 3 rows".to_string(),
348 actual: format!("{n}"),
349 });
350 }
351 if m == 0 {
352 return Err(FdarError::InvalidDimension {
353 parameter: "data",
354 expected: "at least 1 column".to_string(),
355 actual: "0".to_string(),
356 });
357 }
358 if y.len() != n {
359 return Err(FdarError::InvalidDimension {
360 parameter: "y",
361 expected: format!("{n}"),
362 actual: format!("{}", y.len()),
363 });
364 }
365 if let Some(sc) = scalar_covariates {
366 if sc.nrows() != n {
367 return Err(FdarError::InvalidDimension {
368 parameter: "scalar_covariates",
369 expected: format!("{n} rows"),
370 actual: format!("{} rows", sc.nrows()),
371 });
372 }
373 }
374 Ok(())
375}
376
377fn resolve_ncomp(
379 ncomp: usize,
380 data: &FdMatrix,
381 y: &[f64],
382 scalar_covariates: Option<&FdMatrix>,
383 n: usize,
384 m: usize,
385) -> Result<usize, FdarError> {
386 if ncomp == 0 {
387 let cv = fregre_cv(data, y, scalar_covariates, 1, m.min(n - 1).min(20), 5)?;
388 Ok(cv.optimal_k)
389 } else {
390 Ok(ncomp.min(n - 1).min(m))
391 }
392}
393
394pub(crate) fn build_design_matrix(
395 fpca_scores: &FdMatrix,
396 ncomp: usize,
397 scalar_covariates: Option<&FdMatrix>,
398 n: usize,
399) -> FdMatrix {
400 let p_scalar = scalar_covariates.map_or(0, super::matrix::FdMatrix::ncols);
401 let p_total = 1 + ncomp + p_scalar;
402 let mut design = FdMatrix::zeros(n, p_total);
403 for i in 0..n {
404 design[(i, 0)] = 1.0;
405 for k in 0..ncomp {
406 design[(i, 1 + k)] = fpca_scores[(i, k)];
407 }
408 if let Some(sc) = scalar_covariates {
409 for j in 0..p_scalar {
410 design[(i, 1 + ncomp + j)] = sc[(i, j)];
411 }
412 }
413 }
414 design
415}
416
417fn recover_beta_t(fpc_coeffs: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
419 let ncomp = fpc_coeffs.len();
420 let mut beta_t = vec![0.0; m];
421 for k in 0..ncomp {
422 for j in 0..m {
423 beta_t[j] += fpc_coeffs[k] * rotation[(j, k)];
424 }
425 }
426 beta_t
427}
428
429fn compute_beta_se(gamma_se: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
433 let ncomp = gamma_se.len();
434 let mut beta_se = vec![0.0; m];
435 for j in 0..m {
436 let mut var_j = 0.0;
437 for k in 0..ncomp {
438 var_j += rotation[(j, k)].powi(2) * gamma_se[k].powi(2);
439 }
440 beta_se[j] = var_j.sqrt();
441 }
442 beta_se
443}
444
445fn compute_fitted(design: &FdMatrix, coeffs: &[f64]) -> Vec<f64> {
447 let (n, p) = design.shape();
448 (0..n)
449 .map(|i| {
450 let mut yhat = 0.0;
451 for j in 0..p {
452 yhat += design[(i, j)] * coeffs[j];
453 }
454 yhat
455 })
456 .collect()
457}
458
459fn compute_r_squared(y: &[f64], residuals: &[f64], p_total: usize) -> (f64, f64) {
461 let n = y.len();
462 let y_mean = y.iter().sum::<f64>() / n as f64;
463 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
464 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
465 let r_squared = if ss_tot > 0.0 {
466 1.0 - ss_res / ss_tot
467 } else {
468 0.0
469 };
470 let df_model = (p_total - 1) as f64;
471 let r_squared_adj = if n as f64 - df_model - 1.0 > 0.0 {
472 1.0 - (1.0 - r_squared) * (n as f64 - 1.0) / (n as f64 - df_model - 1.0)
473 } else {
474 r_squared
475 };
476 (r_squared, r_squared_adj)
477}
478
479fn ols_solve(x: &FdMatrix, y: &[f64]) -> Result<(Vec<f64>, Vec<f64>), FdarError> {
486 let (n, p) = x.shape();
487 if n < p || p == 0 {
488 return Err(FdarError::InvalidDimension {
489 parameter: "design matrix",
490 expected: format!("n >= p and p > 0 (p={p})"),
491 actual: format!("n={n}, p={p}"),
492 });
493 }
494 let xtx = compute_xtx(x);
495 let xty = compute_xty(x, y);
496 let l = cholesky_factor(&xtx, p)?;
497 let b = cholesky_forward_back(&l, &xty, p);
498 let hat_diag = compute_hat_diagonal(x, &l);
499 Ok((b, hat_diag))
500}
501
502pub(crate) fn sigmoid(x: f64) -> f64 {
504 if x >= 0.0 {
505 1.0 / (1.0 + (-x).exp())
506 } else {
507 let ex = x.exp();
508 ex / (1.0 + ex)
509 }
510}
511
512impl FregreLmResult {
517 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
519 predict_fregre_lm(self, new_data, new_scalar)
520 }
521}
522
523impl FunctionalLogisticResult {
524 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
526 predict_functional_logistic(self, new_data, new_scalar)
527 }
528}