extern crate alloc;
use alloc::vec::Vec;
use crate::constants::DECILES;
use crate::math;
use crate::result::{EffectEstimate, TopQuantile};
use crate::types::{Matrix9, Vector9};
type QuantileStats = (usize, f64, f64, (f64, f64), f64);
pub fn compute_effect_estimate(delta_draws: &[Vector9], theta: f64) -> EffectEstimate {
if delta_draws.is_empty() {
return EffectEstimate::default();
}
let n = delta_draws.len();
let mut max_effects: Vec<f64> = Vec::with_capacity(n);
for delta in delta_draws {
let max_abs = delta.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
max_effects.push(max_abs);
}
let max_effect_ns = max_effects.iter().sum::<f64>() / n as f64;
max_effects.sort_by(|a, b| a.total_cmp(b));
let lo_idx = ((n as f64 * 0.025).round() as usize).min(n - 1);
let hi_idx = ((n as f64 * 0.975).round() as usize).min(n - 1);
let credible_interval_ns = (max_effects[lo_idx], max_effects[hi_idx]);
let top_quantiles = compute_top_quantiles(delta_draws, theta);
EffectEstimate {
max_effect_ns,
credible_interval_ns,
top_quantiles,
}
}
pub fn compute_top_quantiles(delta_draws: &[Vector9], theta: f64) -> Vec<TopQuantile> {
if delta_draws.is_empty() {
return Vec::new();
}
let n = delta_draws.len();
let mut quantile_stats: Vec<QuantileStats> = Vec::with_capacity(9);
for k in 0..9 {
let mut values: Vec<f64> = delta_draws.iter().map(|d| d[k]).collect();
let mean = values.iter().sum::<f64>() / n as f64;
values.sort_by(|a, b| a.total_cmp(b));
let lo_idx = ((n as f64 * 0.025).round() as usize).min(n - 1);
let hi_idx = ((n as f64 * 0.975).round() as usize).min(n - 1);
let ci = (values[lo_idx], values[hi_idx]);
let exceed_count = delta_draws.iter().filter(|d| d[k].abs() > theta).count();
let exceed_prob = exceed_count as f64 / n as f64;
quantile_stats.push((k, DECILES[k], mean, ci, exceed_prob));
}
quantile_stats.sort_by(|a, b| b.4.total_cmp(&a.4));
quantile_stats
.into_iter()
.filter(|(_, _, _, _, exceed_prob)| *exceed_prob > 0.5)
.take(3)
.map(
|(_, quantile_p, mean_ns, ci95_ns, exceed_prob)| TopQuantile {
quantile_p,
mean_ns,
ci95_ns,
exceed_prob,
},
)
.collect()
}
pub fn compute_effect_estimate_analytical(
delta_post: &Vector9,
lambda_post: &Matrix9,
theta: f64,
) -> EffectEstimate {
let max_effect_ns = delta_post.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
let max_k = delta_post
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.abs().total_cmp(&b.abs()))
.map(|(k, _)| k)
.unwrap_or(0);
let se = math::sqrt(lambda_post[(max_k, max_k)].max(1e-12));
let ci_low = (max_effect_ns - 1.96 * se).max(0.0);
let ci_high = max_effect_ns + 1.96 * se;
let top_quantiles = compute_top_quantiles_analytical(delta_post, lambda_post, theta);
EffectEstimate {
max_effect_ns,
credible_interval_ns: (ci_low, ci_high),
top_quantiles,
}
}
fn compute_top_quantiles_analytical(
delta_post: &Vector9,
lambda_post: &Matrix9,
theta: f64,
) -> Vec<TopQuantile> {
let mut quantile_stats: Vec<QuantileStats> = Vec::with_capacity(9);
for k in 0..9 {
let mean = delta_post[k];
let se = math::sqrt(lambda_post[(k, k)].max(1e-12));
let ci = (mean - 1.96 * se, mean + 1.96 * se);
let exceed_prob = compute_exceedance_prob(mean, se, theta);
quantile_stats.push((k, DECILES[k], mean, ci, exceed_prob));
}
quantile_stats.sort_by(|a, b| b.4.total_cmp(&a.4));
quantile_stats
.into_iter()
.filter(|(_, _, _, _, exceed_prob)| *exceed_prob > 0.5)
.take(3)
.map(
|(_, quantile_p, mean_ns, ci95_ns, exceed_prob)| TopQuantile {
quantile_p,
mean_ns,
ci95_ns,
exceed_prob,
},
)
.collect()
}
fn compute_exceedance_prob(mu: f64, sigma: f64, theta: f64) -> f64 {
if sigma < 1e-12 {
return if mu.abs() > theta { 1.0 } else { 0.0 };
}
let phi_upper = math::normal_cdf((theta - mu) / sigma);
let phi_lower = math::normal_cdf((-theta - mu) / sigma);
1.0 - (phi_upper - phi_lower)
}
pub fn regularize_covariance(sigma: &Matrix9) -> Matrix9 {
let trace: f64 = (0..9).map(|i| sigma[(i, i)]).sum();
let mean_var = trace / 9.0;
let min_var = (0.01 * mean_var).max(1e-10);
let jitter = 1e-10 + mean_var * 1e-8;
let mut regularized = *sigma;
for i in 0..9 {
regularized[(i, i)] = regularized[(i, i)].max(min_var) + jitter;
}
regularized
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effect_estimate_basic() {
let draws: Vec<Vector9> = (0..100)
.map(|i| {
let val = (i as f64) * 0.1;
Vector9::from_row_slice(&[val, val, val, val, val, val, val, val, val])
})
.collect();
let estimate = compute_effect_estimate(&draws, 5.0);
assert!(
estimate.max_effect_ns > 4.0,
"max effect should be significant"
);
assert!(
estimate.credible_interval_ns.0 < estimate.max_effect_ns,
"CI lower should be below mean"
);
assert!(
estimate.credible_interval_ns.1 > estimate.max_effect_ns,
"CI upper should be above mean"
);
}
#[test]
fn test_effect_estimate_empty() {
let estimate = compute_effect_estimate(&[], 5.0);
assert_eq!(estimate.max_effect_ns, 0.0);
assert!(estimate.top_quantiles.is_empty());
}
#[test]
fn test_top_quantiles_threshold() {
let draws: Vec<Vector9> = (0..100)
.map(|_| Vector9::from_row_slice(&[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0]))
.collect();
let top = compute_top_quantiles(&draws, 5.0);
assert!(!top.is_empty());
assert!((top[0].quantile_p - 0.9).abs() < 0.01);
assert!(top[0].exceed_prob > 0.99);
}
#[test]
fn test_regularize_covariance() {
let mut sigma = Matrix9::zeros();
for i in 0..9 {
sigma[(i, i)] = if i == 0 { 0.0 } else { 1.0 }; }
let regularized = regularize_covariance(&sigma);
for i in 0..9 {
assert!(
regularized[(i, i)] > 0.0,
"diagonal {} should be positive",
i
);
}
}
#[test]
fn test_exceedance_prob() {
let prob_high = compute_exceedance_prob(100.0, 10.0, 50.0);
assert!(prob_high > 0.99, "large mean should exceed threshold");
let prob_low = compute_exceedance_prob(1.0, 1.0, 50.0);
assert!(prob_low < 0.01, "small mean should not exceed threshold");
let prob_2sigma = compute_exceedance_prob(0.0, 1.0, 2.0);
assert!(
(prob_2sigma - 0.0455).abs() < 0.01,
"2σ threshold should have ~4.5% exceedance"
);
}
}