fdars_core/scalar_on_function/mod.rs
1//! Scalar-on-function regression with mixed scalar/functional covariates.
2//!
3//! Implements models of the form:
4//! ```text
5//! y = α + ∫β(t)X(t)dt + γᵀz + ε
6//! ```
7//! where X(t) is a functional predictor, z is a vector of scalar covariates,
8//! β(t) is the functional coefficient, and γ is the vector of scalar coefficients.
9//!
10//! # Methods
11//!
12//! - [`fregre_lm`]: FPC-based functional linear model with optional scalar covariates
13//! - [`fregre_l1`]: L1 (median) robust functional regression via IRLS
14//! - [`fregre_huber`]: Huber M-estimation robust functional regression via IRLS
15//! - [`fregre_np_mixed`]: Nonparametric kernel regression with product kernels
16//! - [`functional_logistic`]: Logistic regression for binary outcomes
17//! - [`fregre_cv`]: Cross-validation for number of FPC components
18
19use crate::error::FdarError;
20use crate::linalg::cholesky_solve as linalg_cholesky_solve;
21use crate::matrix::FdMatrix;
22use crate::regression::{FpcaResult, PlsResult};
23
24mod additive;
25mod bootstrap;
26mod cv;
27mod fregre_lm;
28mod glm;
29mod logistic;
30mod multi;
31mod nonparametric;
32mod pls;
33mod robust;
34#[cfg(test)]
35mod tests;
36
37// Re-export all public items from submodules
38pub use additive::{
39 fam, fregre_gkam, fregre_gsam, history_index, permutation_test_fam, variable_selection,
40 FamConfig, FamResult, GkamConfig, GkamResult, GsamConfig, GsamResult, HistoryIndexConfig,
41 HistoryIndexResult, PermTestConfig, PermTestResult, PermTestStatistic, VarSelectConfig,
42 VarSelectPenalty, VarSelectResult,
43};
44pub use bootstrap::{bootstrap_ci_fregre_lm, bootstrap_ci_functional_logistic};
45pub use cv::{fregre_basis_cv, fregre_np_cv};
46pub use fregre_lm::{fregre_cv, fregre_lm, model_selection_ncomp, predict_fregre_lm};
47pub use glm::{functional_glm, predict_functional_glm};
48// GlmFamily and FunctionalGlmResult are defined in this module (mod.rs) and
49// exported from here directly — no glm:: re-export needed for those types.
50pub use logistic::{functional_logistic, predict_functional_logistic};
51pub use multi::{fregre_lm_multi, fregre_lm_multi_cv, predict_fregre_lm_multi, MultiCvResult};
52pub use nonparametric::{
53 fregre_np_from_distances, fregre_np_mixed, predict_fregre_np, predict_fregre_np_from_distances,
54};
55pub use pls::{fregre_pls, predict_fregre_pls};
56pub use robust::{fregre_huber, fregre_l1, predict_fregre_robust};
57
58// ---------------------------------------------------------------------------
59// Result types
60// ---------------------------------------------------------------------------
61
62/// Result of functional linear regression.
63#[derive(Debug, Clone, PartialEq)]
64#[non_exhaustive]
65pub struct FregreLmResult {
66 /// Intercept α
67 pub intercept: f64,
68 /// Functional coefficient β(t), evaluated on the original grid (length m)
69 pub beta_t: Vec<f64>,
70 /// Pointwise standard errors of β(t) (length m)
71 pub beta_se: Vec<f64>,
72 /// Scalar coefficients γ (one per scalar covariate)
73 pub gamma: Vec<f64>,
74 /// Fitted values ŷ (length n)
75 pub fitted_values: Vec<f64>,
76 /// Residuals y - ŷ (length n)
77 pub residuals: Vec<f64>,
78 /// R² statistic
79 pub r_squared: f64,
80 /// Adjusted R²
81 pub r_squared_adj: f64,
82 /// Standard errors of all coefficients (intercept, FPC scores, scalar covariates)
83 pub std_errors: Vec<f64>,
84 /// Number of FPC components used
85 pub ncomp: usize,
86 /// FPCA result (for projecting new data)
87 pub fpca: FpcaResult,
88 /// Regression coefficients on (FPC scores, scalar covariates) — internal
89 pub coefficients: Vec<f64>,
90 /// Residual standard error
91 pub residual_se: f64,
92 /// GCV criterion value (if computed)
93 pub gcv: f64,
94 /// Akaike Information Criterion
95 pub aic: f64,
96 /// Bayesian Information Criterion
97 pub bic: f64,
98}
99
100/// Result of nonparametric functional regression with mixed predictors.
101#[derive(Debug, Clone, PartialEq)]
102#[non_exhaustive]
103pub struct FregreNpResult {
104 /// Fitted values ŷ (length n)
105 pub fitted_values: Vec<f64>,
106 /// Residuals y - ŷ (length n)
107 pub residuals: Vec<f64>,
108 /// R² statistic
109 pub r_squared: f64,
110 /// Bandwidth for functional distance kernel
111 pub h_func: f64,
112 /// Bandwidth for scalar covariates kernel
113 pub h_scalar: f64,
114 /// Leave-one-out CV error
115 pub cv_error: f64,
116}
117
118/// Result of robust (L1 or Huber) functional regression.
119#[derive(Debug, Clone, PartialEq)]
120#[non_exhaustive]
121pub struct FregreRobustResult {
122 /// Intercept
123 pub intercept: f64,
124 /// Functional coefficient β(t), evaluated on the original grid (length m)
125 pub beta_t: Vec<f64>,
126 /// Fitted values ŷ (length n)
127 pub fitted_values: Vec<f64>,
128 /// Residuals y - ŷ (length n)
129 pub residuals: Vec<f64>,
130 /// Regression coefficients (intercept, FPC scores, scalar covariates)
131 pub coefficients: Vec<f64>,
132 /// Number of FPC components used
133 pub ncomp: usize,
134 /// FPCA result (for projecting new data)
135 pub fpca: FpcaResult,
136 /// Number of IRLS iterations performed
137 pub iterations: usize,
138 /// Whether the IRLS algorithm converged
139 pub converged: bool,
140 /// Final IRLS weights (length n)
141 pub weights: Vec<f64>,
142 /// R² statistic
143 pub r_squared: f64,
144}
145
146/// Result of functional logistic regression.
147#[derive(Debug, Clone, PartialEq)]
148#[non_exhaustive]
149pub struct FunctionalLogisticResult {
150 /// Intercept α
151 pub intercept: f64,
152 /// Functional coefficient β(t), evaluated on the original grid (length m)
153 pub beta_t: Vec<f64>,
154 /// Pointwise standard errors of β(t) (length m)
155 pub beta_se: Vec<f64>,
156 /// Scalar coefficients γ (one per scalar covariate)
157 pub gamma: Vec<f64>,
158 /// Predicted probabilities P(Y=1) (length n)
159 pub probabilities: Vec<f64>,
160 /// Predicted class labels (0 or 1)
161 pub predicted_classes: Vec<usize>,
162 /// Number of FPC components used
163 pub ncomp: usize,
164 /// Classification accuracy on training data
165 pub accuracy: f64,
166 /// Standard errors of all coefficients (intercept, FPC scores, scalar covariates)
167 pub std_errors: Vec<f64>,
168 /// Regression coefficients on (FPC scores, scalar covariates) — internal
169 pub coefficients: Vec<f64>,
170 /// Log-likelihood at convergence
171 pub log_likelihood: f64,
172 /// Number of IRLS iterations
173 pub iterations: usize,
174 /// FPCA result (for projecting new data)
175 pub fpca: FpcaResult,
176 /// Akaike Information Criterion
177 pub aic: f64,
178 /// Bayesian Information Criterion
179 pub bic: f64,
180}
181
182/// Result of cross-validation for K selection.
183#[derive(Debug, Clone, PartialEq)]
184#[non_exhaustive]
185pub struct FregreCvResult {
186 /// Candidate K values tested
187 pub k_values: Vec<usize>,
188 /// CV error for each K
189 pub cv_errors: Vec<f64>,
190 /// Optimal K (minimizing CV error)
191 pub optimal_k: usize,
192 /// Minimum CV error
193 pub min_cv_error: f64,
194 /// Out-of-fold predictions at optimal K (length n, each predicted when held out)
195 pub oof_predictions: Vec<f64>,
196 /// Fold assignment for each observation (0..n_folds)
197 pub fold_assignments: Vec<usize>,
198 /// Per-fold MSE at optimal K
199 pub fold_errors: Vec<f64>,
200}
201
202/// Result of PLS-based scalar-on-function regression.
203#[derive(Debug, Clone, PartialEq)]
204#[non_exhaustive]
205pub struct PlsRegressionResult {
206 /// Intercept α
207 pub intercept: f64,
208 /// Functional coefficient β(t), evaluated on the original grid (length m)
209 pub beta_t: Vec<f64>,
210 /// Scalar coefficients γ (one per scalar covariate)
211 pub gamma: Vec<f64>,
212 /// Fitted values ŷ (length n)
213 pub fitted_values: Vec<f64>,
214 /// Residuals y - ŷ (length n)
215 pub residuals: Vec<f64>,
216 /// R² statistic
217 pub r_squared: f64,
218 /// Adjusted R²
219 pub r_squared_adj: f64,
220 /// Number of PLS components used
221 pub ncomp: usize,
222 /// PLS result (for projecting new data)
223 pub pls: PlsResult,
224 /// Regression coefficients on (intercept, PLS scores, scalar covariates)
225 pub coefficients: Vec<f64>,
226 /// Residual standard error
227 pub residual_se: f64,
228 /// Akaike Information Criterion
229 pub aic: f64,
230 /// Bayesian Information Criterion
231 pub bic: f64,
232}
233
234/// Result of multi-predictor functional linear regression.
235#[derive(Debug, Clone, PartialEq)]
236#[non_exhaustive]
237pub struct MultiFregreLmResult {
238 /// Intercept α
239 pub intercept: f64,
240 /// Functional coefficients beta_k(t) for each predictor, each length m_k.
241 pub beta_t: Vec<Vec<f64>>,
242 /// Scalar coefficients γ (one per scalar covariate)
243 pub gamma: Vec<f64>,
244 /// Fitted values ŷ (length n)
245 pub fitted_values: Vec<f64>,
246 /// Residuals y - ŷ (length n)
247 pub residuals: Vec<f64>,
248 /// R²
249 pub r_squared: f64,
250 /// Adjusted R²
251 pub r_squared_adj: f64,
252 /// Number of FPC components used per functional predictor
253 pub ncomp: Vec<usize>,
254 /// FPCA results for each functional predictor (for projection)
255 pub fpcas: Vec<FpcaResult>,
256 /// Regression coefficients [intercept, scores_1..., scores_2..., ..., scalars...]
257 pub coefficients: Vec<f64>,
258 /// Residual standard error
259 pub residual_se: f64,
260 /// AIC
261 pub aic: f64,
262 /// BIC
263 pub bic: f64,
264}
265
266/// Criterion used for model selection.
267#[derive(Debug, Clone, Copy, PartialEq)]
268pub enum SelectionCriterion {
269 /// Akaike Information Criterion
270 Aic,
271 /// Bayesian Information Criterion
272 Bic,
273 /// Generalized Cross-Validation
274 Gcv,
275}
276
277/// Result of ncomp model selection.
278#[derive(Debug, Clone, PartialEq)]
279#[non_exhaustive]
280pub struct ModelSelectionResult {
281 /// Best number of FPC components by the chosen criterion
282 pub best_ncomp: usize,
283 /// (ncomp, AIC, BIC, GCV) for each candidate
284 pub criteria: Vec<(usize, f64, f64, f64)>,
285}
286
287/// Result of bootstrap confidence intervals for β(t).
288#[derive(Debug, Clone, PartialEq)]
289#[non_exhaustive]
290pub struct BootstrapCiResult {
291 /// Pointwise lower bound (length m).
292 pub lower: Vec<f64>,
293 /// Pointwise upper bound (length m).
294 pub upper: Vec<f64>,
295 /// Original β(t) estimate (length m).
296 pub center: Vec<f64>,
297 /// Simultaneous lower bound (sup-norm adjusted, length m).
298 pub sim_lower: Vec<f64>,
299 /// Simultaneous upper bound (sup-norm adjusted, length m).
300 pub sim_upper: Vec<f64>,
301 /// Number of bootstrap replicates that converged.
302 pub n_boot_success: usize,
303}
304
305/// Result of lambda selection for basis regression via cross-validation.
306#[derive(Debug, Clone, PartialEq)]
307#[non_exhaustive]
308pub struct FregreBasisCvResult {
309 /// Optimal smoothing parameter lambda.
310 pub optimal_lambda: f64,
311 /// Mean CV error for each lambda.
312 pub cv_errors: Vec<f64>,
313 /// SE of CV error across folds for each lambda.
314 pub cv_se: Vec<f64>,
315 /// Lambda values tested.
316 pub lambda_values: Vec<f64>,
317 /// Minimum mean CV error.
318 pub min_cv_error: f64,
319}
320
321/// Result of bandwidth selection for nonparametric regression via CV.
322#[derive(Debug, Clone, PartialEq)]
323#[non_exhaustive]
324pub struct FregreNpCvResult {
325 /// Optimal bandwidth.
326 pub optimal_h: f64,
327 /// Mean CV error for each bandwidth.
328 pub cv_errors: Vec<f64>,
329 /// SE of CV error across folds for each bandwidth.
330 pub cv_se: Vec<f64>,
331 /// Bandwidth values tested.
332 pub h_values: Vec<f64>,
333 /// Minimum mean CV error.
334 pub min_cv_error: f64,
335}
336
337/// Exponential-family distribution for [`functional_glm`].
338///
339/// Each variant specifies the canonical link function and variance function
340/// for one member of the exponential family.
341///
342/// | Variant | Link g(μ) | Inverse link g⁻¹(η) | Variance V(μ) |
343/// |-----------|------------|---------------------|--------------|
344/// | Binomial | logit | sigmoid | μ(1−μ) |
345/// | Poisson | log | exp | μ |
346/// | Gamma | inverse | 1/η | μ² |
347/// | Gaussian | identity | η | 1 |
348#[derive(Debug, Clone, Copy, PartialEq)]
349#[non_exhaustive]
350#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
351pub enum GlmFamily {
352 /// Logit link; binary outcomes (y ∈ {0, 1}).
353 Binomial,
354 /// Log link; non-negative integer counts (y ∈ {0, 1, 2, …}).
355 Poisson,
356 /// Inverse link (canonical); strictly positive continuous responses (y > 0).
357 Gamma,
358 /// Identity link; continuous unbounded responses.
359 Gaussian,
360}
361
362/// Result of [`functional_glm`] for a scalar response over functional predictors.
363///
364/// Contains the fitted model parameters, diagnostic statistics, and the embedded
365/// [`crate::regression::FpcaResult`] for projecting new data.
366#[derive(Debug, Clone, PartialEq)]
367#[non_exhaustive]
368#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
369pub struct FunctionalGlmResult {
370 /// Intercept α
371 pub intercept: f64,
372 /// Functional coefficient β(t), evaluated on the original grid (length m)
373 pub beta_t: Vec<f64>,
374 /// Pointwise standard errors of β(t) (length m)
375 pub beta_se: Vec<f64>,
376 /// Scalar coefficients γ (one per scalar covariate)
377 pub gamma: Vec<f64>,
378 /// Fitted mean values μ = g⁻¹(η) (length n)
379 pub fitted_values: Vec<f64>,
380 /// Linear predictors η = Xβ (length n)
381 pub linear_predictors: Vec<f64>,
382 /// Number of FPC components used
383 pub ncomp: usize,
384 /// All regression coefficients [intercept, γ₁…γ_K, z₁…z_P]
385 pub coefficients: Vec<f64>,
386 /// Standard errors of all coefficients (intercept, FPC scores, scalar covariates)
387 pub std_errors: Vec<f64>,
388 /// Log-likelihood at convergence (kernel; see module doc for AIC comparability note)
389 pub log_likelihood: f64,
390 /// GLM deviance D = 2(LL_saturated − LL_fitted)
391 pub deviance: f64,
392 /// Number of IRLS iterations performed
393 pub iterations: usize,
394 /// FPCA result (embedded for projecting new data)
395 pub fpca: crate::regression::FpcaResult,
396 /// Akaike Information Criterion: −2·log_likelihood + 2·p
397 pub aic: f64,
398 /// Bayesian Information Criterion: −2·log_likelihood + p·ln(n)
399 pub bic: f64,
400 /// Exponential-family distribution used for this fit
401 pub family: GlmFamily,
402}
403
404impl FunctionalGlmResult {
405 /// Predict response for new functional data. Delegates to [`predict_functional_glm`].
406 ///
407 /// # Errors
408 ///
409 /// Propagates [`FdarError::InvalidDimension`] from [`predict_functional_glm`]
410 /// when `new_data` / `new_scalar` shapes do not match the fitted model.
411 pub fn predict(
412 &self,
413 new_data: &FdMatrix,
414 new_scalar: Option<&FdMatrix>,
415 ) -> Result<Vec<f64>, FdarError> {
416 predict_functional_glm(self, new_data, new_scalar)
417 }
418}
419
420// ---------------------------------------------------------------------------
421// Shared linear algebra helpers (delegated to crate::linalg)
422// ---------------------------------------------------------------------------
423
424// Re-export for use by submodules and explain/ modules that import from
425// `crate::scalar_on_function::{cholesky_factor, cholesky_forward_back, compute_xtx}`.
426pub(crate) use crate::linalg::cholesky_factor;
427pub(crate) use crate::linalg::cholesky_forward_back;
428pub(crate) use crate::linalg::compute_xtx;
429
430/// Compute X'y (length p).
431fn compute_xty(x: &FdMatrix, y: &[f64]) -> Vec<f64> {
432 let (n, p) = x.shape();
433 (0..p)
434 .map(|k| {
435 let mut s = 0.0;
436 for i in 0..n {
437 s += x[(i, k)] * y[i];
438 }
439 s
440 })
441 .collect()
442}
443
444/// Solve Ax = b via Cholesky decomposition (A must be symmetric positive definite).
445pub(super) fn cholesky_solve(a: &[f64], b: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
446 linalg_cholesky_solve(a, b, p)
447}
448
449/// Compute hat matrix diagonal: H_ii = x_i' (X'X)^{-1} x_i, given Cholesky factor L of X'X.
450pub(crate) fn compute_hat_diagonal(x: &FdMatrix, l: &[f64]) -> Vec<f64> {
451 let (n, p) = x.shape();
452 let mut hat_diag = vec![0.0; n];
453 for i in 0..n {
454 let mut v = vec![0.0; p];
455 for j in 0..p {
456 v[j] = x[(i, j)];
457 for k in 0..j {
458 v[j] -= l[j * p + k] * v[k];
459 }
460 v[j] /= l[j * p + j];
461 }
462 hat_diag[i] = v.iter().map(|vi| vi * vi).sum();
463 }
464 hat_diag
465}
466
467/// Compute diagonal of (X'X)^{-1} given Cholesky factor L, then SE = sqrt(sigma² * diag).
468fn compute_ols_std_errors(l: &[f64], p: usize, sigma2: f64) -> Vec<f64> {
469 let mut se = vec![0.0; p];
470 for j in 0..p {
471 let mut v = vec![0.0; p];
472 v[j] = 1.0;
473 for k in 0..p {
474 for kk in 0..k {
475 v[k] -= l[k * p + kk] * v[kk];
476 }
477 v[k] /= l[k * p + k];
478 }
479 se[j] = (sigma2 * v.iter().map(|vi| vi * vi).sum::<f64>()).sqrt();
480 }
481 se
482}
483
484// ---------------------------------------------------------------------------
485// Design matrix and coefficient recovery
486// ---------------------------------------------------------------------------
487
488/// Build design matrix: \[1, ξ_1, ..., ξ_K, z_1, ..., z_p\].
489/// Validate inputs for fregre_lm / functional_logistic.
490fn validate_fregre_inputs(
491 n: usize,
492 m: usize,
493 y: &[f64],
494 scalar_covariates: Option<&FdMatrix>,
495) -> Result<(), FdarError> {
496 if n < 3 {
497 return Err(FdarError::InvalidDimension {
498 parameter: "data",
499 expected: "at least 3 rows".to_string(),
500 actual: format!("{n}"),
501 });
502 }
503 if m == 0 {
504 return Err(FdarError::InvalidDimension {
505 parameter: "data",
506 expected: "at least 1 column".to_string(),
507 actual: "0".to_string(),
508 });
509 }
510 if y.len() != n {
511 return Err(FdarError::InvalidDimension {
512 parameter: "y",
513 expected: format!("{n}"),
514 actual: format!("{}", y.len()),
515 });
516 }
517 if let Some(sc) = scalar_covariates {
518 if sc.nrows() != n {
519 return Err(FdarError::InvalidDimension {
520 parameter: "scalar_covariates",
521 expected: format!("{n} rows"),
522 actual: format!("{} rows", sc.nrows()),
523 });
524 }
525 }
526 Ok(())
527}
528
529/// Resolve ncomp: auto-select via CV if 0, otherwise clamp.
530fn resolve_ncomp(
531 ncomp: usize,
532 data: &FdMatrix,
533 y: &[f64],
534 scalar_covariates: Option<&FdMatrix>,
535 n: usize,
536 m: usize,
537) -> Result<usize, FdarError> {
538 if ncomp == 0 {
539 let cv = fregre_cv(data, y, scalar_covariates, 1, m.min(n - 1).min(20), 5)?;
540 Ok(cv.optimal_k)
541 } else {
542 Ok(ncomp.min(n - 1).min(m))
543 }
544}
545
546pub(crate) fn build_design_matrix(
547 fpca_scores: &FdMatrix,
548 ncomp: usize,
549 scalar_covariates: Option<&FdMatrix>,
550 n: usize,
551) -> FdMatrix {
552 let p_scalar = scalar_covariates.map_or(0, super::matrix::FdMatrix::ncols);
553 let p_total = 1 + ncomp + p_scalar;
554 let mut design = FdMatrix::zeros(n, p_total);
555 for i in 0..n {
556 design[(i, 0)] = 1.0;
557 for k in 0..ncomp {
558 design[(i, 1 + k)] = fpca_scores[(i, k)];
559 }
560 if let Some(sc) = scalar_covariates {
561 for j in 0..p_scalar {
562 design[(i, 1 + ncomp + j)] = sc[(i, j)];
563 }
564 }
565 }
566 design
567}
568
569/// Recover functional coefficient β(t) = Σ_k γ_k φ_k(t).
570fn recover_beta_t(fpc_coeffs: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
571 let ncomp = fpc_coeffs.len();
572 let mut beta_t = vec![0.0; m];
573 for k in 0..ncomp {
574 for j in 0..m {
575 beta_t[j] += fpc_coeffs[k] * rotation[(j, k)];
576 }
577 }
578 beta_t
579}
580
581/// Pointwise standard error of β(t) via error propagation through FPCA rotation.
582///
583/// SE[β(t_j)]² = Σ_k φ_k(t_j)² · SE[γ_k]²
584fn compute_beta_se(gamma_se: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
585 let ncomp = gamma_se.len();
586 let mut beta_se = vec![0.0; m];
587 for j in 0..m {
588 let mut var_j = 0.0;
589 for k in 0..ncomp {
590 var_j += rotation[(j, k)].powi(2) * gamma_se[k].powi(2);
591 }
592 beta_se[j] = var_j.sqrt();
593 }
594 beta_se
595}
596
597/// Compute fitted values ŷ = X β.
598fn compute_fitted(design: &FdMatrix, coeffs: &[f64]) -> Vec<f64> {
599 let (n, p) = design.shape();
600 (0..n)
601 .map(|i| {
602 let mut yhat = 0.0;
603 for j in 0..p {
604 yhat += design[(i, j)] * coeffs[j];
605 }
606 yhat
607 })
608 .collect()
609}
610
611/// Compute R² and adjusted R².
612fn compute_r_squared(y: &[f64], residuals: &[f64], p_total: usize) -> (f64, f64) {
613 let n = y.len();
614 let y_mean = y.iter().sum::<f64>() / n as f64;
615 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
616 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
617 let r_squared = if ss_tot > 0.0 {
618 1.0 - ss_res / ss_tot
619 } else {
620 0.0
621 };
622 let df_model = (p_total - 1) as f64;
623 let r_squared_adj = if n as f64 - df_model - 1.0 > 0.0 {
624 1.0 - (1.0 - r_squared) * (n as f64 - 1.0) / (n as f64 - df_model - 1.0)
625 } else {
626 r_squared
627 };
628 (r_squared, r_squared_adj)
629}
630
631// ---------------------------------------------------------------------------
632// OLS solver
633// ---------------------------------------------------------------------------
634
635/// Solve ordinary least squares: min ||Xb - y||² via normal equations with Cholesky.
636/// Returns (coefficients, hat_matrix_diagonal) or error if singular.
637fn ols_solve(x: &FdMatrix, y: &[f64]) -> Result<(Vec<f64>, Vec<f64>), FdarError> {
638 let (n, p) = x.shape();
639 if n < p || p == 0 {
640 return Err(FdarError::InvalidDimension {
641 parameter: "design matrix",
642 expected: format!("n >= p and p > 0 (p={p})"),
643 actual: format!("n={n}, p={p}"),
644 });
645 }
646 let xtx = compute_xtx(x);
647 let xty = compute_xty(x, y);
648 let l = cholesky_factor(&xtx, p)?;
649 let b = cholesky_forward_back(&l, &xty, p);
650 let hat_diag = compute_hat_diagonal(x, &l);
651 Ok((b, hat_diag))
652}
653
654/// Sigmoid function: 1 / (1 + exp(-x))
655pub(crate) fn sigmoid(x: f64) -> f64 {
656 if x >= 0.0 {
657 1.0 / (1.0 + (-x).exp())
658 } else {
659 let ex = x.exp();
660 ex / (1.0 + ex)
661 }
662}
663
664// ---------------------------------------------------------------------------
665// Predict methods on result structs
666// ---------------------------------------------------------------------------
667
668impl FregreLmResult {
669 /// Predict new responses. Delegates to [`predict_fregre_lm`].
670 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
671 predict_fregre_lm(self, new_data, new_scalar)
672 }
673}
674
675impl FregreRobustResult {
676 /// Predict new responses. Delegates to [`predict_fregre_robust`].
677 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
678 predict_fregre_robust(self, new_data, new_scalar)
679 }
680}
681
682impl FunctionalLogisticResult {
683 /// Predict P(Y=1) for new data. Delegates to [`predict_functional_logistic`].
684 pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
685 predict_functional_logistic(self, new_data, new_scalar)
686 }
687}
688
689impl MultiFregreLmResult {
690 /// Predict new responses. Delegates to [`predict_fregre_lm_multi`].
691 ///
692 /// # Errors
693 ///
694 /// Returns [`FdarError`] if prediction fails due to dimension mismatches.
695 pub fn predict(
696 &self,
697 new_predictors: &[&FdMatrix],
698 new_scalar: Option<&FdMatrix>,
699 ) -> Result<Vec<f64>, FdarError> {
700 predict_fregre_lm_multi(self, new_predictors, new_scalar)
701 }
702}