Medical Diagnosis with Bayesian Networks

Build a probabilistic diagnosis engine that infers diseases from symptoms.

Introduction

30 Second Summary

When a doctor hears you coughing, they don't immediately know what's wrong. They weigh the odds, considering how common each illness is and how well your symptoms match each possibility.

In this project, you will build a medical diagnosis engine that uses the same probabilistic reasoning to compute disease probabilities from observed symptoms. You will write Python code that updates its beliefs as new evidence arrives.

What You'll Build

You will build a Python-based diagnosis engine powered by Bayesian networks that computes the probability of diseases given observed symptoms.

By the end of this project, you'll have:

  • A working Rain → Wet Grass network that demonstrates how evidence updates probability through Bayesian inference.
  • A medical Asthma → Wheeze network that computes posterior probabilities when symptoms are observed.
  • A multi-cause diagnosis network that reasons about multiple diseases and symptoms simultaneously, showing how each piece of evidence shifts beliefs.
  • Secret Mission: Add "explaining away" to your network and demonstrate how confirming one cause reduces the probability of another.

Are there any prerequisites?

You only need basic Python knowledge (variables, functions, and print statements). No probability or statistics background is needed.

Before We Start

Before We Start...

Set Up Your Python Environment

Before you can build a diagnosis engine, you need the right tools installed on your machine.

In this step, you will install Python and a library called pgmpy that handles all the probability math behind Bayesian networks.

In this step, get ready to:
  • Install and verify Python 3.10 or higher.
  • Install the pgmpy library for probabilistic graphical models.
  • Create your project file and confirm everything works together.
Install Python and verify

Python is the programming language you will use to build the diagnosis engine. This project requires Python 3.10 or higher.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Python version by running this command:
python --version

You should see a version number printed in your terminal.

✔️ I see version 3.10 or higher

Your Python version is compatible. You are ready to move on to installing pgmpy.

ⓧ I see an older version

You have Python installed, but it is below version 3.10. You need to upgrade.

  • Go to python.org/downloads in your browser.
  • Download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, make sure to check Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python --version again to confirm it shows 3.10 or higher.

macOS users with an old system Python

macOS sometimes ships with an older Python. If python --version still shows an old version after installing, try python3 --version instead. Use python3 for all commands in this project if that is the case.

ⓧ Command not found

Python is not installed on your machine yet. Follow these steps to install it.

  • Go to python.org/downloads in your browser.
  • Download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, make sure to check Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python --version again to confirm it shows 3.10 or higher.

macOS users

On macOS, the command may be python3 --version instead of python --version. If that is the case, use python3 and pip3 for all commands in this project.

Install pgmpy

pgmpy is a Python library built for working with probabilistic graphical models. It handles the math behind Bayesian networks so you can focus on defining relationships and running queries.

  • Install pgmpy by running this command in your terminal:
pip install pgmpy==1.1.2

What does this command do?

`pip install` downloads and installs a Python package from the Python Package Index (PyPI).

The ==1.1.2 part pins the exact version so your results match this project. This also installs dependencies like numpy, networkx, and pandas automatically.

  • Wait for the installation to finish. You should see a success message at the end.

Seeing a permission error or pip not found?

If you see a permissions error, try adding --user to the end of the command: pip install pgmpy==1.1.2 --user.

If pip is not found, try pip3 install pgmpy==1.1.2 instead. This is common on macOS where pip3 is the default package manager for Python 3.

Help me fix my pip installation issue

Verify the installation

Now you will create the project file and confirm that pgmpy is properly installed by importing its main class.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a new folder for this project and move into it by running these commands:
mkdir diagnosis-engine
cd diagnosis-engine
  • Create the project file by running:
touch diagnosis_engine.py
  • Confirm the file exists by running:
ls

You should see diagnosis_engine.py in the output.

  • Open diagnosis_engine.py in your text editor of choice (VS Code, Cursor, or any editor you prefer).
  • Add the following code to the file:
from pgmpy.models import DiscreteBayesianNetwork
print("pgmpy is ready!")

What does this code do?

`from pgmpy.models import DiscreteBayesianNetwork` imports the main class you will use to define Bayesian network structures throughout this project.

The print statement confirms the import worked. If pgmpy is not installed correctly, Python will throw an error before reaching the print line.

  • Save diagnosis_engine.py.
  • Run the file by executing this command in your terminal:
python diagnosis_engine.py

You should see pgmpy is ready! printed in your terminal.

Seeing a ModuleNotFoundError?

If you see ModuleNotFoundError: No module named 'pgmpy', your pip and python commands may be pointing to different Python installations.

Try running python3 diagnosis_engine.py instead. If that does not work, reinstall using python -m pip install pgmpy==1.1.2 to ensure pip installs into the same Python you are running.

Help me fix my ModuleNotFoundError

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is what diagnosis_engine.py should look like at this point:

from pgmpy.models import DiscreteBayesianNetwork
print("pgmpy is ready!")

Your environment is ready. Next up, you will build your first Bayesian network and see how evidence changes beliefs about whether it rained.

Build the Rain → Wet Grass Network

Your Python environment is ready and pgmpy is installed. Now it is time to build your first Bayesian network.

A Bayesian network is a graph that models how variables influence each other through probability. You will start with a simple two-node scenario: does rain cause the grass to be wet?

In this step, get ready to:
  • Define a network structure with nodes and edges.
  • Encode prior and conditional probabilities as CPDs.
  • Run inference to see how evidence updates your belief about rain.
Define the network structure (nodes and edges)

A Bayesian network is made of nodes and edges. Each node represents a variable. Each edge (arrow) represents a causal relationship.

Your first network has two nodes: Rain and WetGrass. The edge Rain → WetGrass means "rain influences whether the grass is wet."

  • In diagnosis_engine.py, select all the existing code and replace it with the following:
from pgmpy.models import DiscreteBayesianNetwork
from pgmpy.factors.discrete import TabularCPD
from pgmpy.inference import VariableElimination

# ==============================================================
# PART 1: Rain -> Wet Grass (Building Intuition)
# ==============================================================
print("=" * 60)
print("PART 1: Rain -> Wet Grass Network")
print("=" * 60)

# Define the network structure: one edge from Rain to WetGrass
rain_model = DiscreteBayesianNetwork([("Rain", "WetGrass")])

What does this code do?

  • The first three lines import the classes you need: DiscreteBayesianNetwork for the graph structure, TabularCPD for probability tables, and VariableElimination for running queries.
  • The print statements create a visual header so you can identify Part 1 output in the terminal.
  • DiscreteBayesianNetwork([("Rain", "WetGrass")]) creates a directed graph with two nodes connected by one edge. The tuple means "Rain causes WetGrass."
  • Save diagnosis_engine.py.
  • Run the script with python diagnosis_engine.py. You should see the header banner printed in your terminal.

Not seeing the header output?

  • Make sure you saved the file before running the command.
  • Check that you deleted the old print("pgmpy is ready!") line. The file should start with the from pgmpy.models import.

Why isn't my diagnosis_engine.py printing the header?

Define priors and conditional probabilities (CPDs)

The network structure only describes which variables are connected. You still need to tell it the actual probabilities. These are called CPDs (Conditional Probability Distributions).

  • Add the prior probability for Rain below your rain_model line:
# Define priors and conditional probabilities
# Prior: P(Rain=yes) = 0.2, P(Rain=no) = 0.8
cpd_rain = TabularCPD(
    variable="Rain",
    variable_card=2,
    values=[[0.8], [0.2]],
    state_names={"Rain": ["no", "yes"]},
)

What is a prior?

A prior is your belief about a variable before observing any evidence. Here, P(Rain=yes) = 0.2 means it rains on 20% of days. The state_names parameter gives each state a human-readable label so your output shows "yes" and "no" instead of numbers.

You will not see any new output yet. The next chunk adds the conditional probability and validates the full model.

  • Add the conditional probability for WetGrass and the validation code below cpd_rain:
# Conditional: P(WetGrass | Rain)
# If Rain=no: P(WetGrass=yes) = 0.1 (sprinkler, dew, etc.)
# If Rain=yes: P(WetGrass=yes) = 0.9
cpd_wet = TabularCPD(
    variable="WetGrass",
    variable_card=2,
    values=[
        [0.9, 0.1],  # P(WetGrass=no | Rain=no), P(WetGrass=no | Rain=yes)
        [0.1, 0.9],  # P(WetGrass=yes | Rain=no), P(WetGrass=yes | Rain=yes)
    ],
    evidence=["Rain"],
    evidence_card=[2],
    state_names={"WetGrass": ["no", "yes"], "Rain": ["no", "yes"]},
)

# Add CPDs and validate
rain_model.add_cpds(cpd_rain, cpd_wet)
assert rain_model.check_model()

What is a conditional probability?

A conditional probability answers: "Given that Rain is in a certain state, how likely is WetGrass to be wet?" P(WetGrass=yes | Rain=yes) = 0.9 means if it rains, there is a 90% chance the grass is wet.

P(WetGrass=yes | Rain=no) = 0.1 accounts for other causes of wet grass like sprinklers. The assert rain_model.check_model() line verifies all probabilities sum to 1 and the structure is valid.

  • Save diagnosis_engine.py and run python diagnosis_engine.py again.
  • You should see the same header output with no errors. If the assert passes silently, your model's probabilities are valid.

Seeing an AssertionError?

  • Double check that the values in cpd_rain are [[0.8], [0.2]]. Each column must sum to 1.0.
  • In cpd_wet, confirm each column sums to 1.0. The first column is [0.9, 0.1] and the second is [0.1, 0.9].

Why is my Bayesian network model failing validation?

Run inference with evidence

Your network knows the structure and the probabilities. Now you can ask it questions. This is called inference.

The question you will ask is: "I looked outside and the grass is wet. How likely is it that it rained?" The wet grass is your evidence.

  • Add the inference code at the bottom of diagnosis_engine.py, below the assert line:
# Inference: What is P(Rain | WetGrass=yes)?
rain_infer = VariableElimination(rain_model)

print("\nPrior probability of Rain (before observing anything):")
prior = rain_infer.query(variables=["Rain"])
print(prior)

print("\nPosterior probability of Rain GIVEN we observe Wet Grass:")
posterior = rain_infer.query(variables=["Rain"], evidence={"WetGrass": "yes"})
print(posterior)
print("\nIntuition: The prior P(Rain=yes) was 0.20.")
print("After observing wet grass, P(Rain=yes) jumps to ~0.69!")
print("Evidence changed our belief. That is Bayesian inference.")

What does this code do?

  • VariableElimination(rain_model) creates an inference engine that can answer probability questions about your network.
  • The first query (rain_infer.query(variables=["Rain"])) asks for the prior. With no evidence, it returns the original 20%/80% split.
  • The second query passes evidence={"WetGrass": "yes"}. This is the observation. The engine uses Bayes' theorem to compute the posterior probability of rain given wet grass.
  • Save diagnosis_engine.py and run python diagnosis_engine.py.
  • You should see a probability table for the prior showing P(Rain=yes) = 0.20, and a second table for the posterior showing P(Rain=yes) jumping to approximately 0.69.

Why does the probability jump from 0.20 to 0.69?

Before you looked outside, you believed there was only a 20% chance of rain. But wet grass is strongly associated with rain (90% chance of wet grass when it rains). Seeing wet grass is strong evidence, so your belief in rain jumps.

It does not jump all the way to 100% because there is still a 10% chance the grass got wet from something else (like a sprinkler). This is the power of Bayesian inference. Evidence updates your beliefs proportionally.

Seeing unexpected output or errors?

  • You may see a progress message like "Finding Elimination Order" in your output. This is normal and does not affect your results.
  • If you see a KeyError, check that the evidence dictionary uses the exact string "WetGrass" (capital W and G) matching your node name.
  • If you see NameError: name 'rain_infer' is not defined, make sure the inference code is placed below the assert rain_model.check_model() line.

Help me debug my Rain -> WetGrass inference query

✔️ Awesome, I've got everything!

Great. Double check you have saved diagnosis_engine.py and your output shows the probability tables.

ⓧ I'd like to double check the full code

Here is the complete diagnosis_engine.py at this point:

from pgmpy.models import DiscreteBayesianNetwork
from pgmpy.factors.discrete import TabularCPD
from pgmpy.inference import VariableElimination

# ==============================================================
# PART 1: Rain -> Wet Grass (Building Intuition)
# ==============================================================
print("=" * 60)
print("PART 1: Rain -> Wet Grass Network")
print("=" * 60)

# Define the network structure: one edge from Rain to WetGrass
rain_model = DiscreteBayesianNetwork([("Rain", "WetGrass")])

# Define priors and conditional probabilities
# Prior: P(Rain=yes) = 0.2, P(Rain=no) = 0.8
cpd_rain = TabularCPD(
    variable="Rain",
    variable_card=2,
    values=[[0.8], [0.2]],
    state_names={"Rain": ["no", "yes"]},
)

# Conditional: P(WetGrass | Rain)
# If Rain=no: P(WetGrass=yes) = 0.1 (sprinkler, dew, etc.)
# If Rain=yes: P(WetGrass=yes) = 0.9
cpd_wet = TabularCPD(
    variable="WetGrass",
    variable_card=2,
    values=[
        [0.9, 0.1],  # P(WetGrass=no | Rain=no), P(WetGrass=no | Rain=yes)
        [0.1, 0.9],  # P(WetGrass=yes | Rain=no), P(WetGrass=yes | Rain=yes)
    ],
    evidence=["Rain"],
    evidence_card=[2],
    state_names={"WetGrass": ["no", "yes"], "Rain": ["no", "yes"]},
)

# Add CPDs and validate
rain_model.add_cpds(cpd_rain, cpd_wet)
assert rain_model.check_model()

# Inference: What is P(Rain | WetGrass=yes)?
rain_infer = VariableElimination(rain_model)

print("\nPrior probability of Rain (before observing anything):")
prior = rain_infer.query(variables=["Rain"])
print(prior)

print("\nPosterior probability of Rain GIVEN we observe Wet Grass:")
posterior = rain_infer.query(variables=["Rain"], evidence={"WetGrass": "yes"})
print(posterior)
print("\nIntuition: The prior P(Rain=yes) was 0.20.")
print("After observing wet grass, P(Rain=yes) jumps to ~0.69!")
print("Evidence changed our belief. That is Bayesian inference.")

You just proved that observing wet grass makes rain much more likely. Next up, you will apply this same pattern to medicine and compute the probability of asthma given a wheezing patient.

Build the Asthma → Wheeze Medical Network

In the previous step, you saw how observing wet grass updated your belief about rain from 20% to 69%. That is Bayesian inference in action.

Now you will apply the exact same pattern to a medical scenario. Instead of rain causing wet grass, you will model asthma causing wheezing. The structure is identical, but the domain makes the power of probabilistic reasoning much more tangible.

In this step, get ready to:
  • Define a medical Bayesian network with Asthma causing Wheeze.
  • Set a realistic disease prior (base rate) and conditional symptom probabilities.
  • Diagnose a wheezing patient and interpret how evidence shifts the posterior.
Define the medical network structure

Your Rain network had two nodes connected by a causal arrow. The medical network follows the same pattern with different variables.

  • In diagnosis_engine.py, scroll to the bottom of the file (below all the Part 1 code).
  • Add the Part 2 section header and network structure by copying this code at the end of the file:
# ==============================================================
# PART 2: Asthma -> Wheeze (Medical Diagnosis)
# ==============================================================
print("\n" + "=" * 60)
print("PART 2: Asthma -> Wheeze (Medical Diagnosis)")
print("=" * 60)

# Define the medical network
asthma_model = DiscreteBayesianNetwork([("Asthma", "Wheeze")])

What does this code do?

You are creating a new Bayesian network with one edge: Asthma causes Wheeze. Asthma is the hidden disease (the cause), and Wheeze is the observable symptom (the effect).

The edge direction matters. ("Asthma", "Wheeze") means "having asthma can cause wheezing." The arrow encodes a causal claim about the world.

  • Save diagnosis_engine.py and run it with:
python diagnosis_engine.py
  • Below the Part 1 output, you should see the Part 2 header banner printed.

Seeing a SyntaxError?

Make sure you pasted the new code BELOW all of the Part 1 code, not inside it. The Part 2 section should start after the last print statement of Part 1.

Help me debug a SyntaxError in my diagnosis_engine.py file

Set medical priors and conditional probabilities

Just like Rain needed a prior probability, Asthma needs a base rate. In the general population, about 5% of patients have asthma. This is the probability before you observe any symptoms.

  • Add the CPD definitions below the asthma_model line you just added:
# Prior: P(Asthma=yes) = 0.05 (5% base rate)
cpd_asthma = TabularCPD(
    variable="Asthma",
    variable_card=2,
    values=[[0.95], [0.05]],
    state_names={"Asthma": ["no", "yes"]},
)

# Conditional: P(Wheeze | Asthma)
# Asthmatics wheeze 80% of the time
# Non-asthmatics wheeze 10% of the time (allergies, colds, etc.)
cpd_wheeze = TabularCPD(
    variable="Wheeze",
    variable_card=2,
    values=[
        [0.9, 0.2],  # P(Wheeze=no | Asthma=no), P(Wheeze=no | Asthma=yes)
        [0.1, 0.8],  # P(Wheeze=yes | Asthma=no), P(Wheeze=yes | Asthma=yes)
    ],
    evidence=["Asthma"],
    evidence_card=[2],
    state_names={"Wheeze": ["no", "yes"], "Asthma": ["no", "yes"]},
)

asthma_model.add_cpds(cpd_asthma, cpd_wheeze)
assert asthma_model.check_model()

What do these numbers mean?

The prior P(Asthma=yes) = 0.05 says 5% of patients have asthma before you observe anything. This is the starting belief.

The conditional probabilities encode medical knowledge. Asthmatics wheeze 80% of the time (strong association). Non-asthmatics still wheeze 10% of the time due to allergies, colds, and other causes. This gap is what makes wheezing informative but not conclusive.

  • Save diagnosis_engine.py and run it again.
  • If you do not see an AssertionError, your model's CPDs are valid and the probabilities sum correctly.

Seeing an AssertionError?

This means the probabilities in your CPDs do not sum to 1 for each column. Double-check that the values arrays match the code above exactly. Each column must add up to 1.0 (e.g., 0.9 + 0.1 = 1.0).

Help me fix an AssertionError in my Bayesian network CPDs

Diagnose with evidence and interpret the posterior

A patient walks in wheezing. You want to answer: given this symptom, how likely is it that they have asthma? This is where inference turns your network into a diagnosis tool.

  • Add the inference code below the assert asthma_model.check_model() line:
# Diagnosis: Patient is wheezing. What is P(Asthma | Wheeze=yes)?
asthma_infer = VariableElimination(asthma_model)

print("\nPrior probability of Asthma (before seeing patient):")
prior_asthma = asthma_infer.query(variables=["Asthma"])
print(prior_asthma)

print("\nPosterior: P(Asthma | patient is wheezing):")
posterior_asthma = asthma_infer.query(
    variables=["Asthma"], evidence={"Wheeze": "yes"}
)
print(posterior_asthma)
print("\nIntuition: Prior was 5%. After observing wheeze, it rises to ~30%.")
print("The symptom is suggestive but not conclusive (other things cause wheezing).")

What does this code do?

You set evidence by passing {"Wheeze": "yes"} to the query. This tells the network: "I observed this patient wheezing. Update all beliefs accordingly."

The network computes the posterior probability of Asthma. The prior was 5%, but wheezing is strongly linked to asthma, so the posterior rises to approximately 30%. It does not jump to 100% because non-asthmatics also wheeze sometimes.

  • Save diagnosis_engine.py and run the script:
python diagnosis_engine.py
  • In the Part 2 output, you should see the prior probability of Asthma at 0.05, then the posterior after observing wheezing rise to approximately 0.30.

Output shows unexpected probabilities?

If the posterior is not near 0.30, check that your cpd_asthma uses [[0.95], [0.05]] (not [[0.05], [0.95]]). The first row is always the "no" state because of the state_names ordering.

Help me understand why my posterior probability looks wrong

Why doesn't the posterior reach 100%?

Even though wheezing is strongly associated with asthma, the network knows that 10% of non-asthmatics also wheeze. Since 95% of people do NOT have asthma, that 10% adds up to a lot of non-asthmatic wheezers. The network balances both explanations.

This is Bayes' theorem at work. The prior (how common the disease is) matters just as much as the conditional (how strongly the symptom links to the disease).

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete diagnosis_engine.py file up to this point:

from pgmpy.models import DiscreteBayesianNetwork
from pgmpy.factors.discrete import TabularCPD
from pgmpy.inference import VariableElimination

# ==============================================================
# PART 1: Rain -> Wet Grass (Building Intuition)
# ==============================================================
print("=" * 60)
print("PART 1: Rain -> Wet Grass Network")
print("=" * 60)

# Define the network structure: one edge from Rain to WetGrass
rain_model = DiscreteBayesianNetwork([("Rain", "WetGrass")])

# Define priors and conditional probabilities
# Prior: P(Rain=yes) = 0.2, P(Rain=no) = 0.8
cpd_rain = TabularCPD(
    variable="Rain",
    variable_card=2,
    values=[[0.8], [0.2]],
    state_names={"Rain": ["no", "yes"]},
)

# Conditional: P(WetGrass | Rain)
# If Rain=no: P(WetGrass=yes) = 0.1 (sprinkler, dew, etc.)
# If Rain=yes: P(WetGrass=yes) = 0.9
cpd_wet = TabularCPD(
    variable="WetGrass",
    variable_card=2,
    values=[
        [0.9, 0.1],  # P(WetGrass=no | Rain=no), P(WetGrass=no | Rain=yes)
        [0.1, 0.9],  # P(WetGrass=yes | Rain=no), P(WetGrass=yes | Rain=yes)
    ],
    evidence=["Rain"],
    evidence_card=[2],
    state_names={"WetGrass": ["no", "yes"], "Rain": ["no", "yes"]},
)

# Add CPDs and validate
rain_model.add_cpds(cpd_rain, cpd_wet)
assert rain_model.check_model()

# Inference: What is P(Rain | WetGrass=yes)?
rain_infer = VariableElimination(rain_model)

print("\nPrior probability of Rain (before observing anything):")
prior = rain_infer.query(variables=["Rain"])
print(prior)

print("\nPosterior probability of Rain GIVEN we observe Wet Grass:")
posterior = rain_infer.query(variables=["Rain"], evidence={"WetGrass": "yes"})
print(posterior)
print("\nIntuition: The prior P(Rain=yes) was 0.20.")
print("After observing wet grass, P(Rain=yes) jumps to ~0.69!")
print("Evidence changed our belief. That is Bayesian inference.")

# ==============================================================
# PART 2: Asthma -> Wheeze (Medical Diagnosis)
# ==============================================================
print("\n" + "=" * 60)
print("PART 2: Asthma -> Wheeze (Medical Diagnosis)")
print("=" * 60)

# Define the medical network
asthma_model = DiscreteBayesianNetwork([("Asthma", "Wheeze")])

# Prior: P(Asthma=yes) = 0.05 (5% base rate)
cpd_asthma = TabularCPD(
    variable="Asthma",
    variable_card=2,
    values=[[0.95], [0.05]],
    state_names={"Asthma": ["no", "yes"]},
)

# Conditional: P(Wheeze | Asthma)
# Asthmatics wheeze 80% of the time
# Non-asthmatics wheeze 10% of the time (allergies, colds, etc.)
cpd_wheeze = TabularCPD(
    variable="Wheeze",
    variable_card=2,
    values=[
        [0.9, 0.2],  # P(Wheeze=no | Asthma=no), P(Wheeze=no | Asthma=yes)
        [0.1, 0.8],  # P(Wheeze=yes | Asthma=no), P(Wheeze=yes | Asthma=yes)
    ],
    evidence=["Asthma"],
    evidence_card=[2],
    state_names={"Wheeze": ["no", "yes"], "Asthma": ["no", "yes"]},
)

asthma_model.add_cpds(cpd_asthma, cpd_wheeze)
assert asthma_model.check_model()

# Diagnosis: Patient is wheezing. What is P(Asthma | Wheeze=yes)?
asthma_infer = VariableElimination(asthma_model)

print("\nPrior probability of Asthma (before seeing patient):")
prior_asthma = asthma_infer.query(variables=["Asthma"])
print(prior_asthma)

print("\nPosterior: P(Asthma | patient is wheezing):")
posterior_asthma = asthma_infer.query(
    variables=["Asthma"], evidence={"Wheeze": "yes"}
)
print(posterior_asthma)
print("\nIntuition: Prior was 5%. After observing wheeze, it rises to ~30%.")
print("The symptom is suggestive but not conclusive (other things cause wheezing).")

Your single-cause diagnosis engine is working. Next, you will expand to a multi-cause network where two diseases both cause the same symptoms, and watch how adding more evidence narrows down the diagnosis step by step.

Build a Multi-Cause Diagnosis Network

Your single-cause network proved that observing a symptom shifts the probability of a disease. But real diagnosis is rarely that simple.

In this step, you will expand to a multi-cause network where two diseases (Asthma and Infection) each cause two symptoms (Wheeze and Cough). This lets the engine reason about which disease is more likely when multiple symptoms appear together.

In this step, get ready to:
  • Define a network with multiple diseases and symptoms connected by causal edges.
  • Set priors and conditional probability tables for every node.
  • Run step-by-step inference to watch posteriors shift as evidence accumulates.
Expand to multiple diseases and symptoms

Real diagnosis involves multiple possible causes producing overlapping symptoms. Your new network has two diseases (Asthma and Infection) and two symptoms (Wheeze and Cough). Each disease can cause each symptom, creating four edges.

  • In diagnosis_engine.py, scroll to the bottom of your file and add the Part 3 header and network structure by pasting this code:
# ==============================================================
# PART 3: Multi-Cause Diagnosis Network
# ==============================================================
print("\n" + "=" * 60)
print("PART 3: Multi-Cause Diagnosis Network")
print("=" * 60)
print("Network: Asthma -> Wheeze <- Infection")
print("         Asthma -> Cough  <- Infection")

# Two diseases, two symptoms
multi_model = DiscreteBayesianNetwork([
    ("Asthma", "Wheeze"),
    ("Infection", "Wheeze"),
    ("Asthma", "Cough"),
    ("Infection", "Cough"),
])

What does this code do?

This defines a directed graph with four edges. Each edge represents a causal relationship.

  • ("Asthma", "Wheeze") means Asthma can cause Wheeze.
  • ("Infection", "Wheeze") means Infection can also cause Wheeze.
  • Both symptoms now have TWO parent nodes. The network can reason about competing explanations for the same symptom.
  • Save diagnosis_engine.py and run it:
python diagnosis_engine.py
  • After the Part 1 and Part 2 output, you should see the Part 3 header with PART 3: Multi-Cause Diagnosis Network and the network diagram description printed.

Seeing a NameError?

Make sure the new code sits BELOW the Part 2 code, not inside it. The imports at the top of your file already cover DiscreteBayesianNetwork, so you should not need to import again.

Help me debug a NameError in my multi-cause network definition.

Define all CPDs for the expanded network

Each node needs a probability table. Root nodes (diseases) get priors. Child nodes (symptoms) get conditional probabilities that depend on their parent diseases.

  • Add the prior CPDs for both diseases below the multi_model definition:
# Priors
cpd_asthma_m = TabularCPD(
    variable="Asthma",
    variable_card=2,
    values=[[0.95], [0.05]],
    state_names={"Asthma": ["no", "yes"]},
)

cpd_infection = TabularCPD(
    variable="Infection",
    variable_card=2,
    values=[[0.85], [0.15]],
    state_names={"Infection": ["no", "yes"]},
)

What do these priors mean?

  • P(Asthma=yes) = 0.05 means 5% of patients have asthma before we observe any symptoms.
  • P(Infection=yes) = 0.15 means 15% of patients have an infection. Infections are more common than asthma in the general population.
  • These are the base rates that evidence will update.

You won't see a change yet. The next chunk defines the conditional probabilities and validates the full model.

  • Now add the conditional probability tables for both symptoms, then attach all CPDs and validate the model:
# P(Wheeze | Asthma, Infection)
# Columns ordered: (Asthma=no,Inf=no), (Asthma=no,Inf=yes), (Asthma=yes,Inf=no), (Asthma=yes,Inf=yes)
cpd_wheeze_m = TabularCPD(
    variable="Wheeze",
    variable_card=2,
    values=[
        [0.95, 0.3, 0.2, 0.05],  # P(Wheeze=no | ...)
        [0.05, 0.7, 0.8, 0.95],  # P(Wheeze=yes | ...)
    ],
    evidence=["Asthma", "Infection"],
    evidence_card=[2, 2],
    state_names={
        "Wheeze": ["no", "yes"],
        "Asthma": ["no", "yes"],
        "Infection": ["no", "yes"],
    },
)

# P(Cough | Asthma, Infection)
cpd_cough = TabularCPD(
    variable="Cough",
    variable_card=2,
    values=[
        [0.9, 0.2, 0.6, 0.1],  # P(Cough=no | ...)
        [0.1, 0.8, 0.4, 0.9],  # P(Cough=yes | ...)
    ],
    evidence=["Asthma", "Infection"],
    evidence_card=[2, 2],
    state_names={
        "Cough": ["no", "yes"],
        "Asthma": ["no", "yes"],
        "Infection": ["no", "yes"],
    },
)

multi_model.add_cpds(cpd_asthma_m, cpd_infection, cpd_wheeze_m, cpd_cough)
assert multi_model.check_model()

How does the column ordering work?

When a symptom has two parents, pgmpy needs a probability for every combination of parent states. With two binary parents, that is four columns.

  • Column 1: both parents are "no". Column 2: first parent "no", second parent "yes". And so on.
  • The bottom row of cpd_wheeze_m tells us: if BOTH Asthma and Infection are present, P(Wheeze=yes) = 0.95. If neither is present, P(Wheeze=yes) = 0.05.
  • The assert multi_model.check_model() line confirms that every column sums to 1.0 and all CPDs match the network structure.
  • Save diagnosis_engine.py and run it:
python diagnosis_engine.py
  • The script should complete without raising an AssertionError. If it finishes with only the Part 1, 2, and 3 header output (no traceback), your model is valid.

Getting an AssertionError?

This means your CPD values do not match the network structure. Check these common causes:

  • Make sure each values row has exactly 4 numbers (one per parent combination).
  • Confirm each column sums to 1.0. For example, 0.95 + 0.05 = 1.0 in the first column of cpd_wheeze_m.
  • Verify that evidence_card=[2, 2] matches the number of states for each parent listed in evidence.

Help me fix my AssertionError in the multi-cause network.

Run step-by-step evidence propagation

Now comes the payoff. You will query the network three times, adding one piece of evidence each time, and watch how the posterior probabilities shift with each observation.

  • Add the inference queries below the assert line:
multi_infer = VariableElimination(multi_model)

# Step-by-step evidence propagation
print("\n--- Step 1: No evidence (just priors) ---")
q1 = multi_infer.query(variables=["Asthma", "Infection"], joint=False)
print(q1["Asthma"])
print(q1["Infection"])

print("\n--- Step 2: Observe Wheeze=yes ---")
q2 = multi_infer.query(
    variables=["Asthma", "Infection"],
    evidence={"Wheeze": "yes"},
    joint=False,
)
print(q2["Asthma"])
print(q2["Infection"])
print("Both diseases become more likely when we observe wheezing!")

print("\n--- Step 3: Also observe Cough=yes ---")
q3 = multi_infer.query(
    variables=["Asthma", "Infection"],
    evidence={"Wheeze": "yes", "Cough": "yes"},
    joint=False,
)
print(q3["Asthma"])
print(q3["Infection"])
print("Adding more evidence further narrows down the probable causes.")

What does this code do?

  • joint=False tells pgmpy to return separate probability tables for each disease instead of one combined table.
  • Query 1 shows the raw priors (5% Asthma, 15% Infection) with no evidence at all.
  • Query 2 adds Wheeze=yes as evidence. Both diseases should jump up because both can cause wheezing.
  • Query 3 stacks Cough=yes on top. More evidence narrows down the probable cause further.
  • Save diagnosis_engine.py and run it:
python diagnosis_engine.py
  • Scroll to the Part 3 output. You should see three labeled sections showing probability tables for Asthma and Infection at each evidence step.
  • Confirm that P(Asthma=yes) and P(Infection=yes) both increase from Step 1 to Step 2, and shift again in Step 3.

Why do both diseases go up when we observe wheezing?

Both Asthma and Infection can cause Wheeze. When the network observes wheezing, it reasons: "something must be causing this." Both possible causes become more probable. Adding cough as a second piece of evidence helps the network distinguish between them further because the two diseases have different probabilities of causing cough.

Seeing unexpected probability values?

If your probabilities do not change between steps, check that you passed the evidence dictionary correctly. The keys must match the exact node names ("Wheeze" and "Cough") and the values must be "yes" (matching the state_names you defined).

Help me debug my evidence propagation queries.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete diagnosis_engine.py file up to this point. Compare it with yours:

from pgmpy.models import DiscreteBayesianNetwork
from pgmpy.factors.discrete import TabularCPD
from pgmpy.inference import VariableElimination

# ==============================================================
# PART 1: Rain -> Wet Grass (Building Intuition)
# ==============================================================
print("=" * 60)
print("PART 1: Rain -> Wet Grass Network")
print("=" * 60)

# Define the network structure: one edge from Rain to WetGrass
rain_model = DiscreteBayesianNetwork([("Rain", "WetGrass")])

# Define priors and conditional probabilities
# Prior: P(Rain=yes) = 0.2, P(Rain=no) = 0.8
cpd_rain = TabularCPD(
    variable="Rain",
    variable_card=2,
    values=[[0.8], [0.2]],
    state_names={"Rain": ["no", "yes"]},
)

# Conditional: P(WetGrass | Rain)
# If Rain=no: P(WetGrass=yes) = 0.1 (sprinkler, dew, etc.)
# If Rain=yes: P(WetGrass=yes) = 0.9
cpd_wet = TabularCPD(
    variable="WetGrass",
    variable_card=2,
    values=[
        [0.9, 0.1],  # P(WetGrass=no | Rain=no), P(WetGrass=no | Rain=yes)
        [0.1, 0.9],  # P(WetGrass=yes | Rain=no), P(WetGrass=yes | Rain=yes)
    ],
    evidence=["Rain"],
    evidence_card=[2],
    state_names={"WetGrass": ["no", "yes"], "Rain": ["no", "yes"]},
)

# Add CPDs and validate
rain_model.add_cpds(cpd_rain, cpd_wet)
assert rain_model.check_model()

# Inference: What is P(Rain | WetGrass=yes)?
rain_infer = VariableElimination(rain_model)

print("\nPrior probability of Rain (before observing anything):")
prior = rain_infer.query(variables=["Rain"])
print(prior)

print("\nPosterior probability of Rain GIVEN we observe Wet Grass:")
posterior = rain_infer.query(variables=["Rain"], evidence={"WetGrass": "yes"})
print(posterior)
print("\nIntuition: The prior P(Rain=yes) was 0.20.")
print("After observing wet grass, P(Rain=yes) jumps to ~0.69!")
print("Evidence changed our belief. That is Bayesian inference.")

# ==============================================================
# PART 2: Asthma -> Wheeze (Medical Diagnosis)
# ==============================================================
print("\n" + "=" * 60)
print("PART 2: Asthma -> Wheeze (Medical Diagnosis)")
print("=" * 60)

# Define the medical network
asthma_model = DiscreteBayesianNetwork([("Asthma", "Wheeze")])

# Prior: P(Asthma=yes) = 0.05 (5% base rate)
cpd_asthma = TabularCPD(
    variable="Asthma",
    variable_card=2,
    values=[[0.95], [0.05]],
    state_names={"Asthma": ["no", "yes"]},
)

# Conditional: P(Wheeze | Asthma)
# Asthmatics wheeze 80% of the time
# Non-asthmatics wheeze 10% of the time (allergies, colds, etc.)
cpd_wheeze = TabularCPD(
    variable="Wheeze",
    variable_card=2,
    values=[
        [0.9, 0.2],  # P(Wheeze=no | Asthma=no), P(Wheeze=no | Asthma=yes)
        [0.1, 0.8],  # P(Wheeze=yes | Asthma=no), P(Wheeze=yes | Asthma=yes)
    ],
    evidence=["Asthma"],
    evidence_card=[2],
    state_names={"Wheeze": ["no", "yes"], "Asthma": ["no", "yes"]},
)

asthma_model.add_cpds(cpd_asthma, cpd_wheeze)
assert asthma_model.check_model()

# Diagnosis: Patient is wheezing. What is P(Asthma | Wheeze=yes)?
asthma_infer = VariableElimination(asthma_model)

print("\nPrior probability of Asthma (before seeing patient):")
prior_asthma = asthma_infer.query(variables=["Asthma"])
print(prior_asthma)

print("\nPosterior: P(Asthma | patient is wheezing):")
posterior_asthma = asthma_infer.query(
    variables=["Asthma"], evidence={"Wheeze": "yes"}
)
print(posterior_asthma)
print("\nIntuition: Prior was 5%. After observing wheeze, it rises to ~30%.")
print("The symptom is suggestive but not conclusive (other things cause wheezing).")

# ==============================================================
# PART 3: Multi-Cause Diagnosis Network
# ==============================================================
print("\n" + "=" * 60)
print("PART 3: Multi-Cause Diagnosis Network")
print("=" * 60)
print("Network: Asthma -> Wheeze <- Infection")
print("         Asthma -> Cough  <- Infection")

# Two diseases, two symptoms
multi_model = DiscreteBayesianNetwork([
    ("Asthma", "Wheeze"),
    ("Infection", "Wheeze"),
    ("Asthma", "Cough"),
    ("Infection", "Cough"),
])

# Priors
cpd_asthma_m = TabularCPD(
    variable="Asthma",
    variable_card=2,
    values=[[0.95], [0.05]],
    state_names={"Asthma": ["no", "yes"]},
)

cpd_infection = TabularCPD(
    variable="Infection",
    variable_card=2,
    values=[[0.85], [0.15]],
    state_names={"Infection": ["no", "yes"]},
)

# P(Wheeze | Asthma, Infection)
# Columns ordered: (Asthma=no,Inf=no), (Asthma=no,Inf=yes), (Asthma=yes,Inf=no), (Asthma=yes,Inf=yes)
cpd_wheeze_m = TabularCPD(
    variable="Wheeze",
    variable_card=2,
    values=[
        [0.95, 0.3, 0.2, 0.05],  # P(Wheeze=no | ...)
        [0.05, 0.7, 0.8, 0.95],  # P(Wheeze=yes | ...)
    ],
    evidence=["Asthma", "Infection"],
    evidence_card=[2, 2],
    state_names={
        "Wheeze": ["no", "yes"],
        "Asthma": ["no", "yes"],
        "Infection": ["no", "yes"],
    },
)

# P(Cough | Asthma, Infection)
cpd_cough = TabularCPD(
    variable="Cough",
    variable_card=2,
    values=[
        [0.9, 0.2, 0.6, 0.1],  # P(Cough=no | ...)
        [0.1, 0.8, 0.4, 0.9],  # P(Cough=yes | ...)
    ],
    evidence=["Asthma", "Infection"],
    evidence_card=[2, 2],
    state_names={
        "Cough": ["no", "yes"],
        "Asthma": ["no", "yes"],
        "Infection": ["no", "yes"],
    },
)

multi_model.add_cpds(cpd_asthma_m, cpd_infection, cpd_wheeze_m, cpd_cough)
assert multi_model.check_model()

multi_infer = VariableElimination(multi_model)

# Step-by-step evidence propagation
print("\n--- Step 1: No evidence (just priors) ---")
q1 = multi_infer.query(variables=["Asthma", "Infection"], joint=False)
print(q1["Asthma"])
print(q1["Infection"])

print("\n--- Step 2: Observe Wheeze=yes ---")
q2 = multi_infer.query(
    variables=["Asthma", "Infection"],
    evidence={"Wheeze": "yes"},
    joint=False,
)
print(q2["Asthma"])
print(q2["Infection"])
print("Both diseases become more likely when we observe wheezing!")

print("\n--- Step 3: Also observe Cough=yes ---")
q3 = multi_infer.query(
    variables=["Asthma", "Infection"],
    evidence={"Wheeze": "yes", "Cough": "yes"},
    joint=False,
)
print(q3["Asthma"])
print(q3["Infection"])
print("Adding more evidence further narrows down the probable causes.")

Your multi-cause network can now reason about competing explanations for symptoms. Next up, you will tackle an even more powerful phenomenon: explaining away, where confirming one cause actually reduces the probability of another.

Secret mission

Explaining Away

Your multi-cause network has a hidden superpower. When two diseases both explain a symptom, confirming one cause actually reduces the probability of the other. Can you demonstrate this "explaining away" effect and prove your network reasons like a real diagnostician?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files and packages installed, or delete them entirely.

Resources you used:

  • diagnosis_engine.py (local Python file).
  • pgmpy Python package (installed via pip).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with Bayesian networks or want to extend the project further.

  • Your diagnosis_engine.py file stays on your Desktop, ready to run anytime.
  • The pgmpy package remains installed and available for future Python scripts.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your file and package will be waiting for you when you return.

  • Close your text editor and terminal.
  • Everything stays installed. Just reopen diagnosis_engine.py and run it again when you are ready.

Delete - I don't want to use this again

Remove all project files and uninstall the package to start fresh.

  • Delete the project file by running this command in your terminal:
cd ~/Desktop
rm diagnosis_engine.py

On Windows, use this instead:

cd ~\Desktop
del diagnosis_engine.py
  • Uninstall pgmpy by running:
pip uninstall pgmpy -y

What about pgmpy's dependencies?

When you installed pgmpy, it also pulled in packages like numpy, networkx, and pandas. Uninstalling pgmpy alone does not remove these.

If you do not use these packages for other projects, you can uninstall them individually with pip uninstall <package-name>. If you are unsure, leave them installed. They take minimal disk space.

That's a Wrap!

That's a Wrap!

Nice work! You just built a probabilistic diagnosis engine that reasons about diseases and symptoms using Bayesian inference.

You've learned how to:

  • Build Bayesian networks of increasing complexity, defining nodes (variables), edges (causal relationships), priors (base rates), and conditional probabilities.
  • Use evidence (observed symptoms) to compute posterior probabilities (updated beliefs) through exact inference.
  • Propagate multiple pieces of evidence step-by-step through a multi-cause network, watching how each observation narrows down the probable diseases.
  • Demonstrated explaining away, where confirming one cause reduces the probability of alternative causes.

Ready to quiz yourself?