# Bio Applied Clinical Genomics

> 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.

- Skill: `pavel-kravchenko/bio-applied-clinical-genomics` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/bio-applied-clinical-genomics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/bio-applied-clinical-genomics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/bio-applied-clinical-genomics

---


# 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

```text
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.

```python
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': 'supporting',
}


def classify_variant(criteria: list[str]) -> str:
    """Classify a variant from ACMG/AMP evidence codes.

    Implements the combining rules from Richards et al. 2015, Table 5.
    Returns one of: Pathogenic, Likely Pathogenic, VUS, Likely Benign, Benign.
    """
    path_criteria = [c for c in criteria if c.startswith(('PVS', 'PS', 'PM', 'PP'))]
    benign_criteria = [c for c in criteria if c.startswith(('BA', 'BS', 'BP'))]

    pvs = sum(1 for c in path_criteria if CRITERIA_STRENGTH.get(c) == 'very_strong')
    ps = sum(1 for c in path_criteria if CRITERIA_STRENGTH.get(c) == 'strong')
    pm = sum(1 for c in path_criteria if CRITERIA_STRENGTH.get(c) == 'moderate')
    pp = sum(1 for c in path_criteria if CRITERIA_STRENGTH.get(c) == 'supporting')
    ba = sum(1 for c in benign_criteria if CRITERIA_STRENGTH.get(c) == 'stand_alone')
    bs = sum(1 for c in benign_criteria if CRITERIA_STRENGTH.get(c) == 'strong')
    bp = sum(1 for c in benign_criteria if CRITERIA_STRENGTH.get(c) == 'supporting')

    # Benign rules checked first -- BA1 alone is stand-alone benign
    if ba >= 1:
        return 'Benign'
    if bs >= 2:
        return 'Benign'
    if bs >= 1 and bp >= 1:
        return 'Likely Benign'
    if bp >= 2:
        return 'Likely Benign'

    # Pathogenic rules
    if pvs >= 1 and (ps >= 1 or pm >= 2 or (pm >= 1 and pp >= 1) or pp >= 2):
        return 'Pathogenic'
    if ps >= 2:
        return 'Pathogenic'
    if ps >= 1 and (pm >= 3 or (pm >= 2 and pp >= 2) or (pm >= 1 and pp >= 4)):
        return 'Pathogenic'

    # Likely Pathogenic rules
    if pvs >= 1 and pm >= 1:
        return 'Likely Pathogenic'
    if ps >= 1 and 1 <= pm <= 2:
        return 'Likely Pathogenic'
    if ps >= 1 and pp >= 2:
        return 'Likely Pathogenic'
    if pm >= 3:
        return 'Likely Pathogenic'
    if pm >= 2 and pp >= 2:
        return 'Likely Pathogenic'
    if pm >= 1 and pp >= 4:
        return 'Likely Pathogenic'
    if pvs >= 1:
        # Not stated verbatim in Table 5 but a common pragmatic convention
        # (used by tools like InterVar) for a lone PVS1.
        return 'Likely Pathogenic'

    return 'VUS'
```

**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.

```python
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 = []
    for uid in ids:
        entry = result_block.get(uid, {})
        if entry:
            records.append({
                'uid': uid,
                'title': entry.get('title', ''),
                'clinical_significance': entry.get('clinical_significance', {}).get('description', 'N/A'),
                'review_status': entry.get('clinical_significance', {}).get('review_status', 'N/A'),
                'gene': entry.get('gene_sort', ''),
            })
    return {'query': variant_description, 'count': total, 'records': 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.

```python
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)  # simplified; gnomAD uses weighted average
        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.

