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}