Build a Drug Interaction Checker

Use NLP and a knowledge graph to detect dangerous drug combinations.

Introduction

30 Second Summary

If you take more than one medication, your doctor has to mentally track whether any of those drugs interact badly with each other. Now imagine doing that for patients on seven or more medications at once. That's polypharmacy, and dangerous drug combinations are one of the leading causes of preventable hospital admissions.

In this project, you will build a Python system that reads clinical notes, automatically extracts drug names using Natural Language Processing, and checks every pair against a knowledge graph of 48,000+ real FDA-approved drug interactions.

What You'll Build

A command-line polypharmacy safety checker that processes unstructured clinical text and flags dangerous drug combinations with official FDA guidance.

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

  • A drug NER pipeline that identifies medication names in free-text clinical notes.
  • A NetworkX knowledge graph populated with 1,500+ drug nodes and 48,000+ interaction edges from real FDA data.
  • An interaction checker that performs all-pairs analysis and generates a safety report enriched with openFDA drug label data.
  • Secret Mission: Add network centrality analysis to find the most dangerous drugs in your graph and visualize a patient's interaction subgraph.

Are there any prerequisites?

You should be comfortable with basic Python (functions, dictionaries, and loops). No prior experience with NLP, graph theory, or healthcare data is needed.

Extract Drug Names from Clinical Text with NER

Your Python environment is ready with all dependencies installed. Now it's time to build the first component of your polypharmacy checker: a drug name extractor.

Clinical notes are messy, unstructured text. A doctor writes "started ibuprofen 400mg TID for joint pain" and your system needs to find "ibuprofen" buried in that sentence. Named Entity Recognition (NER) solves this by scanning text token-by-token and identifying entities of a specific type.

In this step, get ready to:
  • Build a drug NER function that extracts medications from free text.
  • Process a realistic 7-drug clinical note and display the results.
  • Understand how the library handles brand names and multi-word drugs.
Build the NER pipeline

The drug-named-entity-recognition library works by taking a list of tokenized strings and checking each token (and consecutive token pairs) against a DrugBank-powered dictionary. It returns the canonical drug name, synonyms, and database identifiers for each match.

  • In VS Code, create a new file called drug_ner.py in your project folder.
  • Add the import and the extraction function by pasting this code:
from drug_named_entity_recognition import find_drugs


def extract_drugs_from_text(text):
    """Extract drug names from clinical text using NER."""
    # Split the clinical text into individual tokens
    tokens = text.split()
    # Run the NER library with fuzzy matching enabled
    results = find_drugs(tokens, is_fuzzy_match=True)

    # Build a clean list of extracted drug information
    extracted = []
    for drug_info, start_idx, end_idx in results:
        extracted.append({
            "name": drug_info["name"],
            "drugbank_id": drug_info.get("drugbank_id", ""),
            "synonyms": drug_info.get("synonyms", set()),
            "span": " ".join(tokens[start_idx:end_idx + 1])
        })

    return extracted

What does this code do?

The function takes raw clinical text and processes it in three stages:

  • text.split() tokenizes the input into individual words. The library needs a list of strings, not raw text.
  • find_drugs(tokens, is_fuzzy_match=True) scans the tokens against a DrugBank dictionary and returns matches as tuples of (drug_info_dict, start_index, end_index).
  • The loop restructures each result into a clean dictionary with the drug's canonical name, its DrugBank ID, known synonyms, and the original matched text span.
  • Save drug_ner.py.
  • You won't see output yet. The next chunk adds a test script that runs this function.
Process a sample clinical note

Now you'll add a test block that feeds a realistic multi-drug clinical note into your extraction function and prints the results as a formatted table.

  • In drug_ner.py, add the following code below the extract_drugs_from_text function:
if __name__ == "__main__":
    # A realistic 7-drug elderly patient clinical note
    clinical_note = (
        "Patient is a 72-year-old male on warfarin 5mg daily for atrial fibrillation. "
        "Started ibuprofen 400mg TID for joint pain. Also takes metformin 1000mg BID, "
        "lisinopril 20mg daily, aspirin 81mg daily, omeprazole 20mg daily, "
        "and simvastatin 40mg at bedtime."
    )

    print("=" * 60)
    print("DRUG NER EXTRACTION")
    print("=" * 60)
    print(f"\nClinical Note:\n{clinical_note}\n")
    print("-" * 60)
    print(f"{'Drug Name':<20} {'DrugBank ID':<15} {'Matched Text'}")
    print("-" * 60)

    # Run the NER pipeline and display results
    drugs = extract_drugs_from_text(clinical_note)
    for drug in drugs:
        print(f"{drug['name']:<20} {drug['drugbank_id']:<15} {drug['span']}")

    print(f"\nTotal drugs found: {len(drugs)}")

What does this test block do?

The if __name__ == "__main__": guard means this code only runs when you execute drug_ner.py directly. It won't run when other files import the extract_drugs_from_text function later.

The clinical note contains 7 drugs (warfarin, ibuprofen, metformin, lisinopril, aspirin, omeprazole, simvastatin) mixed in with dosages, frequencies, and medical context. The formatted output prints each drug's canonical name, its DrugBank identifier, and the original text that was matched.

  • Save drug_ner.py.
  • Run the script by executing this command in your terminal:
python drug_ner.py
  • You should see a table listing multiple drugs with their DrugBank IDs and matched text spans. The exact number of matches may vary slightly depending on the library version, but you should see most of the 7 drugs identified.

Not seeing any drugs in the output?

  • Make sure your virtual environment is activated. If your terminal prompt doesn't show the environment name, activate it again before running the script.
  • Check that the import line matches exactly: from drug_named_entity_recognition import find_drugs. A typo in the package name causes an import error.
  • If you see ModuleNotFoundError, confirm you installed the library with pip install drug-named-entity-recognition in the same virtual environment you're running from.

Why isn't my drug NER script finding any drugs? I've checked my import and virtual environment.

✔️ Awesome, I've got everything!

Great. Double check you've saved drug_ner.py and move on to the next section.

ⓧ I'd like to double check the full code

Here's the complete drug_ner.py file for comparison:

from drug_named_entity_recognition import find_drugs


def extract_drugs_from_text(text):
    """Extract drug names from clinical text using NER."""
    tokens = text.split()
    results = find_drugs(tokens, is_fuzzy_match=True)

    extracted = []
    for drug_info, start_idx, end_idx in results:
        extracted.append({
            "name": drug_info["name"],
            "drugbank_id": drug_info.get("drugbank_id", ""),
            "synonyms": drug_info.get("synonyms", set()),
            "span": " ".join(tokens[start_idx:end_idx + 1])
        })

    return extracted


if __name__ == "__main__":
    clinical_note = (
        "Patient is a 72-year-old male on warfarin 5mg daily for atrial fibrillation. "
        "Started ibuprofen 400mg TID for joint pain. Also takes metformin 1000mg BID, "
        "lisinopril 20mg daily, aspirin 81mg daily, omeprazole 20mg daily, "
        "and simvastatin 40mg at bedtime."
    )

    print("=" * 60)
    print("DRUG NER EXTRACTION")
    print("=" * 60)
    print(f"\nClinical Note:\n{clinical_note}\n")
    print("-" * 60)
    print(f"{'Drug Name':<20} {'DrugBank ID':<15} {'Matched Text'}")
    print("-" * 60)

    drugs = extract_drugs_from_text(clinical_note)
    for drug in drugs:
        print(f"{drug['name']:<20} {drug['drugbank_id']:<15} {drug['span']}")

    print(f"\nTotal drugs found: {len(drugs)}")
Handle edge cases

Clinical text is full of inconsistencies. Doctors write brand names, generic names, abbreviations, and even misspellings. The code you already wrote handles these edge cases through two mechanisms built into the library.

How does the library handle brand names and multi-word drugs?

Brand-name-to-generic resolution works through the synonyms field. Each drug in the DrugBank dictionary has a set of known synonyms (brand names, international names, and spelling variants). When the library matches "Tylenol", it returns the canonical name "Acetaminophen" along with all synonyms.

Multi-word drug names (like "naproxen sodium" or "calcium carbonate") are handled internally by the library scanning consecutive token pairs. This is why you pass a token list rather than raw text. The span field in your output captures the full matched text, whether it's one token or two.

The is_fuzzy_match=True flag enables n-gram matching to catch common misspellings (like "paraxcetamol" for "paracetamol"). This is critical for real-world clinical text where typos are frequent.

Your NER pipeline can now pull drug names out of messy clinical text. Next up, you'll build a knowledge graph of drug interactions so your system can check whether any of these extracted drugs are dangerous together.

Build the Drug Interaction Knowledge Graph

Your NER pipeline can extract drug names from clinical text. The next question is: how do you know which drug combinations are actually dangerous?

You need a structured database of known interactions to check against. In this step, you'll build a knowledge graph from a real FDA-approved drug interaction dataset. Each drug becomes a node, and each known interaction becomes an edge connecting two drugs.

In this step, get ready to:
  • Download and parse a drug interaction dataset with 1,500+ drugs and 48,000+ interactions.
  • Construct a NetworkX graph where drugs are nodes and interactions are edges.
  • Write query functions to check interactions between any pair of drugs.
Download and parse the interaction dataset

The ChCh-Miner dataset from Stanford's SNAP project is a freely available edge list of ~48,000 drug-drug interaction pairs extracted from FDA-approved drug labels and scientific publications. The raw file uses DrugBank IDs (like DB00316), so you also need a mapping file that converts those IDs to human-readable drug names.

  • Create a new file called knowledge_graph.py in your project folder.
  • Add the following code to set up the imports, dataset URLs, and data-loading functions:
import csv
import urllib.request
import os
import networkx as nx


DATASET_URL = "https://snap.stanford.edu/biodata/datasets/10001/files/ChCh-Miner_durgbank-chem-chem.tsv.gz"
DATASET_FILE = "drug_interactions.tsv.gz"
DRUGBANK_NAMES_URL = "https://raw.githubusercontent.com/kennethleungty/Drug-Interactions-Network-Analysis-and-Prediction/main/Data/drug_names.csv"
DRUGBANK_NAMES_FILE = "drug_names.csv"


def download_file(url, filename):
    """Download a file if it doesn't exist locally."""
    if not os.path.exists(filename):
        print(f"Downloading {filename}...")
        urllib.request.urlretrieve(url, filename)
        print(f"Downloaded {filename}")


def load_drug_names():
    """Load DrugBank ID to drug name mapping."""
    download_file(DRUGBANK_NAMES_URL, DRUGBANK_NAMES_FILE)

    names = {}
    with open(DRUGBANK_NAMES_FILE, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            names[row["drugbank_id"]] = row["name"]
    return names

What does this code do?

This sets up the data layer for your knowledge graph:

  • DATASET_URL points to the ChCh-Miner compressed TSV file hosted by Stanford SNAP.
  • DRUGBANK_NAMES_URL points to a CSV that maps DrugBank IDs to human-readable drug names like "Warfarin" and "Aspirin".
  • download_file() only downloads if the file is missing, so you won't re-download on every run.
  • load_drug_names() reads the CSV into a Python dictionary mapping each DrugBank ID to its name.

Why NetworkX instead of Neo4j?

Neo4j is a dedicated graph database, but it requires Docker, Java, and significant configuration time. NetworkX provides the same graph algorithms in pure Python with zero infrastructure overhead.

For a project focused on learning NER and graph traversal concepts, NetworkX lets you spend time on the target skills rather than database setup.

Construct the NetworkX graph

Now you'll write the function that downloads the interaction dataset, resolves DrugBank IDs to names, and constructs the graph. Each row in the dataset is a pair of DrugBank IDs representing two drugs that interact.

  • Add the build_interaction_graph() function below load_drug_names() in knowledge_graph.py:
def build_interaction_graph():
    """Build a NetworkX graph from the drug interaction dataset."""
    import gzip

    download_file(DATASET_URL, DATASET_FILE)
    drug_names = load_drug_names()

    G = nx.Graph()

    with gzip.open(DATASET_FILE, "rt") as f:
        reader = csv.reader(f, delimiter="\t")
        next(reader)  # skip header
        for row in reader:
            if len(row) >= 2:
                drug1_id, drug2_id = row[0], row[1]
                drug1_name = drug_names.get(drug1_id, drug1_id)
                drug2_name = drug_names.get(drug2_id, drug2_id)

                G.add_node(drug1_name, drugbank_id=drug1_id)
                G.add_node(drug2_name, drugbank_id=drug2_id)
                G.add_edge(drug1_name, drug2_name)

    print(f"Knowledge graph built: {G.number_of_nodes()} drugs, "
          f"{G.number_of_edges()} interactions")
    return G

What does this code do?

This function performs three jobs in sequence:

  • Downloads the compressed interaction file (1.5 MB) if it's not already on disk.
  • Loads the DrugBank-ID-to-name mapping so nodes get readable labels.
  • Iterates every row in the TSV (each row is one interaction pair), creates named nodes for both drugs, and draws an edge between them.

The result is an undirected graph where you can ask "does drug A interact with drug B?" by checking if an edge exists between them.

  • Save knowledge_graph.py.

Seeing a download error?

If urllib.request.urlretrieve fails with a timeout or connection error, check that you have an active internet connection. The Stanford SNAP server occasionally has slow response times for the 1.5 MB file.

If you see a certificate error on macOS, you may need to install Python's SSL certificates. Run the Install Certificates.command script from your Python installation folder.

help me fix a download error in my knowledge graph script

Query the graph

Your graph is built, but you need functions to actually ask it questions. You'll write two query functions: one that checks whether a specific pair of drugs interacts, and one that finds all interactions among a list of drugs.

  • Add the check_interaction() function below build_interaction_graph() in knowledge_graph.py:
def check_interaction(G, drug1, drug2):
    """Check if two drugs have a known interaction."""
    drug1_lower = drug1.lower()
    drug2_lower = drug2.lower()

    node_map = {node.lower(): node for node in G.nodes()}

    node1 = node_map.get(drug1_lower)
    node2 = node_map.get(drug2_lower)

    if node1 and node2:
        return G.has_edge(node1, node2)
    return False

What does this code do?

check_interaction() takes two drug names and returns True or False depending on whether the graph contains an edge between them.

The node_map dictionary handles case-insensitive matching. This is important because the NER library might return "warfarin" while the graph stores "Warfarin".

  • Add the find_all_interactions() function below check_interaction():
def find_all_interactions(G, drug_list):
    """Find all pairwise interactions among a list of drugs."""
    interactions = []
    node_map = {node.lower(): node for node in G.nodes()}

    resolved = []
    for drug in drug_list:
        node = node_map.get(drug.lower())
        if node:
            resolved.append(node)

    for i in range(len(resolved)):
        for j in range(i + 1, len(resolved)):
            if G.has_edge(resolved[i], resolved[j]):
                interactions.append((resolved[i], resolved[j]))

    return interactions

What does this code do?

find_all_interactions() is the core of the polypharmacy checker. Given a list of drugs a patient is taking, it checks every possible pair for a known interaction.

For 7 drugs, that's 21 pairs to check (7 choose 2). The function resolves each drug name to its graph node first, then iterates all combinations. This is the same all-pairs approach used in real clinical decision support systems.

  • Add the test block at the bottom of knowledge_graph.py to run the graph and query it:
if __name__ == "__main__":
    G = build_interaction_graph()

    print(f"\nGraph Statistics:")
    print(f"  Nodes (drugs): {G.number_of_nodes()}")
    print(f"  Edges (interactions): {G.number_of_edges()}")
    print(f"  Average degree: {sum(dict(G.degree()).values()) / G.number_of_nodes():.1f}")

    test_drugs = ["Warfarin", "Aspirin", "Ibuprofen", "Metformin"]
    print(f"\nChecking interactions among: {test_drugs}")
    interactions = find_all_interactions(G, test_drugs)
    for d1, d2 in interactions:
        print(f"  ⚠️  {d1} <-> {d2}")

What does the test block do?

The if __name__ == "__main__" block builds the full graph (downloading files on first run), prints statistics about the graph size, and then tests the query function with four common drugs: Warfarin, Aspirin, Ibuprofen, and Metformin.

You'll see which pairs among those four have known interactions flagged in the dataset.

  • Save knowledge_graph.py.
  • Run the script in your terminal:
python knowledge_graph.py
  • The first run downloads two files (about 1.5 MB total). You should then see output showing 1,500+ nodes, 48,000+ edges, and flagged interaction pairs among the test drugs.

Seeing 0 nodes or 0 edges?

Check that drug_interactions.tsv.gz downloaded correctly. Open your project folder and verify the file is roughly 1.5 MB. A 0-byte file means the download was interrupted.

If the file exists but the graph is empty, confirm the delimiter="\t" argument is present in csv.reader. The dataset uses tabs, not commas.

help me debug why my knowledge graph has 0 nodes

✔️ Awesome, I've got everything!

Great. Double check you've saved knowledge_graph.py and confirmed the graph statistics printed correctly.

ⓧ I'd like to double check the full code

import csv
import urllib.request
import os
import networkx as nx


DATASET_URL = "https://snap.stanford.edu/biodata/datasets/10001/files/ChCh-Miner_durgbank-chem-chem.tsv.gz"
DATASET_FILE = "drug_interactions.tsv.gz"
DRUGBANK_NAMES_URL = "https://raw.githubusercontent.com/kennethleungty/Drug-Interactions-Network-Analysis-and-Prediction/main/Data/drug_names.csv"
DRUGBANK_NAMES_FILE = "drug_names.csv"


def download_file(url, filename):
    """Download a file if it doesn't exist locally."""
    if not os.path.exists(filename):
        print(f"Downloading {filename}...")
        urllib.request.urlretrieve(url, filename)
        print(f"Downloaded {filename}")


def load_drug_names():
    """Load DrugBank ID to drug name mapping."""
    download_file(DRUGBANK_NAMES_URL, DRUGBANK_NAMES_FILE)

    names = {}
    with open(DRUGBANK_NAMES_FILE, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            names[row["drugbank_id"]] = row["name"]
    return names


def build_interaction_graph():
    """Build a NetworkX graph from the drug interaction dataset."""
    import gzip

    download_file(DATASET_URL, DATASET_FILE)
    drug_names = load_drug_names()

    G = nx.Graph()

    with gzip.open(DATASET_FILE, "rt") as f:
        reader = csv.reader(f, delimiter="\t")
        next(reader)  # skip header
        for row in reader:
            if len(row) >= 2:
                drug1_id, drug2_id = row[0], row[1]
                drug1_name = drug_names.get(drug1_id, drug1_id)
                drug2_name = drug_names.get(drug2_id, drug2_id)

                G.add_node(drug1_name, drugbank_id=drug1_id)
                G.add_node(drug2_name, drugbank_id=drug2_id)
                G.add_edge(drug1_name, drug2_name)

    print(f"Knowledge graph built: {G.number_of_nodes()} drugs, "
          f"{G.number_of_edges()} interactions")
    return G


def check_interaction(G, drug1, drug2):
    """Check if two drugs have a known interaction."""
    drug1_lower = drug1.lower()
    drug2_lower = drug2.lower()

    node_map = {node.lower(): node for node in G.nodes()}

    node1 = node_map.get(drug1_lower)
    node2 = node_map.get(drug2_lower)

    if node1 and node2:
        return G.has_edge(node1, node2)
    return False


def find_all_interactions(G, drug_list):
    """Find all pairwise interactions among a list of drugs."""
    interactions = []
    node_map = {node.lower(): node for node in G.nodes()}

    resolved = []
    for drug in drug_list:
        node = node_map.get(drug.lower())
        if node:
            resolved.append(node)

    for i in range(len(resolved)):
        for j in range(i + 1, len(resolved)):
            if G.has_edge(resolved[i], resolved[j]):
                interactions.append((resolved[i], resolved[j]))

    return interactions


if __name__ == "__main__":
    G = build_interaction_graph()

    print(f"\nGraph Statistics:")
    print(f"  Nodes (drugs): {G.number_of_nodes()}")
    print(f"  Edges (interactions): {G.number_of_edges()}")
    print(f"  Average degree: {sum(dict(G.degree()).values()) / G.number_of_nodes():.1f}")

    test_drugs = ["Warfarin", "Aspirin", "Ibuprofen", "Metformin"]
    print(f"\nChecking interactions among: {test_drugs}")
    interactions = find_all_interactions(G, test_drugs)
    for d1, d2 in interactions:
        print(f"  ⚠️  {d1} <-> {d2}")

Your knowledge graph is loaded and queryable. Next up, you'll connect the NER pipeline from Step 2 to this graph so the system can automatically extract drugs from clinical text and check all their interactions in one pass.

Connect NER Output to the Knowledge Graph

You now have two working modules: one that extracts drug names from text, and one that models 48,000+ interactions as a graph. Individually they're useful, but the real power comes from connecting them into a single pipeline.

In this step, you'll create an interaction checker that feeds NER output directly into the knowledge graph, queries the openFDA API for official guidance on each flagged pair, and produces a formatted safety report.

In this step, get ready to:
  • Build a pipeline that connects drug extraction to graph queries.
  • Enrich flagged interactions with official FDA drug label data.
  • Test the full system against a realistic polypharmacy patient scenario.
Build the interaction checker pipeline

The pipeline needs to bring together your NER module, the knowledge graph, and an external API into one orchestrating script. You'll start with the module that queries the openFDA drug label API for official interaction guidance.

  • Create a new file called interaction_checker.py in your project folder.
  • Add the following imports and the openFDA query function:
import requests
from drug_ner import extract_drugs_from_text
from knowledge_graph import build_interaction_graph, find_all_interactions


def query_openfda_interactions(drug_name):
    """Fetch drug interaction text from the openFDA drug label API."""
    base_url = "https://api.fda.gov/drug/label.json"
    params = {
        "search": f"drug_interactions:{drug_name}",
        "limit": 1
    }

    try:
        response = requests.get(base_url, params=params, timeout=10)
        if response.status_code == 200:
            data = response.json()
            results = data.get("results", [])
            if results:
                interactions_text = results[0].get("drug_interactions", [""])[0]
                return interactions_text[:500] if interactions_text else None
    except (requests.RequestException, KeyError, IndexError):
        pass

    return None

What does this code do?

The imports pull in your two existing modules so this file can orchestrate the full pipeline.

  • query_openfda_interactions sends a GET request to the openFDA drug label endpoint at https://api.fda.gov/drug/label.json. It searches the drug_interactions field for the given drug name.
  • If the API returns a result, the function extracts the interaction text and truncates it to 500 characters.
  • The broad except clause handles network failures, missing JSON keys, and empty result lists gracefully by returning None.

Why openFDA instead of a drug interaction database?

The NLM Drug-Drug Interaction API was permanently discontinued on January 2, 2024. The openFDA drug label API provides the same interaction guidance text from Section 7 of FDA-approved labeling, requires no API key for basic use, and returns JSON.

  • Save interaction_checker.py.
Generate the safety report function

The core pipeline function takes raw clinical text and the graph, then orchestrates NER extraction, graph lookups, and FDA enrichment into a single report.

  • In interaction_checker.py, add the run_polypharmacy_check function below your query_openfda_interactions function:
def run_polypharmacy_check(clinical_text, graph):
    """Run the full polypharmacy checking pipeline."""
    print("=" * 60)
    print("POLYPHARMACY INTERACTION CHECKER")
    print("=" * 60)

    # Step 1: Extract drugs from text
    print("\n[1] Extracting drugs from clinical text...")
    extracted_drugs = extract_drugs_from_text(clinical_text)
    drug_names = [d["name"] for d in extracted_drugs]

    print(f"    Found {len(drug_names)} drugs: {', '.join(drug_names)}")

    # Step 2: Check interactions in knowledge graph
    print("\n[2] Checking knowledge graph for interactions...")
    interactions = find_all_interactions(graph, drug_names)

    if not interactions:
        print("    \u2705 No known interactions found.")
        return

    print(f"    \u26a0\ufe0f  Found {len(interactions)} interaction(s)!\n")

    # Step 3: Generate safety report
    print("=" * 60)
    print("SAFETY REPORT")
    print("=" * 60)

    for idx, (drug1, drug2) in enumerate(interactions, 1):
        print(f"\n--- Interaction #{idx} ---")
        print(f"  Drug A: {drug1}")
        print(f"  Drug B: {drug2}")
        print(f"  Status: \u26a0\ufe0f  KNOWN INTERACTION")

        # Fetch FDA label data
        fda_text = query_openfda_interactions(drug1)
        if fda_text and drug2.lower() in fda_text.lower():
            print(f"  FDA Guidance: {fda_text[:200]}...")
        else:
            print(f"  FDA Guidance: Consult prescribing information for details.")

    # Summary
    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"  Total drugs in regimen: {len(drug_names)}")
    print(f"  Interactions detected: {len(interactions)}")
    print(f"  Risk level: {'HIGH' if len(interactions) >= 3 else 'MODERATE' if len(interactions) >= 1 else 'LOW'}")
    print("=" * 60)

What does this pipeline do?

This function is the orchestrator that connects all three pieces of your system.

  • Step 1 calls extract_drugs_from_text from your NER module to pull drug names out of the clinical note.
  • Step 2 passes those names to find_all_interactions which checks every pair against the knowledge graph's 48,000+ edges.
  • Step 3 loops through each flagged pair, queries the openFDA API for official guidance, and prints a structured report.
  • The summary assigns a risk level: HIGH (3+ interactions), MODERATE (1-2), or LOW (none).
  • Save interaction_checker.py.
Test with a polypharmacy scenario

The real test is a realistic polypharmacy case. You'll simulate a 72-year-old patient on 7 medications and see what your system flags.

  • Add the following __main__ block at the bottom of interaction_checker.py:
if __name__ == "__main__":
    # Build the knowledge graph (downloads data on first run)
    print("Loading knowledge graph...")
    G = build_interaction_graph()

    # Simulate a polypharmacy patient
    clinical_note = (
        "Patient is a 72-year-old male on warfarin 5mg daily for atrial fibrillation. "
        "Started ibuprofen 400mg TID for joint pain. Also takes metformin 1000mg BID, "
        "lisinopril 20mg daily, aspirin 81mg daily, omeprazole 20mg daily, "
        "and simvastatin 40mg at bedtime."
    )

    run_polypharmacy_check(clinical_note, G)

What does this test case cover?

This clinical note contains 7 drugs that are commonly co-prescribed in elderly patients. Several of these have well-documented dangerous interactions.

Warfarin combined with aspirin or ibuprofen increases bleeding risk significantly. Your system should flag these pairs automatically from the knowledge graph.

  • Save interaction_checker.py.
  • Run the full pipeline by executing this command in your terminal:
python interaction_checker.py

You should see the knowledge graph loading, followed by the NER extracting drug names, and then a SAFETY REPORT listing flagged interaction pairs with FDA guidance text.

The SUMMARY section at the end shows the total drugs, interactions detected, and an overall risk level.

Not seeing any interactions flagged?

  • Make sure drug_interactions.tsv.gz and drug_names.csv are in the same folder as interaction_checker.py. These files were downloaded when you ran knowledge_graph.py in the previous step.
  • If you see ModuleNotFoundError, confirm that drug_ner.py and knowledge_graph.py are in the same directory as your new file.
  • If openFDA guidance shows "Consult prescribing information" for every pair, that's normal. The API may not have a label match for every drug name. The interaction is still confirmed by your knowledge graph.

Help me debug my interaction checker pipeline.

✔️ Awesome, I've got everything!

Great. Double check you've saved interaction_checker.py and your pipeline is producing interaction results.

ⓧ I'd like to double check the full code

Here is the complete interaction_checker.py file:

import requests
from drug_ner import extract_drugs_from_text
from knowledge_graph import build_interaction_graph, find_all_interactions


def query_openfda_interactions(drug_name):
    """Fetch drug interaction text from the openFDA drug label API."""
    base_url = "https://api.fda.gov/drug/label.json"
    params = {
        "search": f"drug_interactions:{drug_name}",
        "limit": 1
    }

    try:
        response = requests.get(base_url, params=params, timeout=10)
        if response.status_code == 200:
            data = response.json()
            results = data.get("results", [])
            if results:
                interactions_text = results[0].get("drug_interactions", [""])[0]
                return interactions_text[:500] if interactions_text else None
    except (requests.RequestException, KeyError, IndexError):
        pass

    return None


def run_polypharmacy_check(clinical_text, graph):
    """Run the full polypharmacy checking pipeline."""
    print("=" * 60)
    print("POLYPHARMACY INTERACTION CHECKER")
    print("=" * 60)

    # Step 1: Extract drugs from text
    print("\n[1] Extracting drugs from clinical text...")
    extracted_drugs = extract_drugs_from_text(clinical_text)
    drug_names = [d["name"] for d in extracted_drugs]

    print(f"    Found {len(drug_names)} drugs: {', '.join(drug_names)}")

    # Step 2: Check interactions in knowledge graph
    print("\n[2] Checking knowledge graph for interactions...")
    interactions = find_all_interactions(graph, drug_names)

    if not interactions:
        print("    \u2705 No known interactions found.")
        return

    print(f"    \u26a0\ufe0f  Found {len(interactions)} interaction(s)!\n")

    # Step 3: Generate safety report
    print("=" * 60)
    print("SAFETY REPORT")
    print("=" * 60)

    for idx, (drug1, drug2) in enumerate(interactions, 1):
        print(f"\n--- Interaction #{idx} ---")
        print(f"  Drug A: {drug1}")
        print(f"  Drug B: {drug2}")
        print(f"  Status: \u26a0\ufe0f  KNOWN INTERACTION")

        # Fetch FDA label data
        fda_text = query_openfda_interactions(drug1)
        if fda_text and drug2.lower() in fda_text.lower():
            print(f"  FDA Guidance: {fda_text[:200]}...")
        else:
            print(f"  FDA Guidance: Consult prescribing information for details.")

    # Summary
    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"  Total drugs in regimen: {len(drug_names)}")
    print(f"  Interactions detected: {len(interactions)}")
    print(f"  Risk level: {'HIGH' if len(interactions) >= 3 else 'MODERATE' if len(interactions) >= 1 else 'LOW'}")
    print("=" * 60)


if __name__ == "__main__":
    # Build the knowledge graph (downloads data on first run)
    print("Loading knowledge graph...")
    G = build_interaction_graph()

    # Simulate a polypharmacy patient
    clinical_note = (
        "Patient is a 72-year-old male on warfarin 5mg daily for atrial fibrillation. "
        "Started ibuprofen 400mg TID for joint pain. Also takes metformin 1000mg BID, "
        "lisinopril 20mg daily, aspirin 81mg daily, omeprazole 20mg daily, "
        "and simvastatin 40mg at bedtime."
    )

    run_polypharmacy_check(clinical_note, G)

Your pipeline now takes unstructured clinical text and automatically surfaces dangerous drug combinations with official FDA guidance. In the Secret Mission, you'll add network centrality analysis and visualization to identify the most dangerous drugs in your graph.

Enrich with openFDA Drug Label Data

In the previous step, you built a working polypharmacy checker that detects interactions from the knowledge graph.

Knowing that two drugs interact is useful, but a clinician needs more than just a flag. They need official guidance text from the drug's FDA-approved label. In this step, you'll examine how your query_openfda_interactions function fetches this guidance from the openFDA drug label API, and confirm that rate-limiting and error handling work correctly.

In this step, get ready to:
  • Understand how the openFDA drug label API query works.
  • Trace how FDA interaction text flows into the safety report.
  • Run the full pipeline and confirm FDA enrichment appears in the output.
Query the openFDA API

Your knowledge graph tells you THAT two drugs interact. The openFDA drug label API tells you WHAT to do about it. It provides the official drug_interactions section (Section 7) from FDA-approved labeling.

  • In VS Code, switch back to interaction_checker.py and find the query_openfda_interactions function near the top of the file.
def query_openfda_interactions(drug_name):
    """Fetch drug interaction text from the openFDA drug label API."""
    base_url = "https://api.fda.gov/drug/label.json"
    params = {
        "search": f"drug_interactions:{drug_name}",
        "limit": 1
    }

    try:
        response = requests.get(base_url, params=params, timeout=10)
        if response.status_code == 200:
            data = response.json()
            results = data.get("results", [])
            if results:
                interactions_text = results[0].get("drug_interactions", [""])[0]
                return interactions_text[:500] if interactions_text else None
    except (requests.RequestException, KeyError, IndexError):
        pass

    return None

What does this function do?

This function queries the openFDA drug label endpoint at https://api.fda.gov/drug/label.json. It searches the drug_interactions field for the given drug name and requests one matching label.

  • search=drug_interactions:{drug_name} finds labels whose Section 7 (Drug Interactions) mentions this drug.
  • limit=1 returns only the first match, keeping the response fast.
  • timeout=10 prevents the script from hanging if the FDA servers are slow.
  • The function returns the first 500 characters of the interaction text, or None if anything goes wrong.

Why openFDA instead of other drug APIs?

The openFDA drug label API requires no API key for basic use. It returns official FDA-approved labeling text in JSON format. This is the same prescribing information that pharmacists and physicians reference when evaluating drug safety.

Other drug interaction APIs (like the NLM Drug-Drug Interaction API) were discontinued in January 2024. openFDA remains free, actively maintained, and updated weekly.

Parse and display FDA interaction details

The raw API response is a JSON object containing the full drug label. Your code extracts just the interaction section and checks whether the second drug in the pair is mentioned.

  • Scroll down to the run_polypharmacy_check function and find the # Fetch FDA label data comment inside the interaction loop.
        # Fetch FDA label data
        fda_text = query_openfda_interactions(drug1)
        if fda_text and drug2.lower() in fda_text.lower():
            print(f"  FDA Guidance: {fda_text[:200]}...")
        else:
            print(f"  FDA Guidance: Consult prescribing information for details.")

What does this parsing logic do?

  • For each flagged interaction pair (drug1, drug2), it fetches the FDA label for drug1.
  • If the label text mentions drug2, the guidance is directly relevant. The code prints the first 200 characters as a preview.
  • If drug2 is not mentioned (or the API returned nothing), it prints a generic fallback message. This ensures the report always has guidance text for every flagged pair.
Run the full pipeline and verify FDA enrichment

Now confirm that the end-to-end pipeline works correctly. The script will extract drugs from the clinical note, check the knowledge graph, query the openFDA API for each interaction, and produce a formatted safety report.

  • Run the full interaction checker by executing this command in your terminal:
python interaction_checker.py

You should see output that includes a SAFETY REPORT section. Each interaction entry shows Drug A, Drug B, the interaction status, and an FDA Guidance line.

What should I see?

The output starts with the knowledge graph loading, followed by the NER extraction finding drugs in the clinical note. Then the SAFETY REPORT lists each flagged pair.

For some pairs, you will see actual FDA guidance text pulled from the drug label (starting with "FDA Guidance:" followed by a text excerpt). For others, you will see the fallback message "Consult prescribing information for details." Both outcomes confirm the pipeline is working correctly.

The SUMMARY section at the bottom shows the total drug count, number of interactions detected, and a risk level (HIGH, MODERATE, or LOW) based on how many interactions were found.

Not seeing FDA guidance text?

  • Check your internet connection. The openFDA API requires an active connection to fetch label data.
  • If you see "Consult prescribing information for details" for every interaction, the API may be rate-limiting your requests. Wait a few seconds and try again.
  • Verify that requests is installed in your virtual environment by running pip list | grep requests.

Ask for help: Why am I not seeing FDA guidance text in my safety report output?

How does error handling protect the pipeline?

The try/except block in query_openfda_interactions catches three exception types: requests.RequestException (network failures, timeouts), KeyError (missing JSON fields), and IndexError (empty result arrays).

This means the pipeline never crashes if the FDA API is unavailable. It gracefully falls back to the generic message and keeps processing the remaining interactions. This is critical for clinical tools that must always produce output.

  • Save interaction_checker.py if you made any changes while exploring.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

import requests
from drug_ner import extract_drugs_from_text
from knowledge_graph import build_interaction_graph, find_all_interactions


def query_openfda_interactions(drug_name):
    """Fetch drug interaction text from the openFDA drug label API."""
    base_url = "https://api.fda.gov/drug/label.json"
    params = {
        "search": f"drug_interactions:{drug_name}",
        "limit": 1
    }

    try:
        response = requests.get(base_url, params=params, timeout=10)
        if response.status_code == 200:
            data = response.json()
            results = data.get("results", [])
            if results:
                interactions_text = results[0].get("drug_interactions", [""])[0]
                return interactions_text[:500] if interactions_text else None
    except (requests.RequestException, KeyError, IndexError):
        pass

    return None


def run_polypharmacy_check(clinical_text, graph):
    """Run the full polypharmacy checking pipeline."""
    print("=" * 60)
    print("POLYPHARMACY INTERACTION CHECKER")
    print("=" * 60)

    # Step 1: Extract drugs from text
    print("\n[1] Extracting drugs from clinical text...")
    extracted_drugs = extract_drugs_from_text(clinical_text)
    drug_names = [d["name"] for d in extracted_drugs]

    print(f"    Found {len(drug_names)} drugs: {', '.join(drug_names)}")

    # Step 2: Check interactions in knowledge graph
    print("\n[2] Checking knowledge graph for interactions...")
    interactions = find_all_interactions(graph, drug_names)

    if not interactions:
        print("    ✅ No known interactions found.")
        return

    print(f"    ⚠️  Found {len(interactions)} interaction(s)!\n")

    # Step 3: Generate safety report
    print("=" * 60)
    print("SAFETY REPORT")
    print("=" * 60)

    for idx, (drug1, drug2) in enumerate(interactions, 1):
        print(f"\n--- Interaction #{idx} ---")
        print(f"  Drug A: {drug1}")
        print(f"  Drug B: {drug2}")
        print(f"  Status: ⚠️  KNOWN INTERACTION")

        # Fetch FDA label data
        fda_text = query_openfda_interactions(drug1)
        if fda_text and drug2.lower() in fda_text.lower():
            print(f"  FDA Guidance: {fda_text[:200]}...")
        else:
            print(f"  FDA Guidance: Consult prescribing information for details.")

    # Summary
    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"  Total drugs in regimen: {len(drug_names)}")
    print(f"  Interactions detected: {len(interactions)}")
    print(f"  Risk level: {'HIGH' if len(interactions) >= 3 else 'MODERATE' if len(interactions) >= 1 else 'LOW'}")
    print("=" * 60)


if __name__ == "__main__":
    # Build the knowledge graph (downloads data on first run)
    print("Loading knowledge graph...")
    G = build_interaction_graph()

    # Simulate a polypharmacy patient
    clinical_note = (
        "Patient is a 72-year-old male on warfarin 5mg daily for atrial fibrillation. "
        "Started ibuprofen 400mg TID for joint pain. Also takes metformin 1000mg BID, "
        "lisinopril 20mg daily, aspirin 81mg daily, omeprazole 20mg daily, "
        "and simvastatin 40mg at bedtime."
    )

    run_polypharmacy_check(clinical_note, G)

Your pipeline now runs end-to-end: NER extracts drug names from clinical text, the knowledge graph flags interacting pairs, and the openFDA API provides official FDA guidance for each flagged interaction. Next up, you'll tackle the Secret Mission and add network analysis to identify the most dangerous drugs in your graph.

Secret mission

Network Analysis and Visualization

Your interaction checker flags dangerous drug pairs. But which drugs in the entire database are the most dangerous connectors? Can you visualize a patient's personal risk network as a graph? Extend your pipeline with centrality analysis and matplotlib visualization to surface deeper insights.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future experimentation or delete them to free up disk space.

Resources you used:

  • Project Python files: drug_ner.py, knowledge_graph.py, interaction_checker.py.
  • Downloaded data files: drug_interactions.tsv.gz and drug_names.csv.
  • Generated visualization: patient_interactions.png.
  • Python virtual environment and installed packages.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the drug interaction checker, adding new clinical notes, or extending the knowledge graph.

  • Your project files, downloaded datasets, and virtual environment stay on your machine.
  • The datasets take up roughly 2MB of disk space total.
  • To return to the project later, open the folder in VS Code and reactivate your virtual environment.

Pause - I'll come back to this later

Deactivate your virtual environment to free up your terminal session. Your files stay on disk so you can pick up where you left off.

  • In your terminal, deactivate the virtual environment by running:
deactivate
  • Close VS Code if you no longer need it open.
  • When you return, reactivate the environment with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).

Delete - I don't want to use this again

Remove all project files, downloaded datasets, and the virtual environment. This frees up disk space and leaves no trace of the project.

  • Deactivate your virtual environment first by running:
deactivate
  • Navigate to your Desktop and delete the project folder by running:

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • This removes all Python files, the virtual environment, downloaded datasets, and the generated visualization.

Nice Work!

Nice Work!

Nice work! You've built a working polypharmacy drug interaction detection system that combines clinical NLP with knowledge graph analysis.

You've learned how to:

  • Extract drug names from unstructured clinical text using Named Entity Recognition (NER) and map them to standard DrugBank identifiers.
  • Construct a knowledge graph with 1,500+ drug nodes and 48,000+ interaction edges using NetworkX, then query it for pairwise drug-drug interactions.
  • Enrich flagged interactions with official FDA drug label guidance by querying the openFDA API and generating a formatted safety report with risk levels.
  • Performed network centrality analysis to identify the most dangerous drugs in the graph and visualized a patient's drug interaction subgraph using matplotlib.

Ready to quiz yourself?