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)) }
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})
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)}, ")
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} )