antecedent_validate/
rcc.rs1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
6
7use std::sync::Arc;
8
9use antecedent_core::{ExecutionContext, VariableId};
10use antecedent_data::TableView;
11use antecedent_estimate::{EstimationWorkspace, LinearAdjustmentAte};
12use antecedent_identify::IdentifiedEstimand;
13
14use crate::common::{
15 RefutationProblem, RefutationReport, fill_gaussian, linear_estimator_no_bootstrap,
16 refit_effect, replicate_p_value, with_extra_float,
17};
18use crate::error::ValidationError;
19
20#[derive(Clone, Debug)]
22pub struct RandomCommonCause {
23 pub replicates: u32,
25 pub alpha: f64,
28 pub estimator: LinearAdjustmentAte,
30}
31
32impl Default for RandomCommonCause {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl RandomCommonCause {
39 #[must_use]
41 pub fn new() -> Self {
42 Self { replicates: 20, alpha: 0.05, estimator: linear_estimator_no_bootstrap() }
43 }
44
45 pub fn refute(
51 &self,
52 problem: &RefutationProblem<'_>,
53 workspace: &mut EstimationWorkspace,
54 ctx: &ExecutionContext,
55 ) -> Result<RefutationReport, ValidationError> {
56 if self.replicates < 2 {
57 return Err(ValidationError::NotApplicable {
58 message: "random common cause requires replicates >= 2",
59 });
60 }
61 let method = problem.estimand.method_kind().ok();
62 let static_ok = method == Some(antecedent_expr::EstimandMethod::BackdoorAdjustment);
63 let temporal_ok = method == Some(antecedent_expr::EstimandMethod::TemporalBackdoorUnfolded)
64 && problem.temporal.is_some();
65 if !static_ok && !temporal_ok {
66 return Err(ValidationError::NotApplicable {
67 message: "random common cause requires backdoor.adjustment or temporal.backdoor.unfolded",
68 });
69 }
70 let n = problem.data.row_count();
71 let mut noise = vec![0.0; n];
72 let mut ates = Vec::with_capacity(self.replicates as usize);
73 for r in 0..self.replicates {
74 fill_gaussian(&mut noise, ctx, 0xA7E0_0002_0000_u64.wrapping_add(u64::from(r)));
75 let (data, new_id) = with_extra_float(
76 problem.data,
77 &format!("__rcc_{r}"),
78 Arc::<[f64]>::from(noise.clone()),
79 )?;
80 let est = if temporal_ok {
81 refit_effect(problem, &data, problem.estimand, &[new_id], workspace, ctx)?
82 } else {
83 let estimand = extend_adjustment(problem.estimand, new_id);
84 refit_effect(problem, &data, &estimand, &[], workspace, ctx)?
85 };
86 ates.push(est.ate);
87 }
88 let mean_ate = ates.iter().sum::<f64>() / f64::from(self.replicates);
89 let p_value = replicate_p_value(&ates, problem.original.ate);
90 let passed = p_value >= self.alpha;
91 Ok(RefutationReport {
92 refuter: Arc::from("random.common_cause"),
93 original_ate: problem.original.ate,
94 refuted_ate: mean_ate,
95 comparison: p_value,
96 informative: true,
97 passed,
98 failure_condition: if passed {
99 None
100 } else {
101 Some(Arc::from(format!(
102 "refit ATE distribution (mean {mean_ate}) is inconsistent with the \
103 original estimate (p={p_value} < alpha={})",
104 self.alpha
105 )))
106 },
107 replicates: self.replicates,
108 })
109 }
110}
111
112fn extend_adjustment(base: &IdentifiedEstimand, extra: VariableId) -> IdentifiedEstimand {
113 let mut zs: Vec<VariableId> = base.adjustment_set.to_vec();
114 zs.push(extra);
115 IdentifiedEstimand::new(
116 Arc::clone(&base.method),
117 Arc::from(zs),
118 Arc::clone(&base.instruments),
119 Arc::clone(&base.mediators),
120 base.functional,
121 base.rd_design,
122 )
123}