diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..26d33521a
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 000000000..105ce2da2
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..f56ad02b9
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..69050637e
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..94a25f7f4
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/y0.iml b/.idea/y0.iml
new file mode 100644
index 000000000..7bf7bee0d
--- /dev/null
+++ b/.idea/y0.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/y0/controls.py b/src/y0/controls.py
index 4cb856189..86ceb724f 100644
--- a/src/y0/controls.py
+++ b/src/y0/controls.py
@@ -2,6 +2,7 @@
"""Predicates for good, bad, and neutral controls."""
+from .algorithm.conditional_independencies import are_d_separated
from .dsl import Probability, Variable
from .graph import NxMixedGraph
@@ -44,3 +45,27 @@ def is_bad_control(graph: NxMixedGraph, query: Probability, variable: Variable)
"""
_control_precondition(graph, query, variable)
raise NotImplementedError
+
+
+def is_outcome_ancestor(
+ graph: NxMixedGraph, cause: Variable, effect: Variable, variable: Variable
+) -> bool:
+ """Check if the variable is an outcome ancestor given a causal query and graph.
+
+ > In Model 8, Z is not a confounder nor does it block any back-door paths. Likewise,
+ controlling for Z does not open any back-door paths from X to Y . Thus, in terms of
+ asymptotic bias, Z is a “neutral control.” Analysis shows, however, that controlling for
+ Z reduces the variation of the outcome variable Y , and helps to improve the precision
+ of the ACE estimate in finite samples (Hahn, 2004; White and Lu, 2011; Henckel et al.,
+ 2019; Rotnitzky and Smucler, 2019).
+
+ :param graph: An ADMG
+ :param cause: The intervention in the causal query
+ :param effect: The outcome of the causal query
+ :param variable: The variable to check
+ :return: If the variable is a bad control
+ """
+ if variable == cause:
+ return False
+ judgement = are_d_separated(graph, cause, variable)
+ return judgement.separated and variable in graph.ancestors_inclusive(effect)
diff --git a/tests/test_controls.py b/tests/test_controls.py
index 50e0358cf..0dae7570c 100644
--- a/tests/test_controls.py
+++ b/tests/test_controls.py
@@ -4,7 +4,7 @@
import unittest
-from y0.controls import is_bad_control, is_good_control
+from y0.controls import is_bad_control, is_good_control, is_outcome_ancestor
from y0.dsl import U1, U2, A, M, P, U, W, X, Y, Z
from y0.graph import NxMixedGraph
@@ -95,3 +95,7 @@ def test_bad_controls(self):
for model in bad_test_models:
with self.subTest():
self.assertTrue(is_bad_control(model, P(Y @ X), Z))
+
+ def test_neutral_controls(self):
+ """Test neutral controls."""
+ self.assertTrue(is_outcome_ancestor(model_8, X, Y, Z))