Skip to main content

antecedent_expr/
estimand.rs

1//! Identified estimand types shared by identify and estimate.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::fmt;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use antecedent_core::VariableId;
10
11use crate::ExprId;
12
13/// Typed identification method tag (wire form remains the Display string).
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
15pub enum EstimandMethod {
16    /// Classic backdoor adjustment.
17    BackdoorAdjustment,
18    /// Efficient backdoor adjustment set.
19    BackdoorEfficient,
20    /// Front-door identification.
21    FrontDoor,
22    /// Instrumental variable.
23    Iv,
24    /// Sharp regression discontinuity.
25    RdSharp,
26    /// Temporal backdoor after finite unfolding.
27    TemporalBackdoorUnfolded,
28    /// Temporal mediation — total effect.
29    TemporalMediationTotal,
30    /// Temporal mediation — direct effect.
31    TemporalMediationDirect,
32    /// Temporal mediation — mediated effect.
33    TemporalMediationMediated,
34    /// General semi-Markovian ID / IDC (Shpitser–Pearl).
35    GeneralId,
36    /// Path-restricted natural effect (Avin–Shpitser–Pearl).
37    PathSpecificNatural,
38}
39
40impl EstimandMethod {
41    /// Canonical wire / Display string.
42    #[must_use]
43    pub const fn as_str(self) -> &'static str {
44        match self {
45            Self::BackdoorAdjustment => "backdoor.adjustment",
46            Self::BackdoorEfficient => "backdoor.efficient",
47            Self::FrontDoor => "frontdoor",
48            Self::Iv => "iv",
49            Self::RdSharp => "rd.sharp",
50            Self::TemporalBackdoorUnfolded => "temporal.backdoor.unfolded",
51            Self::TemporalMediationTotal => "temporal_mediation.total",
52            Self::TemporalMediationDirect => "temporal_mediation.direct",
53            Self::TemporalMediationMediated => "temporal_mediation.mediated",
54            Self::GeneralId => "general.id",
55            Self::PathSpecificNatural => "path_specific.natural",
56        }
57    }
58
59    /// Whether this is any temporal-mediation variant.
60    #[must_use]
61    pub const fn is_temporal_mediation(self) -> bool {
62        matches!(
63            self,
64            Self::TemporalMediationTotal
65                | Self::TemporalMediationDirect
66                | Self::TemporalMediationMediated
67        )
68    }
69
70    /// Whether this is a backdoor-family adjustment estimand.
71    #[must_use]
72    pub const fn is_backdoor_family(self) -> bool {
73        matches!(self, Self::BackdoorAdjustment | Self::BackdoorEfficient)
74    }
75}
76
77impl fmt::Display for EstimandMethod {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(self.as_str())
80    }
81}
82
83impl FromStr for EstimandMethod {
84    type Err = String;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Ok(match s {
88            "backdoor.adjustment" => Self::BackdoorAdjustment,
89            "backdoor.efficient" => Self::BackdoorEfficient,
90            "frontdoor" => Self::FrontDoor,
91            "iv" => Self::Iv,
92            "rd.sharp" => Self::RdSharp,
93            "temporal.backdoor.unfolded" => Self::TemporalBackdoorUnfolded,
94            "temporal_mediation.total" => Self::TemporalMediationTotal,
95            "temporal_mediation.direct" => Self::TemporalMediationDirect,
96            "temporal_mediation.mediated" => Self::TemporalMediationMediated,
97            "general.id" => Self::GeneralId,
98            "path_specific.natural" => Self::PathSpecificNatural,
99            other => return Err(format!("unknown estimand method `{other}`")),
100        })
101    }
102}
103
104impl From<EstimandMethod> for Arc<str> {
105    fn from(value: EstimandMethod) -> Self {
106        Arc::from(value.as_str())
107    }
108}
109
110/// One identified estimand.
111///
112/// Backdoor estimands use [`Self::adjustment_set`]; IV estimands populate
113/// [`Self::instruments`]; front-door estimands populate [`Self::mediators`].
114/// Sharp RD estimands populate [`Self::rd_design`]. Unused role slices are empty.
115#[derive(Clone, Debug)]
116#[non_exhaustive]
117pub struct IdentifiedEstimand {
118    /// Method tag (wire string; parse with [`Self::method_kind`]).
119    pub method: Arc<str>,
120    /// Adjustment set (dense variable ids). Empty when not an adjustment estimand.
121    pub adjustment_set: Arc<[VariableId]>,
122    /// Instrument variables (dense ids). Empty unless IV.
123    pub instruments: Arc<[VariableId]>,
124    /// Mediator variables for front-door / two-stage. Empty unless front-door.
125    pub mediators: Arc<[VariableId]>,
126    /// Functional expression id in `arena`.
127    pub functional: ExprId,
128    /// Sharp RD design parameters (when method is `rd.sharp`).
129    pub rd_design: Option<RdDesignParams>,
130}
131
132/// Design parameters carried on a sharp-RD estimand.
133#[derive(Clone, Copy, Debug, PartialEq)]
134#[non_exhaustive]
135pub struct RdDesignParams {
136    /// Running (assignment) variable.
137    pub running_variable: VariableId,
138    /// Discontinuity cutoff.
139    pub cutoff: f64,
140    /// Symmetric bandwidth around the cutoff.
141    pub bandwidth: f64,
142}
143
144impl RdDesignParams {
145    /// Construct RD design parameters.
146    #[must_use]
147    pub const fn new(running_variable: VariableId, cutoff: f64, bandwidth: f64) -> Self {
148        Self { running_variable, cutoff, bandwidth }
149    }
150}
151
152impl IdentifiedEstimand {
153    /// Full constructor (required outside this crate because the type is `#[non_exhaustive]`).
154    #[must_use]
155    pub fn new(
156        method: impl Into<Arc<str>>,
157        adjustment_set: Arc<[VariableId]>,
158        instruments: Arc<[VariableId]>,
159        mediators: Arc<[VariableId]>,
160        functional: ExprId,
161        rd_design: Option<RdDesignParams>,
162    ) -> Self {
163        Self {
164            method: method.into(),
165            adjustment_set,
166            instruments,
167            mediators,
168            functional,
169            rd_design,
170        }
171    }
172
173    /// Parse the method tag into a typed [`EstimandMethod`].
174    ///
175    /// # Errors
176    ///
177    /// Unknown method string.
178    pub fn method_kind(&self) -> Result<EstimandMethod, String> {
179        EstimandMethod::from_str(self.method.as_ref())
180    }
181
182    /// Backdoor-style estimand with an adjustment set and empty IV/mediator roles.
183    #[must_use]
184    pub fn backdoor(
185        method: impl Into<Arc<str>>,
186        adjustment_set: Arc<[VariableId]>,
187        functional: ExprId,
188    ) -> Self {
189        Self::new(method, adjustment_set, Arc::from([]), Arc::from([]), functional, None)
190    }
191
192    /// IV estimand with instruments and empty adjustment/mediators.
193    #[must_use]
194    pub fn instrumental(
195        method: impl Into<Arc<str>>,
196        instruments: Arc<[VariableId]>,
197        functional: ExprId,
198    ) -> Self {
199        Self::new(method, Arc::from([]), instruments, Arc::from([]), functional, None)
200    }
201
202    /// Front-door estimand with mediators and empty adjustment/instruments.
203    #[must_use]
204    pub fn frontdoor(
205        method: impl Into<Arc<str>>,
206        mediators: Arc<[VariableId]>,
207        functional: ExprId,
208    ) -> Self {
209        Self::new(method, Arc::from([]), Arc::from([]), mediators, functional, None)
210    }
211
212    /// Sharp regression-discontinuity estimand (not backdoor-shaped).
213    #[must_use]
214    pub fn rd_sharp(functional: ExprId, design: RdDesignParams) -> Self {
215        Self::new(
216            Arc::from(EstimandMethod::RdSharp.as_str()),
217            Arc::from([]),
218            Arc::from([]),
219            Arc::from([]),
220            functional,
221            Some(design),
222        )
223    }
224
225    /// Temporal mediation estimand (mediators + `temporal_mediation.*` method tag).
226    #[must_use]
227    pub fn temporal_mediation(
228        method: impl Into<Arc<str>>,
229        mediators: Arc<[VariableId]>,
230        functional: ExprId,
231    ) -> Self {
232        Self::frontdoor(method, mediators, functional)
233    }
234}