Exemple #1
0
    def initialize_tabular_cpd(self, cbn: CausalBayesianNetwork) -> None:
        """Initialize the probability table for the inherited TabularCPD.

        Requires that all parents in the CID have already been instantiated.
        """
        self.check_function_arguments_match_parent_names(cbn)
        self.cbn = cbn
        if self.force_domain:
            if not set(self.possible_values(cbn)).issubset(self.force_domain):
                raise ValueError(
                    "variable {} can take value outside given state_names".
                    format(self.variable))

        domain: Sequence[
            Outcome] = self.force_domain if self.force_domain else self.possible_values(
                cbn)

        def complete_prob_dictionary(
            prob_dictionary: Dict[Outcome, Union[int, float]]
        ) -> Dict[Outcome, Union[int, float]]:
            """Complete a probability dictionary with probabilities for missing outcomes"""
            missing_outcomes = set(domain) - set(prob_dictionary.keys())
            missing_prob_mass = 1 - sum(
                prob_dictionary.values())  # type: ignore
            for outcome in missing_outcomes:
                prob_dictionary[outcome] = missing_prob_mass / len(
                    missing_outcomes)
            return prob_dictionary

        card = len(domain)
        evidence = cbn.get_parents(self.variable)
        evidence_card = [cbn.get_cardinality(p) for p in evidence]
        probability_list = []
        for pv in self.parent_values(cbn):
            probabilities = complete_prob_dictionary(
                self.stochastic_function(**pv))
            probability_list.append([probabilities[t] for t in domain])
        probability_matrix = np.array(probability_list).T
        if not np.allclose(probability_matrix.sum(axis=0), 1, atol=0.01):
            raise ValueError(
                f"The values for {self.variable} do not sum to 1 \n{probability_matrix}"
            )
        if (probability_matrix < 0).any() or (probability_matrix > 1).any():
            raise ValueError(
                f"The probabilities for {self.variable} are not within range 0-1\n{probability_matrix}"
            )
        self.domain = domain

        super().__init__(self.variable,
                         card,
                         probability_matrix,
                         evidence,
                         evidence_card,
                         state_names={self.variable: self.domain})
Exemple #2
0
 def parent_values(
         self, cbn: CausalBayesianNetwork) -> Iterator[Dict[str, Outcome]]:
     """Return a list of lists for the values each parent can take (based on the parent state names)"""
     parent_values_list = []
     for p in cbn.get_parents(self.variable):
         try:
             parent_values_list.append(cbn.model.domain[p])
         except KeyError:
             raise ParentsNotReadyException(
                 f"Parent {p} of {self.variable} not yet instantiated")
     for parent_values in itertools.product(*parent_values_list):
         yield {
             p.lower(): parent_values[i]
             for i, p in enumerate(cbn.get_parents(self.variable))
         }
Exemple #3
0
 def test_introduced_bias_reversed_sign(self) -> None:
     cbn = CausalBayesianNetwork([("A", "D"), ("A", "Y")])
     cbn.add_cpds(A=discrete_uniform([0, 1]), D=lambda a: 0, Y=lambda a: a)
     assert introduced_total_effect(cbn, "A", "D", "Y") == pytest.approx(-1)
     cbn.add_cpds(Y=lambda a: -a)
     assert introduced_total_effect(
         cbn, "A", "D", "Y", adapt_marginalized=True) == pytest.approx(-1)
Exemple #4
0
 def check_function_arguments_match_parent_names(
         self, cbn: CausalBayesianNetwork) -> None:
     """Raises a ValueError if the parents in the CID don't match the argument to the specified function"""
     sig = inspect.signature(self.stochastic_function).parameters
     arg_kinds = [arg_kind.kind.name for arg_kind in sig.values()]
     args = set(sig)
     lower_case_parents = {
         p.lower()
         for p in cbn.get_parents(self.variable)
     }
     if "VAR_KEYWORD" not in arg_kinds and args != lower_case_parents:
         raise ValueError(
             f"function for {self.variable} mismatch parents on"
             f" {args.symmetric_difference(lower_case_parents)}, ")
Exemple #5
0
def test_random_cpd_copy() -> None:
    """check that a copy of a random cpd yields the same distribution"""
    cbn = CausalBayesianNetwork([("A", "B")])
    cbn.add_cpds(
        RandomCPD("A"),
        FunctionCPD("B", lambda a: a),
    )
    cbn2 = cbn.copy()
    assert cbn.expected_value(["B"], {}) == cbn2.expected_value(["B"], {})
Exemple #6
0
 def test_introduced_bias_reversed_sign(self) -> None:
     cbn = CausalBayesianNetwork([("A", "D"), ("A", "Y")])
     cbn.add_cpds(
         UniformRandomCPD("A", [0, 1]),
         FunctionCPD("D", lambda a: 0),
         FunctionCPD("Y", lambda a: a),
     )
     assert introduced_total_effect(cbn, "A", "D", "Y") == pytest.approx(-1)
     cbn.add_cpds(FunctionCPD("Y", lambda a: -a))
     assert introduced_total_effect(
         cbn, "A", "D", "Y", adapt_marginalized=True) == pytest.approx(-1)
Exemple #7
0
 def test_valid_context(cbn_3node: CausalBayesianNetwork) -> None:
     with pytest.raises(ValueError):
         cbn_3node.query(["U"], {"S": 0})
Exemple #8
0
 def test_query_disconnected_components() -> None:
     cbn = CausalBayesianNetwork([("A", "B")])
     cbn.add_cpds(A=RandomCPD(), B=RandomCPD())
     cbn.query(["A"], {}, intervention={
         "B": 0
     })  # the intervention separates A and B into separare components
Exemple #9
0
 def test_query(cbn_3node: CausalBayesianNetwork) -> None:
     assert cbn_3node.query(["U"], {"D": 2}).values[2] == float(1.0)
Exemple #10
0
 def test_remove_cpds(cbn_3node: CausalBayesianNetwork) -> None:
     cbn_3node.remove_cpds("S")
     assert "S" not in cbn_3node.model
     assert cbn_3node.get_cpds("S") is None
     cbn_3node.remove_cpds("D")
     cbn_3node.remove_cpds("U")
Exemple #11
0
 def remove_node(cbn_3node: CausalBayesianNetwork) -> None:
     cbn_3node.remove_node("S")
     cbn_3node.remove_cpds("D")
     cbn_3node.remove_cpds("U")
     assert cbn_3node.nodes == []
Exemple #12
0
 def test_copy_without_cpds(cbn_3node: CausalBayesianNetwork) -> None:
     assert len(cbn_3node.copy_without_cpds().cpds) == 0
Exemple #13
0
    def __init__(
        self,
        variable: str,
        stochastic_function: Callable[..., Union[Outcome, Mapping[Outcome, Union[int, float]]]],
        cbn: CausalBayesianNetwork,
        domain: Optional[Sequence[Outcome]] = None,
        label: str = None,
    ) -> None:
        """Initialize StochasticFunctionCPD with a variable name and a stochastic function.


        Parameters
        ----------
        variable: The variable name.

        stochastic_function: A stochastic function that maps parent outcomes to a distribution
        over outcomes for this variable (see doc-string for class).
        The different parents are identified by name: the arguments to the function must
        be lowercase versions of the names of the parent variables. For example, if X has
        parents Y, S1, and Obs, the arguments to function must be y, s1, and obs.

        domain: An optional specification of the variable's domain.
            Must include all values this variable can take as a result of its function.

        label: An optional label used to describe this distribution.
        """
        self.variable = variable
        self.func = stochastic_function
        self.cbn = cbn

        assert isinstance(domain, (list, type(None)))
        self.force_domain: Optional[Sequence[Outcome]] = domain

        assert isinstance(label, (str, type(None)))
        self.label = label if label is not None else self.compute_label(stochastic_function)

        self.check_function_arguments_match_parent_names()
        if self.force_domain:
            if not set(self.possible_values()).issubset(self.force_domain):
                raise ValueError("variable {} can take value outside given state_names".format(self.variable))

        self.domain = self.force_domain if self.force_domain else self.possible_values()

        def complete_prob_dictionary(
            prob_dictionary: Mapping[Outcome, Union[int, float]]
        ) -> Mapping[Outcome, Union[int, float]]:
            """Complete a probability dictionary with probabilities for missing outcomes"""
            prob_dictionary = {key: value for key, value in prob_dictionary.items() if value is not None}
            missing_outcomes = set(self.domain) - set(prob_dictionary.keys())
            missing_prob_mass = 1 - sum(prob_dictionary.values())  # type: ignore
            for outcome in missing_outcomes:
                prob_dictionary[outcome] = missing_prob_mass / len(missing_outcomes)
            return prob_dictionary

        card = len(self.domain)
        evidence = cbn.get_parents(self.variable)
        evidence_card = [cbn.get_cardinality(p) for p in evidence]
        probability_list = []
        for pv in self.parent_values():
            probabilities = complete_prob_dictionary(self.stochastic_function(**pv))
            probability_list.append([probabilities[t] for t in self.domain])
        probability_matrix = np.array(probability_list).T
        if not np.allclose(probability_matrix.sum(axis=0), 1, rtol=0, atol=0.01):  # type: ignore
            raise ValueError(f"The values for {self.variable} do not sum to 1 \n{probability_matrix}")
        if (probability_matrix < 0).any() or (probability_matrix > 1).any():  # type: ignore
            raise ValueError(f"The probabilities for {self.variable} are not within range 0-1\n{probability_matrix}")

        super().__init__(
            self.variable, card, probability_matrix, evidence, evidence_card, state_names={self.variable: self.domain}
        )