Exemplo n.º 1
0
def admits_voi(cid: CID, decision: str, node: str) -> bool:
    r"""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 ValueError(
            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 KeyError(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise KeyError(f"{decision} is not present in the cid")
    if not cid.sufficient_recall():
        raise ValueError("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)
Exemplo n.º 2
0
def get_sequential_cid() -> CID:
    """
    This CID is a subtle case of sufficient recall, as the decision rule 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"),
        ],
        decisions=["D1", "D2"],
        utilities=["U1", "U2"],
    )

    cid.add_cpds(
        UniformRandomCPD("S1", [0, 1]),
        DecisionDomain("D1", [0, 1]),
        FunctionCPD("U1", lambda s1, d1: int(s1 == d1)),  # type: ignore
        FunctionCPD("S2", lambda d1: d1),  # type: ignore
        DecisionDomain("D2", [0, 1]),
        FunctionCPD("U2", lambda s2, d2: int(s2 == d2)),  # type: ignore
    )
    return cid
Exemplo n.º 3
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"),
        ],
        decisions=["D"],
        utilities=["U"],
    )

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

    cid.add_cpds(cpd_a, cpd_d, cpd_z, cpd_x, cpd_y, cpd_u)
    return cid
Exemplo n.º 4
0
def get_sequential_cid() -> CID:
    """
    This CID is a subtle case of sufficient recall, as the decision rule 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"),
        ],
        decisions=["D1", "D2"],
        utilities=["U1", "U2"],
    )

    cid.add_cpds(
        S1=discrete_uniform([0, 1]),
        D1=[0, 1],
        U1=lambda s1, d1: int(s1 == d1),
        S2=lambda d1: d1,
        D2=[0, 1],
        U2=lambda s2, d2: int(s2 == d2),
    )
    return cid
Exemplo n.º 5
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"),
        ],
        decisions=["D"],
        utilities=["U"],
    )

    cid.add_cpds(
        A=discrete_uniform([0, 1]),
        Z=discrete_uniform([0, 1]),
        X=lambda a, z: a * z,
        D=[0, 1],
        Y=lambda x, z: x + z,
        U=lambda d, y: -((d - y)**2),
    )
    return cid
Exemplo n.º 6
0
def get_insufficient_recall_cid() -> CID:
    cid = CID([("A", "U"), ("B", "U")], decisions=["A", "B"], utilities=["U"])
    cid.add_cpds(
        DecisionDomain("A", [0, 1]),
        DecisionDomain("B", [0, 1]),
        FunctionCPD("U", lambda a, b: a * b),  # type: ignore
    )
    return cid
Exemplo n.º 7
0
def get_3node_cid() -> CID:
    cid = CID([("S", "D"), ("S", "U"), ("D", "U")],
              decisions=["D"],
              utilities=["U"])
    cpd_s = UniformRandomCPD("S", [-1, 1])
    cpd_u = FunctionCPD("U", lambda s, d: s * d)  # type: ignore
    cpd_d = DecisionDomain("D", [-1, 1])
    cid.add_cpds(cpd_d, cpd_s, cpd_u)
    return cid
Exemplo n.º 8
0
def get_quantitative_voi_cid() -> CID:
    cid = CID([("S", "X"), ("X", "D"), ("D", "U"), ("S", "U")],
              decisions=["D"],
              utilities=["U"])
    cpd_s = UniformRandomCPD("S", [-1, 1])
    # X takes the value of S with probability 0.8
    cpd_x = StochasticFunctionCPD("X", lambda s: {s: 0.8}, domain=[-1, 1])
    cpd_d = DecisionDomain("D", [-1, 0, 1])
    cpd_u = FunctionCPD("U", lambda s, d: int(s) * int(d))  # type: ignore
    cid.add_cpds(cpd_s, cpd_x, cpd_d, cpd_u)
    return cid
Exemplo n.º 9
0
def get_quantitative_voi_cid() -> CID:
    cid = CID([("S", "X"), ("X", "D"), ("D", "U"), ("S", "U")],
              decisions=["D"],
              utilities=["U"])
    cid.add_cpds(
        S=discrete_uniform([-1, 1]),
        X=lambda s: noisy_copy(s, probability=0.8, domain=[-1, 1]),
        D=[-1, 0, 1],
        U=lambda s, d: int(s) * int(d),
    )
    return cid
Exemplo n.º 10
0
def get_5node_cid_with_scaled_utility() -> CID:
    cid = CID(
        [("S1", "D"), ("S1", "U1"), ("S2", "D"), ("S2", "U2"), ("D", "U1"),
         ("D", "U2")],
        decisions=["D"],
        utilities=["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))  # type: ignore
    cpd_u2 = FunctionCPD("U2", lambda s2, d: 2 * int(s2 == d))  # type: ignore
    cpd_d = DecisionDomain("D", [0, 1])
    cid.add_cpds(cpd_d, cpd_s1, cpd_s2, cpd_u1, cpd_u2)
    return cid
Exemplo n.º 11
0
def get_2dec_cid() -> CID:
    cid = CID(
        [("S1", "S2"), ("S1", "D1"), ("D1", "S2"), ("S2", "U"), ("S2", "D2"),
         ("D2", "U")],
        decisions=["D1", "D2"],
        utilities=["U"],
    )
    cpd_s1 = UniformRandomCPD("S1", [0, 1])
    cpd_d1 = DecisionDomain("D1", [0, 1])
    cpd_d2 = DecisionDomain("D2", [0, 1])
    cpd_s2 = FunctionCPD("S2", lambda s1, d1: int(s1 == d1))  # type: ignore
    cpd_u = FunctionCPD("U", lambda s2, d2: int(s2 == d2))  # type: ignore
    cid.add_cpds(cpd_s1, cpd_d1, cpd_s2, cpd_d2, cpd_u)
    return cid
Exemplo n.º 12
0
def quantitative_voc(cid: CID, node: str) -> float:
    r"""
    Returns the quantitative value of control (voc) of a variable corresponding to a node in a parameterised CID.

    A node X ∈ V \ {D} in a single-decision CID has quantitative voi equal to
    max_EU_(π, g^x)[M_g^x] - max_EU_(π)[M]
    ie the maximum utility attainable under any policy π and any soft intervention g^x in M_g^x minus the maximum
    utility attainable under any policy π in M where:
    - M is the original CID
    - M_g^x is the the original CID modified with a new soft intervention g^x
    (ie a new function g^x: dom(Pa^X) -> dom(X)) on variable X.

    ("Agent Incentives: a Causal Perspective" by Everitt, Carey, Langlois, Ortega, and Legg, 2020)
    """
    if node not in cid.nodes:
        raise KeyError(f"{node} is not present in the cid")

    # optimal policy in the original CID.
    cid.impute_optimal_policy()
    ev1: float = cid.expected_utility({})
    cid.make_decision(node)
    # optimal policy in the modified CID where the agent can now decide the CPD for node.
    cid.impute_optimal_policy()
    ev2: float = cid.expected_utility({})
    return ev2 - ev1
Exemplo n.º 13
0
def get_2dec_cid() -> CID:
    cid = CID(
        [("S1", "S2"), ("S1", "D1"), ("D1", "S2"), ("S2", "U"), ("S2", "D2"),
         ("D2", "U")],
        decisions=["D1", "D2"],
        utilities=["U"],
    )
    cid.add_cpds(
        S1=discrete_uniform([0, 1]),
        D1=[0, 1],
        D2=[0, 1],
        S2=lambda s1, d1: int(s1 == d1),
        U=lambda s2, d2: int(s2 == d2),
    )
    return cid
Exemplo n.º 14
0
def get_5node_cid_with_scaled_utility() -> CID:
    cid = CID(
        [("S1", "D"), ("S1", "U1"), ("S2", "D"), ("S2", "U2"), ("D", "U1"),
         ("D", "U2")],
        decisions=["D"],
        utilities=["U1", "U2"],
    )
    cid.add_cpds(
        S1=discrete_uniform([0, 1]),
        S2=discrete_uniform([0, 1]),
        U1=lambda s1, d: 10 * int(s1 == d),
        U2=lambda s2, d: 2 * int(s2 == d),
        D=[0, 1],
    )
    return cid
Exemplo n.º 15
0
def quantitative_voi(cid: CID, decision: str, node: str) -> float:
    r"""
    Returns the quantitative value of information (voi) of a variable corresponding to a node in a parameterised CID.

    A node X ∈ V \ Desc(D) in a single-decision CID has quantitative voi equal to
    EU_max[M(X->D)] - EU_max[M(X \ ->D)]
    ie the maximum utility attainable in M(X->D) minus the maximum utility attainable in M(X \ ->D) where
    - M(X->D) is the CID that contains the directed edge X -> D
    - M(X \ ->D) is the CID without the directed edge X -> D.

    ("Agent Incentives: a Causal Perspective" by Everitt, Carey, Langlois, Ortega, and Legg, 2020)
    """
    if node not in cid.nodes:
        raise KeyError(f"{node} is not present in the cid")
    if node in {decision}.union(set(nx.descendants(cid, decision))):
        raise ValueError(
            f"{node} is a decision node or is a descendent of the decision node. \
                VOI only applies to nodes which are not descendents of the decision node."
        )
    new_cid = cid.copy()
    new_cid.add_edge(node, decision)
    new_cid.impute_optimal_policy()
    ev1: float = new_cid.expected_utility({})
    new_cid.remove_all_decision_rules()
    new_cid.remove_edge(node, decision)
    new_cid.impute_optimal_policy()
    ev2: float = new_cid.expected_utility({})
    return ev1 - ev2
Exemplo n.º 16
0
def admits_ri(cid: CID, decision: str, node: str) -> bool:
    r"""Check if a CID admits a response incentive on a 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 ValueError(
            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 KeyError(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise KeyError(f"{decision} is not present in the cid")
    if not cid.sufficient_recall():
        raise ValueError(
            "Response inventives are only implemented for graphs with sufficient recall"
        )
    if node == decision:
        return False

    req_graph = requisite_graph(cid)
    try:
        next(find_all_dir_paths(req_graph, node, decision))
    except StopIteration:
        return False
    else:
        return True
Exemplo n.º 17
0
def admits_voc(cid: CID, node: str) -> bool:
    """Check if a CID admits positive value of control for a node.

    A CID G admits positive value of control for a node X ∈ V
    if and only if X is not a decision node and there is a directed path X --> U
    in the reduced graph G∗.
    """
    if len(cid.agents) > 1:
        raise ValueError(
            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 KeyError(f"{node} is not present in the cid")
    if not cid.sufficient_recall():
        raise ValueError("VoC only implemented graphs with sufficient recall")
    if node in cid.decisions:
        return False

    req_graph = requisite_graph(cid)
    agent_utilities = cid.utilities

    for util in agent_utilities:
        if node == util or util in nx.descendants(req_graph, node):
            return True

    return False
Exemplo n.º 18
0
def get_grade_predictor() -> CID:
    cid = CID(
        [("R", "HS"), ("HS", "E"), ("HS", "P"), ("E", "Gr"), ("Gr", "Ac"), ("Ge", "P"), ("P", "Ac")],
        decisions=["P"],
        utilities=["Ac"],
    )

    return cid
Exemplo n.º 19
0
def get_modified_content_recommender() -> CID:
    cid = CID(
        [("O", "I"), ("O", "M"), ("M", "P"), ("P", "I"), ("P", "C"), ("M", "C")],
        decisions=["P"],
        utilities=["C"],
    )

    return cid
Exemplo n.º 20
0
def admits_indir_voc(cid: CID, decision: str, node: str) -> bool:
    r"""Check if a single-decision CID admits indirect positive value of control for a 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 len(cid.agents) > 1:
        raise ValueError(
            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 KeyError(f"{node} is not present in the cid")
    if decision not in cid.nodes:
        raise KeyError(f"{decision} is not present in the cid")
    if not cid.sufficient_recall():
        raise ValueError("VoC only implemented graphs with sufficient recall")

    agent_utilities = cid.utilities
    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, node):
        return False

    req_graph_node_descendants = set(nx.descendants(req_graph, node))
    for util in agent_utilities:
        if util != node and util not in req_graph_node_descendants:
            continue
        if not is_active_backdoor_trail(req_graph, node, util, con_nodes):
            continue
        if any(decision in path
               for path in find_all_dir_paths(req_graph, node, util)):
            return True

    return False
Exemplo n.º 21
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"),
        ],
        decisions=["AP"],
        utilities=["Accu"],
    )

    return cid
Exemplo n.º 22
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"),
        ],
        decisions=["C"],
        utilities=["F"],
    )

    return cid
Exemplo n.º 23
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"),
        ],
        decisions=["D1", "D2"],
        utilities=["U"],
    )
    return cid
Exemplo n.º 24
0
def get_minimal_cid() -> CID:
    cid = CID([("A", "B")], decisions=["A"], utilities=["B"])
    cid.add_cpds(A=[0, 1], B=lambda a: a)
    return cid
Exemplo n.º 25
0
def get_minimal_cid() -> CID:
    cid = CID([("A", "B")], decisions=["A"], utilities=["B"])
    cpd_a = DecisionDomain("A", [0, 1])
    cpd_b = FunctionCPD("B", lambda a: a)  # type: ignore
    cid.add_cpds(cpd_a, cpd_b)
    return cid
Exemplo n.º 26
0
def random_cid(
    number_of_nodes: int = 8,
    number_of_decisions: int = 1,
    number_of_utilities: int = 1,
    add_cpds: bool = True,
    sufficient_recall: bool = False,
    edge_density: float = 0.4,
    max_in_degree: int = 4,
    max_resampling_attempts: int = 100,
) -> CID:
    """
    Generate a random CID.

    Parameters:
    -----------
    number_of nodes: The total number of nodes in the CID.

    number_of_decisions: The number of decisions in the CID.

    number_of_utilities: The number of utilities in the CID.

    add_cpds: True if we should pararemeterise the CID as a model.
    This adds [0,1] domains to every decision node and RandomCPDs to every utility and chance node in the CID.

    sufficient_recall: True the agent should have sufficient recall of all of its previous decisions.
    An Agent has sufficient recall in a CID if the relevance graph is acyclic.

    edge_density: The density of edges in the CID's DAG as a proportion of the maximum possible number of nodes
    in the DAG - n*(n-1)/2

    max_in_degree: The maximal number of edges incident to a node in the CID's DAG.

    max_resampling_attempts: The maxmimum number of resampling of random DAGs attempts in order to try
    to satisfy all constraints.

    Returns
    -------
    A CID that satisfies the given constraints or a ValueError if it was unable to meet the constraints in the
    specified number of attempts.

    """
    mb = random_macidbase(
        number_of_nodes=number_of_nodes,
        agent_decisions_num=(number_of_decisions,),
        agent_utilities_num=(number_of_utilities,),
        add_cpds=False,
        sufficient_recall=sufficient_recall,
        edge_density=edge_density,
        max_in_degree=max_in_degree,
        max_resampling_attempts=max_resampling_attempts,
    )

    dag = DAG(mb.edges)
    decision_nodes = mb.decisions
    utility_nodes = mb.utilities

    # change the naming style of decision and utility nodes
    dec_name_change = {old_dec_name: "D" + str(i) for i, old_dec_name in enumerate(decision_nodes)}
    util_name_change = {old_util_name: "U" + str(i) for i, old_util_name in enumerate(utility_nodes)}
    node_name_change_map = {**dec_name_change, **util_name_change}
    dag = nx.relabel_nodes(dag, node_name_change_map)

    cid = CID(dag.edges, decisions=list(dec_name_change.values()), utilities=list(util_name_change.values()))

    if add_cpds:
        add_random_cpds(cid)
    return cid
Exemplo n.º 27
0
def get_insufficient_recall_cid() -> CID:
    cid = CID([("A", "U"), ("B", "U")], decisions=["A", "B"], utilities=["U"])
    cid.add_cpds(A=[0, 1], B=[0, 1], U=lambda a, b: a * b)
    return cid
Exemplo n.º 28
0
def total_effect(cid: CID, a: str, x: str, a0: int = 0, a1: int = 1) -> float:
    "the total effect on x from intervening on a with a2 rather than a1"
    total_effect = (
        cid.expected_value([x], {}, intervention={a: a1})[0] - cid.expected_value([x], {}, intervention={a: a0})[0]
    )
    return total_effect  # type: ignore
Exemplo n.º 29
0
def get_3node_cid() -> CID:
    cid = CID([("S", "D"), ("S", "U"), ("D", "U")],
              decisions=["D"],
              utilities=["U"])
    cid.add_cpds(S=discrete_uniform([-1, 1]), U=lambda s, d: s * d, D=[-1, 1])
    return cid