use super::{
AdvancedBayesianFloat, AdvancedBayesianResult, MCMCDiagnostics, ModelFitMetrics,
PosteriorSummary, PredictiveDistribution,
};
use crate::distributions::chi_square::ChiSquare;
use crate::error::StatsResult;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
pub(crate) fn logsumexp<F: AdvancedBayesianFloat>(values: &[F]) -> F {
let max = values.iter().copied().fold(F::neg_infinity(), F::max);
if !max.is_finite() {
return max;
}
let sum = values
.iter()
.fold(F::zero(), |acc, v| acc + (*v - max).exp());
max + sum.ln()
}
fn mean<F: AdvancedBayesianFloat>(v: &Array1<F>) -> F {
if v.is_empty() {
return F::zero();
}
v.iter().fold(F::zero(), |a, b| a + *b) / F::from(v.len()).expect("len fits in any Float")
}
fn variance<F: AdvancedBayesianFloat>(v: &Array1<F>) -> F {
if v.is_empty() {
return F::zero();
}
let m = mean(v);
let ss = v.iter().fold(F::zero(), |a, x| a + (*x - m) * (*x - m));
ss / F::from(v.len()).expect("len fits in any Float")
}
fn unbiased_variance<F: AdvancedBayesianFloat>(v: &Array1<F>) -> F {
let n = v.len();
if n < 2 {
return F::zero();
}
let m = mean(v);
let ss = v.iter().fold(F::zero(), |a, x| a + (*x - m) * (*x - m));
ss / F::from(n - 1).expect("n - 1 fits in any Float")
}
fn sub_array<F: AdvancedBayesianFloat>(v: &Array1<F>, start: usize, end: usize) -> Array1<F> {
Array1::from_shape_fn(end - start, |i| v[start + i])
}
pub(crate) fn autocorrelation<F: AdvancedBayesianFloat>(
v: &Array1<F>,
max_lag: usize,
) -> Array1<F> {
let n = v.len();
let m = mean(v);
let c0 = v.iter().fold(F::zero(), |a, x| a + (*x - m) * (*x - m));
let max_lag = max_lag.min(n.saturating_sub(1));
Array1::from_shape_fn(max_lag, |lag_minus_1| {
let lag = lag_minus_1 + 1;
if c0 <= F::zero() || lag >= n {
return F::zero();
}
let mut ck = F::zero();
for t in 0..(n - lag) {
ck = ck + (v[t] - m) * (v[t + lag] - m);
}
ck / c0
})
}
pub(crate) fn effective_sample_size<F: AdvancedBayesianFloat>(v: &Array1<F>) -> F {
let n = v.len();
let n_f = F::from(n).expect("n fits in any Float");
if n < 4 {
return n_f;
}
let max_lag = (n - 1).min(1000);
let rho = autocorrelation(v, max_lag);
let mut sum_rho = F::zero();
let mut k = 0usize;
while k + 1 < rho.len() {
let pair = rho[k] + rho[k + 1];
if pair <= F::zero() {
break;
}
sum_rho = sum_rho + pair;
k += 2;
}
let denom = F::one() + F::from(2.0).expect("fits") * sum_rho;
let ess = n_f / denom.max(F::from(1e-6).expect("fits"));
ess.min(n_f).max(F::one())
}
pub(crate) fn split_rhat<F: AdvancedBayesianFloat>(v: &Array1<F>) -> F {
let n = v.len();
if n < 4 {
return F::one();
}
let half = n / 2;
let chain_a = sub_array(v, 0, half);
let chain_b = sub_array(v, n - half, n);
let mean_a = mean(&chain_a);
let mean_b = mean(&chain_b);
let overall_mean = (mean_a + mean_b) / F::from(2.0).expect("fits");
let half_f = F::from(half).expect("fits");
let half_minus1 = F::from((half - 1).max(1)).expect("fits");
let ss_a = chain_a
.iter()
.fold(F::zero(), |a, x| a + (*x - mean_a) * (*x - mean_a));
let ss_b = chain_b
.iter()
.fold(F::zero(), |a, x| a + (*x - mean_b) * (*x - mean_b));
let w = (ss_a / half_minus1 + ss_b / half_minus1) / F::from(2.0).expect("fits");
if w <= F::zero() {
return F::one();
}
let b = half_f
* ((mean_a - overall_mean) * (mean_a - overall_mean)
+ (mean_b - overall_mean) * (mean_b - overall_mean));
let var_hat = ((half_f - F::one()) / half_f) * w + b / half_f;
(var_hat / w).max(F::zero()).sqrt()
}
pub(crate) fn geweke_z<F: AdvancedBayesianFloat>(v: &Array1<F>) -> F {
let n = v.len();
if n < 10 {
return F::zero();
}
let n_a = (n / 10).max(1);
let n_b = (n / 2).max(1);
let chain_a = sub_array(v, 0, n_a);
let chain_b = sub_array(v, n - n_b, n);
let mean_a = mean(&chain_a);
let mean_b = mean(&chain_b);
let var_a = variance(&chain_a) / F::from(n_a).expect("fits");
let var_b = variance(&chain_b) / F::from(n_b).expect("fits");
let denom = (var_a + var_b).sqrt();
if denom <= F::zero() {
return F::zero();
}
(mean_a - mean_b) / denom
}
pub(crate) fn heidelberger_welch_stationary<F: AdvancedBayesianFloat>(v: &Array1<F>) -> bool {
let n = v.len();
if n < 8 {
return true;
}
let half = n / 2;
let chain_a = sub_array(v, 0, half);
let chain_b = sub_array(v, n - half, n);
let mean_a = mean(&chain_a);
let mean_b = mean(&chain_b);
let var_a = variance(&chain_a) / F::from(half).expect("fits");
let var_b = variance(&chain_b) / F::from(half).expect("fits");
let denom = (var_a + var_b).sqrt();
if denom <= F::zero() {
return true;
}
let z = ((mean_a - mean_b) / denom).abs();
z < F::from(2.0).expect("fits")
}
pub(crate) fn mc_standard_error<F: AdvancedBayesianFloat>(v: &Array1<F>, ess: F) -> F {
let sd = variance(v).sqrt();
sd / ess.max(F::one()).sqrt()
}
pub(crate) fn lppd<F: AdvancedBayesianFloat>(pointwise_loglik: &Array2<F>) -> F {
let s = pointwise_loglik.nrows();
let n = pointwise_loglik.ncols();
if s == 0 || n == 0 {
return F::zero();
}
let log_s = F::from(s).expect("fits").ln();
let mut total = F::zero();
for i in 0..n {
let col: Vec<F> = (0..s).map(|k| pointwise_loglik[[k, i]]).collect();
total = total + logsumexp(&col) - log_s;
}
total
}
pub(crate) fn p_waic<F: AdvancedBayesianFloat>(pointwise_loglik: &Array2<F>) -> F {
let s = pointwise_loglik.nrows();
let n = pointwise_loglik.ncols();
let mut total = F::zero();
for i in 0..n {
let col = Array1::from_shape_fn(s, |k| pointwise_loglik[[k, i]]);
total = total + unbiased_variance(&col);
}
total
}
pub(crate) fn dic<F: AdvancedBayesianFloat>(
pointwise_loglik: &Array2<F>,
loglik_at_point_estimate: &Array1<F>,
) -> (F, F) {
let s = pointwise_loglik.nrows().max(1);
let two = F::from(2.0).expect("fits");
let sum_ll_at_point: F = loglik_at_point_estimate
.iter()
.fold(F::zero(), |a, b| a + *b);
let d_hat = -two * sum_ll_at_point;
let mut sum_over_draws = F::zero();
for row in 0..pointwise_loglik.nrows() {
let row_sum: F =
(0..pointwise_loglik.ncols()).fold(F::zero(), |a, i| a + pointwise_loglik[[row, i]]);
sum_over_draws = sum_over_draws + row_sum;
}
let d_bar = -two * sum_over_draws / F::from(s).expect("fits");
let p_dic = d_bar - d_hat;
let dic_value = d_hat + two * p_dic;
(dic_value, p_dic)
}
fn autocorr_lag(n_draws: usize) -> usize {
20.min(n_draws.saturating_sub(1)).max(1)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn assemble_advanced_result<F: AdvancedBayesianFloat>(
posterior_samples: Array2<F>,
pointwise_loglik: &Array2<F>,
loglik_at_point_estimate: &Array1<F>,
log_marginal_likelihood: F,
predictions: PredictiveDistribution<F>,
y: &ArrayView1<F>,
) -> StatsResult<AdvancedBayesianResult<F>> {
let n_params = posterior_samples.ncols();
let n_draws = posterior_samples.nrows();
let lag = autocorr_lag(n_draws);
let mut means = Array1::<F>::zeros(n_params);
let mut stds = Array1::<F>::zeros(n_params);
let mut credible_intervals = Array2::<F>::zeros((n_params, 2));
let mut ess = Array1::<F>::zeros(n_params);
let mut rhat = Array1::<F>::zeros(n_params);
let mut autocorrelations = Array2::<F>::zeros((n_params, lag));
let mut geweke_diagnostic = Array1::<F>::zeros(n_params);
let mut heidelberger_welch = Array1::<bool>::from_elem(n_params, true);
let mut mc_errors = Array1::<F>::zeros(n_params);
for j in 0..n_params {
let col = posterior_samples.column(j).to_owned();
let m = mean(&col);
let sd = variance(&col).sqrt();
means[j] = m;
stds[j] = sd;
let mut sorted: Vec<F> = col.iter().copied().collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let lo_idx = ((n_draws as f64) * 0.025).floor() as usize;
let hi_idx = (((n_draws as f64) * 0.975).ceil() as usize).min(n_draws.saturating_sub(1));
credible_intervals[[j, 0]] = sorted.get(lo_idx).copied().unwrap_or(m);
credible_intervals[[j, 1]] = sorted.get(hi_idx).copied().unwrap_or(m);
let e = effective_sample_size(&col);
ess[j] = e;
rhat[j] = split_rhat(&col);
let ac = autocorrelation(&col, lag);
for (lag_idx, val) in ac.iter().enumerate() {
autocorrelations[[j, lag_idx]] = *val;
}
geweke_diagnostic[j] = geweke_z(&col);
heidelberger_welch[j] = heidelberger_welch_stationary(&col);
mc_errors[j] = mc_standard_error(&col, e);
}
let posterior_summary = PosteriorSummary {
means,
stds,
credible_intervals,
ess,
rhat,
};
let diagnostics = MCMCDiagnostics {
acceptance_rates: Array1::from_elem(1, F::one()),
autocorrelations,
geweke_diagnostic,
heidelberger_welch,
mc_errors,
};
let lppd_val = lppd(pointwise_loglik);
let p_waic_val = p_waic(pointwise_loglik);
let waic_val = F::from(-2.0).expect("fits") * (lppd_val - p_waic_val);
let (dic_val, _p_dic) = dic(pointwise_loglik, loglik_at_point_estimate);
let n_obs = y.len();
let mut chi2_stat = F::zero();
let mut ppl_g = F::zero();
let mut ppl_p = F::zero();
for i in 0..n_obs {
let v = predictions.variances[i].max(F::from(1e-12).expect("fits"));
let resid = y[i] - predictions.means[i];
chi2_stat = chi2_stat + (resid * resid) / v;
ppl_g = ppl_g + resid * resid;
ppl_p = ppl_p + predictions.variances[i].max(F::zero());
}
let df_val = ((n_obs as isize) - (n_params as isize)).max(1);
let df = F::from(df_val).expect("fits");
let posterior_p_value = ChiSquare::new(df, F::zero(), F::one())
.ok()
.map(|chi2| F::one() - chi2.cdf(chi2_stat))
.unwrap_or_else(|| F::from(0.5).expect("fits"));
let ppl = ppl_g + ppl_p;
let model_fit = ModelFitMetrics {
dic: dic_val,
waic: waic_val,
lppd: lppd_val,
p_eff: p_waic_val,
posterior_p_value,
log_marginal_likelihood,
ppl,
loo_cv: F::zero(),
cvic: F::zero(),
};
Ok(AdvancedBayesianResult {
posterior_samples,
posterior_summary,
diagnostics,
model_fit,
predictions,
})
}