#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
use std::sync::Arc;
use antecedent_core::ExecutionContext;
use antecedent_data::TableView;
use antecedent_estimate::{EstimationWorkspace, LinearAdjustmentAte};
use crate::common::{
RefutationProblem, RefutationReport, complete_case_rows, fill_gaussian, float64_full,
linear_estimator_no_bootstrap, masked_sample_sd, refit_effect, with_replaced_float,
};
use crate::error::ValidationError;
#[derive(Clone, Debug)]
pub struct UnobservedCommonCause {
pub replicates: u32,
pub effect_on_treatment: f64,
pub effect_on_outcome: f64,
pub std_delta_threshold: f64,
pub estimator: LinearAdjustmentAte,
}
impl Default for UnobservedCommonCause {
fn default() -> Self {
Self::new()
}
}
impl UnobservedCommonCause {
#[must_use]
pub fn new() -> Self {
Self {
replicates: 20,
effect_on_treatment: 0.5,
effect_on_outcome: 0.5,
std_delta_threshold: 1.0,
estimator: linear_estimator_no_bootstrap(),
}
}
pub fn refute(
&self,
problem: &RefutationProblem<'_>,
workspace: &mut EstimationWorkspace,
ctx: &ExecutionContext,
) -> Result<RefutationReport, ValidationError> {
if self.replicates == 0 {
return Err(ValidationError::NotApplicable {
message: "unobserved common cause requires replicates > 0",
});
}
if !matches!(
problem.estimand.method_kind().ok(),
Some(
antecedent_expr::EstimandMethod::BackdoorAdjustment
| antecedent_expr::EstimandMethod::TemporalBackdoorUnfolded
)
) {
return Err(ValidationError::NotApplicable {
message: "unobserved common cause requires backdoor.adjustment or \
temporal.backdoor.unfolded",
});
}
let n = problem.data.row_count();
let t0 = float64_full(problem.data, problem.treatment())?;
let y0 = float64_full(problem.data, problem.outcome())?;
let mut ids = vec![problem.treatment(), problem.outcome()];
if problem.temporal.is_none() {
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 (kt, ky) = (self.effect_on_treatment * sd_t, self.effect_on_outcome * sd_y);
let mut u = vec![0.0; n];
let mut sum_delta = 0.0;
let mut sum_ate = 0.0;
for r in 0..self.replicates {
fill_gaussian(&mut u, ctx, 0xA7E0_0006_0000_u64.wrapping_add(u64::from(r)));
let t: Vec<f64> = t0.iter().zip(&u).map(|(&t, &u)| t + kt * u).collect();
let y: Vec<f64> = y0.iter().zip(&u).map(|(&y, &u)| y + ky * u).collect();
let data = with_replaced_float(problem.data, problem.treatment(), Arc::from(t))?;
let data = with_replaced_float(&data, problem.outcome(), Arc::from(y))?;
let est = refit_effect(
problem,
&data,
problem.estimand,
&[],
&self.estimator,
workspace,
ctx,
)?;
sum_delta += (est.ate - problem.original.ate).abs();
sum_ate += est.ate;
}
let mean_delta = sum_delta / f64::from(self.replicates);
let mean_ate = sum_ate / f64::from(self.replicates);
let std_delta = mean_delta / (sd_y / sd_t);
let passed = std_delta < self.std_delta_threshold;
Ok(RefutationReport {
refuter: Arc::from("unobserved.common_cause"),
original_ate: problem.original.ate,
refuted_ate: mean_ate,
comparison: std_delta,
informative: true,
passed,
failure_condition: if passed {
None
} else {
Some(Arc::from(format!(
"standardized mean |ΔATE|={std_delta} exceeded threshold {} under simulated \
confounding (effect_on_treatment={} sd, effect_on_outcome={} sd)",
self.std_delta_threshold, self.effect_on_treatment, self.effect_on_outcome
)))
},
replicates: self.replicates,
})
}
}