Skip to main content

fdars_core/boosting_regression/
mod.rs

1//! Component-wise gradient boosting and Bayesian regression for functional responses.
2//!
3//! Implements REG-06: FDboost-style penalized functional base-learner boosting,
4//! GAMLSS distributional boosting, conjugate Gibbs Bayesian FOSR, and stability selection.
5//!
6//! # Methods
7//!
8//! - [`boost_fosr`] — Component-wise boosted function-on-scalar regression (REG-06-01)
9//! - [`boost_fofr`] — Component-wise boosted function-on-function regression (REG-06-02)
10//! - [`gamlss_fosr`] — GAMLSS distributional boosting: location + scale (REG-06-03)
11//! - [`bayesian_fosr`] — Bayesian FOSR via conjugate Gibbs sampler (REG-06-04)
12//! - [`stability_selection`] — FDboost-style stability selection (REG-06-05)
13//!
14//! # References
15//!
16//! Hothorn et al. (2010). Model-Based Boosting. *Journal of Statistical Software*.
17//! Hofner et al. (2016). gamboostLSS. *Journal of Statistical Software*, 74(1).
18//! Jiang et al. (2025). arXiv:2505.05633 (Bayesian FoSR).
19//! Meinshausen & Bühlmann (2010). Stability Selection. *JRSS-B*, 72(4).
20//!
21//! Divergences from R baselines (FDboost 1.1-4, refund, stabs) documented per function.
22
23use crate::matrix::FdMatrix;
24
25pub mod bayesian;
26pub mod boost_fofr;
27pub mod boost_fosr;
28pub mod gamlss;
29pub mod stability;
30
31// ---------------------------------------------------------------------------
32// Config structs
33// ---------------------------------------------------------------------------
34
35/// Configuration for component-wise gradient boosting (FOSR, FoFR, GAMLSS, stability).
36///
37/// All base-learners use the same `nbasis`, `order`, `lfd_order`, and `lambda`
38/// to ensure equal effective degrees of freedom, preventing selection bias toward
39/// more flexible learners (see Pitfall 4 in RESEARCH.md).
40///
41/// **Divergence from FDboost:** Fixed `nu` and `mstop` rather than CV-based early stopping;
42/// the GCV path is tracked for diagnostic purposes but not used for stopping.
43///
44/// Construct via `BoostingConfig::default()`, then assign the fields you need (e.g. `let mut c = BoostingConfig::default(); c.field = …;`). This struct is `#[non_exhaustive]`, so external crates cannot build it with a struct literal — not even functional-update `..Default::default()` form.
45#[non_exhaustive]
46#[derive(Debug, Clone, PartialEq)]
47pub struct BoostingConfig {
48    /// Number of boosting iterations (must be ≥ 1).
49    pub mstop: usize,
50    /// Learning rate ν ∈ (0, 1] (FDboost default: 0.1).
51    pub nu: f64,
52    /// Number of B-spline basis functions per base-learner (must be ≥ 4).
53    ///
54    /// The actual number of basis functions is `nknots + order` where
55    /// `nknots = nbasis - order`. With `order = 4` (cubic), `nbasis = 10`
56    /// gives 6 interior knots.
57    pub nbasis: usize,
58    /// B-spline order (typically 4 for cubic splines).
59    pub order: usize,
60    /// Penalty derivative order (typically 2 for roughness).
61    pub lfd_order: usize,
62    /// Smoothing parameter λ > 0 for penalized base-learners.
63    pub lambda: f64,
64    /// Number of predictor FPC components for FoFR base-learners (REG-06-02).
65    pub ncomp_x: usize,
66    /// RNG seed (used by stability selection and future extensions; unused in pure boosting).
67    pub seed: u64,
68}
69
70impl Default for BoostingConfig {
71    /// FDboost-convention defaults: `mstop = 100`, `nu = 0.1`, cubic (`order = 4`)
72    /// B-spline base-learners with `nbasis = 10`, second-derivative penalty
73    /// (`lfd_order = 2`), `lambda = 1.0`, `ncomp_x = 3`, `seed = 0`.
74    fn default() -> Self {
75        Self {
76            mstop: 100,
77            nu: 0.1,
78            nbasis: 10,
79            order: 4,
80            lfd_order: 2,
81            lambda: 1.0,
82            ncomp_x: 3,
83            seed: 0,
84        }
85    }
86}
87
88/// Configuration for the Bayesian FOSR Gibbs sampler (REG-06-04).
89///
90/// Uses conjugate Normal-Inverse-Gamma priors. Defaults match the weakly-informative
91/// settings recommended by Jiang et al. (2025): `τ² = 100`, `IG(0.001, 0.001)`.
92///
93/// **Divergence from refund:** refund's Bayesian FOSR uses spline basis priors;
94/// this implementation uses FPCA score compression via `fdata_to_pc` for
95/// simplicity and zero new dependencies.
96///
97/// Construct via `BayesianConfig::default()`, then assign the fields you need (e.g. `let mut c = BayesianConfig::default(); c.field = …;`). This struct is `#[non_exhaustive]`, so external crates cannot build it with a struct literal — not even functional-update `..Default::default()` form.
98#[non_exhaustive]
99#[derive(Debug, Clone, PartialEq)]
100pub struct BayesianConfig {
101    /// Number of FPC components for score compression (must be ≥ 1).
102    pub ncomp: usize,
103    /// Prior variance τ² on FPC-space coefficients (default: 100.0).
104    ///
105    /// Large `τ²` gives a weakly informative prior; very small values are dogmatic.
106    pub tau2: f64,
107    /// Inverse-Gamma prior shape a₀ (default: 0.001 — weakly informative).
108    pub ig_a0: f64,
109    /// Inverse-Gamma prior rate b₀ (default: 0.001 — weakly informative).
110    pub ig_b0: f64,
111    /// Number of Gibbs iterations retained after burn-in (must be ≥ 1).
112    pub n_iter: usize,
113    /// Burn-in iterations discarded (must be < `n_iter` iterations will run).
114    pub burn_in: usize,
115    /// Thinning interval — keep every `thin`-th draw (must be ≥ 1).
116    pub thin: usize,
117    /// RNG seed — chain is fully deterministic: `StdRng::seed_from_u64(seed)`.
118    pub seed: u64,
119}
120
121impl Default for BayesianConfig {
122    /// Weakly-informative defaults per Jiang et al. (2025): `tau2 = 100.0`,
123    /// `IG(0.001, 0.001)`, with `ncomp = 4`, `n_iter = 400`, `burn_in = 200`,
124    /// `thin = 1` (these mirror `bayesian::tests::default_config`), and `seed = 0`
125    /// as the deterministic default (the test config uses its own seed).
126    fn default() -> Self {
127        Self {
128            ncomp: 4,
129            tau2: 100.0,
130            ig_a0: 0.001,
131            ig_b0: 0.001,
132            n_iter: 400,
133            burn_in: 200,
134            thin: 1,
135            seed: 0,
136        }
137    }
138}
139
140/// Configuration for FDboost-style stability selection (REG-06-05).
141///
142/// Implements the Meinshausen-Bühlmann subsampling scheme with ⌊n/2⌋ rows
143/// per replicate. The PFER bound `E[V] ≤ q² / ((2·π_thr − 1)·p)` is reported
144/// as an informational diagnostic.
145///
146/// Construct via `StabilityConfig::default()`, then assign the fields you need (e.g. `let mut c = StabilityConfig::default(); c.field = …;`). This struct is `#[non_exhaustive]`, so external crates cannot build it with a struct literal — not even functional-update `..Default::default()` form.
147#[non_exhaustive]
148#[derive(Debug, Clone, PartialEq)]
149pub struct StabilityConfig {
150    /// Number of resamples B (must be ≥ 1; default: 100).
151    pub n_resamples: usize,
152    /// Selection threshold π ∈ (0.5, 1.0] (default: 0.9).
153    ///
154    /// Base-learner j is declared "stable" if its selection frequency ≥ `pi_thr`.
155    pub pi_thr: f64,
156    /// Base RNG seed; replicate `b` uses `seed.wrapping_add(b as u64)` for isolation.
157    pub seed: u64,
158}
159
160impl Default for StabilityConfig {
161    /// Meinshausen-Bühlmann defaults: `n_resamples = 100`, `pi_thr = 0.9`,
162    /// `seed = 0`.
163    fn default() -> Self {
164        Self {
165            n_resamples: 100,
166            pi_thr: 0.9,
167            seed: 0,
168        }
169    }
170}
171
172// ---------------------------------------------------------------------------
173// Result structs
174// ---------------------------------------------------------------------------
175
176/// Result of component-wise boosted function-on-scalar regression (REG-06-01).
177///
178/// Follows the `FosrResult` field convention; adds the boosting path diagnostics
179/// (`selected_learners`, `gcv_path`, `mstop`, `nu`).
180///
181/// **Divergence from FDboost:** fixed `mstop` / fixed `nu`; no CV-based early stopping.
182/// GCV path is tracked for post-hoc diagnostics only (see `gcv_path`).
183#[derive(Debug, Clone, PartialEq)]
184#[non_exhaustive]
185pub struct BoostFosrResult {
186    /// Intercept function F₀(t) = Ȳ(t) (pointwise mean of Y, length m_t).
187    pub intercept: Vec<f64>,
188    /// Accumulated coefficient functions β_j(t) per predictor (p × m_t).
189    ///
190    /// Row j holds the total contribution of base-learner j across all iterations
191    /// where it was selected, scaled by ν.
192    pub beta: FdMatrix,
193    /// Fitted functional values F̂(xᵢ, t) = Σ contributions (n × m_t).
194    pub fitted: FdMatrix,
195    /// Residual curves Y − F̂ (n × m_t).
196    pub residuals: FdMatrix,
197    /// Pointwise R²(t) at each response grid point (length m_t).
198    pub r_squared_t: Vec<f64>,
199    /// Integrated R² (scalar summary).
200    pub r_squared: f64,
201    /// Number of boosting iterations used.
202    pub mstop: usize,
203    /// Learning rate ν used.
204    pub nu: f64,
205    /// Index j* of the base-learner selected at each boosting iteration (length mstop).
206    pub selected_learners: Vec<usize>,
207    /// ‖residual‖_F² recorded after each iteration (length mstop).
208    ///
209    /// Should be non-increasing for L2 loss (use for path diagnostics / GCV).
210    pub gcv_path: Vec<f64>,
211}
212
213/// Result of component-wise boosted function-on-function regression (REG-06-02).
214///
215/// Functional predictors are compressed via FPCA score projection; the boosting
216/// core operates on the resulting scalar design matrices (bfpc variant).
217///
218/// **Divergence from FDboost:** uses FPC score compression (`fdata_to_pc`) rather
219/// than FDboost's `bsignal` B-spline joint expansion. Simpler and dependency-free.
220#[derive(Debug, Clone, PartialEq)]
221#[non_exhaustive]
222pub struct BoostFofrResult {
223    /// Intercept function F₀(t) = Ȳ(t) (length m_y).
224    pub intercept: Vec<f64>,
225    /// Fitted response curves (n × m_y).
226    pub fitted: FdMatrix,
227    /// Residual curves (n × m_y).
228    pub residuals: FdMatrix,
229    /// Pointwise R²(t) at each response grid point (length m_y).
230    pub r_squared_t: Vec<f64>,
231    /// Overall R².
232    pub r_squared: f64,
233    /// FPCA result for each functional predictor (one per predictor).
234    pub fpca_x: Vec<crate::regression::FpcaResult>,
235    /// Accumulated FPC-space score coefficients per predictor (Vec[j] is K_j × m_y).
236    pub score_coefs: Vec<FdMatrix>,
237    /// Reconstructed coefficient surfaces β_j(s,t) per predictor (Vec[j] is m_x × m_y).
238    pub beta_surfaces: Vec<FdMatrix>,
239    /// Index j* of the base-learner selected at each boosting iteration (length mstop).
240    pub selected_learners: Vec<usize>,
241    /// ‖residual‖_F² per boosting iteration (length mstop).
242    pub gcv_path: Vec<f64>,
243    /// Number of boosting iterations used.
244    pub mstop: usize,
245    /// Learning rate ν used.
246    pub nu: f64,
247}
248
249/// Result of GAMLSS-style distributional functional regression (REG-06-03).
250///
251/// Models a Gaussian functional response Y(t) with location μ(t) and scale σ(t).
252/// Cyclic component-wise boosting alternates between boosting μ and log-σ.
253///
254/// **Divergence from gamboostLSS:** uses cyclic rather than noncyclic (non-cyclic
255/// per-iteration selection is superior for variable selection but more complex).
256/// Links: identity for μ, log for σ.
257#[derive(Debug, Clone, PartialEq)]
258#[non_exhaustive]
259pub struct GamlssResult {
260    /// Fitted location μ̂(t) per observation (n × m_t).
261    pub mu_fitted: FdMatrix,
262    /// Fitted scale σ̂(t) per observation (n × m_t); always positive.
263    pub sigma_fitted: FdMatrix,
264    /// Intercept for the μ model: F̂_μ,₀(t) = Ȳ(t) (length m_t).
265    pub mu_intercept: Vec<f64>,
266    /// Intercept for the log-σ model: 0 → exp(0) = 1 (length m_t).
267    pub sigma_intercept: Vec<f64>,
268    /// Accumulated μ coefficient functions (p × m_t).
269    pub mu_beta: FdMatrix,
270    /// Accumulated log-σ coefficient functions (p × m_t).
271    pub sigma_beta: FdMatrix,
272    /// Final Gaussian log-likelihood at convergence.
273    pub log_likelihood: f64,
274    /// Log-likelihood per cyclic iteration (length mstop).
275    pub ll_path: Vec<f64>,
276    /// Number of cyclic boosting iterations used.
277    pub mstop: usize,
278    /// Learning rate ν used.
279    pub nu: f64,
280}
281
282/// Result of Bayesian function-on-scalar regression via conjugate Gibbs (REG-06-04).
283///
284/// Posterior summaries are computed from thinned post-burn-in draws. Credible bands
285/// are pointwise (not simultaneous) quantiles over the retained draws.
286///
287/// **Divergence from refund:** uses FPCA score compression via `fdata_to_pc`
288/// rather than spline basis priors. Pointwise credible bands only (no simultaneous bands).
289#[derive(Debug, Clone, PartialEq)]
290#[non_exhaustive]
291pub struct BayesianFosrResult {
292    /// Posterior mean coefficient functions β̄(t) (p × m_t).
293    pub beta_mean: FdMatrix,
294    /// Pointwise 2.5% credible band (p × m_t).
295    pub beta_lower: FdMatrix,
296    /// Pointwise 97.5% credible band (p × m_t).
297    pub beta_upper: FdMatrix,
298    /// Posterior-mean fitted values (n × m_t).
299    pub fitted: FdMatrix,
300    /// Posterior-mean residuals (n × m_t).
301    pub residuals: FdMatrix,
302    /// Posterior mean σ²(t) across the response grid (length m_t).
303    pub sigma2_mean: Vec<f64>,
304    /// Number of Gibbs iterations retained (after burn-in, before thinning).
305    pub n_iter: usize,
306    /// Burn-in iterations discarded.
307    pub burn_in: usize,
308    /// Thinning interval used.
309    pub thin: usize,
310    /// FPC components used for score compression.
311    pub ncomp: usize,
312}
313
314/// Result of FDboost-style stability selection (REG-06-05).
315///
316/// Aggregates base-learner selection frequencies over B subsamples of size ⌊n/2⌋.
317/// The PFER bound is informational: `E[V] ≤ q² / ((2·π_thr − 1)·p)`.
318#[derive(Debug, Clone, PartialEq)]
319#[non_exhaustive]
320pub struct StabilityResult {
321    /// Selection frequency π̂[j] ∈ [0, 1] for each base-learner j = 0..p.
322    pub selection_freq: Vec<f64>,
323    /// Indices j where π̂[j] ≥ pi_thr (the "stable set").
324    pub stable_set: Vec<usize>,
325    /// Threshold π used.
326    pub pi_thr: f64,
327    /// PFER upper bound: `q² / ((2·pi_thr − 1)·p)` where q = mean per-subsample selection count.
328    pub pfer_bound: f64,
329    /// Number of resamples B used.
330    pub n_resamples: usize,
331}
332
333// ---------------------------------------------------------------------------
334// Barrel re-exports
335// ---------------------------------------------------------------------------
336
337pub use self::bayesian::bayesian_fosr;
338pub use self::boost_fofr::boost_fofr;
339pub use self::boost_fosr::boost_fosr;
340pub use self::gamlss::gamlss_fosr;
341pub use self::stability::stability_selection;
342
343#[cfg(test)]
344mod tests {
345    use super::{BayesianConfig, BoostingConfig, StabilityConfig};
346
347    #[test]
348    fn config_defaults_match_documented_values() {
349        assert_eq!(
350            BoostingConfig::default(),
351            BoostingConfig {
352                mstop: 100,
353                nu: 0.1,
354                nbasis: 10,
355                order: 4,
356                lfd_order: 2,
357                lambda: 1.0,
358                ncomp_x: 3,
359                seed: 0,
360            }
361        );
362        assert_eq!(
363            BayesianConfig::default(),
364            BayesianConfig {
365                ncomp: 4,
366                tau2: 100.0,
367                ig_a0: 0.001,
368                ig_b0: 0.001,
369                n_iter: 400,
370                burn_in: 200,
371                thin: 1,
372                seed: 0,
373            }
374        );
375        assert_eq!(
376            StabilityConfig::default(),
377            StabilityConfig {
378                n_resamples: 100,
379                pi_thr: 0.9,
380                seed: 0,
381            }
382        );
383    }
384}