use std::sync::Arc;
use antecedent_core::ExecutionContext;
use antecedent_estimate::{EstimationWorkspace, LinearAdjustmentAte};
use antecedent_identify::IdentifiedEstimand;
use crate::common::{
RefutationProblem, RefutationReport, complete_case_rows, linear_estimator_no_bootstrap,
masked_sample_sd, refit_effect,
};
use crate::error::ValidationError;
#[derive(Clone, Debug)]
pub struct GraphRefuter {
pub rel_delta_threshold: f64,
pub estimator: LinearAdjustmentAte,
}
impl Default for GraphRefuter {
fn default() -> Self {
Self::new()
}
}
impl GraphRefuter {
#[must_use]
pub fn new() -> Self {
Self { rel_delta_threshold: 0.5, estimator: linear_estimator_no_bootstrap() }
}
pub fn refute(
&self,
problem: &RefutationProblem<'_>,
workspace: &mut EstimationWorkspace,
ctx: &ExecutionContext,
) -> Result<RefutationReport, ValidationError> {
if problem.estimand.method_kind().ok()
!= Some(antecedent_expr::EstimandMethod::BackdoorAdjustment)
{
return Err(ValidationError::NotApplicable {
message: "adjustment drop-covariate requires backdoor.adjustment estimand",
});
}
if problem.estimand.adjustment_set.is_empty() {
return Ok(RefutationReport {
refuter: Arc::from("adjustment.drop_covariate"),
original_ate: problem.original.ate,
refuted_ate: problem.original.ate,
comparison: 0.0,
informative: false,
passed: true,
failure_condition: None,
replicates: 0,
});
}
let mut ids = vec![problem.treatment(), problem.outcome()];
ids.extend_from_slice(&problem.estimand.adjustment_set);
let (mask, _valid) = complete_case_rows(problem.data, &ids)?;
let sd_t = masked_sample_sd(problem.data, problem.treatment(), &mask)?.max(1e-12);
let sd_y = masked_sample_sd(problem.data, problem.outcome(), &mask)?.max(1e-12);
let floor = 1e-3 * (sd_y / sd_t);
let mut worst_delta = 0.0_f64;
let mut worst_ate = problem.original.ate;
let mut worst_dropped = problem.estimand.adjustment_set[0];
for drop_idx in 0..problem.estimand.adjustment_set.len() {
let reduced = drop_adjustment_at(problem.estimand, drop_idx);
let est = refit_effect(problem, problem.data, &reduced, &[], workspace, ctx)?;
let delta =
(est.ate - problem.original.ate).abs() / problem.original.ate.abs().max(floor);
if delta >= worst_delta {
worst_delta = delta;
worst_ate = est.ate;
worst_dropped = problem.estimand.adjustment_set[drop_idx];
}
}
let passed = worst_delta < self.rel_delta_threshold;
Ok(RefutationReport {
refuter: Arc::from("adjustment.drop_covariate"),
original_ate: problem.original.ate,
refuted_ate: worst_ate,
comparison: worst_delta,
informative: true,
passed,
failure_condition: if passed {
None
} else {
Some(Arc::from(format!(
"relative |Ξ”ATE|={worst_delta} exceeded threshold {} after dropping \
adjustment covariate {worst_dropped:?} (leave-one-out max)",
self.rel_delta_threshold
)))
},
replicates: u32::try_from(problem.estimand.adjustment_set.len()).unwrap_or(u32::MAX),
})
}
}
fn drop_adjustment_at(base: &IdentifiedEstimand, drop_idx: usize) -> IdentifiedEstimand {
let zs: Vec<_> = base
.adjustment_set
.iter()
.copied()
.enumerate()
.filter(|(i, _)| *i != drop_idx)
.map(|(_, z)| z)
.collect();
IdentifiedEstimand::new(
Arc::clone(&base.method),
Arc::from(zs),
Arc::clone(&base.instruments),
Arc::clone(&base.mediators),
base.functional,
base.rd_design,
)
}