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 \
68 temporal.backdoor.unfolded",
69 });
70 }
71 let n = problem.data.row_count();
72 let mut noise = vec![0.0; n];
73 let mut ates = Vec::with_capacity(self.replicates as usize);
74 for r in 0..self.replicates {
75 fill_gaussian(&mut noise, ctx, 0xA7E0_0002_0000_u64.wrapping_add(u64::from(r)));
76 let (data, new_id) = with_extra_float(
77 problem.data,
78 &format!("__rcc_{r}"),
79 Arc::<[f64]>::from(noise.clone()),
80 )?;
81 let est = if temporal_ok {
82 refit_effect(
83 problem,
84 &data,
85 problem.estimand,
86 &[new_id],
87 &self.estimator,
88 workspace,
89 ctx,
90 )?
91 } else {
92 let estimand = extend_adjustment(problem.estimand, new_id);
93 refit_effect(problem, &data, &estimand, &[], &self.estimator, workspace, ctx)?
94 };
95 ates.push(est.ate);
96 }
97 let mean_ate = ates.iter().sum::<f64>() / f64::from(self.replicates);
98 let p_value = replicate_p_value(&ates, problem.original.ate);
99 let passed = p_value >= self.alpha;
100 Ok(RefutationReport {
101 refuter: Arc::from("random.common_cause"),
102 original_ate: problem.original.ate,
103 refuted_ate: mean_ate,
104 comparison: p_value,
105 informative: true,
106 passed,
107 failure_condition: if passed {
108 None
109 } else {
110 Some(Arc::from(format!(
111 "refit ATE distribution (mean {mean_ate}) is inconsistent with the \
112 original estimate (p={p_value} < alpha={})",
113 self.alpha
114 )))
115 },
116 replicates: self.replicates,
117 })
118 }
119}
120
121fn extend_adjustment(base: &IdentifiedEstimand, extra: VariableId) -> IdentifiedEstimand {
122 let mut zs: Vec<VariableId> = base.adjustment_set.to_vec();
123 zs.push(extra);
124 IdentifiedEstimand::new(
125 Arc::clone(&base.method),
126 Arc::from(zs),
127 Arc::clone(&base.instruments),
128 Arc::clone(&base.mediators),
129 base.functional,
130 base.rd_design,
131 )
132}