Install any skill in seconds. Free to start, no credit card required.
Get Started Free →GWAS and population genetics tool. Processes PLINK (.bed/.bim/.fam), VCF, and BGEN; runs QC (MAF, HWE, missingness), IBD estimation, PCA, and linear/logistic regression GWAS. Outputs Manhattan-ready summary stats. Use regenie or SAIGE for biobanks (>100k samples) needing mixed models.
.claude/skills/jaechang-hits-plink2-gwas-analysis/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 177% | 0% |
| case-11 | ✗→✓ | ▲ Improved | -16% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 589% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 119% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 172% | 0% |
PLINK2 is the high-performance successor to PLINK 1.9, designed for genome-wide association studies (GWAS) and population genetics analysis on large cohorts. It processes genotype data in PLINK binary format (.bed/.bim/.fam), VCF, and BGEN formats — performing sample and variant quality control (QC), kinship estimation, principal component analysis (PCA), and linear/logistic regression association testing. PLINK2 is 10–100× faster than PLINK 1.9 on most tasks due to multithreading and optimized I/O. Output files are compatible with downstream visualization (Manhattan/QQ plots) and meta-analysis tools.
> Check before installing: The tool may already be available (e.g., inside a pixi / conda env). Always run command -v plink2 first and skip the install block if it returns a path. When executing tools inside a pixi project, prefer pixi run <tool> over plain <tool>.
bash# Skip install if already present if command -v plink2 >/dev/null 2>&1; then echo "plink2 already installed: $(plink2 --version)" else # Download PLINK2 pre-compiled binary (Linux) wget https://s3.amazonaws.com/plink2-assets/alpha6/plink2_linux_avx2_20241112.zip unzip plink2_linux_avx2_20241112.zip chmod +x plink2 export PATH="$PWD:$PATH" # macOS # wget https://s3.amazonaws.com/plink2-assets/alpha6/plink2_mac_20241112.zip # unzip plink2_mac_20241112.zip plink2 --version # PLINK v2.00a6LM fi # Python for downstream analysis pip install pandas numpy matplotlib scipy
bash# Run GWAS: linear regression for quantitative trait plink2 \ --bfile cohort_qc \ --pheno phenotypes.txt \ --covar covariates.txt \ --linear hide-covar \ --out results/gwas_result \ --threads 8 # View top hits head results/gwas_result.*.glm.linear | sort -k12,12g | head -20
Convert input genotype data to PLINK binary format for fast processing.
bash# Convert VCF to PLINK binary plink2 \ --vcf cohort_genotyped.vcf.gz \ --make-bed \ --out cohort_plink \ --threads 8 # Convert BGEN (imputed data from Michigan/TopMed imputation server) plink2 \ --bgen cohort_imputed.bgen ref-first \ --sample cohort_imputed.sample \ --make-bed \ --out cohort_imputed_plink \ --threads 8 echo "Files created:" ls -lh cohort_plink.{bed,bim,fam} echo "Samples: $(wc -l < cohort_plink.fam)" echo "Variants: $(wc -l < cohort_plink.bim)"
Remove samples with high missingness or heterozygosity outliers.
bash# Compute per-sample and per-variant missingness plink2 \ --bfile cohort_plink \ --missing \ --out qc/sample_missingness \ --threads 8 # Remove samples with > 2% missingness and variants with > 5% missing plink2 \ --bfile cohort_plink \ --mind 0.02 \ --geno 0.05 \ --make-bed \ --out cohort_sample_qc \ --threads 8 echo "After sample QC:" echo "Samples: $(wc -l < cohort_sample_qc.fam)" echo "Variants: $(wc -l < cohort_sample_qc.bim)"
Filter variants by minor allele frequency, Hardy-Weinberg equilibrium, and imputation quality.
bash# Variant QC: MAF, HWE, missingness plink2 \ --bfile cohort_sample_qc \ --maf 0.01 \ --hwe 1e-6 \ --geno 0.05 \ --make-bed \ --out cohort_variantqc \ --threads 8 echo "After variant QC:" echo "Variants remaining: $(wc -l < cohort_variantqc.bim)" # For imputed data: also filter by INFO score (using VCF INFO field) # plink2 --bfile cohort_variantqc --var-min-qual 0.8 --make-bed --out cohort_info_qc
Compute principal components from LD-pruned variants.
bash# LD pruning: remove one variant from each pair with r² > 0.2 plink2 \ --bfile cohort_variantqc \ --indep-pairwise 50 5 0.2 \ --out qc/ld_pruned \ --threads 8 # Compute PCA on LD-pruned variants (top 20 PCs) plink2 \ --bfile cohort_variantqc \ --extract qc/ld_pruned.prune.in \ --pca 20 \ --out pca/cohort_pca \ --threads 8 echo "PCA files: pca/cohort_pca.eigenvec (PCs) and pca/cohort_pca.eigenval (variance)" head pca/cohort_pca.eigenvec
Perform logistic regression (case-control) or linear regression (quantitative trait).
bash# Case-control GWAS: logistic regression with covariates plink2 \ --bfile cohort_variantqc \ --pheno phenotypes.txt \ --1 \ --covar covariates.txt \ --covar-variance-standardize \ --logistic hide-covar \ --ci 0.95 \ --out results/gwas_cc \ --threads 8 # Quantitative trait GWAS: linear regression plink2 \ --bfile cohort_variantqc \ --pheno phenotypes.txt \ --covar covariates.txt \ --covar-variance-standardize \ --linear hide-covar \ --ci 0.95 \ --out results/gwas_qt \ --threads 8 echo "GWAS complete. Files: results/gwas_*.glm.*" wc -l results/gwas_qt.*.glm.linear
Visualize GWAS results with Python.
pythonimport pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import chi2 # Load GWAS results (PLINK2 linear output) df = pd.read_csv("results/gwas_qt.PHENO1.glm.linear", sep="\t", usecols=["#CHROM", "POS", "ID", "P"]) df.columns = ["CHR", "POS", "SNP", "P"] df = df.dropna(subset=["P"]) df["P"] = pd.to_numeric(df["P"], errors="coerce") df = df[df["P"] > 0].copy() df["-log10P"] = -np.log10(df["P"]) print(f"Variants: {len(df)}") print(f"Genome-wide significant (p<5e-8): {(df['P'] < 5e-8).sum()}") # Manhattan plot fig, ax = plt.subplots(figsize=(14, 4)) colors = plt.cm.Set1.colors chrom_pos = 0 ticks = [] for chrom, group in df.groupby("CHR", sort=False): col = colors[int(chrom) % 2] if str(chrom).isdigit() else "gray" ax.scatter(group["POS"] + chrom_pos, group["-log10P"], c=[col], s=2, alpha=0.7) ticks.append((chrom_pos + group["POS"].mean(), str(chrom))) chrom_pos += group["POS"].max() + 1e7 ax.axhline(-np.log10(5e-8), color="red", linestyle="--", lw=1, label="p=5×10⁻⁸") ax.set_xticks([t[0] for t in ticks[::2]]) ax.set_xticklabels([t[1] for t in ticks[::2]], fontsize=6) ax.set_xlabel("Chromosome") ax.set_ylabel("-log₁₀(p)") ax.set_title("GWAS Manhattan Plot") plt.tight_layout() plt.savefig("manhattan.png", dpi=150) print("Saved: manhattan.png")
| Parameter | Default | Range/Options | Effect | |-----------|---------|---------------|--------| | --maf | — | 0–0.5 | Minor allele frequency threshold; 0.01 removes singletons | | --hwe | — | 0–1 | Hardy-Weinberg equilibrium p-value threshold; 1e-6 typical | | --geno | — | 0–1 | Maximum variant missingness; 0.05 = 5% | | --mind | — | 0–1 | Maximum sample missingness; 0.02 = 2% | | --linear / --logistic | — | flag | Regression mode: linear for quantitative, logistic for case-control | | --covar | — | file path | Covariate file (FID IID COV1 COV2...); add PCs here | | --pca | — | integer | Number of PCs to compute from LD-pruned data | | --indep-pairwise | — | window step r² | LD pruning: window size (variants), step, r² threshold | | --threads | 1 | 1–64 | CPU threads; 8–16 typical for cluster | | --ci | — | 0–1 | Confidence interval for effect size output (0.95 for 95% CI) | | --1 | off | flag | Recode phenotype: 1=control, 2=case → 0=control, 1=case (logistic) |
bash# Compute kinship coefficients (King-robust estimator) plink2 \ --bfile cohort_variantqc \ --extract qc/ld_pruned.prune.in \ --king-cutoff 0.0625 \ --out qc/kinship \ --threads 8 # Remove related individuals (king-cutoff auto-generates exclusion list) plink2 \ --bfile cohort_variantqc \ --remove qc/kinship.king.cutoff.out.id \ --make-bed \ --out cohort_unrelated \ --threads 8 echo "After relatedness QC: $(wc -l < cohort_unrelated.fam) samples"
pythonimport pandas as pd import numpy as np def load_gwas_results(filepath: str, p_threshold: float = 5e-8) -> pd.DataFrame: """Load PLINK2 GWAS linear/logistic output and return genome-wide significant hits.""" df = pd.read_csv(filepath, sep="\t", usecols=["#CHROM", "POS", "ID", "REF", "ALT", "A1", "BETA", "SE", "P"]) df.columns = ["CHR", "POS", "SNP", "REF", "ALT", "A1", "BETA", "SE", "P"] df = df.dropna(subset=["P"]) df["P"] = pd.to_numeric(df["P"], errors="coerce") sig = df[df["P"] < p_threshold].sort_values("P") return sig # Load results for one phenotype hits = load_gwas_results("results/gwas_qt.PHENO1.glm.linear") print(f"Genome-wide significant loci: {len(hits)}") print(hits.head(10)[["CHR", "POS", "SNP", "BETA", "SE", "P"]]) hits.to_csv("gwas_top_hits.tsv", sep="\t", index=False)
| Output | Format | Description | |--------|--------|-------------| | *.glm.linear | TSV | Linear GWAS results: CHR, POS, ID, BETA, SE, P per variant | | *.glm.logistic.hybrid | TSV | Logistic GWAS results: OR, 95% CI, P per variant | | *.eigenvec | TSV | PCA loadings: FID IID PC1 PC2 ... PC20 | | *.eigenval | Text | Eigenvalues for PCA variance explained | | *.king.cutoff.out.id | TSV | Sample IDs to remove for relatedness QC | | *.bed/.bim/.fam | Binary | PLINK binary format after QC filtering |
| Problem | Cause | Solution | |---------|-------|----------| | Error: No samples left after --mind filter | Missingness threshold too strict | Relax to --mind 0.05; check input data quality | | Logistic regression fails to converge | Rare variant, few cases, or collinear covariates | Use --logistic firth for Firth regression; remove correlated covariates | | PCA shows clear outlier cluster | Admixed population or sample contamination | Remove outliers using PC cutoffs; check ancestry with 1000 Genomes reference panel | | --hwe removes too many variants | Population structure inflating HWE test | Apply HWE filter to controls only: --hwe 1e-6 ctrl-only | | BGEN import fails | Wrong ref-first/ref-last flag | Check imputation server documentation; try --bgen file.bgen ref-last | | Very slow association test | Too many covariates or large file | Pre-filter to GWAS-significant window; use --threads 16 | | Sex mismatch warnings | X chromosome heterozygosity outside sex-specific thresholds | Run --check-sex and remove F-statistic outliers | | Memory error on large dataset | RAM insufficient for 500k+ variants | Use --memory 32000 to cap RAM; split by chromosome |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 16,097 | 26,047 | +62% | 1 | 1 | 0% | 2,741 | 5,991 | +119% | 0 | 0 | — |
case-02 | pass→pass | 12,640 | 9,785 | -23% | 1 | 1 | 0% | 2,111 | 5,748 | +172% | 0 | 0 | — |
case-03 | pass→pass | 13,174 | 12,707 | -4% | 1 | 1 | 0% | 2,238 | 6,259 | +180% | 0 | 0 | — |
case-04 | pass→pass | 3,900 | 2,862 | -27% | 1 | 1 | 0% | 703 | 4,479 | +537% | 0 | 0 | — |
case-05 | pass→pass | 7,610 | 3,797 | -50% | 1 | 1 | 0% | 1,372 | 4,533 | +230% | 0 | 0 | — |
case-06 | pass→pass | 25,661 | 2,953 | -88% | 1 | 1 | 0% | 4,236 | 4,384 | +3% | 0 | 0 | — |
case-07 | fail→pass | 9,663 | 2,210 | -77% | 1 | 1 | 0% | 1,547 | 4,288 | +177% | 0 | 0 | — |
case-08 | pass→pass | 3,982 | 2,405 | -40% | 1 | 1 | 0% | 564 | 4,313 | +665% | 0 | 0 | — |
case-09 | pass→pass | 6,591 | 3,132 | -52% | 1 | 1 | 0% | 1,041 | 4,458 | +328% | 0 | 0 | — |
case-10 | pass→pass | 5,112 | 4,145 | -19% | 1 | 1 | 0% | 851 | 4,653 | +447% | 0 | 0 | — |
case-11 | fail→pass | 30,160 | 4,161 | -86% | 1 | 1 | 0% | 5,459 | 4,601 | -16% | 0 | 0 | — |
case-12 | pass→pass | 8,869 | 2,261 | -75% | 1 | 1 | 0% | 1,451 | 4,261 | +194% | 0 | 0 | — |
case-13 | pass→pass | 3,947 | 1,748 | -56% | 1 | 1 | 0% | 614 | 4,228 | +589% | 0 | 0 | — |
case-14 | pass→pass | 7,442 | 2,376 | -68% | 1 | 1 | 0% | 1,446 | 4,262 | +195% | 0 | 0 | — |
case-15 | pass→pass | 3,925 | 2,805 | -29% | 1 | 1 | 0% | 655 | 4,352 | +564% | 0 | 0 | — |
case-16 | pass→pass | 4,011 | 2,363 | -41% | 1 | 1 | 0% | 677 | 4,364 | +545% | 0 | 0 | — |
case-17 | pass→pass | 6,457 | 2,483 | -62% | 1 | 1 | 0% | 978 | 4,346 | +344% | 0 | 0 | — |
case-18 | pass→pass | 5,115 | 2,323 | -55% | 1 | 1 | 0% | 844 | 4,306 | +410% | 0 | 0 | — |
case-19 | pass→pass | 8,339 | 4,086 | -51% | 1 | 1 | 0% | 1,410 | 4,643 | +229% | 0 | 0 | — |
case-20 | fail→pass | 3,783 | 3,022 | -20% | 1 | 1 | 0% | 640 | 4,409 | +589% | 0 | 0 | — |
case-21 | pass→pass | 3,256 | 3,038 | -7% | 1 | 1 | 0% | 532 | 4,467 | +740% | 0 | 0 | — |
case-22 | pass→pass | 7,453 | 5,404 | -27% | 1 | 1 | 0% | 1,195 | 4,747 | +297% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.