Tutorial

Identifying Rare Disease Phenotypes with Human Phenotype Ontology (HPO)

Difficulty: Intermediate Time: 15 min read

Introduction

Diagnosing rare genetic diseases often resembles finding a needle in a haystack. Patients frequently present with a complex array of overlapping symptoms. The Human Phenotype Ontology (HPO) provides a standardized vocabulary of phenotypic abnormalities encountered in human disease. By representing a patient's symptoms as a set of HPO terms, bioinformaticians can computationally compare the patient against thousands of known disease profiles to rank potential diagnoses. In this tutorial, we will use Python to interact with the HPO API and calculate semantic similarity between a patient's phenotype and a known genetic syndrome.

Architecture Overview: The HPO Graph

The HPO is structured as a Directed Acyclic Graph (DAG). A term like "Macrodactyly" is a highly specific child of a broader term like "Abnormality of the digits". This hierarchy allows for fuzzy matching when a patient's exact symptom isn't perfectly recorded.


graph TD
    Root[Phenotypic Abnormality HP:0000118] --> Limbs[Abnormality of the limbs HP:0040064]
    Root --> Head[Abnormality of the head HP:0000234]
    
    Limbs --> Digits[Abnormality of the digits HP:0001155]
    Digits --> Poly[Polydactyly HP:0010442]
    Digits --> Macro[Macrodactyly HP:0001156]
    
    Head --> Face[Abnormality of the face HP:0000271]
    Face --> Hyper[Hypertelorism HP:0000316]
        

Prerequisites

  • Python 3.8+
  • requests library for API calls
  • Basic understanding of ontologies (Directed Acyclic Graphs)

Step 1: Install Dependencies

pip install requests networkx pandas

Step 2: Defining the Patient Phenotype

Let's assume a clinical geneticist has examined a child and noted the following features: an abnormally large head (Macrocephaly), wide-set eyes (Hypertelorism), and extra fingers (Polydactyly). First, we map these to their specific HPO IDs.

# Patient's clinical features mapped to HPO IDs
patient_hpo_terms = {
    "Macrocephaly": "HP:0000256",
    "Hypertelorism": "HP:0000316",
    "Polydactyly": "HP:0010442"
}

print(f"Patient Phenotype: {list(patient_hpo_terms.values())}")

Step 3: Querying the HPO API

We can use the public HPO API (or the Monarch Initiative API) to retrieve information about a specific term or to find diseases associated with a term.

import requests

def get_diseases_for_hpo(hpo_id):
    """Retrieve diseases associated with a given HPO ID."""
    # Using the Jackson Laboratory HPO API
    url = f"https://hpo.jax.org/api/hpo/term/{hpo_id}/diseases"
    response = requests.get(url)
    
    if response.status_code == 200:
        data = response.json()
        return [disease['diseaseName'] for disease in data['diseases']]
    else:
        return []

# Let's find diseases associated with Polydactyly
polydactyly_diseases = get_diseases_for_hpo("HP:0010442")
print(f"Found {len(polydactyly_diseases)} diseases associated with Polydactyly.")
print(f"Examples: {polydactyly_diseases[:5]}")

Step 4: Finding the Intersection (Differential Diagnosis)

A single symptom like Polydactyly might be associated with hundreds of diseases. The power of HPO comes from intersecting multiple terms. By finding diseases that share all of the patient's symptoms, we can drastically narrow down the list of potential diagnoses.

def calculate_differential(patient_terms):
    disease_sets = []
    
    # Get the set of diseases for each HPO term
    for symptom, hpo_id in patient_terms.items():
        diseases = get_diseases_for_hpo(hpo_id)
        disease_sets.append(set(diseases))
        print(f"{symptom} ({hpo_id}) maps to {len(diseases)} diseases.")
    
    # Find the intersection of all sets
    if not disease_sets:
        return set()
        
    candidate_diseases = set.intersection(*disease_sets)
    return candidate_diseases

print("\nCalculating Differential Diagnosis...")
candidates = calculate_differential(patient_hpo_terms)

print(f"\nFound {len(candidates)} candidate diseases matching ALL symptoms:")
for disease in candidates:
    print(f"- {disease}")

Troubleshooting: Strict Intersection vs Semantic Similarity

The strict intersection method above is great for tutorials, but in the real world, it almost always fails. A patient might have a novel presentation of a known disease, or the doctor might have recorded "Abnormality of the digits" instead of the highly specific "Polydactyly".

  • Use Resnik Similarity: Instead of strict intersections, use graph algorithms to calculate the Information Content (IC) of the Lowest Common Ancestor (LCA) between the patient's term and the disease's term in the HPO DAG.
  • Handling Imprecision: If a patient has HP:0001155 (Abnormality of the digits), the algorithm should still give a partial match score to a disease characterized by HP:0010442 (Polydactyly) because they are closely related on the graph.

Conclusion

This simple intersection method forms the foundation of modern rare disease diagnostics. Advanced platforms (like Exomiser or LIRICAL) take this concept further by combining HPO phenotypic similarity scores with genomic sequencing data (VCF files) to pinpoint the exact causative genetic mutation.