Skip to main content

causal_hub/inference/
backdoor_criterion.rs

1use crate::{
2    models::{DiGraph, Graph},
3    set,
4    types::{Result, Set},
5};
6
7/// A trait for backdoor adjustment criterion.
8pub trait BackdoorCriterion {
9    /// Checks if the `Z` is a backdoor adjustment set for `X` and `Y`.
10    ///
11    /// # Arguments
12    ///
13    /// * `x` - A set vertices representing set `X`.
14    /// * `y` - A set vertices representing set `Y`.
15    /// * `z` - A set vertices representing set `Z`.
16    ///
17    /// # Errors
18    ///
19    /// * `IndexOutOfBounds` if any of the vertex in `X`, `Y`, or `Z` are out of bounds.
20    /// * `SetsNotDisjoint` if `X`, `Y` or `Z` are not disjoint sets.
21    /// * `EmptySet` if `X` or `Y` are empty sets.
22    ///
23    /// # Returns
24    ///
25    /// `true` if `X` and `Y` are separated by `Z`, `false` otherwise.
26    ///
27    fn is_backdoor_set(&self, x: &Set<usize>, y: &Set<usize>, z: &Set<usize>) -> Result<bool>;
28
29    /// Checks if the `Z` is a minimal backdoor adjustment set for `X` and `Y`.
30    ///
31    /// # Arguments
32    ///
33    /// * `x` - A set vertices representing set `X`.
34    /// * `y` - A set vertices representing set `Y`.
35    /// * `z` - A set vertices representing set `Z`.
36    /// * `w` - An optional iterable collection of vertices representing set `W`.
37    /// * `v` - An optional iterable collection of vertices representing set `V`.
38    ///
39    /// # Errors
40    ///
41    /// * `IndexOutOfBounds` if any of the vertex in `X`, `Y`, `Z`, `W`, or `V` are out of bounds.
42    /// * `SetsNotDisjoint` if `X`, `Y` or `Z` are not disjoint sets.
43    /// * `EmptySet` if `X` or `Y` are empty sets.
44    /// * `SubsetMismatch` if not `W` <= `Z` <= `V`.
45    ///
46    /// # Returns
47    ///
48    /// `true` if `Z` is a minimal backdoor adjustment set for `X` and `Y`, `false` otherwise.
49    ///
50    fn is_minimal_backdoor_set(
51        &self,
52        x: &Set<usize>,
53        y: &Set<usize>,
54        z: &Set<usize>,
55        w: Option<&Set<usize>>,
56        v: Option<&Set<usize>>,
57    ) -> Result<bool>;
58
59    /// Finds a minimal backdoor adjustment set for the vertex sets `X` and `Y`, if any.
60    ///
61    /// # Arguments
62    ///
63    /// * `x` - A set vertices representing set `X`.
64    /// * `y` - A set vertices representing set `Y`.
65    /// * `w` - An optional iterable collection of vertices representing set `W`.
66    /// * `v` - An optional iterable collection of vertices representing set `V`.
67    ///
68    /// # Errors
69    ///
70    /// * `IndexOutOfBounds` if any of the vertex in `X`, `Y`, `W`, or `V` are out of bounds.
71    /// * `SetsNotDisjoint` if `X` and `Y` are not disjoint sets.
72    /// * `EmptySet` if `X` or `Y` are empty sets.
73    /// * `SubsetMismatch` if not `W` <= `V`.
74    ///
75    /// # Returns
76    ///
77    /// `Some(Set)` containing the minimal backdoor adjustment set,
78    ///  or `None` if no backdoor adjustment set exists.
79    ///
80    fn find_minimal_backdoor_set(
81        &self,
82        x: &Set<usize>,
83        y: &Set<usize>,
84        w: Option<&Set<usize>>,
85        v: Option<&Set<usize>>,
86    ) -> Result<Option<Set<usize>>>;
87}
88
89mod digraph {
90    use super::*;
91    use crate::inference::{GraphicalSeparation, digraph::_assert};
92
93    // Returns the set of vertices:
94    //
95    //     PCP(X, Y) = { W \in (V \ X) | W is on a *proper possible causal path* from X to Y }
96    //
97    // where:
98    //
99    //     * possible causal path in a directed graph is a directed path from X to Y,
100    //     * proper path is a directed path from X to Y that does not contain any vertex in X.
101    //
102    fn _proper_causal_path(graph: &DiGraph, x: &Set<usize>, y: &Set<usize>) -> Result<Set<usize>> {
103        // Initialize the PCP set.
104        let mut pcp = set![];
105
106        // Perform a visit starting from each vertex in X.
107        for &x_i in x {
108            // Initialize stack and visited set.
109            let mut stack = vec![x_i];
110            let mut visited = set![x_i];
111
112            // While there are vertices to visit ...
113            while let Some(z) = stack.pop() {
114                // For each child of the current node ...
115                for w in graph.children(&set![z])? {
116                    // Skip if W is in X or already visited.
117                    if x.contains(&w) || visited.contains(&w) {
118                        continue;
119                    }
120                    // Set W as visited.
121                    visited.insert(w);
122
123                    // Skip if W is in PCP, continue search.
124                    if pcp.contains(&w) {
125                        continue;
126                    }
127                    // Add W to the PCP set.
128                    pcp.insert(w);
129
130                    // Skip if W is in Y, continue search.
131                    if y.contains(&w) {
132                        continue;
133                    }
134                    // Add W to the stack for further exploration.
135                    stack.push(w);
136                }
137            }
138        }
139
140        Ok(pcp)
141    }
142
143    // Returns the proper backdoor graph:
144    //
145    //     G^PDB = G \ { X -> PCP(X, Y) }
146    //
147    fn _proper_backdoor_graph(
148        graph: &DiGraph,
149        x: &Set<usize>,
150        pcp: &Set<usize>,
151    ) -> Result<DiGraph> {
152        // Clone the graph.
153        let mut g_pdb = graph.clone();
154        // Remove all the edge from X to PCP(X, Y).
155        x.iter()
156            .flat_map(|&i| pcp.iter().map(move |&j| (i, j)))
157            .try_for_each(|(i, j)| -> Result<_> {
158                g_pdb.del_edge(i, j)?;
159                Ok(())
160            })?;
161        // Return the modified graph.
162        Ok(g_pdb)
163    }
164
165    impl BackdoorCriterion for DiGraph {
166        fn is_backdoor_set(&self, x: &Set<usize>, y: &Set<usize>, z: &Set<usize>) -> Result<bool> {
167            // Perform sanity checks and convert sets.
168            _assert(self, x, y, Some(z), None::<&Set<_>>, None::<&Set<_>>)?;
169
170            // Constructive backdoor criterion:
171            //
172            // Z is a backdoor set for X and Y if and only if:
173            //
174            //  a) Z <= V \ pDe(PCP(X, Y)), and
175            //  b) Z separates X from Y in G^PDB.
176            //
177
178            // Compute the proper causal path.
179            let pcp = _proper_causal_path(self, x, y)?;
180            // Compute the descendants of the proper causal path.
181            let pde = self.descendants(&pcp)?;
182            // a) Check if Z is a subset of V \ pDe(PCP(X, Y)).
183            if !z.is_subset(&(&self.vertices() - &pde)) {
184                return Ok(false);
185            }
186
187            // Compute the proper backdoor graph.
188            let g_pdb = _proper_backdoor_graph(self, x, &pcp)?;
189            // b) Check if Z separates X from Y in G^PDB.
190            if !g_pdb.is_separator_set(x, y, z)? {
191                return Ok(false);
192            }
193
194            // Otherwise, return true.
195            Ok(true)
196        }
197
198        fn is_minimal_backdoor_set(
199            &self,
200            x: &Set<usize>,
201            y: &Set<usize>,
202            z: &Set<usize>,
203            w: Option<&Set<usize>>,
204            v: Option<&Set<usize>>,
205        ) -> Result<bool> {
206            // Perform sanity checks and convert sets.
207            _assert(self, x, y, Some(z), w, v)?;
208
209            // Set default values for W and V if not provided.
210            let w = match w {
211                Some(w) => w,
212                None => &set![],
213            };
214            let v = match v {
215                Some(v) => v,
216                None => &self.vertices(),
217            };
218
219            // Every minimal backdoor adjustment set is a
220            // minimal separator in the proper backdoor graph
221            // G^PDB under the constraint V' = V \ pDe(PCP(X, Y)).
222
223            // Compute the proper causal path.
224            let pcp = _proper_causal_path(self, x, y)?;
225            // Compute the descendants of the proper causal path.
226            let pde = self.descendants(&pcp)?;
227            // Constraint the restricted vertices.
228            let v_prime = &(v - &pde);
229
230            // Compute the proper backdoor graph.
231            let g_pdb = _proper_backdoor_graph(self, x, &pcp)?;
232
233            // Check if Z is a minimal separator in G^PDB under the constraint V'.
234            g_pdb.is_minimal_separator_set(x, y, z, Some(w), Some(v_prime))
235        }
236
237        fn find_minimal_backdoor_set(
238            &self,
239            x: &Set<usize>,
240            y: &Set<usize>,
241            w: Option<&Set<usize>>,
242            v: Option<&Set<usize>>,
243        ) -> Result<Option<Set<usize>>> {
244            // Perform sanity checks and convert sets.
245            _assert(self, x, y, None::<&Set<_>>, w, v)?;
246
247            // Set default values for W and V if not provided.
248            let w = match w {
249                Some(w) => w,
250                None => &set![],
251            };
252            let v = match v {
253                Some(v) => v,
254                None => &self.vertices(),
255            };
256
257            // Every minimal backdoor adjustment set is a
258            // minimal separator in the proper backdoor graph
259            // G^PDB under the constraint V' = V \ pDe(PCP(X, Y)).
260
261            // Compute the proper causal path.
262            let pcp = _proper_causal_path(self, x, y)?;
263            // Compute the descendants of the proper causal path.
264            let pde = self.descendants(&pcp)?;
265            // Constraint the restricted vertices.
266            let v_prime = &(v - &pde);
267
268            // Compute the proper backdoor graph.
269            let g_pdb = _proper_backdoor_graph(self, x, &pcp)?;
270
271            // Find a minimal separator in G^PDB under the constraint V'.
272            g_pdb.find_minimal_separator_set(x, y, Some(w), Some(v_prime))
273        }
274    }
275
276    #[cfg(test)]
277    mod tests {
278        use super::*;
279
280        #[test]
281        fn proper_causal_path() -> Result<()> {
282            let mut graph = DiGraph::empty(vec!["A", "B", "C", "D", "E"])?;
283            graph.add_edge(0, 1)?;
284            graph.add_edge(0, 2)?;
285            graph.add_edge(1, 2)?;
286            graph.add_edge(1, 3)?;
287            graph.add_edge(2, 3)?;
288            graph.add_edge(3, 4)?;
289
290            assert_eq!(
291                _proper_causal_path(&graph, &set![0], &set![3])?,
292                set![1, 2, 3]
293            );
294            Ok(())
295        }
296    }
297}