def is_identifiable(G, X, Y, print_hedge=False): """Return if the causal effect of variables X on variables Y in G is identifiable based on algorithm ID in Shpitser and Pearl 2008.""" C = c_components X = _set(X) Y = _set(Y) V = observable_nodes(G) Gprime = G.subgraph(G.nodes - X) if X == set(): return True # Line 1 if V - An(G, Y) - Y != set(): # Line 2 return is_identifiable(ancestral_graph(G, Y), X & An(G, Y), Y) W = (V - X) - An(do_X(G, X), Y) - Y if W != set(): return is_identifiable(G, X | W, Y) # Line 3 Sk = C(Gprime) if len(Sk) > 1: # Line 4 return all(is_identifiable(G, V - S, S) for S in Sk) else: S = Sk[0] if any(c == V for c in C(G)): # Line 5 if print_hedge: print(f'Hedge at ({V}, {V&S})') return False if S in C(G): # Line 6 return True else: # Line 7 Sprime = [c for c in C(G) if S < c][0] return is_identifiable(G.subgraph(Sprime | Y), X & Sprime, Y)
def backdoor_criterion_search(DAG, X, Y): """Return all sets of nodes satisfying the backdoor criterion from X to Y in DAG.""" possible_nodes = DAG.nodes() - De(DAG, X) - _set(X) - _set(Y) return [ set(z) for z in powerset(possible_nodes) if meets_backdoor_criterion(DAG, X, Y, z) ]
def d_separated(DAG, X, Y, Z): """Return if Z d-separates X and Y in the DAG.""" X, Y, Z = _set(X), _set(Y), _set(Z) ancestral = ancestral_graph( DAG, X | Y | Z) # Take ancestral graph of nodes in question.. moral = moral_graph(ancestral) # Moralize & disorient... moral_without_givens = moral.subgraph(moral.nodes - Z) # Remove givens... # Any paths between X and Y? return not any( [nx.has_path(moral_without_givens, x, y) for x in X for y in Y])
def specific_adjustment_formula(DAG, X, Y, Z, S): """An expression for the z-specific causal effect of X on Y. Z U S must satisfy the front-door criterion.""" assert meets_backdoor_criterion( DAG, X, Y, _set(Z) | _set(S)), 'Z does not meet backdoor criterion' x, y, z, s = map(val, (X, Y, Z, S)) if len(s) > 0: sub_s = '{' + str(s) + '}' specific_formula = f'\sum_{sub_s}{P(y, given=(x, s, z))}{P(s, given=z)}' return P(y, given=z, do=x) + '=' + specific_formula
def meets_backdoor_criterion(DAG, X, Y, Z): """Return if Z satisfies the backdoor criterion from X to Y in DAG.""" return all(( # i) No node in Z is a descendant of X not De(DAG, X) & _set(Z), # ii) Z d-separates all backdoor paths between X and Y d_separated(backdoor_graph(DAG, X), X, Y, Z)))
def d_sep_graphs(DAG, X, Y, Z, pos=None): """Visualize d-separation.""" X, Y, Z = _set(X), _set(Y), _set(Z) a = ancestral_graph(DAG, X | Y | Z) m = moral_graph(a) mwoz = m.subgraph(m.nodes - Z) if not pos: pos = ex.pos.get(DAG, None) # Try to find a matching pos f, axs = plt.subplots(1, 3, constrained_layout=True) draw(a, title='Ancestral', pos=pos, ax=axs[0], _show_axis_lines=True) draw(m, title='Moral', pos=pos, ax=axs[1], _show_axis_lines=True) draw(mwoz, title='Without Givens', pos=pos, ax=axs[2], _show_axis_lines=True)
def frontdoor_criterion_search(DAG, X, Y): """Return all sets of nodes that satisfy the backdoor criterion from X to Y in DAG.""" return [ set(Z) for Z in powerset(DAG.nodes - _set(X) - _set(Y)) if meets_frontdoor_criterion(DAG, X, Y, Z) ]
def d_separator_search(DAG, X, Y): """Return d_separators for X and Y in DAG.""" return [ set(z) for z in powerset(DAG.nodes - _set(X) - _set(Y)) if d_separated(DAG, X, Y, z) ]
def joint(DAG, do=[]): do = _set(do) return P(val(DAG.nodes - do), do=val(do))
def joint_factorization(DAG, do=[], hidden=[]): """Return an expression for the joint probability distribution factorized according to the relationships encoded in DAG.""" if do: DAG = do_X(DAG, do) factors = [P(val(V), given=pa(DAG, V)) for V in DAG.nodes - _set(do)] return joint(DAG, do=do) + '=' + product(factors)
def val(Variables): """Represent a variable as a value""" return ','.join(sorted(_set(Variables))).lower()
def are_nonadjacent(G, nodes): for node in nodes: return all(other not in G.to_undirected()[node] for other in _set(nodes) - _set(node))
def wrapper(DAG, nodes): return set.union(set(), *(set(f(DAG, node)) for node in _set(nodes)))
def wrapper(DAG, nodes): return set.intersection(*(set(f(DAG, node)) for node in _set(nodes)))
def do_X(DAG, X): """Return the subgraph of DAG with arrows into nodes X removed.""" return nx.subgraph_view(DAG, filter_edge=lambda a, b: b not in _set(X))
def backdoor_graph(DAG, X): """Return the subgraph of DAG with arrows from nodes X removed.""" return nx.subgraph_view(DAG, filter_edge=lambda a, b: a not in _set(X))