use super::boost_fosr::boost_fosr;
use super::{BoostingConfig, StabilityConfig, StabilityResult};
use crate::error::FdarError;
use crate::iter_maybe_parallel;
use crate::matrix::FdMatrix;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
#[cfg(feature = "parallel")]
use rayon::iter::ParallelIterator;
fn subsample_rows(src: &FdMatrix, indices: &[usize]) -> FdMatrix {
let ncols = src.ncols();
let mut out = FdMatrix::zeros(indices.len(), ncols);
for (dst_i, &src_i) in indices.iter().enumerate() {
for j in 0..ncols {
out[(dst_i, j)] = src[(src_i, j)];
}
}
out
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn stability_selection(
data: &FdMatrix,
predictors: &FdMatrix,
argvals: &[f64],
boost_config: &BoostingConfig,
stab_config: &StabilityConfig,
) -> Result<StabilityResult, FdarError> {
let (n, m_t) = data.shape();
let p = predictors.ncols();
if m_t == 0 || predictors.nrows() != n {
return Err(FdarError::InvalidDimension {
parameter: "data/predictors",
expected: format!("m_t > 0 and predictors.nrows() == n (n={n})"),
actual: format!("m_t={m_t}, predictors.nrows()={}", predictors.nrows()),
});
}
if p == 0 {
return Err(FdarError::InvalidDimension {
parameter: "predictors",
expected: "at least 1 predictor column".to_string(),
actual: "0 columns".to_string(),
});
}
if argvals.len() != m_t {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("length == data.ncols() = {m_t}"),
actual: format!("length = {}", argvals.len()),
});
}
if stab_config.n_resamples == 0 {
return Err(FdarError::InvalidParameter {
parameter: "n_resamples",
message: "must be >= 1".to_string(),
});
}
if !(stab_config.pi_thr > 0.5 && stab_config.pi_thr <= 1.0) {
return Err(FdarError::InvalidParameter {
parameter: "pi_thr",
message: format!("must be in (0.5, 1.0], got {}", stab_config.pi_thr),
});
}
let half = n / 2;
if half < 3 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "n >= 6 so that ⌊n/2⌋ >= 3 (minimum for boost_fosr)".to_string(),
actual: format!("n={n} → ⌊n/2⌋={half}"),
});
}
let b_count = stab_config.n_resamples;
let per_resample: Vec<Vec<bool>> = iter_maybe_parallel!(0..b_count)
.map(|b| -> Result<Vec<bool>, FdarError> {
let mut rng = StdRng::seed_from_u64(stab_config.seed.wrapping_add(b as u64));
let mut idx: Vec<usize> = (0..n).collect();
for i in 0..half {
let j = rng.gen_range(i..n);
idx.swap(i, j);
}
let sub = &idx[..half];
let sub_data = subsample_rows(data, sub);
let sub_pred = subsample_rows(predictors, sub);
let fit = boost_fosr(&sub_data, &sub_pred, argvals, boost_config)?;
let mut selected = vec![false; p];
for &j in &fit.selected_learners {
if j < p {
selected[j] = true;
}
}
Ok(selected)
})
.collect::<Result<Vec<Vec<bool>>, FdarError>>()?;
let mut counts = vec![0usize; p];
let mut total_selected = 0usize; for sel in &per_resample {
for (j, &s) in sel.iter().enumerate() {
if s {
counts[j] += 1;
total_selected += 1;
}
}
}
let selection_freq: Vec<f64> = counts.iter().map(|&c| c as f64 / b_count as f64).collect();
let stable_set: Vec<usize> = (0..p)
.filter(|&j| selection_freq[j] >= stab_config.pi_thr)
.collect();
let q = total_selected as f64 / b_count as f64;
let pfer_bound = (q * q) / ((2.0 * stab_config.pi_thr - 1.0) * p as f64);
Ok(StabilityResult {
selection_freq,
stable_set,
pi_thr: stab_config.pi_thr,
pfer_bound,
n_resamples: b_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::uniform_grid;
use std::f64::consts::PI;
fn default_boost() -> BoostingConfig {
BoostingConfig {
mstop: 5,
nu: 0.3,
nbasis: 8,
order: 4,
lfd_order: 2,
lambda: 1.0,
ncomp_x: 3,
seed: 0,
}
}
fn default_stab() -> StabilityConfig {
StabilityConfig {
n_resamples: 30,
pi_thr: 0.6,
seed: 20260824,
}
}
fn make_signal_dataset(n: usize, m: usize, p: usize) -> (FdMatrix, FdMatrix, Vec<f64>) {
let argvals = uniform_grid(m);
let mut pred = vec![0.0f64; n * p];
for i in 0..n {
pred[i] = -1.0 + 2.0 * i as f64 / (n - 1).max(1) as f64;
for j in 1..p {
pred[i + j * n] = ((i as f64 * (1.7 + j as f64) + j as f64 * 0.9).sin()) * 0.8;
}
}
let predictors = FdMatrix::from_column_major(pred.clone(), n, p).unwrap();
let mut y = vec![0.0f64; n * m];
for (t_idx, &tv) in argvals.iter().enumerate() {
let beta = (PI * tv).sin();
for i in 0..n {
let x0 = pred[i];
let noise = 0.01 * ((i as f64 * 1.23 + t_idx as f64 * 0.71).sin());
y[i + t_idx * n] = x0 * beta + noise;
}
}
(
FdMatrix::from_column_major(y, n, m).unwrap(),
predictors,
argvals,
)
}
#[test]
fn stability_selects_strong_signal() {
let (data, predictors, argvals) = make_signal_dataset(50, 15, 4);
let result = stability_selection(
&data,
&predictors,
&argvals,
&default_boost(),
&default_stab(),
)
.unwrap();
assert_eq!(result.selection_freq.len(), 4);
for j in 1..4 {
assert!(
result.selection_freq[0] > result.selection_freq[j],
"strong predictor freq {} must exceed noise predictor {j} freq {}",
result.selection_freq[0],
result.selection_freq[j]
);
}
assert!(
result.stable_set.contains(&0),
"strong predictor must be in the stable set (freq={})",
result.selection_freq[0]
);
}
#[test]
fn stability_freqs_in_range() {
let (data, predictors, argvals) = make_signal_dataset(40, 12, 3);
let result = stability_selection(
&data,
&predictors,
&argvals,
&default_boost(),
&default_stab(),
)
.unwrap();
assert!(result
.selection_freq
.iter()
.all(|&f| (0.0..=1.0).contains(&f)));
assert!(result.pfer_bound.is_finite() && result.pfer_bound >= 0.0);
}
#[test]
fn stability_is_deterministic_under_seed() {
let (data, predictors, argvals) = make_signal_dataset(44, 10, 4);
let r1 = stability_selection(
&data,
&predictors,
&argvals,
&default_boost(),
&default_stab(),
)
.unwrap();
let r2 = stability_selection(
&data,
&predictors,
&argvals,
&default_boost(),
&default_stab(),
)
.unwrap();
assert_eq!(r1.selection_freq, r2.selection_freq);
assert_eq!(r1.stable_set, r2.stable_set);
assert_eq!(r1.pfer_bound, r2.pfer_bound);
}
#[test]
fn stability_errors_on_invalid_params() {
let (data, predictors, argvals) = make_signal_dataset(30, 10, 3);
let mut bad_pi = default_stab();
bad_pi.pi_thr = 0.4; assert!(
stability_selection(&data, &predictors, &argvals, &default_boost(), &bad_pi).is_err()
);
let mut bad_b = default_stab();
bad_b.n_resamples = 0;
assert!(
stability_selection(&data, &predictors, &argvals, &default_boost(), &bad_b).is_err()
);
}
#[test]
fn stability_errors_on_tiny_n() {
let (data, predictors, argvals) = make_signal_dataset(4, 8, 2);
assert!(stability_selection(
&data,
&predictors,
&argvals,
&default_boost(),
&default_stab()
)
.is_err());
}
}