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