Beispiel #1
0
def is_active_indirect_frontdoor_trail(mb: MACIDBase,
                                       start_node: str,
                                       end_node: str,
                                       observed: List[str] = []) -> bool:
    """
    checks whether an active indirect frontdoor path exists given the 'observed' set of variables.
    - A frontdoor path between X and Z is a path in which the first edge comes
    out of the first node (X→···Z).
    - An indirect path contains at least one collider at some node from start_node to end_node.
    """
    considered_nodes = set(observed).union({start_node}, {end_node})
    for node in considered_nodes:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    start_to_end_paths = find_all_undir_paths(mb, start_node, end_node)
    for path in start_to_end_paths:
        is_frontdoor_path = path[0] in mb.get_parents(path[1])
        not_blocked_by_observed = is_active_path(mb, path, observed)
        contains_collider = "collider" in get_motifs(mb, path)
        # default is False since if w = [], any unobserved collider blocks path
        if is_frontdoor_path and not_blocked_by_observed and contains_collider:
            return True
    else:
        return False
Beispiel #2
0
def get_motif(mb: MACIDBase, path: List[str], idx: int) -> str:
    """
    Classify three node structure as a forward (chain), backward (chain), fork,
    collider, or endpoint at index 'idx' along path.
    """
    for node in path:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    if idx > len(path) - 1:
        raise Exception(
            f"The given index {idx} is not valid for the length of this path {len(path)}"
        )

    if len(path) == idx + 1:
        return 'endpoint'

    elif mb.has_edge(path[idx - 1], path[idx]) and mb.has_edge(
            path[idx], path[idx + 1]):
        return 'forward'

    elif mb.has_edge(path[idx + 1], path[idx]) and mb.has_edge(
            path[idx], path[idx - 1]):
        return 'backward'

    elif mb.has_edge(path[idx - 1], path[idx]) and mb.has_edge(
            path[idx + 1], path[idx]):
        return 'collider'

    elif mb.has_edge(path[idx], path[idx - 1]) and mb.has_edge(
            path[idx], path[idx + 1]):
        return 'fork'

    else:
        raise Exception(f"unsure how to classify this path at index {idx}")
Beispiel #3
0
def _get_path_edges(mb: MACIDBase, path: List[str]) -> List[Tuple[str, str]]:
    """
    Returns the structure of a path's edges as a list of pairs.
    In each pair, the first argument states where an edge starts and the second argument states
    where that same edge finishes. For example, if a (colliding) path is D1 -> X <- D2, this function
    returns: [('D1', 'X'), ('D2', 'X')]
    """
    structure = []
    for i in range(len(path) - 1):
        if path[i] in mb.get_parents(path[i + 1]):
            structure.append((path[i], path[i + 1]))
        elif path[i + 1] in mb.get_parents(path[i]):
            structure.append((path[i + 1], path[i]))
    return structure
Beispiel #4
0
def _find_all_undirpath_recurse(mb: MACIDBase, path: List[str],
                                end_node: str) -> List[List[str]]:
    """Find all undirected paths beginning with 'path' as a prefix and ending at the end node."""

    if path[-1] == end_node:
        return [path]
    path_extensions = []
    neighbours = list(mb.get_children(path[-1])) + list(
        mb.get_parents(path[-1]))
    neighbours_not_in_path = set(neighbours).difference(set(path))
    for child in neighbours_not_in_path:
        path_extensions.extend(
            _find_all_undirpath_recurse(mb, path + [child], end_node))
    return path_extensions
Beispiel #5
0
def requisite_graph(cid: MACIDBase) -> MACIDBase:
    """Return the requisite graph of the original CID, also called
    a minimal_reduction, d_reduction, or the trimmed graph.
    - The requisite graph G∗ of a multi-decision CID G is the result of repeatedely
    removing from G all nonrequisite observation links.
    ("Representing and Solving Decision Problems with Limited Information", Lauritzen and Nielsen, 2001)
    """
    requisite_graph = cid.copy()
    decisions = cid.get_valid_order()

    for decision in reversed(decisions):
        non_requisite_nodes = set(cid.get_parents(decision)) - set(requisite_list(requisite_graph, decision))
        for nr in non_requisite_nodes:
            requisite_graph.remove_edge(nr, decision)
    return requisite_graph
Beispiel #6
0
def is_active_path(mb: MACIDBase,
                   path: List[str],
                   observed: List[str] = []) -> bool:
    """
    Check if a specifc path remains active given the 'observed' set of variables.
    """
    considered_nodes = set(path).union(set(observed))
    for node in considered_nodes:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    if len(path) < 3:
        return True

    for _, b, _ in zip(path[:-2], path[1:-1], path[2:]):
        structure = get_motif(mb, path, path.index(b))

        if structure in {'fork', 'forward', 'backward'} and b in observed:
            return False

        if structure == "collider":
            descendants = nx.descendants(mb, b).union({b})
            if not descendants.intersection(set(observed)):
                return False

    return True
Beispiel #7
0
def get_motifs(mb: MACIDBase, path: List[str]) -> List[str]:
    """classify the motif of all nodes along a path as a forward (chain), backward (chain), fork,
     collider or endpoint"""
    for node in path:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    shapes = []
    for i in range(len(path)):
        if i == 0:
            if path[i] in mb.get_parents(path[i + 1]):
                shapes.append('forward')
            else:
                shapes.append('backward')
        else:
            shapes.append(get_motif(mb, path, i))
    return shapes
Beispiel #8
0
def requisite(cid: MACIDBase, decision: str, node: str) -> bool:
    """Return True if cid node is requisite observation for decision (i.e. possibly material).
    - A node can be material if:
    i) it is a parent of D.
    ii) X is d-connected to (U ∩ Desc(D)) given Fa_D \ {X}
    "A note about redundancy in influence diagrams" Fagiuoli and Zaffalon, 1998.
    """
    if decision not in cid.nodes:
        raise Exception(f"{decision} is not present in the cid")
    if node not in cid.get_parents(decision):
        raise Exception(f"{node} is not a parent of {decision}")

    agent_utilities = cid.utility_nodes_agent[cid.whose_node[decision]]
    descended_agent_utilities = set(agent_utilities).intersection(nx.descendants(cid, decision))
    family_d = [decision] + cid.get_parents(decision)
    conditioning_nodes = [i for i in family_d if i != node]
    return any([cid.is_active_trail(node, u_node, conditioning_nodes)
                for u_node in descended_agent_utilities])
Beispiel #9
0
def find_all_undir_paths(mb: MACIDBase, start_node: str,
                         end_node: str) -> List[List[str]]:
    """
    Finds all undirected paths from start node to end node that exist in the (MA)CID.
    """
    for node in [start_node, end_node]:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")
    return _find_all_undirpath_recurse(mb, [start_node], end_node)
Beispiel #10
0
 def __init__(self, cid: MACIDBase, decisions: List[str] = None):
     super().__init__()
     if decisions is None:
         decisions = cid.all_decision_nodes
     self.add_nodes_from(decisions)
     dec_pair_perms = list(itertools.permutations(decisions, 2))
     for dec_pair in dec_pair_perms:
         if cid.is_s_reachable(dec_pair[0], dec_pair[1]):
             self.add_edge(dec_pair[0], dec_pair[1])
Beispiel #11
0
def find_active_path(mb: MACIDBase,
                     start_node: str,
                     end_node: str,
                     observed: List[str] = []) -> List[str]:
    """Find active path from `start_node' to `end_node' given the `observed' set of nodes."""
    considered_nodes = set(observed).union({start_node}, {end_node})
    for node in considered_nodes:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    return _find_active_path_recurse(mb, [start_node], end_node, observed)
Beispiel #12
0
def _find_all_dirpath_recurse(mb: MACIDBase, path: List[str],
                              end_node: str) -> List[List[str]]:
    """Find all directed paths beginning with 'path' as a prefix and ending at the end node"""

    if path[-1] == end_node:
        return [path]
    path_extensions = []
    children = mb.get_children(path[-1])
    for child in children:
        path_extensions.extend(
            _find_all_dirpath_recurse(mb, path + [child], end_node))
    return path_extensions
Beispiel #13
0
def _active_neighbours(mb: MACIDBase, path: List[str],
                       observed: List[str]) -> Set[str]:
    """Find possibly active extensions of path conditional on the `observed' set of nodes."""
    end_of_path = path[-1]
    last_forward = len(path) > 1 and end_of_path in mb.get_children(path[-2])
    possible_colliders: Set[str] = set().union(
        *[set(mb._get_ancestors_of(e)) for e in observed])  # type: ignore

    # if going upward or at a possible collider, it's possible to continue to a parent
    if end_of_path in possible_colliders or not last_forward:
        active_parents = set(mb.get_parents(end_of_path)) - set(observed)
    else:
        active_parents = set()

    # it's possible to go downward if and only if not an observed node
    if end_of_path in observed:
        active_children = set()
    else:
        active_children = set(mb.get_children(end_of_path))

    active_neighbours = active_parents.union(active_children)
    new_active_neighbours = active_neighbours - set(path)
    return new_active_neighbours
Beispiel #14
0
def is_active_backdoor_trail(mb: MACIDBase,
                             start_node: str,
                             end_node: str,
                             observed: List[str] = []) -> bool:
    """
    Returns true if there is a backdoor path that's active given the 'observed' set of nodes.
    - A backdoor path between X and Z is an (undirected) path in which the first edge goes into the first node (X←···Z)
    """
    considered_nodes = set(observed).union({start_node}, {end_node})
    for node in considered_nodes:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    start_to_end_paths = find_all_undir_paths(mb, start_node, end_node)
    for path in start_to_end_paths:

        if len(path) > 1:  # must have path of at least 2 nodes
            is_backdoor_path = path[1] in mb.get_parents(path[0])
            not_blocked_by_observed = is_active_path(mb, path, observed)
            if is_backdoor_path and not_blocked_by_observed:
                return True
    else:
        return False
Beispiel #15
0
def directed_decision_free_path(mb: MACIDBase, start_node: str,
                                end_node: str) -> bool:
    """
    Checks to see if a directed decision free path exists
    """
    for node in [start_node, end_node]:
        if node not in mb.nodes():
            raise Exception(f"The node {node} is not in the (MA)CID")

    start_to_end_paths = find_all_dir_paths(mb, start_node, end_node)
    dec_free_path_exists = any(
        set(mb.all_decision_nodes).isdisjoint(set(path[1:-1])) for path in
        start_to_end_paths)  # ignore path's start_node and end_node
    if start_to_end_paths and dec_free_path_exists:
        return True
    else:
        return False
Beispiel #16
0
def _effective_backdoor_path_not_blocked_by_set_w(
        mb: MACIDBase,
        start: str,
        finish: str,
        effective_set: List[str],
        w: List[str] = []) -> List[str]:
    """
    Returns the effective backdoor path not blocked if we condition on nodes in set w.
    If no such path exists, this returns None.
    """
    start_finish_paths: List[List[str]] = find_all_undir_paths(
        mb, start, finish)
    for path in start_finish_paths:
        is_backdoor_path = path[1] in mb.get_parents(path[0])
        not_blocked_by_w = is_active_path(mb, path, w)
        if is_backdoor_path and _path_is_effective(
                mb, path, effective_set) and not_blocked_by_w:
            return path
    return []
Beispiel #17
0
def requisite_list(cid: MACIDBase, decision: str) -> List[str]:
    """Returns list of requisite nodes for decision"""
    return [node for node in cid.get_parents(decision) if requisite(cid, decision, node)]