causal_hub/inference/
backdoor_criterion.rs

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