antecedent_validate/
overlap.rs1#![allow(clippy::cast_precision_loss)]
6
7use std::sync::Arc;
8
9use antecedent_estimate::OverlapPolicy;
10use antecedent_stats::GlmOptions;
11
12use crate::common::{RefutationProblem, RefutationReport};
13use crate::error::ValidationError;
14
15#[derive(Clone, Debug)]
24pub struct OverlapRefuter {
25 pub eps: f64,
28 pub min_ess_fraction: f64,
30 pub glm_options: GlmOptions,
32}
33
34impl Default for OverlapRefuter {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40impl OverlapRefuter {
41 #[must_use]
43 pub fn new() -> Self {
44 Self { eps: 0.05, min_ess_fraction: 0.5, glm_options: GlmOptions::default() }
45 }
46
47 pub fn refute(
56 &self,
57 problem: &RefutationProblem<'_>,
58 ) -> Result<RefutationReport, ValidationError> {
59 let mut local = antecedent_stats::PropensityWorkspace::default();
60 self.refute_with_propensity(problem, &mut local)
61 }
62
63 pub fn refute_with_propensity(
69 &self,
70 problem: &RefutationProblem<'_>,
71 propensity: &mut antecedent_stats::PropensityWorkspace,
72 ) -> Result<RefutationReport, ValidationError> {
73 let (report, replicates) = match &problem.original.overlap_report {
74 Some(r) => (r.clone(), 0),
75 None => (
76 crate::common::diagnostic_overlap_report_with(
77 problem,
78 &self.glm_options,
79 OverlapPolicy::require_diagnostics(),
80 propensity,
81 )?,
82 1,
83 ),
84 };
85 let nrows = estimation_row_count(problem)? as f64;
86 let Some(ess) = report.ess else {
87 return Ok(RefutationReport {
88 refuter: Arc::from("overlap.assessment"),
89 original_ate: problem.original.ate,
90 refuted_ate: problem.original.ate,
91 comparison: f64::NAN,
92 informative: false,
93 passed: false,
94 failure_condition: Some(Arc::from(
95 "overlap report has no weights; ESS is undefined",
96 )),
97 replicates,
98 });
99 };
100 let ess_fraction = if nrows > 0.0 { ess / nrows } else { 0.0 };
101 let bounds_ok =
102 report.propensity_min >= self.eps && report.propensity_max <= 1.0 - self.eps;
103 let ess_ok = ess_fraction >= self.min_ess_fraction;
104 let passed = bounds_ok && ess_ok;
105 let comparison = 1.0 - ess_fraction;
106 Ok(RefutationReport {
107 refuter: Arc::from("overlap.assessment"),
108 original_ate: problem.original.ate,
109 refuted_ate: problem.original.ate,
110 comparison,
111 informative: true,
112 passed,
113 failure_condition: if passed {
114 None
115 } else {
116 Some(Arc::from(format!(
117 "propensity range [{}, {}] or ess_fraction={ess_fraction} failed eps={} / \
118 min_ess_fraction={}",
119 report.propensity_min, report.propensity_max, self.eps, self.min_ess_fraction
120 )))
121 },
122 replicates,
123 })
124 }
125}
126
127fn estimation_row_count(problem: &RefutationProblem<'_>) -> Result<usize, ValidationError> {
128 let mut ids = vec![problem.treatment(), problem.outcome()];
129 ids.extend_from_slice(&problem.estimand.adjustment_set);
130 let mask = problem.data.complete_case_mask(&ids).map_err(ValidationError::from)?;
131 Ok(mask.iter().filter(|&&k| k).count())
132}