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}