liquid-biopsy/analytical-validation/SKILL.md
Treats a ctDNA assay as a molecule-counting experiment at the Poisson edge and builds its analytical-validation case the measurement-science way. Covers the genome-equivalent currency (~330 haploid copies/ng), the lambda = input_GE x VAF sampling ceiling (lambda>=3 for ~95% detection), the error-suppression ladder (raw NGS ~1e-3 -> single-strand UMI ~1e-4/1e-5 -> duplex <1e-7), the CLSI EP17 LoB/LoD/LoD95/LoQ framework, the per-locus-vs-panel-integrated LoD distinction that lets bespoke MRD reach ppm, contrived/SEQC2 reference standards, and honest LoD reporting conditioned on input mass + consensus depth + replicate detection rate. Use when stating or trusting a sensitivity claim, designing a dilution-series validation, deciding how many genome equivalents are needed at a target VAF, choosing a single-locus vs panel-integrated LoD, or auditing a "detects 0.1% VAF" claim.
npx skillsauth add GPTomics/bioSkills bio-analytical-validationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: numpy 1.26+, scipy 1.12+, statsmodels 0.14+
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signaturesIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"What is the real limit of detection of my ctDNA assay, and can I trust the number I am about to report?" -> Quantify the Poisson sampling ceiling, the error-suppression floor, and the LoB/LoD/LoQ that together define a defensible sensitivity claim.
scipy.stats.poisson for detection-probability math, scipy.stats.norm for CLSI LoB/LoD, statsmodels Probit/Logit for a dilution-series LoD95 fit.A ctDNA assay is a molecule-counting experiment at the Poisson edge. The mutant signal is a fixed, tiny number of physical template molecules in the tube, and the limit of detection is governed by two ceilings: how many genome equivalents were sampled (Poisson), and how low the background error floor was driven (error suppression). 1 ng of human DNA is ~330 haploid genome equivalents; the expected mutant-molecule count is lambda = input_GE x VAF. A 0.1% variant on 1,000 GE (~3.0 ng) has lambda = 1, so e^-1 ~= 37% of the time the mutant template was never in the tube and a perfect sequencer detects nothing. Past the point where every input molecule has been read once (sampling saturation, visible as a deduplication plateau in UMI families), additional read depth re-sequences the same physical molecules and adds zero information. Reporting an LoD as a bare VAF -- with no input mass, no unique-molecule (consensus) depth, no replicate detection rate -- is reporting an undefined quantity.
The second ceiling is the per-base background error rate, which sets the VAF floor independently: a 0.1% variant cannot be distinguished from noise if the assay manufactures that base at 0.1%. Error suppression is a ladder (raw NGS ~1e-3 -> single-strand UMI consensus ~1e-4/1e-5 -> duplex <1e-7), and single-strand consensus does NOT remove template-resident damage (C->T deamination, G->T 8-oxoG) because every PCR copy of that strand inherits the lesion -- only duplex strand-concordance catches it. The achieved LoD is the worse of the two ceilings: error dominates above ~0.1% VAF for tumor-naive single-locus calling, sampling dominates below it. The escape hatch is integration -- a bespoke panel summing mutant molecules across 16-50 loci against summed background reaches single-ppm even though each locus alone is ~1e-3 to 1e-4 (per-locus vs panel-integrated LoD).
| Concept | Definition | Source | |---------|------------|--------| | LoB (Limit of Blank) | Highest signal expected from an analyte-free blank (95th pct): LoB = mean_blank + 1.645SD_blank; the false-positive anchor on true negatives | CLSI EP17-A2 | | LoD (Limit of Detection) | Lowest level reliably distinguishable from LoB: LoD = LoB + 1.645SD_low; a sample at LoD is detected ~95% of the time | CLSI EP17-A2 | | LoD95 | The concentration/VAF where detection probability = 95%; a point on a probit/logistic detection curve, not a separate definition | CLSI EP17-A2; Newman 2016 | | LoQ (Limit of Quantitation) | Lowest level measurable with stated precision (e.g. CV<=20%); LoQ >= LoD always, so a "VAF" near the floor is detectable but not trustworthy | CLSI EP17-A2 | | Per-locus LoD | Single-variant LoD; sampling- and error-limited (~0.05-0.1% VAF typical) | Newman 2014/2016 | | Panel-integrated LoD | Evidence summed across N tracked variants via a >=k-of-N positivity rule (binomial over per-locus Poisson detection), reaching single-ppm at 16-50 loci; ~sqrt(N) variance-averaging is only a loose lower bound | Reinert 2019 | | Reference standards | Contrived defined-VAF cell-line admixtures fragmented to ~160 bp into normal cfDNA; SEQC2 Sample A / HCC1395 truth sets | Fang 2021 (SEQC2) |
| Scenario | Recommended | Why | |----------|-------------|-----| | "How many GE for 95% detection at VAF X?" | Solve lambda = input_GE x VAF >= 3, so input_GE >= 3/VAF | 1 - e^-3 = 0.95; ~30,000 GE (~91 ng at 330 GE/ng) for a single 1e-4 variant -- often more than one tube provides | | "Why is more depth not helping?" | Report unique (consensus) molecular coverage, not raw depth; check the dedup plateau | Past sampling saturation, depth re-reads the same molecules; the ceiling is GE in the tube | | Single hotspot vs bespoke panel for low VAF | Single locus: error/sampling-limited ~0.1%; need ppm -> integrate across 16-50 clonal loci | Per-locus Poisson/error floor is escaped only by summing independent detections (panel-integrated LoD) | | Reporting an LoD | Condition on input mass (GE) + consensus depth + replicate detection rate (e.g. "LoD95 0.1% VAF at 30 ng / 2x duplex / 95% of 20 replicates") | A bare VAF omits the input mass, the unique depth, and per-locus vs integrated -- it is undefined | | Estimating LoD95 from a dilution series | Probit (or logistic) regression of detection (0/1) on VAF; read off the 95% point with a CI | CLSI EP17-A2 detection-curve method; binary detection is a clean GLM target | | Distinguishing detection from quantitation | Set LoD for yes/no calls; set LoQ (CV<=20%) separately for any reported VAF/TF | MRD calls are binary and can sit far below LoQ; a near-floor VAF number is not quantitative | | Validating against truth | Contrived SEQC2 Sample A / HCC1395 admixtures, fragmented to cfDNA-like ~160 bp | Real low-VAF patient material is scarce/unverifiable; commutability with plasma is the caveat |
Goal: Convert an input mass and target VAF into an expected mutant-molecule count and a detection probability, so a sensitivity claim is anchored to molecules rather than to a VAF alone.
Approach: Convert ng to haploid genome equivalents (~330/ng), set lambda = input_GE x VAF, and read the detection probability as a Poisson tail P(X >= k) = 1 - cdf(k-1, lambda); invert for the minimum GE that puts lambda at the >=3 sampling-detection threshold.
import numpy as np
from scipy.stats import poisson
GE_PER_NG = 330 # haploid ~3.3 pg -> strict 1 ng / 3.3 pg = 303; 330 is the common diploid-6.6 pg/rounding convention
def genome_equivalents(input_ng):
return input_ng * GE_PER_NG
def detection_probability(input_ng, vaf, min_mutant_molecules=1):
'''P(at least min_mutant_molecules present) under Poisson(lambda = GE * VAF).'''
lam = genome_equivalents(input_ng) * vaf
return float(poisson.sf(min_mutant_molecules - 1, lam))
def ge_for_sampling_detection(vaf, target_lambda=3.0):
'''GE needed so lambda >= 3 -> ~95% chance the mutant molecule is present at all.'''
return target_lambda / vaf
# A 0.1% variant on 3.0 ng (~990 GE) has lambda ~= 1 -> ~63% detected, ~37% missed by sampling alone.
detection_probability(3.0, 0.001) # ~0.63
ge_for_sampling_detection(1e-4) # 30000 GE (~91 ng) for a single 0.01% variant
Goal: Estimate the VAF at which the assay detects 95% of the time, from a contrived dilution series, and anchor it to the blank-derived false-positive floor.
Approach: Compute LoB from blank replicates (mean + 1.645*SD, one-sided 95th pct), then fit a probit GLM of binary detection on log10(VAF) (CLSI EP17 fits on log concentration) across the dilution series and invert it for the 95% detection point. The series must bracket the 0.95 crossing — all-detected upper levels cause near-complete separation and an unstable slope.
import numpy as np
import statsmodels.api as sm
from scipy.stats import norm
def limit_of_blank(blank_signals):
'''LoB = mean + 1.645*SD; one-sided 95th percentile of analyte-free blanks.'''
blank_signals = np.asarray(blank_signals, dtype=float)
return blank_signals.mean() + 1.645 * blank_signals.std(ddof=1)
def lod95_probit(vaf_levels, detected):
'''Probit fit of detection (0/1) on log10(VAF); returns the VAF where P(detect) = 0.95.'''
log_vaf = np.log10(np.asarray(vaf_levels, dtype=float))
y = np.asarray(detected, dtype=float)
X = sm.add_constant(log_vaf)
fit = sm.GLM(y, X, family=sm.families.Binomial(link=sm.families.links.Probit())).fit()
intercept, slope = fit.params
return 10 ** ((norm.ppf(0.95) - intercept) / slope)
Goal: Combine independent per-locus detection probabilities into the panel-level detection probability that a bespoke MRD assay actually achieves, and find the integrated LoD.
Approach: Treat each tracked locus as an independent Poisson sampler at the same tumor VAF; a panel positive call requires at least k loci detected, so the panel detection probability is the binomial-tail over the per-locus probabilities -- this is why summing 16-50 loci reaches ppm.
import numpy as np
from scipy.stats import poisson, binom
def panel_detection_probability(input_ng, vaf, n_loci, min_loci_positive=2):
'''P(>= min_loci_positive of n_loci detected); >=2-of-N is the Signatera-style positivity rule.'''
per_locus = float(poisson.sf(0, input_ng * 330 * vaf))
return float(binom.sf(min_loci_positive - 1, n_loci, per_locus))
def panel_integrated_lod95(input_ng, n_loci, min_loci_positive=2, grid=None):
'''Lowest VAF on a log grid where the >=k-of-N panel call hits 95%.'''
grid = np.logspace(-6, -2, 400) if grid is None else np.asarray(grid)
probs = [panel_detection_probability(input_ng, v, n_loci, min_loci_positive) for v in grid]
hits = grid[np.asarray(probs) >= 0.95]
return float(hits.min()) if hits.size else float('nan')
| Threshold | Source | Rationale | |-----------|--------|-----------| | ~330 haploid genome equivalents per ng cfDNA | Standard (haploid ~3.3 pg) | Converts input mass to the molecule count that actually sets sensitivity; strict 1 ng / 3.3 pg = 303, with 330 the common diploid-6.6 pg/rounding convention | | lambda = input_GE x VAF; lambda >= 3 for ~95% sampling-detection | Poisson, 1 - e^-3 = 0.95 | Below lambda~3 the mutant template is often simply absent from the tube regardless of sequencing | | Raw NGS error floor ~1e-3 | Schmitt 2012 context; field consensus | Sets the per-base VAF floor before any consensus; a global VAF cutoff above this is noise-limited | | Single-strand UMI consensus ~1e-4 to 1e-5 | Newman 2014/2016 (CAPP-Seq/iDES) | Majority-vote within a UMI family erases PCR/sequencing error not shared across the family | | Duplex sequencing <1e-7 (theory <1/1e9 nt) | Schmitt 2012 PNAS 109:14508 | Requires the variant on BOTH original strands; independent strand errors cannot agree | | iDES adds ~3-15x over baseline; ctDNA to ~4e-5 | Newman 2016 Nat Biotechnol 34:547 | Position/trinucleotide background model subtracts stereotyped artifacts per locus | | ichorCNA tumor-fraction floor ~3% | Adalsteinsson 2017 Nat Commun 8:1324 | Copy-number-based TF estimation; sWGS/ULP-WGS cannot resolve TF below ~3% -- an LoD, not a VAF | | Bespoke panel reaches single-ppm by integrating 16-50 loci | Reinert 2019 JAMA Oncol 5:1124 | Per-locus ~1e-4 floor escaped by summing independent detections; >=2-of-N positivity rule | | LoQ >= LoD (e.g. CV<=20% for quantitation) | CLSI EP17-A2 | Detection (binary) is easier than quantitation (continuous); near-floor VAFs are not trustworthy numbers |
| Error / symptom | Cause | Solution | |-----------------|-------|----------| | "Assay detects 0.1% VAF" with no input mass | VAF reported as a standalone sensitivity spec | Condition the LoD on input GE + consensus depth + replicate detection rate; 0.1% on 100 GE is noise | | Buying more sequencing depth to improve sensitivity | Conflating read depth with molecule count | Past the dedup plateau the assay is sampling-saturated; add plasma volume / conversion efficiency, not depth | | Per-locus LoD quoted as the panel LoD (or vice versa) | Ignoring integration across tracked loci | State which is reported; a 50-variant panel's integrated LoD is orders of magnitude below any single locus | | VAF used as the sensitivity unit | Omitting the molecule count behind the fraction | Pair every VAF with input GE; lambda = GE x VAF is the quantity that determines detection | | Single-strand UMI assumed to remove damage artifacts | Template-resident C->T/G->T inherited by every copy | Use duplex strand-concordance for sub-1e-5 claims; single-strand votes unanimously for the lesion | | Reporting a near-floor VAF as a measured value | Confusing LoD (detect) with LoQ (quantify) | Quantitative VAF/TF only at/above LoQ (CV<=20%); below it report detected/not-detected | | Global VAF cutoff across all loci | Background error is position/context-dependent | Use a per-locus background model (iDES-style); a flat threshold loses sensitivity and specificity |
tools
--- name: bio-phasing-imputation-foundations description: Frames the phasing/imputation pipeline before any tool runs: phasing and imputation are one Li-Stephens copying HMM (recombination is the transition, mutation the emission, the genetic map and Ne set the rates), imputation's honest output is a dosage with a self-estimated quality (INFO/R2/DR2) not a hard genotype, and the stages are ordered and each fails silently (QC, align build and strand to the panel, phase, impute per chromosome, fil
tools
Chooses the enrichment generation before any tool runs, mapping the input shape to a method class - a pre-selected gene list plus a background to over-representation analysis (ORA, hypergeometric), a ranked statistic for all genes to gene set enrichment (GSEA), a signed signaling topology to pathway-topology (SPIA) - then making the null explicit (competitive vs self-contained, gene vs subject sampling) and running a trustworthiness checklist (testable-gene universe, FDR, redundancy collapse, leading-edge check, version reporting). Covers why every clusterProfiler GSEA is the inter-gene-correlation-uncorrected competitive null, why the background not the gene list decides ORA significance, and why no method is universally best. Use when deciding ORA vs GSEA vs topology, which gene-set DB, whether a result is trustworthy, or which null a tool computes. For ORA see go-enrichment, GSEA see gsea, databases kegg-pathways/reactome-pathways/wikipathways; the ranking comes from differential-expression/de-results.
testing
End-to-end GWAS workflow from VCF to association results. Covers PLINK QC, population structure correction, and association testing for case-control or quantitative traits. Use when running genome-wide association studies.
development
Orchestrates the full path from differential expression results to redundancy-collapsed functional enrichment: choose ORA vs GSEA, convert gene IDs per method, run enrichGO/enrichKEGG/enrichPathway/enrichWP or gseGO/gseKEGG (clusterProfiler, ReactomePA, rWikiPathways), and visualize. Routes the ORA-vs-GSEA generation fork and the null/universe/reproducibility theory to pathway-analysis/enrichment-foundations. Use when a DESeq2/edgeR/limma result must become enriched GO terms, KEGG/Reactome/WikiPathways pathways, or a GSEA leading edge; when deciding whether a ranking exists for all genes (GSEA, named decreasing vector) or only a pre-selected list (ORA plus a defensible background universe); or when assembling DE-to-pathway end to end. The DE list and ranking statistic come from differential-expression/de-results; per-method nuance lives in the pathway-analysis skills.