Esempio n. 1
0
def admits_voi(cid: CID, decision: str, node: str) -> bool:
    """Return True if cid admits value of information for node.
    - A CID admits value of information for a node X if:
    i) X is not a descendant of the decision node, D.
    ii) X is d-connected to U given Fa_D \ {X}, where U ∈ U ∩ Desc(D)
    ("Agent Incentives: a Causal Perspective" by Everitt, Carey, Langlois, Ortega, and Legg, 2020)
    """
    agent_utilities = cid.all_utility_nodes

    if node not in cid.nodes:
        raise Exception(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise Exception(f"{decision} is not present in the cid")

    # condition (i)
    elif node == decision or node in nx.descendants(cid, decision):
        return False
    # condition (ii)
    descended_agent_utilities = [
        util for util in agent_utilities
        if util in nx.descendants(cid, decision)
    ]
    d_family = [decision] + cid.get_parents(decision)
    con_nodes = [i for i in d_family if i != node]
    voi = any([
        cid.is_active_trail(node, u_node, con_nodes)
        for u_node in descended_agent_utilities
    ])
    return voi
Esempio n. 2
0
def admits_voi(cid: CID, decision: str, node: str) -> bool:
    """Return True if cid admits value of information for node.
    - A CID admits value of information for a node X if:
    i) X is not a descendant of the decision node, D.
    ii) X is d-connected to U given Fa_D \ {X}, where U ∈ U ∩ Desc(D)
    ("Agent Incentives: a Causal Perspective" by Everitt, Carey, Langlois, Ortega, and Legg, 2020)
    """
    if len(cid.agents) > 1:
        raise Exception(
            f"This CID has {len(cid.agents)} agents. This incentive is currently only \
                        valid for CIDs with one agent.")

    if node not in cid.nodes:
        raise Exception(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise Exception(f"{decision} is not present in the cid")
    if not cid.sufficient_recall():
        raise Exception("Voi only implemented graphs with sufficient recall")
    if node in nx.descendants(cid, decision) or node == decision:
        return False

    cid2 = cid.copy_without_cpds()
    cid2.add_edge(node, decision)
    req_graph = requisite_graph(cid2)
    return node in req_graph.get_parents(decision)
Esempio n. 3
0
def get_sequential_cid() -> CID:
    """
    This CID is a subtle case of sufficient recall, as the strategy for D1 influences
    the expected utility of D2, but D2 can still be chosen without knowing D1, since
    D1 does not influence any utility nodes descending from D2.
    """
    cid = CID([
        ('S1', 'D1'),
        ('D1', 'U1'),
        ('S1', 'U1'),
        ('D1', 'S2'),
        ('S2', 'D2'),
        ('D2', 'U2'),
        ('S2', 'U2'),
    ],
              decision_nodes=['D1', 'D2'],
              utility_nodes=['U1', 'U2'])

    cid.add_cpds(
        UniformRandomCPD('S1', [0, 1]),
        DecisionDomain('D1', [0, 1]),
        FunctionCPD('U1', lambda s1, d1: int(s1 == d1), evidence=['S1', 'D1']),
        FunctionCPD('S2', lambda d1: d1, evidence=['D1']),
        DecisionDomain('D2', [0, 1]),
        FunctionCPD('U2', lambda s2, d2: int(s2 == d2), evidence=['S2', 'D2']),
    )
    return cid
Esempio n. 4
0
def get_insufficient_recall_cid() -> CID:
    cid = CID([('A', 'U'), ('B', 'U')],
              decision_nodes=['A', 'B'],
              utility_nodes=['U'])
    cid.add_cpds(DecisionDomain('A', [0, 1]), DecisionDomain('B', [0, 1]),
                 FunctionCPD('U', lambda a, b: a * b, evidence=['A', 'B']))
    return cid
Esempio n. 5
0
def get_3node_cid() -> CID:
    cid = CID([('S', 'D'), ('S', 'U'), ('D', 'U')],
              decision_nodes=['D'],
              utility_nodes=['U'])
    cpd_s = UniformRandomCPD('S', [0, 1])
    cpd_u = FunctionCPD('U', lambda s, d: int(s == d), evidence=['S', 'D'])
    cpd_d = DecisionDomain('D', [0, 1])
    cid.add_cpds(cpd_d, cpd_s, cpd_u)
    return cid
Esempio n. 6
0
def voi(cid: CID, decision: str, variable: str) -> float:
    # TODO test this method
    new = cid.copy()
    new.add_edge(variable, decision)
    new.impute_optimal_policy()
    ev1: float = new.expected_utility({})
    new = cid.copy()
    new.remove_edge(variable, decision)
    new.impute_optimal_policy()
    ev2: float = new.expected_utility({})
    return ev1 - ev2
Esempio n. 7
0
def get_insufficient_recall_cid() -> CID:
    cid = CID([('A', 'U'), ('B', 'U')],
              decision_nodes=['A', 'B'],
              utility_nodes=['U'])
    cpd_u = TabularCPD('U',
                       2,
                       np.random.randn(2, 4),
                       evidence=['A', 'B'],
                       evidence_card=[2, 2])
    cid.add_cpds(DecisionDomain('A', [0, 1]), DecisionDomain('B', [0, 1]),
                 cpd_u)
    return cid
Esempio n. 8
0
def get_grade_predictor() -> CID:
    cid = CID([('R', 'HS'), ('HS', 'E'), ('HS', 'P'), ('E', 'Gr'),
               ('Gr', 'Ac'), ('Ge', 'P'), ('P', 'Ac')],
              decision_nodes=['P'],
              utility_nodes=['Ac'])

    return cid
Esempio n. 9
0
def get_modified_content_recommender() -> CID:
    cid = CID([('O', 'I'), ('O', 'M'), ('M', 'P'), ('P', 'I'), ('P', 'C'),
               ('M', 'C')],
              decision_nodes=['P'],
              utility_nodes=['C'])

    return cid
Esempio n. 10
0
def admits_ri(cid: CID, decision: str, node: str) -> bool:
    """
    Return True if cid admits a response incentive on node.
     - A CID G admits a response incentive on X ∈ V \ {D} if
    and only if the reduced graph G* min has a directed path X --> D.
    ("Agent Incentives: a Causal Perspective" by Everitt, Carey, Langlois, Ortega, and Legg, 2020)
    """
    if len(cid.agents) > 1:
        raise Exception(
            f"This CID has {len(cid.agents)} agents. This incentive is currently only \
                        valid for CIDs with one agent.")

    if node not in cid.nodes:
        raise Exception(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise Exception(f"{decision} is not present in the cid")
    if not cid.sufficient_recall():
        raise Exception("Voi only implemented graphs with sufficient recall")
    if node == decision:
        return False

    req_graph = requisite_graph(cid)
    if find_all_dir_paths(req_graph, node, decision):
        return True

    return False
Esempio n. 11
0
def admits_indir_voc(cid: CID, decision: str, node: str) -> bool:
    """
    Return True if a single-decision cid admits indirect positive value of control for node.
    - A single-decision CID G admits positive value of control for a node X ∈ V \ {D} if and
    only if there is a directed path X --> U in the reduced graph G∗.
    - The path X --> U may or may not pass through D.
    - The agent has a direct value of control incentive on D if the path does not pass through D.
    - The agent has an indirect value of control incentive on D if the path does pass through D
    and there is also a backdoor path X--U that begins backwards from X (...<- X) and is
    active when conditioning on Fa_D \ {X}
    """
    if node not in cid.nodes:
        raise Exception(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise Exception(f"{decision} is not present in the cid")

    agent_utilities = cid.all_utility_nodes
    req_graph = requisite_graph(cid)
    d_family = [decision] + cid.get_parents(decision)
    con_nodes = [i for i in d_family if i != node]
    if not admits_voc(cid, decision, node):
        return False

    for util in agent_utilities:
        if node == util or util in nx.descendants(req_graph, node):
            backdoor_exists = is_active_backdoor_trail(req_graph, node, util,
                                                       con_nodes)
            x_u_paths = find_all_dir_paths(req_graph, node, util)
            if any(decision in paths
                   for paths in x_u_paths) and backdoor_exists:
                return True

    return False
Esempio n. 12
0
def get_5node_cid_with_scaled_utility() -> CID:
    cid = CID([('S1', 'D'), ('S1', 'U1'), ('S2', 'D'), ('S2', 'U2'),
               ('D', 'U1'), ('D', 'U2')],
              decision_nodes=['D'],
              utility_nodes=['U1', 'U2'])
    cpd_s1 = UniformRandomCPD('S1', [0, 1])
    cpd_s2 = UniformRandomCPD('S2', [0, 1])
    cpd_u1 = FunctionCPD('U1',
                         lambda s1, d: 10 * int(s1 == d),
                         evidence=['S1', 'D'])
    cpd_u2 = FunctionCPD('U2',
                         lambda s2, d: 2 * int(s2 == d),
                         evidence=['S2', 'D'])
    cpd_d = DecisionDomain('D', [0, 1])
    cid.add_cpds(cpd_d, cpd_s1, cpd_s2, cpd_u1, cpd_u2)
    return cid
Esempio n. 13
0
def get_2dec_cid() -> CID:
    cid = CID([('S1', 'S2'), ('S1', 'D1'), ('D1', 'S2'), ('S2', 'U'),
               ('S2', 'D2'), ('D2', 'U')],
              decision_nodes=['D1', 'D2'],
              utility_nodes=['U'])
    cpd_s1 = UniformRandomCPD('S1', [0, 1])
    cpd_d1 = DecisionDomain('D1', [0, 1])
    cpd_d2 = DecisionDomain('D2', [0, 1])
    cpd_s2 = FunctionCPD('S2',
                         lambda s2, d1: int(s2 == d1),
                         evidence=['S1', 'D1'])
    cpd_u = FunctionCPD('U',
                        lambda s2, d2: int(s2 == d2),
                        evidence=['S2', 'D2'])
    cid.add_cpds(cpd_s1, cpd_d1, cpd_s2, cpd_d2, cpd_u)
    return cid
Esempio n. 14
0
def get_fitness_tracker() -> CID:
    cid = CID([('TD', 'TF'), ('TF', 'SC'), ('TF', 'C'), ('EF', 'EWD'),
               ('EWD', 'C'), ('C', 'F'), ('P', 'D'), ('P', 'SC'), ('P', 'F'),
               ('SC', 'C'), ('SC', 'EWD')],
              decision_nodes=['C'],
              utility_nodes=['F'])

    return cid
Esempio n. 15
0
def get_car_accident_predictor() -> CID:
    cid = CID([('B', 'N'), ('N', 'AP'), ('N', 'P'), ('P', 'Race'),
               ('Age', 'Adt'), ('Adt', 'Race'), ('Race', 'Accu'), ('M', 'AP'),
               ('AP', 'Accu')],
              decision_nodes=['AP'],
              utility_nodes=['Accu'])

    return cid
Esempio n. 16
0
def add_sufficient_recalls(cid: CID) -> None:
    """add edges to a cid until all decisions have sufficient recall of all prior decisions"""
    for utility_node in cid.all_utility_nodes:
        # decisions = cid._get_valid_order(cid.decision_nodes)  # cannot be trusted...
        for i, dec1 in enumerate(cid.all_decision_nodes):
            for dec2 in cid.all_decision_nodes[i + 1:]:
                if dec1 in cid._get_ancestors_of(dec2):
                    _add_sufficient_recall(cid, dec1, dec2, utility_node)
                else:
                    _add_sufficient_recall(cid, dec2, dec1, utility_node)
Esempio n. 17
0
def _add_sufficient_recall(cid: CID, dec1: str, dec2: str,
                           utility_node: str) -> None:
    """Add edges to a cid until `dec2` has sufficient recall of `dec1` (to optimize utility)

    this is done by adding edges from non-collider nodes until recall is adequate
    """

    if dec2 in cid._get_ancestors_of(dec1):
        raise ValueError('{} is an ancestor of {}'.format(dec2, dec1))

    cid2 = cid.copy()
    cid2.add_edge('pi', dec1)

    while cid2.is_active_trail('pi',
                               utility_node,
                               observed=cid.get_parents(dec2) + [dec2]):
        path = find_active_path(cid2, 'pi', utility_node,
                                cid.get_parents(dec2) + [dec2])
        if path is None:
            raise Exception(
                "couldn't find path even though there should be an active trail"
            )
        while True:
            i = random.randrange(1, len(path) - 1)
            # print('consider {}--{}--{}'.format(path[i-1], path[i], path[i+1]),end='')
            collider = ((path[i - 1], path[i]) in cid2.edges) and (
                (path[i + 1], path[i]) in cid2.edges)
            if not collider:
                if dec2 not in cid2._get_ancestors_of(path[i]):
                    # print('add {}->{}'.format(path[i], dec2), end=' ')
                    cid.add_edge(path[i], dec2)
                    cid2.add_edge(path[i], dec2)
                    break
Esempio n. 18
0
def get_introduced_bias() -> CID:

    cid = CID(
        [
            ('A', 'X'),  # defining the graph's nodes and edges
            ('Z', 'X'),
            ('Z', 'Y'),
            ('X', 'D'),
            ('X', 'Y'),
            ('D', 'U'),
            ('Y', 'U')
        ],
        decision_nodes=['D'],
        utility_nodes=['U'])

    cpd_a = UniformRandomCPD('A', [0, 1])
    cpd_z = UniformRandomCPD('Z', [0, 1])
    cpd_x = FunctionCPD('X', lambda a, z: a * z, evidence=['A', 'Z'])
    cpd_d = DecisionDomain('D', [0, 1])
    cpd_y = FunctionCPD('Y', lambda x, z: x + z, evidence=['X', 'Z'])
    cpd_u = FunctionCPD('U', lambda d, y: -(d - y)**2, evidence=['D', 'Y'])

    cid.add_cpds(cpd_a, cpd_d, cpd_z, cpd_x, cpd_y, cpd_u)
    return cid
Esempio n. 19
0
def get_trim_example_cid() -> CID:
    cid = CID([
        ('Y1', 'D1'),
        ('Y1', 'Y2'),
        ('Y1', 'D2'),
        ('Y2', 'D2'),
        ('Y2', 'U'),
        ('D1', 'Y2'),
        ('D1', 'D2'),
        ('Z1', 'D1'),
        ('Z1', 'D2'),
        ('Z1', 'Z2'),
        ('Z2', 'D2'),
        ('D2', 'U'),
    ],
              decision_nodes=['D1', 'D2'],
              utility_nodes=['U'])
    return cid
Esempio n. 20
0
def get_minimal_cid() -> CID:
    cid = CID([('A', 'B')], decision_nodes=['A'], utility_nodes=['B'])
    cpd_a = DecisionDomain('A', [0, 1])
    cpd_b = FunctionCPD('B', lambda a: a, evidence=['A'])
    cid.add_cpds(cpd_a, cpd_b)
    return cid
Esempio n. 21
0
def total_effect(cid: CID, a: str, x: str, a1: int, a2: int) -> float:
    "the total effect on x from intervening on a with a2 rather than a1"
    total_effect = cid.expected_value([x], {}, intervene={a: a2})[0] - cid.expected_value([x], {}, intervene={a: a1})[0]
    return total_effect  # type: ignore
Esempio n. 22
0
def random_cid(n_all: int,
               n_decisions: int,
               n_utilities: int,
               edge_density: float = 0.4,
               add_sr_edges: bool = True,
               add_cpds: bool = True,
               seed: int = None) -> CID:
    """Generates a random Cid with the specified number of nodes and edges"""

    all_names, decision_names, utility_names = get_node_names(
        n_all, n_decisions, n_utilities)
    edges = get_edges(all_names,
                      utility_names,
                      edge_density,
                      seed=seed,
                      allow_u_edges=False)
    cid = CID(edges, decision_names, utility_names)

    for uname in utility_names:
        for edge in edges:
            assert uname != edge[0]

    for i, d1 in enumerate(decision_names):
        for j, d2 in enumerate(decision_names[i + 1:]):
            assert d2 not in cid._get_ancestors_of(d1)

    if add_sr_edges:
        add_sufficient_recalls(cid)

    if add_cpds:
        for node in cid.nodes:
            if node in cid.all_decision_nodes:
                cid.add_cpds(DecisionDomain(node, [0, 1]))
            elif not cid.get_parents(node):  # node is a root node
                cid.add_cpds(UniformRandomCPD(node, [0, 1]))
            else:
                cid.add_cpds(
                    RandomlySampledFunctionCPD(node, cid.get_parents(node)))
    return cid