antecedent_validate/
graph_refute.rs1use std::sync::Arc;
11
12use antecedent_core::ExecutionContext;
13use antecedent_estimate::{EstimationWorkspace, LinearAdjustmentAte};
14use antecedent_identify::IdentifiedEstimand;
15
16use crate::common::{
17 RefutationProblem, RefutationReport, complete_case_rows, linear_estimator_no_bootstrap,
18 masked_sample_sd, refit_effect,
19};
20use crate::error::ValidationError;
21
22#[derive(Clone, Debug)]
31pub struct GraphRefuter {
32 pub rel_delta_threshold: f64,
35 pub estimator: LinearAdjustmentAte,
37}
38
39impl Default for GraphRefuter {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl GraphRefuter {
46 #[must_use]
48 pub fn new() -> Self {
49 Self { rel_delta_threshold: 0.5, estimator: linear_estimator_no_bootstrap() }
50 }
51
52 pub fn refute(
58 &self,
59 problem: &RefutationProblem<'_>,
60 workspace: &mut EstimationWorkspace,
61 ctx: &ExecutionContext,
62 ) -> Result<RefutationReport, ValidationError> {
63 if problem.estimand.method_kind().ok()
64 != Some(antecedent_expr::EstimandMethod::BackdoorAdjustment)
65 {
66 return Err(ValidationError::NotApplicable {
67 message: "adjustment drop-covariate requires backdoor.adjustment estimand",
68 });
69 }
70 if problem.estimand.adjustment_set.is_empty() {
71 return Ok(RefutationReport {
72 refuter: Arc::from("adjustment.drop_covariate"),
73 original_ate: problem.original.ate,
74 refuted_ate: problem.original.ate,
75 comparison: 0.0,
76 informative: false,
77 passed: true,
78 failure_condition: None,
79 replicates: 0,
80 });
81 }
82 let mut ids = vec![problem.treatment(), problem.outcome()];
83 ids.extend_from_slice(&problem.estimand.adjustment_set);
84 let (mask, _valid) = complete_case_rows(problem.data, &ids)?;
85 let sd_t = masked_sample_sd(problem.data, problem.treatment(), &mask)?.max(1e-12);
86 let sd_y = masked_sample_sd(problem.data, problem.outcome(), &mask)?.max(1e-12);
87 let floor = 1e-3 * (sd_y / sd_t);
88
89 let mut worst_delta = 0.0_f64;
90 let mut worst_ate = problem.original.ate;
91 let mut worst_dropped = problem.estimand.adjustment_set[0];
92 for drop_idx in 0..problem.estimand.adjustment_set.len() {
93 let reduced = drop_adjustment_at(problem.estimand, drop_idx);
94 let est = refit_effect(
95 problem,
96 problem.data,
97 &reduced,
98 &[],
99 &self.estimator,
100 workspace,
101 ctx,
102 )?;
103 let delta =
106 (est.ate - problem.original.ate).abs() / problem.original.ate.abs().max(floor);
107 if delta >= worst_delta {
108 worst_delta = delta;
109 worst_ate = est.ate;
110 worst_dropped = problem.estimand.adjustment_set[drop_idx];
111 }
112 }
113 let passed = worst_delta < self.rel_delta_threshold;
114 Ok(RefutationReport {
115 refuter: Arc::from("adjustment.drop_covariate"),
116 original_ate: problem.original.ate,
117 refuted_ate: worst_ate,
118 comparison: worst_delta,
119 informative: true,
120 passed,
121 failure_condition: if passed {
122 None
123 } else {
124 Some(Arc::from(format!(
125 "relative |Ξ”ATE|={worst_delta} exceeded threshold {} after dropping \
126 adjustment covariate {worst_dropped:?} (leave-one-out max)",
127 self.rel_delta_threshold
128 )))
129 },
130 replicates: u32::try_from(problem.estimand.adjustment_set.len()).unwrap_or(u32::MAX),
131 })
132 }
133}
134
135fn drop_adjustment_at(base: &IdentifiedEstimand, drop_idx: usize) -> IdentifiedEstimand {
136 let zs: Vec<_> = base
137 .adjustment_set
138 .iter()
139 .copied()
140 .enumerate()
141 .filter(|(i, _)| *i != drop_idx)
142 .map(|(_, z)| z)
143 .collect();
144 IdentifiedEstimand::new(
145 Arc::clone(&base.method),
146 Arc::from(zs),
147 Arc::clone(&base.instruments),
148 Arc::clone(&base.mediators),
149 base.functional,
150 base.rd_design,
151 )
152}