This pyAgrum's notebook is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License
Author: Aymen Merrouche and Pierre-Henri Wuillemin.
from IPython.display import display, Math, Latex,HTML
import pyAgrum as gum
import pyAgrum.lib.notebook as gnb
import pyAgrum.causal as csl
import pyAgrum.causal.notebook as cslnb
import os
In a causal diagram, confounding bias is due to the flow of non-causal information between treatment $X$ and outcome $Y$ through back-door paths. To neutralize this bias, we need to block these paths.
To block a non-causal path, we must perform an adjustment operation for a variable or a set of variables that would block the flow of information on that path. Such a set of variables satisfies what we call the "back-door" criterion. A set of variables $Z$ satisfies the back-door criterion for $(X, Y)$ if and only if:
- $Z$ blocks all back-door paths between $X$ and $Y$. A "back-door path" is any path in the causal diagram between $X$ and $Y$ starting with an arrow pointing towards $X$.
- No variable in $Z$ is a descendant of $X$ on a causal path, if we adjust for such a variable we would block a path that carries causal information hence the causal effect of $X$ on $Y$ would be biased.
If a set of $ Z $ variable satisfies the back-door criterion for $(X,Y)$, the causal effect of $X$ on $Y$ is given by the formula: $$P(y \mid do(x)) = \sum_{z}{P(y \mid x,z) \times P(z)}$$
e1 = gum.fastBN("X->A->Y;A->B")
e1
m1 = csl.CausalModel(e1)
cslnb.showCausalImpact(m1, "Y", doing="X",values={})
# This function returns the set of variables which satisfies the back-door criterion for (X, Y)
# None if there are no back-door paths.
setOfVars = m1.backDoor("X","Y")
print("The set of variables which satisfies the back-door criterion for (X, Y) is :", setOfVars)
No incoming arrows into X, therefore there are no back-door paths between $X$ and $Y$ (as if we did a graph surgery according to the do operator), direct causal path $X \rightarrow A \rightarrow Y$.
e2 = gum.fastBN("A->B->C;A->X->E->Y;B<-D->E")
e2
m2 = csl.CausalModel(e2)
cslnb.showCausalImpact(m2, "Y", doing="X",values={})
# This function returns the set of variables which satisfies the back-door criterion for (X, Y)
# None if there are no back-door paths.
setOfVars = m2.backDoor("X","Y")
print("The set of variables which satisfies the back-door criterion for (X, Y) is :", setOfVars)
There is one back-door path from $X$ to $Y$ : $$X \leftarrow A \rightarrow B \leftarrow D \rightarrow E \rightarrow Y$$ We don't need to control for any set of variables; this back-door path is blocked by collider node $B$ (two incoming arrows) $$A \rightarrow B \leftarrow D$$ Controlling for collider node $B$ would open this causal path (controlling for colliders increases bias), direct causal path $X \rightarrow E \rightarrow Y$.
e3 = gum.fastBN("B->X->Y;X->A<-B->Y")
e3
m3 = csl.CausalModel(e3)
cslnb.showCausalImpact(m3, "Y", doing="X",values={})
# This function returns the set of variables which satisfies the back-door criterion for (X, Y)
# None if there are no back-door paths.
setOfVars = m3.backDoor("X","Y")
print("The set of variables which satisfies the back-door criterion for (X, Y) is :", setOfVars)
There is one back-door path from $X$ to $Y$ : $$Y \leftarrow B \rightarrow X $$ We need to block it by controlling for $B$ wich satisfies the back-door criterion.
e4 = gum.fastBN("X<-A->B<-C->Y")
e4
m4 = csl.CausalModel(e4)
cslnb.showCausalImpact(m4, "Y", doing="X",values={})
# This function returns the set of variables which satisfies the back-door criterion for (X, Y)
# None if there are no back-door paths.
setOfVars = m4.backDoor("X","Y")
print("The set of variables which satisfies the back-door criterion for (X, Y) is :", setOfVars)
There is one back-door path from $X$ to $Y$ : $$X \leftarrow A \rightarrow B \leftarrow C \rightarrow Y$$ We don't need to control for any set of variables, this back-door path is blocked by collider node $B$, the two variables are d-separated, deconfounded, independent. Controlling for collider node $B$ would make them dependant (introducing the M-bias).
e5 = gum.fastBN("X<-B<-A->X->Y<-C->B")
e5
m5 = csl.CausalModel(e5)
cslnb.showCausalImpact(m5, "Y", doing="X",values={})
# This function returns the set of variables which satisfies the back-door criterion for (X, Y)
# None if there are no back-door paths.
setOfVars = m5.backDoor("X","Y")
print("The set of variables which satisfies the back-door criterion for (X, Y) is :", setOfVars)
The difference between this example and the previous one is that we added an arrow between $B$ and $X$ ( $B \rightarrow X$ ), this opens a new back-door path between $X$ and $Y$ that isn't blocked by any colliders $$X \leftarrow B \leftarrow C \rightarrow Y$$ We need to block the non-causal information that flows through it, controlling for $B$ closes this backdoor path (it prevents information from getting from $X$ to $C$). However, this action will open the back-door path that was formerly blocked by collider node $B$ that we are adjusting for now: $$X \leftarrow A \rightarrow B \leftarrow C \rightarrow Y$$ And, in this case, in addition to $B$ we would also control for $C$ or for $A$ to reblock the path we opened and to block the new path.
Another solution is to control for $C$ (it prevents information from getting from $B$ to $Y$) which satisfies the back-door criterion, it blocks the new path without reopening the one that is blocked by $B$.
e6 = gum.fastBN("A->X;A->B;D->A;B->X;C->B;C->E;C->Y;D->C;E->Y;E->X;F->C;F->X;F->Y;G->X;G->Y;X->Y")
e6
m6 = csl.CausalModel(e6)
cslnb.showCausalImpact(m6, "Y", doing="X",values={})
# This function returns the set of variables which satisfies the back-door criterion for (X, Y)
# None if there are no back-door paths.
setOfVars = m6.backDoor("X","Y")
print("The set of variables which satisfies the back-door criterion for (X, Y) is :", setOfVars)
Back-door paths are:
1) - $X \leftarrow G \rightarrow Y$
2) - $X \leftarrow E \rightarrow Y$ and any other back-door paths that go through $E$
3) - $X \leftarrow F \rightarrow Y$ and any other back-door paths that go through $F$
4) - Blocked by collider $B$ : $X \leftarrow A \rightarrow B \leftarrow C \rightarrow Y$ and any other back-door paths that go through $ A$ will go through $C$
5) - $X \leftarrow B \leftarrow C \rightarrow Y$ and any other back-door paths that go through $B$ will go through $C$
Two sets of variables that satisfy the back-door criterion are:
- {$C$,$E$,$F$,$G$} blocking (1), (2), (3) and (5)
- {$A$,$B$,$E$,$F$,$G$} blocking (1), (2), (3), (5), opening (4) and reblocking it.