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}