population-genetics/population-structure/SKILL.md
--- name: bio-population-genetics-population-structure description: Infers and describes population structure with PCA (plink2 --pca, smartpca/EIGENSOFT, FlashPCA2), model-based clustering (ADMIXTURE, fastSTRUCTURE), FST estimators (Weir-Cockerham vs Hudson), and f-statistics (f3/f4/D via AdmixTools/admixr), plus Python plotting of PCs and Q barplots. Every output is a model-conditioned description of variance, not truth: PCs conflate ancestry with LD/inversions/relatedness/batch, ADMIXTURE Q-va
npx skillsauth add GPTomics/bioSkills population-genetics/population-structureInstall 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: PLINK 2.0 (alpha 6+), ADMIXTURE 1.3+, EIGENSOFT 7.2+, scikit-allel 1.3+, numpy 1.26+, pandas 2.2+, matplotlib 3.8+.
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signatures<tool> --version then <tool> --help to confirm flagsIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Version traps that change results, not just syntax: ADMIXTURE is a standalone CLI (admixture --cv input.bed K), never an R or Python package, and --cv defaults to 5-fold. plink2 --pca builds on the variance-standardized relationship matrix from --make-rel/--make-grm and has no Tracy-Widom test; smartpca does. plink2 --make-king gives kinship (cutoff 0.0884 = second-degree), not the deprecated PI_HAT from PLINK 1.9 --genome. f-statistics live in AdmixTools (CLI) wrapped by admixr (R), not in plink. The single source of truth for versions is this block, not headings.
"Analyze the population structure in my genotypes" -> Project genotype covariance into continuous axes or discrete clusters, after pruning the artifacts the model would otherwise mistake for ancestry, and attach the uncertainty machinery any interpreted statistic requires.
plink2 --pca 20 approx (top eigenvectors of the relationship matrix; LD-pruned, relatives removed first)admixture --cv data_pruned.bed 3 (maximum-likelihood mixing weights for K abstract clusters)allel.hudson_fst(ac1, ac2) then num.sum()/den.sum() (FST as a ratio of averages)Scope: PCA, model-based clustering (ADMIXTURE/fastSTRUCTURE), FST estimators, and f-/D-statistics, with Python plotting. QC and KING relatedness route to plink-basics; LD pruning to linkage-disequilibrium; array-scale Python diversity/FST windows to scikit-allel-analysis; phased haplotype work to phasing-imputation/haplotype-phasing; introgression detection to comparative-genomics/introgression-detection.
| Method | Citation | Mechanism / role | When |
|--------|----------|------------------|------|
| plink2 --pca | Patterson 2006; Price 2006 | Eigenvectors of the variance-standardized relationship matrix; approx for large N | Stratification covariates, gross structure, QC outliers |
| smartpca (EIGENSOFT) | Patterson 2006 | PCA plus Tracy-Widom significance, outlier removal, lsqproject projection | Rigorous PCA, aDNA projection, per-PC p-values |
| FlashPCA2 | Abraham 2017 | Randomized PCA for biobank N (>100k) | Very large cohorts |
| ADMIXTURE | Alexander 2009 | Fast ML point estimate of Q (ancestry weights) and P (cluster frequencies) | Genome-wide discrete ancestry proportions |
| fastSTRUCTURE | Raj 2014 | Variational Bayes clustering with chooseK.py K guidance | Fast K exploration |
| Hudson FST | Hudson 1992; Bhatia 2013 | Per-SNP heterozygosity estimator; ratio of averages | Pairwise differentiation, unequal sample sizes, rare variants / SNP-array ascertainment |
| Weir-Cockerham FST | Weir & Cockerham 1984 | ANOVA estimator (a/(a+b+c)); ratio of averages | Classic variance-partition framing, balanced n |
| f3 / f4 / D | Patterson 2012; Durand 2011 | Drift-distance tests of admixture and tree-ness; block jackknife | Admixture detection, gene-flow tests |
| TreeMix | Pickrell 2012 | ML tree plus migration edges from frequency covariance | Tree + migration hypotheses |
| admixr / AdmixTools | Petr 2019; Patterson 2012 | Reproducible R wrappers for qp3Pop/qpDstat/qpAdm/qpGraph | f-statistics and admixture graphs |
| Scenario | Recommended | Why |
|----------|-------------|-----|
| Stratification covariates for GWAS | plink2 --pca (approx >5000) | assumption-light, fast, feeds --glm directly |
| Per-PC significance, outlier removal, aDNA projection | smartpca | Tracy-Widom test plus lsqproject for shrinkage-robust projection |
| Biobank-scale PCA (>100k) | plink2 --pca approx or FlashPCA2 | randomized algorithms scale; exact PCA does not |
| Discrete ancestry proportions | ADMIXTURE over a K span (--cv as guide) | present a span, never the CV argmin alone |
| Fast K exploration | fastSTRUCTURE + chooseK.py | variational, very fast |
| Pairwise differentiation, unequal n or rare variants | Hudson FST, ratio of averages | Bhatia 2013; WC is sensitive to n, population count, and rare variants |
| "Is population C admixed?" | f3(C; A,B) (qp3Pop / admixr) | f3 < 0 proves admixture; block-jackknife Z |
| "Is there gene flow / introgression?" | D / f4 (qpDstat, ABBA-BABA) | |Z| > 3; cannot separate from ancient structure alone |
| Clinal / spatial structure | EEMS, Mantel | clines are isolation-by-distance, not discrete demes |
| Relatedness QC before everything | plink2 --king-cutoff (see plink-basics) | a relative cluster grabs a spurious PC |
Goal: Compute PCs that track ancestry rather than inversions, relatedness, or batch.
Approach: LD-prune, exclude long-range-LD/inversion regions by coordinate, remove relatives before computing axes, drop very-low-MAF variants, then run PCA on the survivors.
# LD-prune so a single dense block cannot dominate a PC (route detail to linkage-disequilibrium).
plink2 --bfile data --indep-pairwise 50 5 0.1 --out prune
# Exclude long-range-LD regions and inversions that survive pruning and create karyotype PCs.
# range_lrld.txt holds MHC chr6:25-35 Mb, 8p23.1, 17q21.31, and LCT/2q21 in plink --exclude range format.
plink2 --bfile data --extract prune.prune.in --exclude range range_lrld.txt --maf 0.01 \
--make-bed --out data_for_pca
# PCA on the LD-pruned, inversion-stripped, relatedness-pruned set. approx is near-required above ~50k.
plink2 --bfile data_for_pca --pca 20 approx --out pca
# Outputs: pca.eigenvec (FID IID PC1..PCn), pca.eigenval (variance per PC).
Relatives must be removed BEFORE computing axes (a cluster of cousins forms its own high-covariance PC); compute the KING cutoff with plink2 --king-cutoff 0.0884 from plink-basics, build PCs on the unrelated set, then project relatives back. plink2 has no Tracy-Widom test: feed the eigenvalues to smartpca twstats or read a scree elbow to decide which PCs are real.
Goal: Attach per-PC significance and project new/ancient samples without shrinkage artifacts.
Approach: Run smartpca with outlier iterations and Tracy-Widom output for the reference build, and use lsqproject: YES to place additional samples robustly to missingness.
# smartpca parameter file (key params verified against the EIGENSOFT POPGEN README):
# numoutevec: 20 # PCs to output (default 10)
# numoutlieriter: 5 # outlier-removal iterations (default 5; 0 disables)
# outliersigmathresh: 6.0 # SD threshold for outlier removal (default 6.0)
# lsqproject: YES # least-squares projection, robust to missing data (aDNA standard)
# poplistname: ref_pops.txt # which populations build the axes (others are projected)
# altnormstyle: NO # NO = Price 2006 EIGENSTRAT normalization; YES = Patterson 2006
smartpca -p smartpca.par
# Tracy-Widom test on the eigenvalues: only PCs with p < ~0.05 plus a scree elbow are interpretable.
twstats -t twtable -i out.eval -o out.tw
Projected scores shrink toward the origin, worse with a small reference panel (<~5000) and more missing data, so an ancient sample plotting "between" two clusters may be shrunk, not admixed; lsqproject is the standard fix. plink2 projects via --pca allele-wts then --score on the .eigenvec.allele weights, but does not correct shrinkage.
Goal: Estimate discrete ancestry proportions while treating K as a model-selection choice, not a discovery.
Approach: Run ADMIXTURE on LD-pruned data across a span of K with cross-validation, plot CV error as a guide, and check Q stability across seeds before interpreting any single K.
# admixture is a standalone CLI; --cv defaults to 5-fold. -jN threads, -BN bootstrap SEs.
for K in $(seq 2 8); do
admixture --cv -j4 data_pruned.bed "$K" 2>&1 | tee "log_K${K}.out"
done
# Outputs per K: data_pruned.K.Q (N x K ancestry weights) and data_pruned.K.P (cluster frequencies).
# CV error prints as: CV error (K=3): 0.512 -- a guide, never "the true number of populations".
grep -h "CV error" log_K*.out
Supervised mode (admixture --supervised data_pruned.bed K, reading data_pruned.pop with one label per individual, blank for unknowns) fixes labeled individuals to their population but assumes the reference populations are themselves unadmixed. Replicate Q at the same K can land in different local optima; align cluster labels across runs with CLUMPP or pong before averaging or plotting, and treat unstable Q as a sign of mis-specified K, not noise to smooth away.
Goal: Estimate pairwise differentiation without the average-of-ratios bias.
Approach: Compute per-SNP Hudson numerators and denominators, keep negative numerators, then divide summed numerators by summed denominators across SNPs.
import allel
import numpy as np
# ac1, ac2 are AlleleCountsArrays for the two populations at the same SNPs (see scikit-allel-analysis).
num, den = allel.hudson_fst(ac1, ac2) # per-SNP Hudson numerator and denominator (Bhatia 2013)
fst = num.sum() / den.sum() # RATIO OF AVERAGES across SNPs; never np.mean(num/den)
# Negative per-SNP numerators are normal sampling behavior near FST=0 and stay in the sum.
# Use Weir-Cockerham only with balanced n: allel.weir_cockerham_fst returns a, b, c variance components.
a, b, c = allel.weir_cockerham_fst(genotype_array, subpops)
fst_wc = np.nansum(a) / np.nansum(a + b + c) # still a ratio of averages
The Hudson estimator is preferred under sample-size asymmetry because Weir-Cockerham's finite-sample correction makes it sensitive to n and to the number of populations; the two can disagree enough to cross a Wright differentiation band. SNP-array ascertainment compresses and warps FST relative to whole-genome sequencing, so cross-study comparisons require matched ascertainment.
Goal: Test admixture and gene flow with honest standard errors.
Approach: Run f3/f4/D through AdmixTools (wrapped by admixr) so each statistic carries a block-jackknife SE, and read Z, never a raw point estimate.
library(admixr)
# admixr wraps AdmixTools (Petr 2019); each call returns the statistic with a block-jackknife SE and Z.
data <- eigenstrat('prefix')
res_f3 <- f3(A = 'PopA', B = 'PopB', C = 'PopC', data = data) # f3(C; A,B) < 0 with Z < -3 proves C admixed
res_d <- d(W = 'PopW', X = 'PopX', Y = 'PopY', Z = 'PopZ', data = data) # |Z| > 3 indicates gene flow
A significantly negative f3 proves the target is admixed (no tree produces a negative f3), but a non-negative f3 is inconclusive, not proof of a clean tree: post-admixture drift in the target itself (a bottleneck after the admixture event), or heavily drifted sources, adds a positive term that can mask the negative cross-product even when admixture is real. A nonzero D or f4 is evidence of a tree violation, not specifically recent introgression: symmetric ancient structure mimics the same ABBA/BABA asymmetry (Durand 2011), so separating them needs admixture-LD decay or explicit modeling.
Trigger: PCA on LD-pruned data with MHC/8p23/17q21.31/LCT regions still in. Mechanism: megabase-long LD in inversions survives --indep-pairwise and dominates a PC. Symptom: a PC loads almost entirely on one chromosome arm and splits samples by karyotype. Fix: --exclude range the long-range-LD and inversion regions by coordinate before PCA.
Trigger: computing PCs before relatedness pruning. Mechanism: a cluster of relatives forms a high-covariance bundle. Symptom: a tight outlier cluster on a top PC that is not a real population. Fix: --king-cutoff 0.0884 first, build PCs on the unrelated set, project relatives back.
Trigger: projecting new/ancient/low-coverage samples onto reference PCs naively. Mechanism: projected scores are biased toward the origin, worse with small panels and missing data. Symptom: a sample plots "between" two clusters and is narrated as admixed. Fix: smartpca lsqproject: YES; do not interpret shrunk scores as intermediacy.
Trigger: picking K at the CV argmin when the curve plateaus or keeps falling. Mechanism: CV error is prediction accuracy, not a population count. Symptom: uninterpretable extra clusters at high K. Fix: run K across a span with at least 10 seeds each (-s), align replicates with pong/CLUMPP, and report the K where CV error plateaus AND Q is seed-stable; if CV and stability disagree, present both and let sampling design plus orthogonal evidence pick the interpreted K.
Trigger: averaging Q across runs or seeds without alignment. Mechanism: cluster labels permute arbitrarily between runs. Symptom: a smeared, meaningless mean barplot. Fix: align labels with CLUMPP or pong before averaging; treat unstable Q as a mis-specification warning.
Trigger: combining per-SNP FST as mean(num/den). Mechanism: low-MAF SNPs have tiny denominators and dominate the mean. Symptom: badly biased genome-wide FST. Fix: ratio of averages, num.sum()/den.sum(); never clamp negative per-SNP values first.
Trigger: naive SNP-level standard errors for f3/f4/D. Mechanism: LD correlates neighboring SNPs, so per-SNP SEs are far too small. Symptom: everything looks significant. Fix: block-jackknife SE (drop ~5 cM blocks); report Z with |Z| > 3.
Trigger: running K-cluster ADMIXTURE on an isolation-by-distance continuum. Mechanism: smooth clines have no discrete demes. Symptom: phantom populations and "admixed" intermediates that are really IBD. Fix: describe clinal structure with EEMS / Mantel, not a STRUCTURE barplot.
| Quantity | Threshold | Source / rationale |
|----------|-----------|--------------------|
| LD pruning for PCA/ADMIXTURE | --indep-pairwise 50 5 0.1 (range 0.05-0.2) | near-independence so structure is not double-counted (see linkage-disequilibrium) |
| MAF floor before PCA | drop MAF < ~0.01 (often < 0.05) | plink2 docs: very-low-MAF variants destabilize PCA |
| Relatedness removal | KING --king-cutoff 0.0884 (2nd-degree) | KING boundaries 0.354/0.177/0.0884/0.0442 = MZ/1st/2nd/3rd |
| PC significance | Tracy-Widom p < 0.05 plus scree elbow | Patterson 2006; TW null for the largest eigenvalue |
| smartpca outlier removal | outliersigmathresh 6.0, numoutlieriter 5 | EIGENSOFT defaults |
| ADMIXTURE CV | default 5-fold; --cv=10 for stability | Alexander/Shringarpure manual |
| f3/f4/D significance | |Z| > 3 (block jackknife) | Patterson 2012 convention (~3 SE) |
| FST bands (Wright guideposts) | 0-0.05 little, 0.05-0.15 moderate, 0.15-0.25 great, >0.25 very great | heuristic only; estimator-, MAF-, ascertainment-dependent |
| Negative per-SNP FST | keep (do not clamp) | unbiased estimators yield negatives near FST=0 by sampling |
Thresholds are conventions, not laws; the FST bands predate SNP arrays, and the estimator, MAF spectrum, ascertainment, and sample size each move FST by a whole band. Verify current best practice before applying numbers blindly.
| Error / symptom | Cause | Solution |
|-----------------|-------|----------|
| import admixture fails | ADMIXTURE is a CLI, not a package | run admixture --cv data.bed K on the shell |
| CV argmin reported as the true K | reading CV error as a population count | present a K span; CV is a guide, check Q stability |
| A PC tracks one chromosome arm | inversion/long-range-LD region left in | --exclude range MHC/8p23/17q21.31/LCT before PCA |
| Outlier cluster is "a new population" | relatives not removed before PCA | --king-cutoff 0.0884 first, project relatives back |
| Genome-wide FST is biased high | average-of-ratios and/or clamped negatives | ratio of averages; keep negative per-SNP values |
| WC and Hudson FST disagree | unequal sample sizes between populations | prefer Hudson under sample-size asymmetry (Bhatia 2013) |
| Everything is f3/D-significant | naive (non-jackknife) standard errors | use block-jackknife SE; report Z, |Z| > 3 |
| Non-negative f3 read as "no admixture" | post-admixture drift in the target (or drifted sources) masks the negative term | non-negative f3 is inconclusive, not a clean tree |
| Smeared mean Q barplot | label switching across replicates | align with CLUMPP/pong before averaging |
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.