#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
use std::sync::Arc;
use antecedent_core::ExecutionContext;
use antecedent_estimate::{EstimationWorkspace, LinearAdjustmentAte};
use antecedent_kernels::shuffle;
use crate::common::{
NoiseReplaceTarget, RefutationProblem, RefutationReport, float64_full,
linear_estimator_no_bootstrap, noise_replace_refute, refit_effect, replicate_p_value,
with_replaced_float,
};
use crate::error::ValidationError;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PlaceboMode {
#[default]
RandomGaussian,
Permute,
}
#[derive(Clone, Debug)]
pub struct PlaceboTreatment {
pub replicates: u32,
pub alpha: f64,
pub estimator: LinearAdjustmentAte,
pub mode: PlaceboMode,
}
impl Default for PlaceboTreatment {
fn default() -> Self {
Self::new()
}
}
impl PlaceboTreatment {
#[must_use]
pub fn new() -> Self {
Self {
replicates: 20,
alpha: 0.05,
estimator: linear_estimator_no_bootstrap(),
mode: PlaceboMode::RandomGaussian,
}
}
pub fn refute(
&self,
problem: &RefutationProblem<'_>,
workspace: &mut EstimationWorkspace,
ctx: &ExecutionContext,
) -> Result<RefutationReport, ValidationError> {
match self.mode {
PlaceboMode::RandomGaussian => noise_replace_refute(
problem,
workspace,
ctx,
&self.estimator,
self.replicates,
self.alpha,
NoiseReplaceTarget::Treatment,
0xA7E0_0001_0000,
"placebo.treatment",
"placebo",
),
PlaceboMode::Permute => self.refute_permute(problem, workspace, ctx),
}
}
fn refute_permute(
&self,
problem: &RefutationProblem<'_>,
workspace: &mut EstimationWorkspace,
ctx: &ExecutionContext,
) -> Result<RefutationReport, ValidationError> {
if self.replicates < 2 {
return Err(ValidationError::NotApplicable {
message: "placebo permute refuter requires replicates >= 2",
});
}
let treatment = problem.treatment();
let factual = float64_full(problem.data, treatment)?;
let mut ates = Vec::with_capacity(self.replicates as usize);
for r in 0..self.replicates {
let mut perm = factual.clone();
let mut rng = ctx.rng.stream(0xA7E0_0001_1000_u64.wrapping_add(u64::from(r)));
shuffle(&mut rng, &mut perm);
let data = with_replaced_float(problem.data, treatment, Arc::from(perm))?;
let est = refit_effect(problem, &data, problem.estimand, &[], workspace, ctx)?;
ates.push(est.ate);
}
let mean_ate = ates.iter().sum::<f64>() / f64::from(self.replicates);
let p_value = replicate_p_value(&ates, 0.0);
let passed = p_value >= self.alpha;
Ok(RefutationReport {
refuter: Arc::from("placebo.treatment.permute"),
original_ate: problem.original.ate,
refuted_ate: mean_ate,
comparison: p_value,
informative: true,
passed,
failure_condition: (!passed).then(|| {
Arc::from(format!(
"placebo permute ATE distribution (mean {mean_ate}) is inconsistent with zero \
(p={p_value} < alpha={})",
self.alpha
))
}),
replicates: self.replicates,
})
}
}