← All tutorials

06 / PROBABILISTIC MODELS

Update beliefs
with evidence.

Build a small discrete Bayesian network in CoolBox. Define a prior and a conditional probability table, then ask how observing wet grass changes the probability of rain.

10 MININTERMEDIATE

1. Draw the dependencies

A Bayesian network represents a joint probability distribution with a directed acyclic graph. Nodes are random variables; the graph and conditional probability tables together define how the joint distribution factorizes.

Use two binary variables: Rain and Grass, with an edge from Rain to Grass. The model says P(Rain, Grass) = P(Rain) × P(Grass | Rain). It is an intentionally simplified example, not a weather forecasting model.

2. Specify probabilities

State order matters: Rain is ["Rain", "No rain"], and Grass is ["Wet", "Dry"]. The rain prior is [0.3, 0.7]. Grass's first row is [0.9, 0.1] given rain; its second is [0.1, 0.9] given no rain. Every row sums to one.

These are supplied assumptions, not parameters learned from data. Wet grass is still possible without rain, which keeps observing grass from identifying rain with certainty. Add all edges before setting tables. With multiple parents, verify the implementation's parent ordering before flattening a table.

3. Query the prior and posterior

Install CoolBox with the matching setup guide. Python's binding takes a two-dimensional CPT and returns one query-state probability with infer. The native C++ implementation accepts flattened tables and returns a full state distribution with query. They are different interfaces to the same example distribution; do not swap their table formats or method names.

PYTHON · EXACT INFERENCE
from ml_toolbox import bayesian_network

network = bayesian_network.BayesianNetwork()
rain = network.add_node("Rain", ["Rain", "No rain"])
grass = network.add_node("Grass", ["Wet", "Dry"])
network.add_edge(rain, grass)

# Rows are parent states; columns are this node's states.
network.set_cpt(rain, [[0.3, 0.7]])
network.set_cpt(grass, [[0.9, 0.1], [0.1, 0.9]])

prior = network.infer(rain, 0, {})
wet = network.infer(grass, 0, {})
posterior = network.infer(rain, 0, {grass: 0})
dry_posterior = network.infer(rain, 0, {grass: 1})
assert abs(wet - 0.34) < 1e-9
assert abs(posterior - 0.27 / 0.34) < 1e-9
print(f"P(rain): {prior:.6f}")
print(f"P(wet): {wet:.6f}")
print(f"P(rain | wet): {posterior:.6f}")
print(f"P(rain | dry): {dry_posterior:.6f}")

The binding's matrix converter accepts these rectangular nested sequences. Its Python package manages its declared dependencies; no plotting package is needed.

4. See the update

Rain influences grass. Probability of rain is 30 percent before observing grass, 79.41 percent given wet grass, and 4.55 percent given dry grass.
Probability comparison for the supplied tables, verified with CoolBox's native C++ inference. Bars visualize exact probabilities, not confidence intervals or learned model accuracy.

The marginal probability of wet grass is 0.3 × 0.9 + 0.7 × 0.1 = 0.34. Bayes' rule gives P(rain | wet) = 0.27 / 0.34 ≈ 0.794118. Observing dry grass instead gives 0.03 / 0.66 ≈ 0.045455.

Do not reverse conditionals: P(wet | rain) = 0.9 is not P(rain | wet). The rain prior and alternative explanations matter. Conditioning on an observation is also not the same as intervening on a variable; these arrows alone do not establish causal effects.

5. Extend carefully

Add a sprinkler parent to Grass to model another explanation, then provide a row for every parent-state combination. Keep all entries finite, between zero and one, and each row normalized. Avoid impossible evidence: conditioning on an event with zero model probability has no defined posterior.

Exact inference can become expensive as a network grows. Start with small graphs, validate manually computable queries, and consult the native discrete-network API before assuming support for parameter learning, continuous variables, or approximate inference.