1use crate::error::FdarError;
17use crate::matrix::FdMatrix;
18use crate::regression::{fdata_to_pc_1d, FpcaResult};
19
20use super::{compute_fitted, compute_r_squared, ols_solve, MultiFregreLmResult};
21
22#[must_use = "expensive computation whose result should not be discarded"]
78pub fn fregre_lm_multi(
79 predictors: &[(&FdMatrix, &[f64], usize)],
80 y: &[f64],
81 scalar_covariates: Option<&FdMatrix>,
82) -> Result<MultiFregreLmResult, FdarError> {
83 if predictors.is_empty() {
85 return Err(FdarError::InvalidParameter {
86 parameter: "predictors",
87 message: "at least one functional predictor is required".to_string(),
88 });
89 }
90
91 let n = predictors[0].0.nrows();
92 if n < 3 {
93 return Err(FdarError::InvalidDimension {
94 parameter: "predictors",
95 expected: "at least 3 observations".to_string(),
96 actual: format!("{n}"),
97 });
98 }
99 if y.len() != n {
100 return Err(FdarError::InvalidDimension {
101 parameter: "y",
102 expected: format!("{n}"),
103 actual: format!("{}", y.len()),
104 });
105 }
106
107 for (k, &(data_k, argvals_k, _)) in predictors.iter().enumerate() {
108 if data_k.nrows() != n {
109 return Err(FdarError::InvalidDimension {
110 parameter: "predictors",
111 expected: format!("{n} rows for predictor {k}"),
112 actual: format!("{} rows", data_k.nrows()),
113 });
114 }
115 if argvals_k.len() != data_k.ncols() {
116 return Err(FdarError::InvalidDimension {
117 parameter: "argvals",
118 expected: format!("{} elements for predictor {k}", data_k.ncols()),
119 actual: format!("{} elements", argvals_k.len()),
120 });
121 }
122 }
123
124 if let Some(sc) = scalar_covariates {
125 if sc.nrows() != n {
126 return Err(FdarError::InvalidDimension {
127 parameter: "scalar_covariates",
128 expected: format!("{n} rows"),
129 actual: format!("{} rows", sc.nrows()),
130 });
131 }
132 }
133
134 let k_preds = predictors.len();
136 let mut fpcas: Vec<FpcaResult> = Vec::with_capacity(k_preds);
137 let mut ncomp_vec: Vec<usize> = Vec::with_capacity(k_preds);
138
139 for &(data_k, argvals_k, ncomp_k) in predictors {
140 let nc = ncomp_k.max(1).min(n - 1).min(data_k.ncols());
141 let fpca = fdata_to_pc_1d(data_k, nc, argvals_k)?;
142 ncomp_vec.push(nc);
143 fpcas.push(fpca);
144 }
145
146 let total_scores: usize = ncomp_vec.iter().sum();
147 let p_scalar = scalar_covariates.map_or(0, FdMatrix::ncols);
148 let p_total = 1 + total_scores + p_scalar;
149
150 let mut design = FdMatrix::zeros(n, p_total);
155 for i in 0..n {
156 design[(i, 0)] = 1.0;
157 }
158
159 let mut col_offset = 1;
160 for (idx, fpca) in fpcas.iter().enumerate() {
161 let nc = ncomp_vec[idx];
162 let m_k = fpca.mean.len();
163 let data_k = predictors[idx].0;
164 for i in 0..n {
165 for k in 0..nc {
166 let mut s = 0.0;
167 for j in 0..m_k {
168 s += (data_k[(i, j)] - fpca.mean[j]) * fpca.rotation[(j, k)] * fpca.weights[j];
169 }
170 design[(i, col_offset + k)] = s;
171 }
172 }
173 col_offset += nc;
174 }
175
176 if let Some(sc) = scalar_covariates {
177 for i in 0..n {
178 for j in 0..p_scalar {
179 design[(i, col_offset + j)] = sc[(i, j)];
180 }
181 }
182 }
183
184 let (coeffs, _hat_diag) = ols_solve(&design, y)?;
186
187 let fitted_values = compute_fitted(&design, &coeffs);
188 let residuals: Vec<f64> = y
189 .iter()
190 .zip(&fitted_values)
191 .map(|(&yi, &yh)| yi - yh)
192 .collect();
193 let (r_squared, r_squared_adj) = compute_r_squared(y, &residuals, p_total);
194
195 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
196 let df_resid = (n as f64 - p_total as f64).max(1.0);
197 let residual_se = (ss_res / df_resid).sqrt();
198
199 let nf = n as f64;
200 let aic = nf * (ss_res / nf).ln() + 2.0 * p_total as f64;
201 let bic = nf * (ss_res / nf).ln() + nf.ln() * p_total as f64;
202
203 let mut beta_t: Vec<Vec<f64>> = Vec::with_capacity(k_preds);
205 let mut coeff_offset = 1;
206 for (idx, fpca) in fpcas.iter().enumerate() {
207 let nc = ncomp_vec[idx];
208 let m_k = fpca.rotation.nrows();
209 let fpc_coeffs = &coeffs[coeff_offset..coeff_offset + nc];
210 let mut beta_k = vec![0.0; m_k];
211 for k in 0..nc {
212 for j in 0..m_k {
213 beta_k[j] += fpc_coeffs[k] * fpca.rotation[(j, k)];
214 }
215 }
216 beta_t.push(beta_k);
217 coeff_offset += nc;
218 }
219
220 let gamma: Vec<f64> = coeffs[1 + total_scores..].to_vec();
222
223 Ok(MultiFregreLmResult {
224 intercept: coeffs[0],
225 beta_t,
226 gamma,
227 fitted_values,
228 residuals,
229 r_squared,
230 r_squared_adj,
231 ncomp: ncomp_vec,
232 fpcas,
233 coefficients: coeffs,
234 residual_se,
235 aic,
236 bic,
237 })
238}
239
240pub fn predict_fregre_lm_multi(
288 fit: &MultiFregreLmResult,
289 new_predictors: &[&FdMatrix],
290 new_scalar: Option<&FdMatrix>,
291) -> Result<Vec<f64>, FdarError> {
292 let k_preds = fit.fpcas.len();
293 if new_predictors.len() != k_preds {
294 return Err(FdarError::InvalidDimension {
295 parameter: "new_predictors",
296 expected: format!("{k_preds} predictors"),
297 actual: format!("{} predictors", new_predictors.len()),
298 });
299 }
300
301 let n_new = new_predictors[0].nrows();
302 for (idx, &pred) in new_predictors.iter().enumerate() {
303 if pred.nrows() != n_new {
304 return Err(FdarError::InvalidDimension {
305 parameter: "new_predictors",
306 expected: format!("{n_new} rows for predictor {idx}"),
307 actual: format!("{} rows", pred.nrows()),
308 });
309 }
310 }
311
312 if let Some(sc) = new_scalar {
313 if sc.nrows() != n_new {
314 return Err(FdarError::InvalidDimension {
315 parameter: "new_scalar",
316 expected: format!("{n_new} rows"),
317 actual: format!("{} rows", sc.nrows()),
318 });
319 }
320 }
321
322 let p_scalar = fit.gamma.len();
326
327 let mut predictions = vec![0.0; n_new];
328 for i in 0..n_new {
329 let mut yhat = fit.intercept;
330
331 let mut coeff_offset = 1;
333 for (idx, &pred) in new_predictors.iter().enumerate() {
334 let fpca = &fit.fpcas[idx];
335 let nc = fit.ncomp[idx];
336 let m_k = fpca.mean.len();
337 for k in 0..nc {
338 let mut s = 0.0;
339 for j in 0..m_k {
340 s += (pred[(i, j)] - fpca.mean[j]) * fpca.rotation[(j, k)] * fpca.weights[j];
341 }
342 yhat += fit.coefficients[coeff_offset + k] * s;
343 }
344 coeff_offset += nc;
345 }
346
347 if let Some(sc) = new_scalar {
349 for j in 0..p_scalar {
350 yhat += fit.gamma[j] * sc[(i, j)];
351 }
352 }
353
354 predictions[i] = yhat;
355 }
356
357 Ok(predictions)
358}
359
360#[must_use = "expensive computation whose result should not be discarded"]
379pub fn fregre_lm_multi_cv(
380 predictors: &[(&FdMatrix, &[f64])],
381 y: &[f64],
382 scalar_covariates: Option<&FdMatrix>,
383 ncomp_max: usize,
384 n_folds: usize,
385 seed: u64,
386) -> Result<MultiCvResult, FdarError> {
387 if predictors.is_empty() {
388 return Err(FdarError::InvalidParameter {
389 parameter: "predictors",
390 message: "need at least one functional predictor".into(),
391 });
392 }
393 let n = predictors[0].0.nrows();
394 if n < n_folds {
395 return Err(FdarError::InvalidDimension {
396 parameter: "data",
397 expected: format!("at least {n_folds} rows"),
398 actual: format!("{n}"),
399 });
400 }
401
402 let folds = crate::cv::create_folds(n, n_folds, seed);
403 let ncomp_max = ncomp_max.min(n - 2);
404 let k_preds = predictors.len();
405
406 let mut candidates = Vec::new();
407 let mut cv_errors = Vec::new();
408 let mut best_ncomp = 1;
409 let mut best_mse = f64::INFINITY;
410 let mut best_oof = vec![f64::NAN; n];
411
412 for nc in 1..=ncomp_max {
413 let mut oof_preds = vec![f64::NAN; n];
414 let mut total_se = 0.0;
415 let mut count = 0;
416
417 for fold in 0..n_folds {
418 let train_idx: Vec<usize> = (0..n).filter(|&i| folds[i] != fold).collect();
419 let test_idx: Vec<usize> = (0..n).filter(|&i| folds[i] == fold).collect();
420 let n_train = train_idx.len();
421 let n_test = test_idx.len();
422 if n_test == 0 || n_train < nc + 2 {
423 continue;
424 }
425
426 let mut train_preds: Vec<(FdMatrix, Vec<f64>, usize)> = Vec::with_capacity(k_preds);
428 let mut test_preds: Vec<FdMatrix> = Vec::with_capacity(k_preds);
429
430 for &(data_k, argvals_k) in predictors {
431 let train_k = data_k.select_rows(&train_idx);
432 let test_k = data_k.select_rows(&test_idx);
433 train_preds.push((train_k, argvals_k.to_vec(), nc));
434 test_preds.push(test_k);
435 }
436
437 let train_y: Vec<f64> = train_idx.iter().map(|&i| y[i]).collect();
438 let train_sc = scalar_covariates.map(|sc| sc.select_rows(&train_idx));
439 let test_sc = scalar_covariates.map(|sc| sc.select_rows(&test_idx));
440
441 let pred_refs: Vec<(&FdMatrix, &[f64], usize)> = train_preds
442 .iter()
443 .map(|(d, a, c)| (d, a.as_slice(), *c))
444 .collect();
445
446 let Ok(fit) = fregre_lm_multi(&pred_refs, &train_y, train_sc.as_ref()) else {
447 continue;
448 };
449
450 let test_pred_refs: Vec<&FdMatrix> = test_preds.iter().collect();
451 let Ok(preds) = predict_fregre_lm_multi(&fit, &test_pred_refs, test_sc.as_ref()) else {
452 continue;
453 };
454
455 for (ti, &i) in test_idx.iter().enumerate() {
456 oof_preds[i] = preds[ti];
457 total_se += (y[i] - preds[ti]).powi(2);
458 count += 1;
459 }
460 }
461
462 let mse = if count > 0 {
463 total_se / count as f64
464 } else {
465 f64::INFINITY
466 };
467
468 candidates.push(nc);
469 cv_errors.push(mse);
470
471 if mse < best_mse {
472 best_mse = mse;
473 best_ncomp = nc;
474 best_oof = oof_preds;
475 }
476 }
477
478 if candidates.is_empty() {
479 return Err(FdarError::ComputationFailed {
480 operation: "fregre_lm_multi_cv",
481 detail: "no valid ncomp produced CV errors".into(),
482 });
483 }
484
485 Ok(MultiCvResult {
486 candidates,
487 cv_errors,
488 optimal_ncomp: best_ncomp,
489 min_cv_mse: best_mse,
490 oof_predictions: best_oof,
491 })
492}
493
494#[derive(Debug, Clone, PartialEq)]
496#[non_exhaustive]
497pub struct MultiCvResult {
498 pub candidates: Vec<usize>,
500 pub cv_errors: Vec<f64>,
502 pub optimal_ncomp: usize,
504 pub min_cv_mse: f64,
506 pub oof_predictions: Vec<f64>,
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use std::f64::consts::PI;
514
515 fn make_multi_data(
519 n: usize,
520 m1: usize,
521 m2: usize,
522 seed: u64,
523 ) -> (FdMatrix, FdMatrix, Vec<f64>, Vec<f64>, Vec<f64>) {
524 let t1: Vec<f64> = (0..m1).map(|j| j as f64 / (m1 - 1).max(1) as f64).collect();
525 let t2: Vec<f64> = (0..m2).map(|j| j as f64 / (m2 - 1).max(1) as f64).collect();
526 let mut x1 = FdMatrix::zeros(n, m1);
527 let mut x2 = FdMatrix::zeros(n, m2);
528 let mut y = vec![0.0; n];
529
530 for i in 0..n {
531 let a1 =
533 ((seed.wrapping_mul(17).wrapping_add(i as u64 * 31) % 1000) as f64 / 500.0) - 1.0;
534 let b1 =
535 ((seed.wrapping_mul(7).wrapping_add(i as u64 * 53) % 1000) as f64 / 500.0) - 1.0;
536 let c1 =
537 ((seed.wrapping_mul(3).wrapping_add(i as u64 * 79) % 1000) as f64 / 500.0) - 1.0;
538
539 let a2 =
540 ((seed.wrapping_mul(11).wrapping_add(i as u64 * 43) % 1000) as f64 / 500.0) - 1.0;
541 let b2 =
542 ((seed.wrapping_mul(23).wrapping_add(i as u64 * 67) % 1000) as f64 / 500.0) - 1.0;
543
544 for j in 0..m1 {
546 x1[(i, j)] =
547 a1 * (2.0 * PI * t1[j]).sin() + b1 * (4.0 * PI * t1[j]).cos() + c1 * t1[j];
548 }
549 for j in 0..m2 {
551 x2[(i, j)] = a2 * (2.0 * PI * t2[j]).cos() + b2 * (6.0 * PI * t2[j]).sin();
552 }
553 y[i] = 2.0 * a1 - 1.5 * b1
554 + 0.8 * a2
555 + 0.3 * b2
556 + 0.05 * (seed.wrapping_add(i as u64) % 10) as f64;
557 }
558 (x1, x2, y, t1, t2)
559 }
560
561 #[test]
562 fn test_fregre_lm_multi_two_predictors() {
563 let (x1, x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
564 let fit = fregre_lm_multi(&[(&x1, &t1, 3), (&x2, &t2, 2)], &y, None).unwrap();
565
566 assert_eq!(fit.fitted_values.len(), 30);
567 assert_eq!(fit.residuals.len(), 30);
568 assert_eq!(fit.beta_t.len(), 2);
569 assert_eq!(fit.beta_t[0].len(), 40);
570 assert_eq!(fit.beta_t[1].len(), 25);
571 assert_eq!(fit.ncomp, vec![3, 2]);
572 assert!(fit.r_squared >= 0.0);
573 assert!(fit.r_squared <= 1.0 + 1e-10);
574 }
575
576 #[test]
577 fn test_fregre_lm_multi_with_scalar() {
578 let (x1, x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
579 let mut sc = FdMatrix::zeros(30, 2);
580 for i in 0..30 {
581 sc[(i, 0)] = i as f64 / 30.0;
582 sc[(i, 1)] = (i as f64 * 0.7).sin();
583 }
584 let fit = fregre_lm_multi(&[(&x1, &t1, 3), (&x2, &t2, 2)], &y, Some(&sc)).unwrap();
585
586 assert_eq!(fit.gamma.len(), 2);
587 assert!(fit.r_squared >= 0.0);
588 }
589
590 #[test]
591 fn test_predict_multi_on_training() {
592 let (x1, x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
593 let fit = fregre_lm_multi(&[(&x1, &t1, 3), (&x2, &t2, 2)], &y, None).unwrap();
594
595 let preds = predict_fregre_lm_multi(&fit, &[&x1, &x2], None).unwrap();
596 assert_eq!(preds.len(), 30);
597 for i in 0..30 {
598 assert!(
599 (preds[i] - fit.fitted_values[i]).abs() < 1e-6,
600 "prediction on training data should match fitted values at index {i}: got {}, expected {}",
601 preds[i], fit.fitted_values[i]
602 );
603 }
604 }
605
606 #[test]
607 fn test_predict_multi_new_data_finite() {
608 let (x1, x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
609 let fit = fregre_lm_multi(&[(&x1, &t1, 3), (&x2, &t2, 2)], &y, None).unwrap();
610
611 let n_new = 10;
613 let mut new_x1 = FdMatrix::zeros(n_new, 40);
614 let mut new_x2 = FdMatrix::zeros(n_new, 25);
615 for i in 0..n_new {
616 let p = (i as f64 + 0.5) * PI / n_new as f64;
617 for j in 0..40 {
618 new_x1[(i, j)] = (2.0 * PI * t1[j] + p).sin() + 0.1;
619 }
620 for j in 0..25 {
621 new_x2[(i, j)] = (2.0 * PI * t2[j] + p).cos() - 0.1;
622 }
623 }
624
625 let preds = predict_fregre_lm_multi(&fit, &[&new_x1, &new_x2], None).unwrap();
626 assert_eq!(preds.len(), n_new);
627 for &p in &preds {
628 assert!(p.is_finite(), "predictions should be finite, got {p}");
629 }
630 }
631
632 #[test]
633 fn test_fregre_lm_multi_mismatched_n() {
634 let (x1, _x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
635 let x2_bad = FdMatrix::zeros(20, 25);
636 let result = fregre_lm_multi(&[(&x1, &t1, 3), (&x2_bad, &t2, 2)], &y, None);
637 assert!(result.is_err());
638 }
639
640 #[test]
641 fn test_fregre_lm_multi_single_predictor_matches_fregre_lm() {
642 let n = 30;
643 let m = 50;
644 let t: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1) as f64).collect();
645 let mut data = FdMatrix::zeros(n, m);
646 let mut y = vec![0.0; n];
647 for i in 0..n {
648 let a =
651 ((42_u64.wrapping_mul(17).wrapping_add(i as u64 * 31) % 1000) as f64 / 500.0) - 1.0;
652 let b =
653 ((42_u64.wrapping_mul(7).wrapping_add(i as u64 * 53) % 1000) as f64 / 500.0) - 1.0;
654 let c =
655 ((42_u64.wrapping_mul(3).wrapping_add(i as u64 * 79) % 1000) as f64 / 500.0) - 1.0;
656 for j in 0..m {
657 data[(i, j)] = a * (2.0 * PI * t[j]).sin() + b * (4.0 * PI * t[j]).cos() + c * t[j];
658 }
659 y[i] = 2.0 * a + 3.0 * b + 0.05 * (42_u64.wrapping_add(i as u64) % 10) as f64;
660 }
661
662 let single = crate::scalar_on_function::fregre_lm(&data, &y, None, 3).unwrap();
663 let multi = fregre_lm_multi(&[(&data, &t, 3)], &y, None).unwrap();
664
665 assert!(
667 (single.r_squared - multi.r_squared).abs() < 1e-8,
668 "single R²={} vs multi R²={}",
669 single.r_squared,
670 multi.r_squared
671 );
672
673 for i in 0..n {
675 assert!(
676 (single.fitted_values[i] - multi.fitted_values[i]).abs() < 1e-8,
677 "fitted values differ at {i}"
678 );
679 }
680 }
681
682 #[test]
683 fn test_fregre_lm_multi_empty_predictors() {
684 let y = vec![1.0, 2.0, 3.0];
685 let result = fregre_lm_multi(&[], &y, None);
686 assert!(result.is_err());
687 }
688
689 #[test]
690 fn test_fregre_lm_multi_cv() {
691 let (x1, x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
692 let cv = fregre_lm_multi_cv(&[(&x1, &t1), (&x2, &t2)], &y, None, 5, 5, 42).unwrap();
693 assert!(!cv.candidates.is_empty());
694 assert!(cv.optimal_ncomp >= 1);
695 assert!(cv.min_cv_mse.is_finite());
696 assert_eq!(cv.oof_predictions.len(), 30);
697 let n_finite = cv.oof_predictions.iter().filter(|x| x.is_finite()).count();
699 assert!(n_finite > 20, "most OOF predictions should be finite");
700 }
701
702 #[test]
703 fn test_fregre_lm_multi_residuals_sum_near_zero() {
704 let (x1, x2, y, t1, t2) = make_multi_data(30, 40, 25, 42);
705 let fit = fregre_lm_multi(&[(&x1, &t1, 3), (&x2, &t2, 2)], &y, None).unwrap();
706
707 let resid_sum: f64 = fit.residuals.iter().sum();
708 assert!(
709 resid_sum.abs() < 1e-8,
710 "residuals should sum to ~0 with intercept, got {resid_sum}"
711 );
712 }
713}