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))