Finding conditional instruments

In this article, we provide a CIfly-based implementation of a sound-and-complete algorithm for finding a conditional instrument in an acyclic directed mixed graph (ADMG). Such graphs are very common in graphical causality. They contain directed and bidirected edges and represent causal models with potential latent confounding. For causal effect identification in such graphs, instrumental variables constitute a fundamental technique under a linearity assumption and they can be effective even in case of latent confounding between the outcome and treatment variables. Conditional instruments allow for an additional set WW to make this strategy more general.

The algorithm we develop here is the first sound and complete one for this setting, that is, one that finds a conditional instrument precisely if a valid one exists. This is achieved by building on the recently developed complete graphical criterion by Henckel et al. (2024a). Algorithmically, we rely on techniques from earlier work by van der Zander, Liśkiewicz (2020) that are, however, not based on an complete criterion. We begin by stating the criterion by Henckel et al. (2024a).

Theorem 1 [Henckel et al. (2024a)] Let {x,y},Z\{x,y\},Z and WW be disjoint node sets in an acyclic directed mixed graph GG. Then, (Z,W)(Z,W) is a valid conditional instrumental set relative to (x,y)(x,y) in GG if, and only if,

  1. (ZW)forbG(x,y)=(Z \cup W) \cap \text{forb}_G(x, y) = \emptyset,
  2. x̸GZWx \not\perp_{G} Z \mid W and
  3. yG~ZWy \perp_{\tilde{G}} Z \mid W, where G~\tilde{G} is GG with all directed edges from xx to a node in causalG(x,y)\text{causal}_G(x,y) removed.

Let us introduce some of the terminology of this criterion more precisely. An acyclic directed mixed graph, ADMG for short, is a graph which may contain both directed and bidirected edges, however, no directed cycle. We denote d-separation in GG by G\perp_G and d-connectivity by ̸G\not\perp_G. For the descendants of a node xx in graph GG, we write deG(x)\text{de}_G(x). We use causalG(x,y)\text{causal}_G(x, y) to denote all nodes on directed paths from xx to yy, excluding xx itself, and the set of forbidden nodes relative to (x,y)(x,y) in GG as forbG=deG(causalG(x,y)){x}\text{forb}_G = \text{de}_G(\text{causal}_G(x, y)) \cup \{x\}.

Let us begin by saying that checking this criterion for given xx, yy, ZZ and WW can be done straightforwardly with CIfly by verifying the conditions one-by-one. Conditions 2 and 3 are d-separation checks that we have already seen on the home page. The first condition can be checked by computing forb\text{forb} which amounts to chaining descendant and ancestor computations similar to what we have done in the CPDAG adjustment criterion verifier. We discuss this in more detail below.

Algorithm for finding conditional instruments

For finding a conditional instrument based on this criterion, one first has to develop a suitable algorithm which can then be implemented with CIfly. This is a non-trivial task and needs some sophistication. In our paper, we derive such an algorithm using the techniques introduced by van der Zander, Liśkiewicz (2020).

The algorithm is based on the notion of a nearest separator that we previously discussed in this article. More precisely, it relies on the following observations:

  • if a conditional instrument ZZ, WW exists, then there also exists a conditional instrument zz, WW, that is one consisting of a single instrument node zz instead of a set ZZ,
  • if there exists a set WW for a node zz such that zz, WW is a conditional instrument, then zz, WW' is also a conditional instrument with WW' being a nearest separator between yy and zz restricted to nodes in anG~(y,z)forbG(x,y)\text{an}_{\tilde{G}}(y, z) \setminus \text{forb}_G(x, y).

While the first observation is straightforward, it would go beyond the scope of this article to prove the second observation. For more details, we refer to the appendix of the CIfly paper.

These observations enable the following algorithmic strategy:

  • iterate over all zVz \in V that are not in forbG(x,y){x,y}\text{forb}_G(x, y) \cup \{x, y\},
  • compute WW as the nearest separator between yy and zz in G~\tilde{G} restricted to nodes in VforbG(x,y){x,y}V \setminus \text{forb}_G(x, y) \cup \{x, y\},
  • if WW exists and x̸GzWx \not\perp_G z \mid W, then return conditional instrument (z,W)(z, W)

As all non-forbidden zz are tried by computing a nearest separator WW (again only containing non-forbidden nodes, thereby ensuring Condition 1), which by definition satisfies Condition 3, and Condition 2 is checked explicitly, the correctness follows. If no conditional instrument is returned, then none exists, and, conversely, every returned instrument is valid. The algorithm runs in time O(p(p+m))O(p \cdot (p + m)) because CIfly algorithms are called for each potential zz. For the implementation, we first need to discuss how to compute forbG(x,y)\text{forb}_G(x, y).

Computing the forbidden set

The forbidden set is defined as forbG=deG(causalG(x,y)){x}\text{forb}_G = \text{de}_G(\text{causal}_G(x, y)) \cup \{x\} where causalG(x,y)\text{causal}_G(x, y) contains all node on a directed path from xx to yy (excluding xx itself). It is easy to see that causalG(x,y)=deG(x)anG(y){x}\text{causal}_G(x, y) = \text{de}_G(x) \cap \text{an}_G(y) \setminus \{x\} because a node is on a directed path between xx and yy it is a descendant of xx and an ancestor of yy. Hence, the forbidden set can be constructed by

  1. computing causalG(x,y)\text{causal}_G(x, y) based on the descendants of xx and the ancestors of yy
  2. computing forbG(x,y)\text{forb}_G(x, y) based on the descendants of causalG(x,y)\text{causal}_G(x, y)

Thus, we need to compute the ancestors and descendants in an ADMG. Clearly, this is a trivial task that can be performed easily without CIfly as well. Still, we do this here with the following two straightforward rule tables.

ancestors_admg.txt
EDGES --> <--, <->
SETS X
START <-- AT X
OUTPUT ...

... | <-- | true

The descendants can be computed analogously to the ancestors.

descendants_admg.txt
EDGES --> <--, <->
SETS X
START --> AT X
OUTPUT ...

... | --> | true

Hence, we obtain the following code for computing causalG(x,y)\text{causal}_G(x, y) and forbG(x,y)\text{forb}_G(x, y).

anc = cf.reach(g, {"X": y}, anc_table)
des = cf.reach(g, {"X": x}, desc_table)
cn = set(anc).intersection(des) - set([x])
forb = cf.reach(g, {"X": cn}, desc_table) + [x]

Full implementation

Now, we are able to provide a full implementation based on the algorithm described above. The only thing we haven’t discussed yet is how to compute G~\tilde{G}, that is the graph obtained by deleting all directed edges from xx to a node in causalG(x,y)\text{causal}_G(x, y) from GG. This graph modification can be done directly for graphs in the CIfly format (based on edgelists), but can also be done in your favorite graph library if you prefer to use the CIfly graph format only in reach calls. We show code for doing this directly CIfly graphs below. We note that one can also avoid the graph modification altogether, when encoding in the rule table that edges between xx and causalG(x,y)\text{causal}_G(x, y) can not be traversed (as we have done in this article).

The function removes edge between the sets from and to which have the specified edge type. This is done by constructing a new graph where these edges are filtered out. Hence, this function has no side effects and runs in time O(p+m)O(p + m). You may implement this differently depending on your use case. We note that the function only works for ordered edges but can be easily adapted to unordered ones similar to the discussing for checking parents and siblings in this article.

def removed_ordered_edges(g, from_vars, to_vars, edge_type):
  if edge_type not in g:
      return g

  # enable efficient lookup of membership in from_vars and to_vars
  from_set = set(from_vars)
  to_set = set(to_vars)
  gmod = dict(g)
  gmod[edge_type] = list(
      filter(
          lambda uv: uv[0] not in from_set or uv[1] not in to_set,
          g[edge_type],
      )
  )
  return gmod

Now, we are able to give the full implementation of the first sound-and-complete algorithm for finding conditional instruments.

sound_and_complete_iv.py
import ciflypy as cf
import ciflypy_examples.utils as utils
from ciflypy_examples.nearest_dsep import find_nearest_separator

ruletables = utils.get_ruletable_path()
anc_table = cf.Ruletable(ruletables / "ancestors_admg.txt")
desc_table = cf.Ruletable(ruletables / "descendants_admg.txt")
dsep_table = cf.Ruletable(ruletables / "dconnected_admg.txt")

def sound_and_complete_instrument(p, g, x, y):
    anc = cf.reach(g, {"X": y}, anc_table)
    des = cf.reach(g, {"X": x}, desc_table)
    cn = set(anc).intersection(des) - set([x])
    forb = cf.reach(g, {"X": cn}, desc_table) + [x]
    non_forb = set(range(p)) - set(forb)

    g_parsed = cf.Graph(g, anc_table)
    g_mod = utils.removed_ordered_edges(g, [x], cn, "-->")
    g_mod_parsed = cf.Graph(g_mod, anc_table)

    for z in non_forb - set([y]):
        W = find_nearest_separator(g_mod_parsed, [y], [z], [], non_forb)
        if W is not None and z in cf.reach(g_parsed, {"X": x, "Z": W}, dsep_table):
            return ([z], list(W))
    return None

References