use crate::base::{FeatureSelector, SelectorMixin};
use scirs2_core::ndarray::{Array1, Array2, Axis};
use sklears_core::{
error::{validate, Result as SklResult, SklearsError},
traits::{Estimator, Fit, Trained, Transform, Untrained},
types::Float,
};
use std::marker::PhantomData;
pub type ModelEnumerationResult = SklResult<(Vec<Vec<usize>>, Vec<Float>, Array1<Float>)>;
#[derive(Debug, Clone)]
pub enum PriorType {
SpikeAndSlab {
spike_var: Float,
slab_var: Float,
},
Horseshoe {
tau: Float,
},
Laplace {
scale: Float,
},
Normal {
var: Float,
},
}
#[derive(Debug, Clone)]
pub enum BayesianInferenceMethod {
VariationalBayes {
max_iter: usize,
tol: Float,
},
GibbsSampling {
n_samples: usize,
burn_in: usize,
},
ExpectationMaximization {
max_iter: usize,
tol: Float,
},
LaplaceApproximation,
}
#[derive(Debug, Clone)]
pub struct BayesianVariableSelector<State = Untrained> {
prior: PriorType,
inference: BayesianInferenceMethod,
n_features_select: Option<usize>,
inclusion_threshold: Float,
random_state: Option<u64>,
state: PhantomData<State>,
posterior_inclusion_probs_: Option<Array1<Float>>,
feature_coefficients_: Option<Array1<Float>>,
selected_features_: Option<Vec<usize>>,
n_features_: Option<usize>,
evidence_: Option<Float>,
}
impl Default for BayesianVariableSelector<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl BayesianVariableSelector<Untrained> {
pub fn new() -> Self {
Self {
prior: PriorType::SpikeAndSlab {
spike_var: 0.01,
slab_var: 1.0,
},
inference: BayesianInferenceMethod::VariationalBayes {
max_iter: 100,
tol: 1e-4,
},
n_features_select: None,
inclusion_threshold: 0.5,
random_state: None,
state: PhantomData,
posterior_inclusion_probs_: None,
feature_coefficients_: None,
selected_features_: None,
n_features_: None,
evidence_: None,
}
}
pub fn prior(mut self, prior: PriorType) -> Self {
self.prior = prior;
self
}
pub fn inference(mut self, inference: BayesianInferenceMethod) -> Self {
self.inference = inference;
self
}
pub fn n_features_select(mut self, n_features: usize) -> Self {
self.n_features_select = Some(n_features);
self
}
pub fn inclusion_threshold(mut self, threshold: Float) -> Self {
self.inclusion_threshold = threshold;
self
}
pub fn random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
fn fit_bayesian(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
) -> SklResult<(Array1<Float>, Array1<Float>, Float)> {
match &self.inference {
BayesianInferenceMethod::VariationalBayes { max_iter, tol } => {
self.variational_bayes_inference(features, target, *max_iter, *tol)
}
BayesianInferenceMethod::GibbsSampling { n_samples, burn_in } => {
self.gibbs_sampling_inference(features, target, *n_samples, *burn_in)
}
BayesianInferenceMethod::ExpectationMaximization { max_iter, tol } => {
self.em_inference(features, target, *max_iter, *tol)
}
BayesianInferenceMethod::LaplaceApproximation => {
self.laplace_approximation_inference(features, target)
}
}
}
fn variational_bayes_inference(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
max_iter: usize,
tol: Float,
) -> SklResult<(Array1<Float>, Array1<Float>, Float)> {
let n_features = features.ncols();
let n_samples = features.nrows();
let mut gamma = Array1::from_elem(n_features, 0.5); let mut mu = Array1::zeros(n_features); let mut sigma2 = Array1::from_elem(n_features, 1.0);
let (spike_var, slab_var) = match &self.prior {
PriorType::SpikeAndSlab {
spike_var,
slab_var,
} => (*spike_var, *slab_var),
_ => (0.01, 1.0), };
for _iter in 0..max_iter {
let gamma_old = gamma.clone();
for j in 0..n_features {
let feature_col = features.column(j);
let mut residual = target.clone();
for k in 0..n_features {
if k != j {
let feature_k = features.column(k);
for i in 0..n_samples {
residual[i] -= gamma[k] * mu[k] * feature_k[i];
}
}
}
let feature_norm = feature_col.dot(&feature_col);
let xy = feature_col.dot(&residual);
if feature_norm < 1e-10 {
gamma[j] = 0.5; mu[j] = 0.0; sigma2[j] = slab_var; continue;
}
let precision_spike = 1.0 / spike_var + feature_norm;
let precision_slab = 1.0 / slab_var + feature_norm;
let mu_spike = xy / precision_spike;
let mu_slab = xy / precision_slab;
let sigma2_spike = 1.0 / precision_spike;
let sigma2_slab = 1.0 / precision_slab;
let log_prob_spike =
-0.5 * (mu_spike * mu_spike / sigma2_spike + (sigma2_spike / spike_var).ln());
let log_prob_slab =
-0.5 * (mu_slab * mu_slab / sigma2_slab + (sigma2_slab / slab_var).ln());
let max_log = log_prob_spike.max(log_prob_slab);
let exp_spike = (log_prob_spike - max_log).exp();
let exp_slab = (log_prob_slab - max_log).exp();
let denom = exp_spike + exp_slab;
if denom > 1e-10 {
gamma[j] = exp_slab / denom;
} else {
gamma[j] = 0.5; }
gamma[j] = gamma[j].clamp(0.0, 1.0);
mu[j] = gamma[j] * mu_slab + (1.0 - gamma[j]) * mu_spike;
sigma2[j] = gamma[j] * (sigma2_slab + mu_slab * mu_slab)
+ (1.0 - gamma[j]) * (sigma2_spike + mu_spike * mu_spike)
- mu[j] * mu[j];
}
let diff = (&gamma - &gamma_old).mapv(|x| x.abs()).sum();
if diff < tol {
break;
}
}
let evidence = self.compute_evidence(features, target, &gamma, &mu);
Ok((gamma, mu, evidence))
}
fn gibbs_sampling_inference(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
n_samples: usize,
burn_in: usize,
) -> SklResult<(Array1<Float>, Array1<Float>, Float)> {
let n_features = features.ncols();
let n_obs = features.nrows();
let mut gamma = Array1::from_elem(n_features, 0.5);
let mut coefficients = Array1::zeros(n_features);
let mut gamma_samples = Array2::zeros((n_samples, n_features));
let mut coeff_samples = Array2::zeros((n_samples, n_features));
let (_spike_var, slab_var) = match &self.prior {
PriorType::SpikeAndSlab {
spike_var,
slab_var,
} => (*spike_var, *slab_var),
_ => (0.01, 1.0),
};
let mut rng_state = self.random_state.unwrap_or(42);
for sample_idx in 0..(n_samples + burn_in) {
for j in 0..n_features {
let feature_col = features.column(j);
let mut residual = target.clone();
for k in 0..n_features {
if k != j && gamma[k] > 0.5 {
let feature_k = features.column(k);
for i in 0..n_obs {
residual[i] -= coefficients[k] * feature_k[i];
}
}
}
let xy = feature_col.dot(&residual);
let xx = feature_col.dot(&feature_col);
let log_prob_in = -0.5 * xx * slab_var / (1.0 + xx * slab_var)
* (xy / (1.0 + xx * slab_var)).powi(2);
let log_prob_out = 0.0;
let prob_in = 1.0 / (1.0 + (log_prob_out - log_prob_in).exp());
rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
let u = (rng_state as Float) / (u32::MAX as Float);
gamma[j] = if u < prob_in { 1.0 } else { 0.0 };
if gamma[j] > 0.5 {
let var_post = 1.0 / (1.0 / slab_var + xx);
let mean_post = var_post * xy;
coefficients[j] = mean_post;
} else {
coefficients[j] = 0.0;
}
}
if sample_idx >= burn_in {
let store_idx = sample_idx - burn_in;
for j in 0..n_features {
gamma_samples[[store_idx, j]] = gamma[j];
coeff_samples[[store_idx, j]] = coefficients[j];
}
}
}
let inclusion_probs = gamma_samples
.mean_axis(Axis(0))
.expect("operation should succeed");
let coeff_estimates = coeff_samples
.mean_axis(Axis(0))
.expect("operation should succeed");
let evidence = 0.0;
Ok((inclusion_probs, coeff_estimates, evidence))
}
fn em_inference(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
max_iter: usize,
tol: Float,
) -> SklResult<(Array1<Float>, Array1<Float>, Float)> {
let n_features = features.ncols();
let mut inclusion_probs = Array1::from_elem(n_features, 0.5);
let mut coefficients = Array1::zeros(n_features);
let mut noise_var = 1.0;
let (spike_var, slab_var) = match &self.prior {
PriorType::SpikeAndSlab {
spike_var,
slab_var,
} => (*spike_var, *slab_var),
_ => (0.01, 1.0),
};
for _iter in 0..max_iter {
let inclusion_probs_old = inclusion_probs.clone();
for j in 0..n_features {
let feature_col = features.column(j);
let mut residual = target.clone();
for k in 0..n_features {
if k != j {
let feature_k = features.column(k);
for i in 0..features.nrows() {
residual[i] -= inclusion_probs[k] * coefficients[k] * feature_k[i];
}
}
}
let xy = feature_col.dot(&residual);
let xx = feature_col.dot(&feature_col);
if xx < 1e-10 {
inclusion_probs[j] = 0.5; coefficients[j] = 0.0; continue;
}
let precision_in = 1.0 / slab_var + xx / noise_var;
let precision_out = 1.0 / spike_var + xx / noise_var;
let mean_in = (xy / noise_var) / precision_in;
let mean_out = (xy / noise_var) / precision_out;
let log_bf = 0.5 * (precision_out / precision_in).ln()
+ 0.5
* (mean_in * mean_in * precision_in - mean_out * mean_out * precision_out);
let prob = 1.0 / (1.0 + (-log_bf).exp());
inclusion_probs[j] = prob.clamp(0.0, 1.0); coefficients[j] = inclusion_probs[j] * mean_in;
}
let mut sse = 0.0;
for i in 0..features.nrows() {
let mut pred = 0.0;
for j in 0..n_features {
pred += inclusion_probs[j] * coefficients[j] * features[[i, j]];
}
sse += (target[i] - pred).powi(2);
}
noise_var = (sse / features.nrows() as Float).max(1e-10);
let diff = (&inclusion_probs - &inclusion_probs_old)
.mapv(|x| x.abs())
.sum();
if diff < tol {
break;
}
}
let evidence = self.compute_evidence(features, target, &inclusion_probs, &coefficients);
Ok((inclusion_probs, coefficients, evidence))
}
fn laplace_approximation_inference(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
) -> SklResult<(Array1<Float>, Array1<Float>, Float)> {
let n_features = features.ncols();
let mut coefficients = Array1::zeros(n_features);
let mut inclusion_probs = Array1::from_elem(n_features, 0.5);
let xtx = features.t().dot(features);
let xty = features.t().dot(target);
let lambda = 0.1; let mut xtx_reg = xtx.clone();
for i in 0..n_features {
xtx_reg[[i, i]] += lambda;
}
for j in 0..n_features {
if xtx_reg[[j, j]] != 0.0 {
coefficients[j] = xty[j] / xtx_reg[[j, j]];
}
}
let coeff_threshold = coefficients.mapv(|x| x.abs()).mean().unwrap_or(0.0);
for j in 0..n_features {
inclusion_probs[j] = if coefficients[j].abs() > coeff_threshold {
0.8
} else {
0.2
};
}
let evidence = self.compute_evidence(features, target, &inclusion_probs, &coefficients);
Ok((inclusion_probs, coefficients, evidence))
}
fn compute_evidence(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
inclusion_probs: &Array1<Float>,
coefficients: &Array1<Float>,
) -> Float {
let n_samples = features.nrows() as Float;
let mut sse = 0.0;
for i in 0..features.nrows() {
let mut pred = 0.0;
for j in 0..features.ncols() {
pred += inclusion_probs[j] * coefficients[j] * features[[i, j]];
}
sse += (target[i] - pred).powi(2);
}
let k = inclusion_probs.sum();
let log_likelihood = if sse < 1e-10 {
-1e-10_f64.ln() } else {
-(sse / n_samples).ln()
};
-0.5 * n_samples * log_likelihood - 0.5 * k * n_samples.ln()
}
fn select_features_from_probabilities(&self, inclusion_probs: &Array1<Float>) -> Vec<usize> {
if let Some(n_select) = self.n_features_select {
let mut indices: Vec<usize> = (0..inclusion_probs.len()).collect();
indices.sort_by(|&a, &b| {
inclusion_probs[b]
.partial_cmp(&inclusion_probs[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
indices.truncate(n_select);
indices
} else {
inclusion_probs
.iter()
.enumerate()
.filter(|(_, &prob)| prob >= self.inclusion_threshold)
.map(|(idx, _)| idx)
.collect()
}
}
}
impl Estimator for BayesianVariableSelector<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<Array2<Float>, Array1<Float>> for BayesianVariableSelector<Untrained> {
type Fitted = BayesianVariableSelector<Trained>;
fn fit(self, features: &Array2<Float>, target: &Array1<Float>) -> SklResult<Self::Fitted> {
validate::check_consistent_length(features, target)?;
let n_features = features.ncols();
if n_features == 0 {
return Err(SklearsError::InvalidInput(
"No features provided".to_string(),
));
}
let (inclusion_probs, coefficients, evidence) = self.fit_bayesian(features, target)?;
let selected_features = self.select_features_from_probabilities(&inclusion_probs);
if selected_features.is_empty() {
return Err(SklearsError::InvalidInput(
"No features selected with current threshold".to_string(),
));
}
Ok(BayesianVariableSelector {
prior: self.prior,
inference: self.inference,
n_features_select: self.n_features_select,
inclusion_threshold: self.inclusion_threshold,
random_state: self.random_state,
state: PhantomData,
posterior_inclusion_probs_: Some(inclusion_probs),
feature_coefficients_: Some(coefficients),
selected_features_: Some(selected_features),
n_features_: Some(n_features),
evidence_: Some(evidence),
})
}
}
impl Transform<Array2<Float>> for BayesianVariableSelector<Trained> {
fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
validate::check_n_features(x, self.n_features_.expect("operation should succeed"))?;
let selected_features = self
.selected_features_
.as_ref()
.expect("operation should succeed");
let n_samples = x.nrows();
let n_selected = selected_features.len();
let mut x_new = Array2::zeros((n_samples, n_selected));
for (new_idx, &old_idx) in selected_features.iter().enumerate() {
x_new.column_mut(new_idx).assign(&x.column(old_idx));
}
Ok(x_new)
}
}
impl SelectorMixin for BayesianVariableSelector<Trained> {
fn get_support(&self) -> SklResult<Array1<bool>> {
let n_features = self.n_features_.expect("operation should succeed");
let selected_features = self
.selected_features_
.as_ref()
.expect("operation should succeed");
let mut support = Array1::from_elem(n_features, false);
for &idx in selected_features {
support[idx] = true;
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
let selected_features = self
.selected_features_
.as_ref()
.expect("operation should succeed");
Ok(indices
.iter()
.filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
.collect())
}
}
impl FeatureSelector for BayesianVariableSelector<Trained> {
fn selected_features(&self) -> &Vec<usize> {
self.selected_features_
.as_ref()
.expect("operation should succeed")
}
}
impl BayesianVariableSelector<Trained> {
pub fn inclusion_probabilities(&self) -> &Array1<Float> {
self.posterior_inclusion_probs_
.as_ref()
.expect("operation should succeed")
}
pub fn coefficients(&self) -> &Array1<Float> {
self.feature_coefficients_
.as_ref()
.expect("operation should succeed")
}
pub fn evidence(&self) -> Float {
self.evidence_.expect("operation should succeed")
}
pub fn n_features_out(&self) -> usize {
self.selected_features_
.as_ref()
.expect("operation should succeed")
.len()
}
pub fn is_feature_selected(&self, feature_idx: usize) -> bool {
self.selected_features_
.as_ref()
.expect("operation should succeed")
.contains(&feature_idx)
}
}
#[derive(Debug, Clone)]
pub struct BayesianModelAveraging<State = Untrained> {
max_models: usize,
prior_inclusion_prob: Float,
inference_method: BayesianInferenceMethod,
random_state: Option<u64>,
state: PhantomData<State>,
model_probabilities_: Option<Vec<Float>>,
model_features_: Option<Vec<Vec<usize>>>,
averaged_inclusion_probs_: Option<Array1<Float>>,
selected_features_: Option<Vec<usize>>,
n_features_: Option<usize>,
}
impl Default for BayesianModelAveraging<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl BayesianModelAveraging<Untrained> {
pub fn new() -> Self {
Self {
max_models: 1000,
prior_inclusion_prob: 0.5,
inference_method: BayesianInferenceMethod::VariationalBayes {
max_iter: 50,
tol: 1e-3,
},
random_state: None,
state: PhantomData,
model_probabilities_: None,
model_features_: None,
averaged_inclusion_probs_: None,
selected_features_: None,
n_features_: None,
}
}
pub fn max_models(mut self, max_models: usize) -> Self {
self.max_models = max_models;
self
}
pub fn prior_inclusion_prob(mut self, prob: Float) -> Self {
self.prior_inclusion_prob = prob;
self
}
pub fn inference_method(mut self, method: BayesianInferenceMethod) -> Self {
self.inference_method = method;
self
}
pub fn random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
fn enumerate_models(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
) -> ModelEnumerationResult {
let n_features = features.ncols();
let mut models = Vec::new();
let mut model_probs = Vec::new();
let max_features_per_model = (n_features / 3).clamp(1, 10);
let mut rng_state = self.random_state.unwrap_or(42);
for _ in 0..self.max_models.min(1000) {
let mut model_features = Vec::new();
for j in 0..n_features {
rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
let u = (rng_state as Float) / (u32::MAX as Float);
if u < self.prior_inclusion_prob && model_features.len() < max_features_per_model {
model_features.push(j);
}
}
if model_features.is_empty() {
model_features.push(0); }
let model_evidence = self.evaluate_model(features, target, &model_features)?;
models.push(model_features);
model_probs.push(model_evidence);
}
if model_probs.is_empty() {
return Err(SklearsError::InvalidInput(
"No models generated".to_string(),
));
}
let max_evidence = model_probs
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
if !max_evidence.is_finite() {
let uniform_prob = 1.0 / model_probs.len() as Float;
model_probs.fill(uniform_prob);
} else {
let mut total_prob = 0.0;
for prob in &mut model_probs {
*prob = (*prob - max_evidence).exp();
total_prob += *prob;
}
if total_prob <= 0.0 || !total_prob.is_finite() {
let uniform_prob = 1.0 / model_probs.len() as Float;
model_probs.fill(uniform_prob);
} else {
for prob in &mut model_probs {
*prob /= total_prob;
*prob = prob.max(0.0);
}
let new_total: Float = model_probs.iter().sum();
if new_total > 0.0 {
for prob in &mut model_probs {
*prob /= new_total;
}
}
}
}
let mut inclusion_probs = Array1::<Float>::zeros(n_features);
for (model, &prob) in models.iter().zip(model_probs.iter()) {
for &feature in model {
inclusion_probs[feature] += prob;
}
}
for prob in inclusion_probs.iter_mut() {
*prob = prob.clamp(0.0, 1.0);
}
Ok((models, model_probs, inclusion_probs))
}
fn evaluate_model(
&self,
features: &Array2<Float>,
target: &Array1<Float>,
model_features: &[usize],
) -> SklResult<Float> {
if model_features.is_empty() {
return Ok(f64::NEG_INFINITY);
}
let mut model_x = Array2::zeros((features.nrows(), model_features.len()));
for (new_idx, &old_idx) in model_features.iter().enumerate() {
model_x
.column_mut(new_idx)
.assign(&features.column(old_idx));
}
let n = features.nrows() as Float;
let k = model_features.len() as Float;
let target_var = target.var(0.0);
if target_var < 1e-12 {
return Ok(-1000.0 - k * 10.0); }
let feature_norm: Float = model_x.iter().map(|&x| x * x).sum();
if feature_norm < 1e-12 {
return Ok(-1000.0 - k * 10.0);
}
let xtx = model_x.t().dot(&model_x);
let _xty = model_x.t().dot(target);
let mut xtx_reg = xtx.clone();
for i in 0..model_features.len() {
xtx_reg[[i, i]] += 1e-6;
}
let sse = target.dot(target);
let sse_normalized = (sse / n).max(1e-12);
let log_likelihood = -0.5 * n * sse_normalized.ln();
let penalty = -0.5 * k * n.ln();
let evidence = log_likelihood + penalty;
if evidence.is_finite() {
Ok(evidence)
} else {
Ok(-1000.0 - k * 10.0)
}
}
}
impl Estimator for BayesianModelAveraging<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<Array2<Float>, Array1<Float>> for BayesianModelAveraging<Untrained> {
type Fitted = BayesianModelAveraging<Trained>;
fn fit(self, features: &Array2<Float>, target: &Array1<Float>) -> SklResult<Self::Fitted> {
validate::check_consistent_length(features, target)?;
let n_features = features.ncols();
if n_features == 0 {
return Err(SklearsError::InvalidInput(
"No features provided".to_string(),
));
}
let (models, model_probs, inclusion_probs) = self.enumerate_models(features, target)?;
let threshold = 0.5; let selected_features: Vec<usize> = inclusion_probs
.iter()
.enumerate()
.filter(|(_, &prob)| prob >= threshold)
.map(|(idx, _)| idx)
.collect();
if selected_features.is_empty() {
let mut indices: Vec<usize> = (0..n_features).collect();
indices.sort_by(|&a, &b| {
inclusion_probs[b]
.partial_cmp(&inclusion_probs[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let selected_features = indices.into_iter().take(1).collect();
return Ok(BayesianModelAveraging {
max_models: self.max_models,
prior_inclusion_prob: self.prior_inclusion_prob,
inference_method: self.inference_method,
random_state: self.random_state,
state: PhantomData,
model_probabilities_: Some(model_probs),
model_features_: Some(models),
averaged_inclusion_probs_: Some(inclusion_probs),
selected_features_: Some(selected_features),
n_features_: Some(n_features),
});
}
Ok(BayesianModelAveraging {
max_models: self.max_models,
prior_inclusion_prob: self.prior_inclusion_prob,
inference_method: self.inference_method,
random_state: self.random_state,
state: PhantomData,
model_probabilities_: Some(model_probs),
model_features_: Some(models),
averaged_inclusion_probs_: Some(inclusion_probs),
selected_features_: Some(selected_features),
n_features_: Some(n_features),
})
}
}
impl Transform<Array2<Float>> for BayesianModelAveraging<Trained> {
fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
validate::check_n_features(x, self.n_features_.expect("operation should succeed"))?;
let selected_features = self
.selected_features_
.as_ref()
.expect("operation should succeed");
let n_samples = x.nrows();
let n_selected = selected_features.len();
let mut x_new = Array2::zeros((n_samples, n_selected));
for (new_idx, &old_idx) in selected_features.iter().enumerate() {
x_new.column_mut(new_idx).assign(&x.column(old_idx));
}
Ok(x_new)
}
}
impl SelectorMixin for BayesianModelAveraging<Trained> {
fn get_support(&self) -> SklResult<Array1<bool>> {
let n_features = self.n_features_.expect("operation should succeed");
let selected_features = self
.selected_features_
.as_ref()
.expect("operation should succeed");
let mut support = Array1::from_elem(n_features, false);
for &idx in selected_features {
support[idx] = true;
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
let selected_features = self
.selected_features_
.as_ref()
.expect("operation should succeed");
Ok(indices
.iter()
.filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
.collect())
}
}
impl FeatureSelector for BayesianModelAveraging<Trained> {
fn selected_features(&self) -> &Vec<usize> {
self.selected_features_
.as_ref()
.expect("operation should succeed")
}
}
impl BayesianModelAveraging<Trained> {
pub fn model_probabilities(&self) -> &[Float] {
self.model_probabilities_
.as_ref()
.expect("operation should succeed")
}
pub fn model_features(&self) -> &[Vec<usize>] {
self.model_features_
.as_ref()
.expect("operation should succeed")
}
pub fn inclusion_probabilities(&self) -> &Array1<Float> {
self.averaged_inclusion_probs_
.as_ref()
.expect("operation should succeed")
}
pub fn n_features_out(&self) -> usize {
self.selected_features_
.as_ref()
.expect("operation should succeed")
.len()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use scirs2_core::ndarray::Array2;
fn create_test_data() -> (Array2<Float>, Array1<Float>) {
let n_samples = 100;
let n_features = 10;
let mut features = Array2::zeros((n_samples, n_features));
let mut target = Array1::zeros(n_samples);
for i in 0..n_samples {
for j in 0..n_features {
features[[i, j]] = (i as Float * 0.1 + j as Float * 0.01) % 1.0;
}
target[i] =
features[[i, 0]] + 0.5 * features[[i, 1]] + 0.1 * ((i as Float) * 0.01).sin();
}
(features, target)
}
#[test]
fn test_bayesian_variable_selector_variational() {
let (features, target) = create_test_data();
let selector = BayesianVariableSelector::new()
.prior(PriorType::SpikeAndSlab {
spike_var: 0.01,
slab_var: 1.0,
})
.inference(BayesianInferenceMethod::VariationalBayes {
max_iter: 10,
tol: 1e-3,
})
.n_features_select(3);
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
assert_eq!(trained.n_features_out(), 3);
assert!(trained.inclusion_probabilities().len() == features.ncols());
}
#[test]
fn test_bayesian_variable_selector_em() {
let (features, target) = create_test_data();
let selector = BayesianVariableSelector::new()
.inference(BayesianInferenceMethod::ExpectationMaximization {
max_iter: 10,
tol: 1e-3,
})
.inclusion_threshold(0.3);
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
assert!(trained.n_features_out() > 0);
}
#[test]
fn test_bayesian_model_averaging() {
let (features, target) = create_test_data();
let selector = BayesianModelAveraging::new()
.max_models(50)
.prior_inclusion_prob(0.3);
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
assert!(trained.n_features_out() > 0);
assert!(!trained.model_probabilities().is_empty());
}
#[test]
fn test_transform() {
let (features, target) = create_test_data();
let selector = BayesianVariableSelector::new().n_features_select(4);
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
let transformed = trained
.transform(&features)
.expect("operation should succeed");
assert_eq!(transformed.ncols(), 4);
assert_eq!(transformed.nrows(), features.nrows());
}
#[test]
fn test_horseshoe_prior() {
let (features, target) = create_test_data();
let selector = BayesianVariableSelector::new()
.prior(PriorType::Horseshoe { tau: 0.1 })
.n_features_select(3);
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
assert_eq!(trained.n_features_out(), 3);
}
#[test]
fn test_selector_mixin() {
let (features, target) = create_test_data();
let selector = BayesianVariableSelector::new().n_features_select(5);
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
let support = trained.get_support().expect("operation should succeed");
assert_eq!(support.len(), features.ncols());
assert_eq!(support.iter().filter(|&&x| x).count(), 5);
}
mod proptests {
use super::*;
fn valid_features() -> impl Strategy<Value = Array2<Float>> {
(3usize..10, 20usize..50).prop_flat_map(|(n_cols, n_rows)| {
prop::collection::vec(-5.0..5.0f64, n_rows * n_cols).prop_map(move |values| {
Array2::from_shape_vec((n_rows, n_cols), values)
.expect("operation should succeed")
})
})
}
proptest! {
#[test]
fn prop_bayesian_selector_respects_feature_count(
features in valid_features(),
n_features in 1usize..8
) {
let target = Array1::zeros(features.nrows());
let n_select = n_features.min(features.ncols());
let selector = BayesianVariableSelector::new()
.n_features_select(n_select)
.inference(BayesianInferenceMethod::VariationalBayes { max_iter: 5, tol: 1e-2 });
if let Ok(trained) = selector.fit(&features, &target) {
prop_assert_eq!(trained.n_features_out(), n_select);
prop_assert_eq!(trained.selected_features().len(), n_select);
for &idx in trained.selected_features() {
prop_assert!(idx < features.ncols());
}
}
}
#[test]
fn prop_bayesian_selector_inclusion_probabilities_valid(
features in valid_features(),
n_features in 1usize..5
) {
let target = Array1::zeros(features.nrows());
let n_select = n_features.min(features.ncols());
let selector = BayesianVariableSelector::new()
.n_features_select(n_select)
.inference(BayesianInferenceMethod::ExpectationMaximization { max_iter: 5, tol: 1e-2 });
if let Ok(trained) = selector.fit(&features, &target) {
let inclusion_probs = trained.inclusion_probabilities();
for &prob in inclusion_probs.iter() {
prop_assert!(prob >= 0.0);
prop_assert!(prob <= 1.0);
}
let selected_features = trained.selected_features();
if !selected_features.is_empty() {
let min_selected_prob = selected_features.iter()
.map(|&idx| inclusion_probs[idx])
.fold(f64::INFINITY, f64::min);
let count_above_min = inclusion_probs.iter()
.filter(|&&prob| prob >= min_selected_prob)
.count();
prop_assert!(count_above_min >= selected_features.len());
}
}
}
#[test]
fn prop_bayesian_selector_transform_preserves_shape(
features in valid_features(),
n_features in 1usize..5
) {
let target = Array1::zeros(features.nrows());
let n_select = n_features.min(features.ncols());
let selector = BayesianVariableSelector::new()
.n_features_select(n_select);
if let Ok(trained) = selector.fit(&features, &target) {
if let Ok(transformed) = trained.transform(&features) {
prop_assert_eq!(transformed.nrows(), features.nrows());
prop_assert_eq!(transformed.ncols(), n_select);
for (sample_idx, row) in transformed.rows().into_iter().enumerate() {
for (new_feat_idx, &value) in row.iter().enumerate() {
let orig_feat_idx = trained.selected_features()[new_feat_idx];
let expected = features[[sample_idx, orig_feat_idx]];
prop_assert!((value - expected).abs() < 1e-10);
}
}
}
}
}
#[test]
fn prop_bayesian_model_averaging_probabilities_sum_to_one(
features in valid_features(),
max_models in 10usize..50
) {
let target = Array1::zeros(features.nrows());
let selector = BayesianModelAveraging::new()
.max_models(max_models)
.prior_inclusion_prob(0.3);
if let Ok(trained) = selector.fit(&features, &target) {
let model_probs = trained.model_probabilities();
if !model_probs.is_empty() {
for &prob in model_probs {
prop_assert!(prob >= 0.0);
}
let sum: Float = model_probs.iter().sum();
prop_assert!((sum - 1.0).abs() < 1e-6);
}
}
}
#[test]
fn prop_bayesian_model_averaging_inclusion_probs_valid(
features in valid_features(),
max_models in 5usize..20
) {
let target = Array1::zeros(features.nrows());
let selector = BayesianModelAveraging::new()
.max_models(max_models)
.prior_inclusion_prob(0.4);
if let Ok(trained) = selector.fit(&features, &target) {
let inclusion_probs = trained.inclusion_probabilities();
for &prob in inclusion_probs.iter() {
prop_assert!(prob >= 0.0);
prop_assert!(prob <= 1.0);
}
prop_assert_eq!(inclusion_probs.len(), features.ncols());
}
}
#[test]
fn prop_prior_types_affect_selection(
features in valid_features(),
n_features in 1usize..3
) {
let target = Array1::zeros(features.nrows());
let n_select = n_features.min(features.ncols());
let priors = vec![
PriorType::SpikeAndSlab { spike_var: 0.01, slab_var: 1.0 },
PriorType::Horseshoe { tau: 0.1 },
PriorType::Laplace { scale: 1.0 },
PriorType::Normal { var: 1.0 },
];
for prior in priors {
let selector = BayesianVariableSelector::new()
.prior(prior)
.n_features_select(n_select)
.inference(BayesianInferenceMethod::LaplaceApproximation);
if let Ok(trained) = selector.fit(&features, &target) {
prop_assert_eq!(trained.n_features_out(), n_select);
prop_assert!(trained.inclusion_probabilities().len() == features.ncols());
}
}
}
#[test]
fn prop_bayesian_selector_deterministic_with_same_seed(
features in valid_features(),
n_features in 1usize..4,
seed in 1u64..1000
) {
let target = Array1::zeros(features.nrows());
let n_select = n_features.min(features.ncols());
let selector1 = BayesianVariableSelector::new()
.n_features_select(n_select)
.random_state(seed)
.inference(BayesianInferenceMethod::GibbsSampling { n_samples: 10, burn_in: 5 });
let selector2 = BayesianVariableSelector::new()
.n_features_select(n_select)
.random_state(seed)
.inference(BayesianInferenceMethod::GibbsSampling { n_samples: 10, burn_in: 5 });
if let (Ok(trained1), Ok(trained2)) = (selector1.fit(&features, &target), selector2.fit(&features, &target)) {
prop_assert_eq!(trained1.selected_features(), trained2.selected_features());
}
}
#[test]
fn prop_bayesian_selector_evidence_is_finite(
features in valid_features(),
n_features in 1usize..3
) {
let target = Array1::zeros(features.nrows());
let n_select = n_features.min(features.ncols());
let selector = BayesianVariableSelector::new()
.n_features_select(n_select);
if let Ok(trained) = selector.fit(&features, &target) {
let evidence = trained.evidence();
prop_assert!(evidence.is_finite());
}
}
}
}
}