use crate::error::{MetricsError, Result};
use scirs2_core::ndarray::ArrayStatCompat;
use scirs2_core::ndarray::{Array1, Array2, Axis};
use statrs::statistics::Statistics;
#[derive(Debug, Clone)]
pub struct BayesianComparisonResults {
pub bayes_factor: f64,
pub log_bayes_factor: f64,
pub evidence_a: f64,
pub evidence_b: f64,
pub interpretation: String,
}
#[derive(Debug, Clone)]
pub struct BayesianInformationResults {
pub bic: f64,
pub waic: f64,
pub loo_cv: f64,
pub dic: f64,
pub p_waic: f64,
pub model_rank: usize,
}
#[derive(Debug, Clone)]
pub struct PosteriorPredictiveResults {
pub bayesian_p_value: f64,
pub observed_statistic: f64,
pub predicted_statistic_mean: f64,
pub predicted_statistic_std: f64,
pub tail_probability: f64,
pub model_adequate: bool,
}
#[derive(Debug, Clone)]
pub struct CredibleIntervalResults {
pub lower_bound: f64,
pub upper_bound: f64,
pub credible_level: f64,
pub posterior_mean: f64,
pub posterior_median: f64,
pub contains_null: bool,
pub hpd_interval: (f64, f64),
}
#[derive(Debug, Clone)]
pub struct BayesianModelAveragingResults {
pub averaged_prediction: Array1<f64>,
pub model_weights: Array1<f64>,
pub individual_predictions: Array2<f64>,
pub model_uncertainty: Array1<f64>,
pub total_variance: Array1<f64>,
}
pub struct BayesianModelComparison {
evidence_method: EvidenceMethod,
num_samples: usize,
}
#[derive(Debug, Clone, Copy)]
pub enum EvidenceMethod {
HarmonicMean,
ThermodynamicIntegration,
BridgeSampling,
NestedSampling,
}
impl Default for BayesianModelComparison {
fn default() -> Self {
Self::new()
}
}
impl BayesianModelComparison {
pub fn new() -> Self {
Self {
evidence_method: EvidenceMethod::HarmonicMean,
num_samples: 1000,
}
}
pub fn with_evidence_method(mut self, method: EvidenceMethod) -> Self {
self.evidence_method = method;
self
}
pub fn with_num_samples(mut self, numsamples: usize) -> Self {
self.num_samples = numsamples;
self
}
pub fn compare_models(
&self,
log_likelihood_a: &Array1<f64>,
log_likelihood_b: &Array1<f64>,
log_prior_a: Option<&Array1<f64>>,
log_prior_b: Option<&Array1<f64>>,
) -> Result<BayesianComparisonResults> {
if log_likelihood_a.len() != log_likelihood_b.len() {
return Err(MetricsError::InvalidInput(
"Likelihood arrays must have same length".to_string(),
));
}
let evidence_a = self.estimate_evidence(log_likelihood_a, log_prior_a)?;
let evidence_b = self.estimate_evidence(log_likelihood_b, log_prior_b)?;
let log_bayes_factor = evidence_a - evidence_b;
let bayes_factor = log_bayes_factor.exp();
let interpretation = Self::interpret_bayes_factor(bayes_factor);
Ok(BayesianComparisonResults {
bayes_factor,
log_bayes_factor,
evidence_a,
evidence_b,
interpretation,
})
}
fn estimate_evidence(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
) -> Result<f64> {
match self.evidence_method {
EvidenceMethod::HarmonicMean => self.harmonic_mean_evidence(log_likelihood, logprior),
EvidenceMethod::ThermodynamicIntegration => {
self.thermodynamic_integration_evidence(log_likelihood, logprior)
}
EvidenceMethod::BridgeSampling => {
self.bridge_sampling_evidence(log_likelihood, logprior)
}
EvidenceMethod::NestedSampling => {
self.nested_sampling_evidence(log_likelihood, logprior)
}
}
}
fn harmonic_mean_evidence(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
) -> Result<f64> {
if log_likelihood.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty _likelihood array".to_string(),
));
}
let log_posterior: Array1<f64> = if let Some(prior) = logprior {
if prior.len() != log_likelihood.len() {
return Err(MetricsError::InvalidInput(
"Prior and _likelihood arrays must have same length".to_string(),
));
}
log_likelihood + prior
} else {
log_likelihood.clone()
};
let max_log_posterior = log_posterior
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let sum_inv_exp: f64 = log_posterior
.iter()
.map(|&x| (-x + max_log_posterior).exp())
.sum();
let harmonic_mean_log =
-((sum_inv_exp / log_posterior.len() as f64).ln()) + max_log_posterior;
Ok(harmonic_mean_log)
}
fn thermodynamic_integration_evidence(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
) -> Result<f64> {
if log_likelihood.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty log _likelihood array".to_string(),
));
}
let numtemps = 20;
let temperatures = self.generate_temperature_schedule(numtemps)?;
let ess = self.estimate_effective_sample_size(log_likelihood)?;
let thinning_factor = (log_likelihood.len() as f64 / ess.max(1.0)).ceil() as usize;
let thinned_indices: Vec<usize> = (0..log_likelihood.len())
.step_by(thinning_factor.max(1))
.collect();
let mut mean_log_likelihoods = Vec::new();
for &beta in &temperatures {
let mean_log_like = if beta == 0.0 {
self.compute_marginal_log_likelihood(log_likelihood, logprior)?
} else {
self.compute_tempered_expectation(log_likelihood, logprior, beta, &thinned_indices)?
};
mean_log_likelihoods.push(mean_log_like);
}
let integral = self.adaptive_integration(&temperatures, &mean_log_likelihoods)?;
Ok(integral)
}
fn generate_temperature_schedule(&self, numtemps: usize) -> Result<Vec<f64>> {
if numtemps < 2 {
return Err(MetricsError::InvalidInput(
"Need at least 2 temperature points".to_string(),
));
}
let mut temperatures = Vec::with_capacity(numtemps);
for i in 0..numtemps {
let t = i as f64 / (numtemps - 1) as f64;
let beta = if t < 0.5 {
2.0 * t * t
} else {
2.0 * t - 1.0
};
temperatures.push(beta.clamp(0.0, 1.0));
}
temperatures[0] = 0.0;
temperatures[numtemps - 1] = 1.0;
Ok(temperatures)
}
fn estimate_effective_sample_size(&self, samples: &Array1<f64>) -> Result<f64> {
let n = samples.len();
if n < 4 {
return Ok(n as f64);
}
let mean = samples.mean_or(0.0);
let variance = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
if variance == 0.0 {
return Ok(n as f64);
}
let max_lag = n / 4;
let mut autocorr_sum = 1.0; let mut tau_int = 1.0;
for lag in 1..max_lag {
if n <= lag {
break;
}
let mut covariance = 0.0;
let count = n - lag;
for i in 0..count {
covariance += (samples[i] - mean) * (samples[i + lag] - mean);
}
covariance /= count as f64;
let autocorr = covariance / variance;
if autocorr < 0.01 {
break;
}
autocorr_sum += 2.0 * autocorr;
tau_int = autocorr_sum;
if lag as f64 >= 6.0 * tau_int {
break;
}
}
let ess = n as f64 / (2.0 * tau_int);
Ok(ess.max(1.0))
}
fn compute_marginal_log_likelihood(
&self,
log_likelihood: &Array1<f64>,
_log_prior: Option<&Array1<f64>>,
) -> Result<f64> {
let n = log_likelihood.len() as f64;
let harmonic_mean = if log_likelihood
.iter()
.any(|&x| x.is_infinite() || x.is_nan())
{
log_likelihood
.iter()
.filter(|&&x| x.is_finite())
.map(|&x| (-x).exp())
.sum::<f64>()
} else {
log_likelihood.iter().map(|&x| (-x).exp()).sum::<f64>()
};
if harmonic_mean > 0.0 {
Ok(-((harmonic_mean / n).ln()))
} else {
Ok(-1000.0) }
}
fn compute_tempered_expectation(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
beta: f64,
indices: &[usize],
) -> Result<f64> {
if indices.is_empty() {
return Ok(0.0);
}
let mut weighted_sum = 0.0;
let mut weight_sum = 0.0;
let max_log_like = indices
.iter()
.map(|&i| log_likelihood[i])
.fold(f64::NEG_INFINITY, f64::max);
for &i in indices {
let log_like = log_likelihood[i];
let log_prior_val = logprior.map(|lp| lp[i]).unwrap_or(0.0);
let _log_tempered_posterior = beta * log_like + log_prior_val;
let log_weight = (beta - 1.0) * (log_like - max_log_like);
let weight = log_weight.exp();
if weight.is_finite() && weight > 0.0 {
weighted_sum += weight * log_like;
weight_sum += weight;
}
}
if weight_sum > 0.0 {
Ok(weighted_sum / weight_sum)
} else {
let avg =
indices.iter().map(|&i| log_likelihood[i]).sum::<f64>() / indices.len() as f64;
Ok(avg)
}
}
fn adaptive_integration(&self, x: &[f64], y: &[f64]) -> Result<f64> {
if x.len() != y.len() || x.len() < 2 {
return Err(MetricsError::InvalidInput(
"Invalid integration data".to_string(),
));
}
let n = x.len();
let mut integral = 0.0;
if n >= 3 && n % 2 == 1 {
let h = (x[n - 1] - x[0]) / (n - 1) as f64;
integral = y[0] + y[n - 1];
for i in 1..n - 1 {
let coeff = if i % 2 == 1 { 4.0 } else { 2.0 };
integral += coeff * y[i];
}
integral *= h / 3.0;
} else {
for i in 0..n - 1 {
let h = x[i + 1] - x[i];
integral += 0.5 * h * (y[i] + y[i + 1]);
}
}
Ok(integral)
}
fn bridge_sampling_evidence(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
) -> Result<f64> {
if log_likelihood.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty log _likelihood array".to_string(),
));
}
let n_samples = log_likelihood.len();
let n_prior_samples = (n_samples / 2).max(100); let prior_samples =
self.generate_prior_samples(log_likelihood, logprior, n_prior_samples)?;
let log_evidence = self.iterative_bridge_sampling(
log_likelihood,
logprior,
&prior_samples,
20, 1e-6, )?;
Ok(log_evidence)
}
fn generate_prior_samples(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
n_samples: usize,
) -> Result<Array1<f64>> {
let mut prior_log_likes = Vec::new();
if let Some(lp) = logprior {
let weights = self.compute_prior_weights(lp)?;
for _ in 0..n_samples {
let sampled_idx = self.weighted_sample(&weights)?;
if sampled_idx < log_likelihood.len() {
prior_log_likes.push(log_likelihood[sampled_idx]);
}
}
} else {
let min_log_like = log_likelihood.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let _range = log_likelihood
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b))
- min_log_like;
for i in 0..n_samples {
let bias_factor = 0.3; let u = (i as f64 + bias_factor) / n_samples as f64;
let target_quantile = u * 0.5;
let idx = ((target_quantile * log_likelihood.len() as f64) as usize)
.min(log_likelihood.len() - 1);
prior_log_likes.push(log_likelihood[idx]);
}
}
if prior_log_likes.is_empty() {
return Err(MetricsError::InvalidInput(
"Failed to generate _prior _samples".to_string(),
));
}
Ok(Array1::from_vec(prior_log_likes))
}
fn compute_prior_weights(&self, logprior: &Array1<f64>) -> Result<Array1<f64>> {
let max_log_prior = logprior.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let weights = logprior
.iter()
.map(|&lp| (lp - max_log_prior).exp())
.collect::<Vec<f64>>();
Ok(Array1::from_vec(weights))
}
fn weighted_sample(&self, weights: &Array1<f64>) -> Result<usize> {
let total_weight: f64 = weights.sum();
if total_weight <= 0.0 {
return Ok(0); }
let n = weights.len();
let u = 0.5; let target = u * total_weight;
let mut cumsum = 0.0;
for (i, &w) in weights.iter().enumerate() {
cumsum += w;
if cumsum >= target {
return Ok(i);
}
}
Ok(n - 1)
}
fn iterative_bridge_sampling(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
prior_samples: &Array1<f64>,
max_iter: usize,
tolerance: f64,
) -> Result<f64> {
let n1 = log_likelihood.len(); let n2 = prior_samples.len();
let mut log_r = self.initialize_bridge_estimate(log_likelihood, prior_samples)?;
for _iter in 0..max_iter {
let log_r_new =
self.bridge_iteration(log_likelihood, logprior, prior_samples, log_r, n1, n2)?;
if (log_r_new - log_r).abs() < tolerance {
return Ok(log_r_new);
}
log_r = log_r_new;
}
Ok(log_r)
}
fn initialize_bridge_estimate(
&self,
posterior_log_likes: &Array1<f64>,
prior_log_likes: &Array1<f64>,
) -> Result<f64> {
let posterior_mean = posterior_log_likes.mean_or(0.0);
let prior_mean = prior_log_likes.mean_or(0.0);
Ok(posterior_mean - prior_mean)
}
fn bridge_iteration(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
prior_samples: &Array1<f64>,
log_r_current: f64,
n1: usize,
n2: usize,
) -> Result<f64> {
let s1 = n1 as f64;
let s2 = n2 as f64;
let mut num_1 = 0.0;
let mut den_1 = 0.0;
for (i, &log_like) in log_likelihood.iter().enumerate() {
let log_prior_val = logprior.map(|lp| lp[i]).unwrap_or(0.0);
let log_p1 = log_like + log_prior_val; let log_p2 = log_prior_val;
let log_denom =
self.log_sum_exp(&[(s1 * log_p1).ln() + log_r_current, (s2 * log_p2).ln()]);
let bridge_weight_1 = ((s2 * log_p2).ln() - log_denom).exp();
let bridge_weight_2 = ((s1 * log_p1).ln() + log_r_current - log_denom).exp();
if bridge_weight_1.is_finite() && bridge_weight_2.is_finite() {
num_1 += bridge_weight_1;
den_1 += bridge_weight_2;
}
}
let mut num_2 = 0.0;
let mut den_2 = 0.0;
for &prior_log_like in prior_samples.iter() {
let log_p1 = prior_log_like; let log_p2 = 0.0;
let log_denom =
self.log_sum_exp(&[(s1 * log_p1).ln() + log_r_current, (s2 * log_p2).ln()]);
let bridge_weight_1 = ((s1 * log_p1).ln() + log_r_current - log_denom).exp();
let bridge_weight_2 = ((s2 * log_p2).ln() - log_denom).exp();
if bridge_weight_1.is_finite() && bridge_weight_2.is_finite() {
num_2 += bridge_weight_1;
den_2 += bridge_weight_2;
}
}
let total_num = num_1 + num_2;
let total_den = den_1 + den_2;
if total_den > 0.0 && total_num > 0.0 {
Ok((total_num / total_den).ln())
} else {
Ok(log_r_current) }
}
fn log_sum_exp(&self, logvalues: &[f64]) -> f64 {
if logvalues.is_empty() {
return f64::NEG_INFINITY;
}
let max_val = logvalues.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
if max_val.is_infinite() {
return max_val;
}
let sum_exp: f64 = logvalues.iter().map(|&x| (x - max_val).exp()).sum();
max_val + sum_exp.ln()
}
fn nested_sampling_evidence(
&self,
log_likelihood: &Array1<f64>,
logprior: Option<&Array1<f64>>,
) -> Result<f64> {
if log_likelihood.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty _likelihood array".to_string(),
));
}
let n_samples = log_likelihood.len();
let nlive = (n_samples / 10).clamp(10, 100);
let (log_evidence, log_evidence_error) =
self.nested_sampling_integration(log_likelihood, logprior, nlive)?;
let corrected_log_evidence = self.apply_nested_sampling_corrections(
log_evidence,
log_evidence_error,
nlive,
n_samples,
)?;
Ok(corrected_log_evidence)
}
fn nested_sampling_integration(
&self,
log_likelihood: &Array1<f64>,
_log_prior: Option<&Array1<f64>>,
nlive: usize,
) -> Result<(f64, f64)> {
let mut indexed_samples: Vec<(usize, f64)> = log_likelihood
.iter()
.enumerate()
.map(|(i, &ll)| (i, ll))
.collect();
indexed_samples.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let live_start = indexed_samples.len().saturating_sub(nlive);
let mut live_points: Vec<(usize, f64)> = indexed_samples[live_start..].to_vec();
let mut log_weights = Vec::new();
let mut log_likes = Vec::new();
let mut log_prior_volumes = Vec::new();
let mut log_x = 0.0;
let n_iterations = indexed_samples.len().saturating_sub(nlive);
for iter in 0..n_iterations {
let (min_idx, min_log_like) = live_points
.iter()
.enumerate()
.min_by(|(_, (_, a)), (_, (_, b))| {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, (_idx, ll))| (i, *ll))
.unwrap_or((0, f64::NEG_INFINITY));
let shrinkage_factor = self.estimate_shrinkage_factor(nlive, iter)?;
let new_log_x = log_x + shrinkage_factor.ln();
let log_width = self.log_sum_exp(&[log_x, new_log_x]) - (2.0_f64).ln();
log_weights.push(log_width);
log_likes.push(min_log_like);
log_prior_volumes.push(log_x);
log_x = new_log_x;
if iter < n_iterations - 1 {
let replacement_idx = indexed_samples[iter].0;
let replacement_log_like = indexed_samples[iter].1;
live_points[min_idx] = (replacement_idx, replacement_log_like);
}
}
let final_log_x = log_x - (nlive as f64).ln();
for (_, log_like) in &live_points {
log_weights.push(final_log_x);
log_likes.push(*log_like);
log_prior_volumes.push(final_log_x);
}
let (log_evidence, log_evidence_error) =
self.compute_evidence_and_error(&log_weights, &log_likes, &log_prior_volumes)?;
Ok((log_evidence, log_evidence_error))
}
fn estimate_shrinkage_factor(&self, nlive: usize, iteration: usize) -> Result<f64> {
let base_shrinkage = 1.0 / nlive as f64;
let variation = 0.1 * (iteration as f64 * 0.1).sin(); let shrinkage = base_shrinkage * (1.0 + variation);
Ok(shrinkage.max(1e-10)) }
fn compute_evidence_and_error(
&self,
log_weights: &[f64],
log_likes: &[f64],
log_prior_volumes: &[f64],
) -> Result<(f64, f64)> {
if log_weights.len() != log_likes.len() || log_weights.is_empty() {
return Err(MetricsError::InvalidInput(
"Mismatched or empty arrays".to_string(),
));
}
let log_terms: Vec<f64> = log_weights
.iter()
.zip(log_likes.iter())
.map(|(&log_w, &log_l)| log_w + log_l)
.collect();
let log_evidence = self.log_sum_exp(&log_terms);
let log_evidence_error =
self.estimate_evidence_error(&log_terms, log_evidence, log_prior_volumes)?;
Ok((log_evidence, log_evidence_error))
}
fn estimate_evidence_error(
&self,
log_terms: &[f64],
log_evidence: f64,
log_prior_volumes: &[f64],
) -> Result<f64> {
if log_terms.is_empty() {
return Ok(0.0);
}
let mut relative_contributions = Vec::new();
for &log_term in log_terms {
let contribution = (log_term - log_evidence).exp();
relative_contributions.push(contribution);
}
let h_info: f64 = relative_contributions
.iter()
.filter(|&&x| x > 0.0)
.map(|&x| -x * x.ln())
.sum();
let log_volume_range = if log_prior_volumes.len() > 1 {
log_prior_volumes
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b))
- log_prior_volumes
.iter()
.fold(f64::INFINITY, |a, &b| a.min(b))
} else {
1.0
};
let log_error = 0.5 * (h_info.ln() + log_volume_range);
Ok(log_error)
}
fn apply_nested_sampling_corrections(
&self,
log_evidence: f64,
log_evidence_error: f64,
nlive: usize,
n_total: usize,
) -> Result<f64> {
let live_point_correction = -(nlive as f64).ln() / 2.0;
let sample_size_correction = if n_total > 100 {
-(n_total as f64).ln() / (2.0 * n_total as f64)
} else {
-0.01 };
let conservative_correction = -log_evidence_error.abs();
let corrected_evidence =
log_evidence + live_point_correction + sample_size_correction + conservative_correction;
Ok(corrected_evidence)
}
fn interpret_bayes_factor(bf: f64) -> String {
if bf < 1.0 {
let inv_bf = 1.0 / bf;
if inv_bf < 3.0 {
"Barely worth mentioning (favors B)".to_string()
} else if inv_bf < 10.0 {
"Substantial evidence for B".to_string()
} else if inv_bf < 30.0 {
"Strong evidence for B".to_string()
} else if inv_bf < 100.0 {
"Very strong evidence for B".to_string()
} else {
"Extreme evidence for B".to_string()
}
} else if bf < 3.0 {
"Barely worth mentioning (favors A)".to_string()
} else if bf < 10.0 {
"Substantial evidence for A".to_string()
} else if bf < 30.0 {
"Strong evidence for A".to_string()
} else if bf < 100.0 {
"Very strong evidence for A".to_string()
} else {
"Extreme evidence for A".to_string()
}
}
#[allow(dead_code)]
fn calculate_variance(&self, data: &Array1<f64>) -> Result<f64> {
if data.is_empty() {
return Ok(0.0);
}
let mean = data.mean_or(0.0);
let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
Ok(variance)
}
}
pub struct BayesianInformationCriteria {
num_samples: usize,
}
impl Default for BayesianInformationCriteria {
fn default() -> Self {
Self::new()
}
}
impl BayesianInformationCriteria {
pub fn new() -> Self {
Self { num_samples: 1000 }
}
pub fn with_num_samples(mut self, numsamples: usize) -> Self {
self.num_samples = numsamples;
self
}
pub fn evaluate_model(
&self,
log_likelihoodsamples: &Array2<f64>, num_parameters: usize,
num_observations: usize,
) -> Result<BayesianInformationResults> {
if log_likelihoodsamples.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty likelihood _samples".to_string(),
));
}
let (waic, p_waic) = self.calculate_waic(log_likelihoodsamples)?;
let loo_cv = self.calculate_loo_cv(log_likelihoodsamples)?;
let mean_log_likelihood: f64 = log_likelihoodsamples.mean_or(0.0);
let bic = -2.0 * mean_log_likelihood * num_observations as f64
+ (num_parameters as f64) * (num_observations as f64).ln();
let dic = self.calculate_dic(log_likelihoodsamples)?;
Ok(BayesianInformationResults {
bic,
waic,
loo_cv,
dic,
p_waic,
model_rank: 0, })
}
fn calculate_waic(&self, log_likelihoodsamples: &Array2<f64>) -> Result<(f64, f64)> {
let (n_samples, n_obs) = log_likelihoodsamples.dim();
if n_samples == 0 || n_obs == 0 {
return Err(MetricsError::InvalidInput(
"Empty likelihood _samples".to_string(),
));
}
let mut lppd = 0.0; let mut p_waic = 0.0;
for i in 0..n_obs {
let obs_likelihoods = log_likelihoodsamples.column(i);
let max_ll = obs_likelihoods
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let sum_exp: f64 = obs_likelihoods.iter().map(|&x| (x - max_ll).exp()).sum();
let log_mean_exp = (sum_exp / n_samples as f64).ln() + max_ll;
lppd += log_mean_exp;
let mean_ll = obs_likelihoods.mean();
let var_ll: f64 = obs_likelihoods
.iter()
.map(|&x| (x - mean_ll).powi(2))
.sum::<f64>()
/ n_samples as f64;
p_waic += var_ll;
}
let waic = -2.0 * (lppd - p_waic);
Ok((waic, p_waic))
}
fn calculate_loo_cv(&self, log_likelihoodsamples: &Array2<f64>) -> Result<f64> {
let (n_samples, n_obs) = log_likelihoodsamples.dim();
if n_samples == 0 || n_obs == 0 {
return Err(MetricsError::InvalidInput(
"Empty likelihood _samples".to_string(),
));
}
let mut loo_sum = 0.0;
for i in 0..n_obs {
let obs_likelihoods = log_likelihoodsamples.column(i);
let weights = self.calculate_psis_weights(&obs_likelihoods.to_owned())?;
let weighted_sum: f64 = obs_likelihoods
.iter()
.zip(weights.iter())
.map(|(&ll, &w)| w * ll.exp())
.sum();
let weight_sum: f64 = weights.sum();
if weight_sum > 1e-10 {
loo_sum += (weighted_sum / weight_sum).ln();
}
}
Ok(-2.0 * loo_sum)
}
fn calculate_dic(&self, log_likelihoodsamples: &Array2<f64>) -> Result<f64> {
let mean_deviance = -2.0 * log_likelihoodsamples.mean_or(0.0);
let posterior_mean_ll = log_likelihoodsamples
.mean_axis(Axis(0))
.expect("Operation failed");
let deviance_at_mean = -2.0 * posterior_mean_ll.sum();
let p_dic = mean_deviance - deviance_at_mean;
let dic = mean_deviance + p_dic;
Ok(dic)
}
fn calculate_psis_weights(&self, logweights: &Array1<f64>) -> Result<Array1<f64>> {
let n = logweights.len();
if n == 0 {
return Ok(Array1::zeros(0));
}
let max_weight = logweights.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let weights: Array1<f64> = logweights.mapv(|x| (x - max_weight).exp());
let sum_weights = weights.sum();
if sum_weights > 1e-10 {
Ok(weights / sum_weights)
} else {
Ok(Array1::from_elem(n, 1.0 / n as f64))
}
}
}
pub struct PosteriorPredictiveCheck {
test_statistic: TestStatisticType,
num_samples: usize,
}
#[derive(Debug, Clone)]
pub enum TestStatisticType {
Mean,
Variance,
Minimum,
Maximum,
Custom(String),
}
impl Default for PosteriorPredictiveCheck {
fn default() -> Self {
Self::new()
}
}
impl PosteriorPredictiveCheck {
pub fn new() -> Self {
Self {
test_statistic: TestStatisticType::Mean,
num_samples: 1000,
}
}
pub fn with_test_statistic(mut self, teststatistic: TestStatisticType) -> Self {
self.test_statistic = teststatistic;
self
}
pub fn with_num_samples(mut self, numsamples: usize) -> Self {
self.num_samples = numsamples;
self
}
pub fn check_model_adequacy(
&self,
observed_data: &Array1<f64>,
posterior_predictive_samples: &Array2<f64>, ) -> Result<PosteriorPredictiveResults> {
if posterior_predictive_samples.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty posterior predictive _samples".to_string(),
));
}
let (n_samples, n_obs) = posterior_predictive_samples.dim();
if observed_data.len() != n_obs {
return Err(MetricsError::InvalidInput(
"Observed _data length doesn't match predictive _samples".to_string(),
));
}
let observed_statistic = self.calculate_test_statistic(observed_data)?;
let mut predicted_statistics = Vec::with_capacity(n_samples);
for i in 0..n_samples {
let sample = posterior_predictive_samples.row(i).to_owned();
let statistic = self.calculate_test_statistic(&sample)?;
predicted_statistics.push(statistic);
}
let predicted_statistics = Array1::from_vec(predicted_statistics);
let predicted_statistic_std = self.calculate_std(&predicted_statistics)?;
let predicted_statistic_mean = predicted_statistics.clone().mean();
let count_extreme = predicted_statistics
.iter()
.filter(|&&x| x >= observed_statistic)
.count();
let bayesian_p_value = count_extreme as f64 / n_samples as f64;
let tail_probability = 2.0 * bayesian_p_value.min(1.0 - bayesian_p_value);
let model_adequate = bayesian_p_value > 0.05 && bayesian_p_value < 0.95;
Ok(PosteriorPredictiveResults {
bayesian_p_value,
observed_statistic,
predicted_statistic_mean,
predicted_statistic_std,
tail_probability,
model_adequate,
})
}
fn calculate_test_statistic(&self, data: &Array1<f64>) -> Result<f64> {
if data.is_empty() {
return Ok(0.0);
}
match &self.test_statistic {
TestStatisticType::Mean => Ok(data.mean_or(0.0)),
TestStatisticType::Variance => {
let mean = data.mean_or(0.0);
let variance =
data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
Ok(variance)
}
TestStatisticType::Minimum => Ok(data.iter().fold(f64::INFINITY, |a, &b| a.min(b))),
TestStatisticType::Maximum => Ok(data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b))),
TestStatisticType::Custom(_name) => {
Ok(data.mean_or(0.0))
}
}
}
fn calculate_std(&self, data: &Array1<f64>) -> Result<f64> {
if data.is_empty() {
return Ok(0.0);
}
let mean = data.mean_or(0.0);
let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
Ok(variance.sqrt())
}
}
pub struct CredibleIntervalCalculator {
credible_level: f64,
null_value: Option<f64>,
}
impl Default for CredibleIntervalCalculator {
fn default() -> Self {
Self::new()
}
}
impl CredibleIntervalCalculator {
pub fn new() -> Self {
Self {
credible_level: 0.95,
null_value: None,
}
}
pub fn with_credible_level(mut self, level: f64) -> Result<Self> {
if level <= 0.0 || level >= 1.0 {
return Err(MetricsError::InvalidInput(
"Credible level must be between 0 and 1".to_string(),
));
}
self.credible_level = level;
Ok(self)
}
pub fn with_null_value(mut self, nullvalue: f64) -> Self {
self.null_value = Some(nullvalue);
self
}
pub fn calculate_intervals(
&self,
posterior_samples: &Array1<f64>,
) -> Result<CredibleIntervalResults> {
if posterior_samples.is_empty() {
return Err(MetricsError::InvalidInput(
"Empty posterior _samples".to_string(),
));
}
let mut sortedsamples = posterior_samples.to_vec();
sortedsamples.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
let n = sortedsamples.len();
let alpha = 1.0 - self.credible_level;
let lower_idx = ((alpha / 2.0) * n as f64).floor() as usize;
let upper_idx = ((1.0 - alpha / 2.0) * n as f64).ceil() as usize - 1;
let lower_bound = sortedsamples[lower_idx.min(n - 1)];
let upper_bound = sortedsamples[upper_idx.min(n - 1)];
let posterior_mean = posterior_samples.mean_or(0.0);
let posterior_median = if n.is_multiple_of(2) {
(sortedsamples[n / 2 - 1] + sortedsamples[n / 2]) / 2.0
} else {
sortedsamples[n / 2]
};
let contains_null = if let Some(null_val) = self.null_value {
null_val >= lower_bound && null_val <= upper_bound
} else {
false
};
let hpd_interval = self.calculate_hpd_interval(&sortedsamples)?;
Ok(CredibleIntervalResults {
lower_bound,
upper_bound,
credible_level: self.credible_level,
posterior_mean,
posterior_median,
contains_null,
hpd_interval,
})
}
fn calculate_hpd_interval(&self, sortedsamples: &[f64]) -> Result<(f64, f64)> {
let n = sortedsamples.len();
let interval_length = (self.credible_level * n as f64).round() as usize;
if interval_length >= n {
return Ok((sortedsamples[0], sortedsamples[n - 1]));
}
let mut min_width = f64::INFINITY;
let mut best_lower = sortedsamples[0];
let mut best_upper = sortedsamples[n - 1];
for i in 0..=(n - interval_length) {
let lower = sortedsamples[i];
let upper = sortedsamples[i + interval_length - 1];
let width = upper - lower;
if width < min_width {
min_width = width;
best_lower = lower;
best_upper = upper;
}
}
Ok((best_lower, best_upper))
}
}
pub struct BayesianModelAveraging {
weighting_method: ModelWeightingMethod,
}
#[derive(Debug, Clone, Copy)]
pub enum ModelWeightingMethod {
MarginalLikelihood,
InformationCriteria,
CrossValidation,
Equal,
}
impl Default for BayesianModelAveraging {
fn default() -> Self {
Self::new()
}
}
impl BayesianModelAveraging {
pub fn new() -> Self {
Self {
weighting_method: ModelWeightingMethod::InformationCriteria,
}
}
pub fn with_weighting_method(mut self, method: ModelWeightingMethod) -> Self {
self.weighting_method = method;
self
}
pub fn average_models(
&self,
predictions: &Array2<f64>, modelscores: &Array1<f64>, ) -> Result<BayesianModelAveragingResults> {
let (n_models, n_obs) = predictions.dim();
if modelscores.len() != n_models {
return Err(MetricsError::InvalidInput(
"Number of model _scores must match number of models".to_string(),
));
}
if n_models == 0 || n_obs == 0 {
return Err(MetricsError::InvalidInput(
"Empty predictions array".to_string(),
));
}
let model_weights = self.calculate_model_weights(modelscores)?;
let mut averaged_prediction = Array1::zeros(n_obs);
for i in 0..n_obs {
let mut weighted_sum = 0.0;
for j in 0..n_models {
weighted_sum += model_weights[j] * predictions[[j, i]];
}
averaged_prediction[i] = weighted_sum;
}
let mut model_uncertainty = Array1::zeros(n_obs);
for i in 0..n_obs {
let mut weighted_variance = 0.0;
for j in 0..n_models {
let diff = predictions[[j, i]] - averaged_prediction[i];
weighted_variance += model_weights[j] * diff * diff;
}
model_uncertainty[i] = weighted_variance;
}
let mut within_model_variance = Array1::<f64>::zeros(n_obs);
for i in 0..n_models {
let prediction_row = predictions.row(i);
let residual_sq = (&prediction_row - &averaged_prediction).mapv(|x| x * x);
within_model_variance = within_model_variance + residual_sq * model_weights[i];
}
let total_variance = &model_uncertainty + &within_model_variance;
Ok(BayesianModelAveragingResults {
averaged_prediction,
model_weights,
individual_predictions: predictions.clone(),
model_uncertainty,
total_variance,
})
}
fn calculate_model_weights(&self, modelscores: &Array1<f64>) -> Result<Array1<f64>> {
match self.weighting_method {
ModelWeightingMethod::MarginalLikelihood => {
self.marginal_likelihood_weights(modelscores)
}
ModelWeightingMethod::InformationCriteria => {
self.information_criteria_weights(modelscores)
}
ModelWeightingMethod::CrossValidation => self.cross_validation_weights(modelscores),
ModelWeightingMethod::Equal => {
let n = modelscores.len();
Ok(Array1::from_elem(n, 1.0 / n as f64))
}
}
}
fn marginal_likelihood_weights(
&self,
log_marginal_likelihoods: &Array1<f64>,
) -> Result<Array1<f64>> {
let max_log_ml = log_marginal_likelihoods
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let exp_weights: Array1<f64> = log_marginal_likelihoods.mapv(|x| (x - max_log_ml).exp());
let sum_weights = exp_weights.sum();
if sum_weights > 1e-10 {
Ok(exp_weights / sum_weights)
} else {
let n = log_marginal_likelihoods.len();
Ok(Array1::from_elem(n, 1.0 / n as f64))
}
}
fn information_criteria_weights(
&self,
information_criteria: &Array1<f64>,
) -> Result<Array1<f64>> {
let min_ic = information_criteria
.iter()
.fold(f64::INFINITY, |a, &b| a.min(b));
let delta_ic: Array1<f64> = information_criteria.mapv(|x| x - min_ic);
let exp_weights: Array1<f64> = delta_ic.mapv(|x| (-0.5 * x).exp());
let sum_weights = exp_weights.sum();
if sum_weights > 1e-10 {
Ok(exp_weights / sum_weights)
} else {
let n = information_criteria.len();
Ok(Array1::from_elem(n, 1.0 / n as f64))
}
}
fn cross_validation_weights(&self, cvscores: &Array1<f64>) -> Result<Array1<f64>> {
let min_score = cvscores.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let shifted_scores: Array1<f64> = cvscores.mapv(|x| x - min_score + 1e-6);
let sum_scores = shifted_scores.sum();
if sum_scores > 1e-10 {
Ok(shifted_scores / sum_scores)
} else {
let n = cvscores.len();
Ok(Array1::from_elem(n, 1.0 / n as f64))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array;
#[test]
fn test_bayesian_model_comparison() {
let comparison = BayesianModelComparison::new();
let log_likelihood_a = Array1::from_vec(vec![-1.0, -1.5, -2.0, -1.2, -1.8]);
let log_likelihood_b = Array1::from_vec(vec![-2.0, -2.5, -3.0, -2.2, -2.8]);
let result = comparison
.compare_models(&log_likelihood_a, &log_likelihood_b, None, None)
.expect("Operation failed");
assert!(result.bayes_factor > 0.0);
assert!(result.evidence_a > result.evidence_b);
assert!(!result.interpretation.is_empty());
}
#[test]
fn test_bayesian_information_criteria() {
let bic_calc = BayesianInformationCriteria::new();
let log_likelihoodsamples =
Array2::from_shape_fn((5, 10), |(i, j)| -1.0 - 0.1 * i as f64 - 0.05 * j as f64);
let result = bic_calc
.evaluate_model(&log_likelihoodsamples, 3, 10)
.expect("Operation failed");
assert!(result.waic > 0.0);
assert!(result.loo_cv > 0.0);
assert!(result.bic > 0.0);
assert!(result.dic.is_finite());
assert!(result.p_waic >= 0.0);
}
#[test]
fn test_posterior_predictive_check() {
let ppc = PosteriorPredictiveCheck::new().with_test_statistic(TestStatisticType::Mean);
let observed_data = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let posterior_samples =
Array2::from_shape_fn((100, 5), |(i, j)| 1.0 + j as f64 + 0.1 * (i as f64 - 50.0));
let result = ppc
.check_model_adequacy(&observed_data, &posterior_samples)
.expect("Operation failed");
assert!(result.bayesian_p_value >= 0.0 && result.bayesian_p_value <= 1.0);
assert!(result.tail_probability >= 0.0 && result.tail_probability <= 1.0);
assert!(
!result.model_adequate
|| (result.bayesian_p_value > 0.05 && result.bayesian_p_value < 0.95)
);
}
#[test]
fn test_credible_interval_calculator() {
let ci_calc = CredibleIntervalCalculator::new()
.with_credible_level(0.95)
.expect("Operation failed")
.with_null_value(0.0);
let posterior_samples =
Array1::from_vec(vec![-0.5, -0.2, 0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5]);
let result = ci_calc
.calculate_intervals(&posterior_samples)
.expect("Operation failed");
assert!(result.lower_bound < result.upper_bound);
assert!(result.credible_level == 0.95);
assert!(result.posterior_mean > 0.0);
assert!(result.hpd_interval.0 <= result.hpd_interval.1);
}
#[test]
fn test_bayesian_model_averaging() {
let bma = BayesianModelAveraging::new()
.with_weighting_method(ModelWeightingMethod::InformationCriteria);
let predictions = Array2::from_shape_vec(
(3, 5),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 1.1, 2.1, 2.9, 4.1, 4.9, 0.9, 1.9, 3.1, 3.9, 5.1, ],
)
.expect("Operation failed");
let modelscores = Array1::from_vec(vec![100.0, 102.0, 105.0]);
let result = bma
.average_models(&predictions, &modelscores)
.expect("Operation failed");
assert_eq!(result.averaged_prediction.len(), 5);
assert_eq!(result.model_weights.len(), 3);
assert!((result.model_weights.sum() - 1.0).abs() < 1e-6);
assert_eq!(result.model_uncertainty.len(), 5);
}
#[test]
fn test_bayes_factor_interpretation() {
assert!(BayesianModelComparison::interpret_bayes_factor(0.5).contains("favors B"));
assert!(BayesianModelComparison::interpret_bayes_factor(2.0)
.contains("Barely worth mentioning"));
assert!(
BayesianModelComparison::interpret_bayes_factor(15.0).contains("Strong evidence for A")
);
assert!(BayesianModelComparison::interpret_bayes_factor(150.0)
.contains("Extreme evidence for A"));
}
#[test]
fn test_evidence_methods() {
let _comparison = BayesianModelComparison::new();
let log_likelihood = Array1::from_vec(vec![-1.0, -1.5, -2.0, -1.2, -1.8]);
let methods = vec![
EvidenceMethod::HarmonicMean,
EvidenceMethod::ThermodynamicIntegration,
EvidenceMethod::BridgeSampling,
EvidenceMethod::NestedSampling,
];
for method in methods {
let comparison_with_method =
BayesianModelComparison::new().with_evidence_method(method);
let evidence = comparison_with_method
.estimate_evidence(&log_likelihood, None)
.expect("Operation failed");
assert!(evidence.is_finite());
}
}
}