Skip to main content

antecedent_validate/
data_subset.rs

1//! Data-subset refuter.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5#![allow(clippy::cast_precision_loss)]
6
7use std::sync::Arc;
8
9use antecedent_core::ExecutionContext;
10use antecedent_estimate::{EstimationWorkspace, LinearAdjustmentAte};
11
12use crate::common::{
13    RefutationProblem, RefutationReport, linear_estimator_no_bootstrap, refit_effect,
14    replicate_p_value, with_row_subset,
15};
16use crate::error::ValidationError;
17
18/// Randomly subset rows and re-estimate; expect the ATE to move little.
19#[derive(Clone, Debug)]
20pub struct DataSubsetRefuter {
21    /// Replicate count (fresh subset draw per replicate).
22    pub replicates: u32,
23    /// Fraction of rows kept per replicate.
24    pub subset_fraction: f64,
25    /// Pass if the subset ATE distribution is consistent with the original estimate at
26    /// this significance level (two-sided normal test on the replicates, `p >= alpha`).
27    pub alpha: f64,
28    /// Estimator used for refits (bootstrap disabled).
29    pub estimator: LinearAdjustmentAte,
30}
31
32impl Default for DataSubsetRefuter {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl DataSubsetRefuter {
39    /// Defaults: 20 replicates, 80% subset fraction, significance level 0.05.
40    #[must_use]
41    pub fn new() -> Self {
42        Self {
43            replicates: 20,
44            subset_fraction: 0.8,
45            alpha: 0.05,
46            estimator: linear_estimator_no_bootstrap(),
47        }
48    }
49
50    /// Run the data-subset refuter.
51    ///
52    /// # Errors
53    ///
54    /// Data or estimation failures.
55    pub fn refute(
56        &self,
57        problem: &RefutationProblem<'_>,
58        workspace: &mut EstimationWorkspace,
59        ctx: &ExecutionContext,
60    ) -> Result<RefutationReport, ValidationError> {
61        if self.replicates < 2 {
62            return Err(ValidationError::NotApplicable {
63                message: "data subset requires replicates >= 2",
64            });
65        }
66        if !(self.subset_fraction > 0.0 && self.subset_fraction < 1.0) {
67            return Err(ValidationError::NotApplicable {
68                message: "data subset requires subset_fraction in (0, 1)",
69            });
70        }
71        let mut ates = Vec::with_capacity(self.replicates as usize);
72        for r in 0..self.replicates {
73            let data = with_row_subset(
74                problem.data,
75                self.subset_fraction,
76                ctx,
77                0xA7E0_0007_0000_u64.wrapping_add(u64::from(r)),
78            )?;
79            let est = refit_effect(
80                problem,
81                &data,
82                problem.estimand,
83                &[],
84                &self.estimator,
85                workspace,
86                ctx,
87            )?;
88            ates.push(est.ate);
89        }
90        let mean_ate = ates.iter().sum::<f64>() / f64::from(self.replicates);
91        let p_value = replicate_p_value(&ates, problem.original.ate);
92        let passed = p_value >= self.alpha;
93        Ok(RefutationReport {
94            refuter: Arc::from("data.subset"),
95            original_ate: problem.original.ate,
96            refuted_ate: mean_ate,
97            comparison: p_value,
98            informative: true,
99            passed,
100            failure_condition: if passed {
101                None
102            } else {
103                Some(Arc::from(format!(
104                    "subset ATE distribution (mean {mean_ate}) is inconsistent with the \
105                     original estimate (p={p_value} < alpha={}) across {}% subsets",
106                    self.alpha,
107                    self.subset_fraction * 100.0
108                )))
109            },
110            replicates: self.replicates,
111        })
112    }
113}