| name | bio-applied-clinical-genomics |
| description | Classify germline variant pathogenicity with ACMG/AMP 5-tier criteria (PVS1/PS1-4/PM1-6/PP1-5/BA1/BS1-4/BP1-7), query ClinVar via NCBI E-utilities, and filter by gnomAD population frequency to draft a clinical variant report. Use when doing ACMG classification, deciding Pathogenic/Likely Pathogenic/VUS/Likely Benign/Benign calls, looking up a variant in ClinVar, or writing a clinical genomics/diagnostic report. |
| tool_type | python |
| primary_tool | Python (ACMG/AMP rules engine + NCBI E-utilities/requests) |
Applied Clinical Genomics: ACMG/AMP Variant Classification
When to Use
- Classifying a germline variant's pathogenicity from a list of ACMG/AMP evidence codes (PVS1, PS1-4, PM1-6, PP1-5, BA1, BS1-4, BP1-7).
- Looking up a variant or gene in ClinVar to check prior clinical significance and review status.
- Filtering candidate variants by gnomAD-style population allele frequency before applying ACMG rules (PM2/BA1/BS1 gating).
- Drafting the interpretation section of a diagnostic, carrier, or predictive-testing clinical report.
- Deciding which type of genetic test (diagnostic, carrier, pharmacogenomic, prenatal, tumor) applies to a clinical scenario.
Version Compatibility
- ACMG/AMP guidelines: Richards et al. 2015 (Table 5 combining rules), as refined by ClinGen Sequence Variant Interpretation (SVI) working group recommendations (ongoing updates to PVS1, PM2, BS1/BA1 thresholds — always check the current ClinGen SVI recommendations for a given gene/criterion before finalizing a call).
- ClinVar/NCBI E-utilities: JSON
esearch/esummary endpoints, stable API, no versioning concerns; requests ≥2.28, Python ≥3.10 (uses list[str], dict | None syntax).
Prerequisites
pip install requests (only needed for live ClinVar queries; classification logic has no dependencies).
- Familiarity with VCF fields (see
bio-variant-calling-vcf-basics) and variant annotation (bio-variant-calling-variant-annotation).
- A gnomAD or in-house population frequency source for PM2/BA1/BS1 gating.
Types of Genetic Testing
| Type | Purpose | Typical Approach |
|---|
| Diagnostic | Identify cause of existing disease | WES/WGS or gene panels |
| Predictive | Assess future disease risk | Targeted testing |
| Carrier | Identify heterozygous carriers | Carrier panels |
| Pharmacogenomic | Guide drug selection/dosing | PGx panels |
| Prenatal/Newborn | Screen or diagnose fetus/newborn | cfDNA, targeted panels |
| Somatic/Tumor | Guide cancer treatment | Tumor panels, WES |
ACMG/AMP 5-Tier Classification
Pathogenic > Likely Pathogenic > VUS > Likely Benign > Benign
(P) (LP) (LB) (B)
- LP/P: >90% certainty disease-causing, reportable and actionable.
- VUS: insufficient evidence either way, not acted upon clinically.
- LB/B: >90% certainty benign, reportable as not disease-causing.
Pathogenic evidence:
| Strength | Codes | Examples |
|---|
| Very Strong | PVS1 | Null variant in a gene where loss-of-function is a known disease mechanism |
| Strong | PS1-PS4 | Same AA change as known pathogenic; confirmed de novo; functional study; prevalence in affected cohort |
| Moderate | PM1-PM6 | Mutational hotspot; absent from population databases; protein length change; novel missense in low-missense-tolerant gene |
| Supporting | PP1-PP5 | Co-segregation with disease; computational evidence; phenotype specificity; reputable source without independent evidence |
Benign evidence:
| Strength | Codes | Examples |
|---|
| Stand-alone | BA1 | Allele frequency >5% in any gnomAD population |
| Strong | BS1-BS4 | Frequency exceeds expected for the disorder; healthy adult carrier; functional no-effect; non-segregation |
| Supporting | BP1-BP7 | Missense in a gene where only truncating variants cause disease; benign in silico consensus; synonymous with no splice impact |
Core Workflow
Goal: Turn a list of ACMG/AMP evidence codes into one of the 5 classification tiers using the Richards et al. 2015 Table 5 combining rules.
Approach: Bucket the codes by strength, then walk the benign rules first (BA1 short-circuits to Benign), then the pathogenic/likely-pathogenic rules in order of decreasing evidence.
CRITERIA_STRENGTH = {
'PVS1': 'very_strong',
'PS1': 'strong', 'PS2': 'strong', 'PS3': 'strong', 'PS4': 'strong',
'PM1': 'moderate', 'PM2': 'moderate', 'PM3': 'moderate',
'PM4': 'moderate', 'PM5': 'moderate', 'PM6': 'moderate',
'PP1': 'supporting', 'PP2': 'supporting', 'PP3': 'supporting',
'PP4': 'supporting', 'PP5': 'supporting',
'BA1': 'stand_alone',
'BS1': 'strong', 'BS2': 'strong', 'BS3': 'strong', 'BS4': 'strong',
'BP1': 'supporting', 'BP2': 'supporting', 'BP3': 'supporting',
'BP4': 'supporting', 'BP5': 'supporting', 'BP6': 'supporting', 'BP7': ,
}
() -> :
path_criteria = [c c criteria c.startswith((, , , ))]
benign_criteria = [c c criteria c.startswith((, , ))]
pvs = ( c path_criteria CRITERIA_STRENGTH.get(c) == )
ps = ( c path_criteria CRITERIA_STRENGTH.get(c) == )
pm = ( c path_criteria CRITERIA_STRENGTH.get(c) == )
pp = ( c path_criteria CRITERIA_STRENGTH.get(c) == )
ba = ( c benign_criteria CRITERIA_STRENGTH.get(c) == )
bs = ( c benign_criteria CRITERIA_STRENGTH.get(c) == )
bp = ( c benign_criteria CRITERIA_STRENGTH.get(c) == )
ba >= :
bs >= :
bs >= bp >= :
bp >= :
pvs >= (ps >= pm >= (pm >= pp >= ) pp >= ):
ps >= :
ps >= (pm >= (pm >= pp >= ) (pm >= pp >= )):
pvs >= pm >= :
ps >= <= pm <= :
ps >= pp >= :
pm >= :
pm >= pp >= :
pm >= pp >= :
pvs >= :
Goal: Look up prior clinical significance for a variant/gene in ClinVar before finalizing a call (feeds PP5/BP6-style "reputable source" evidence).
Approach: Two-step NCBI E-utilities call — esearch for ClinVar UIDs matching a term, then esummary to pull clinical significance and review status for each hit.
import requests
def query_clinvar(variant_description: str, retmax: int = 5) -> dict:
"""Query ClinVar for a variant/gene via NCBI E-utilities.
Args:
variant_description: e.g. 'BRCA1[gene] AND pathogenic[clinical_significance]'
or a specific HGVS term like 'NM_007294.4:c.5266dupC'.
retmax: max number of records to summarize.
Returns:
dict with 'query', 'count' (total hits), and 'records' (list of dicts).
"""
base = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils'
search = requests.get(f'{base}/esearch.fcgi', params={
'db': 'clinvar', 'term': variant_description,
'retmax': retmax, 'retmode': 'json',
}, timeout=10)
search.raise_for_status()
ids = search.json().get('esearchresult', {}).get('idlist', [])
total = int(search.json()['esearchresult'].get('count', 0))
if not ids:
return {'query': variant_description, 'count': 0, 'records': []}
summary = requests.get(f'{base}/esummary.fcgi', params={
'db': 'clinvar', 'id': ','.join(ids), 'retmode': 'json',
}, timeout=10)
summary.raise_for_status()
result_block = summary.json().get('result', {})
records = []
uid ids:
entry = result_block.get(uid, {})
entry:
records.append({
: uid,
: entry.get(, ),
: entry.get(, {}).get(, ),
: entry.get(, {}).get(, ),
: entry.get(, ),
})
{: variant_description, : total, : records}
Goal: Gate candidate variants by population frequency (PM2 "absent/rare" and BA1/BS1 "too common") before spending time on other evidence.
Approach: Compare per-population frequencies against a popmax threshold and a global-average threshold; report the reason each variant passed or failed.
def filter_by_frequency(
variants: list[dict], max_af: float = 0.01, max_popmax: float = 0.01
) -> tuple[list[dict], list[tuple]]:
"""Split variants into rare (candidate disease-causing) vs. common (BA1/BS1-range).
Args:
variants: dicts with a 'freq' key mapping population -> allele frequency
(e.g. gnomAD 'afr', 'nfe', 'eas', ...).
max_af: global allele frequency threshold (average across populations).
max_popmax: max allowed frequency in any single population (gnomAD popmax).
Returns:
(rare, filtered_out) where filtered_out is (variant, freq, reason) tuples.
"""
rare, filtered_out = [], []
for var in variants:
freqs = var['freq'].values()
popmax = max(freqs)
global_af = sum(freqs) / len(freqs)
if popmax > max_popmax:
filtered_out.append((var, popmax, 'popmax exceeds threshold'))
elif global_af > max_af:
filtered_out.append((var, global_af, 'global AF exceeds threshold'))
else:
rare.append(var)
return rare, filtered_out
Pitfalls
- Don't stack correlated evidence: PS1 (same AA change reported pathogenic) and PM5 (different AA change, same residue) are related but not interchangeable — don't invent a rule combination not in Table 5.
- PM2 is not stand-alone: "absent from population databases" alone should not drive a Pathogenic/Likely Pathogenic call in current ClinGen SVI guidance — always pair with other evidence.
- ClinVar submissions vary in quality: check
review_status (e.g. "reviewed by expert panel" vs. "no assertion criteria provided") before trusting a ClinVar significance label as PP5 evidence.
- gnomAD population coverage differs by ancestry: a variant absent in one gnomAD population may still be common in an underrepresented population; low popmax confidence in small subpopulations can look artificially rare.
- Coordinate systems: BED is 0-based half-open; VCF/ClinVar HGVS are 1-based inclusive — mixing them causes off-by-one variant lookups.
- Multiple testing: when scanning many candidate variants against frequency/prediction thresholds, remember this is filtering, not hypothesis testing — FDR correction applies to statistical association tests, not ACMG evidence gating.
See Also
bio-clinical-databases-clinvar-lookup — deeper ClinVar query patterns and result parsing.
bio-clinical-databases-gnomad-frequencies — full gnomAD population frequency access.
bio-clinical-databases-variant-prioritization — combining multiple evidence sources to rank candidates.
bio-variant-calling-clinical-interpretation — upstream VCF annotation feeding into ACMG classification.
clinical-reports — formatting the final report document.