PLINK2 — GWAS and Population Genetics
Overview
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.
When to Use
- Running GWAS on a case-control or quantitative trait cohort after genotyping array QC
- Performing sample QC: missingness, heterozygosity outliers, sex check, cryptic relatedness
- Computing genome-wide LD pruning for PCA or relatedness estimation
- Running PCA on genotype data to identify population stratification
- Converting between PLINK binary, VCF, and BGEN formats
- Filtering variants by MAF, HWE, missingness, or INFO score in VCF/imputed data
- Use omics-plotting SKILL to render Manhattan and QQ plots from the association results
- Use regenie or SAIGE instead for biobank-scale GWAS (>100k samples) requiring mixed model association to control for population structure
- Use VCFtools as an alternative for VCF-specific population genetics statistics
Prerequisites
- Software: PLINK2 (pre-compiled binary; no pip/conda package)
- Input: PLINK binary files (.bed/.bim/.fam) or VCF/BGEN from array genotyping or imputation
Check before installing: The tool may already be available (e.g., inside a
pixi/condaenv). Always runcommand -v plink2first and skip the install block if it returns a path. When executing tools inside a pixi project, preferpixi 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
Pre-flight Interview
Settle these with the user before writing any analysis code.
yamldecisions: - id: D1 param: phenotype kind: required source: data ask: "Which column holds the trait being tested, and is it a measurement or a case/control status?" default: null - id: D2 param: regressionMode kind: derived source: data depends_on: [D1] ask: "Should the association test be linear or logistic?" default: "linear for a quantitative trait, logistic for case/control" - id: D3 param: qcThresholds kind: required source: user ask: "Where should variants and samples be cut for allele frequency, missingness, and Hardy-Weinberg departure?" default: "MAF 0.01, variant missingness 0.05, sample missingness 0.02, HWE 1e-6" - id: D4 param: covariates kind: required source: data ask: "Which covariates belong in the model - age, sex, batch, study site?" default: null - id: D5 param: populationStructure kind: required source: user ask: "How many principal components should be included to absorb ancestry differences?" default: "10, computed from LD-pruned variants" - id: D6 param: ldPruning kind: optional source: user depends_on: [D5] ask: "Which window and correlation threshold should thin variants before computing the components?" default: "50 variant window, step 5, r-squared 0.2" - id: D7 param: threads kind: never_ask source: data reason: "Affects runtime only, not the association statistics" default: "min(8, available_cores)"
D5 hangs on D4 because the components are themselves covariates - how many to include is a question about the covariate set, not a separate knob. Omitting them entirely is the classic way to produce a genome-wide significant result that reflects ancestry rather than the trait.
Quick Start
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
Workflow
Step 1: Convert VCF/Imputed Data to PLINK Binary Format
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)"
Step 2: Sample QC — Missingness and Heterozygosity
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)"
Step 3: Variant QC — MAF, HWE, and INFO Score Filtering
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
Step 4: LD Pruning and PCA for Population Stratification
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
Step 5: Run GWAS Association Analysis
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
Step 6: Plot Manhattan and QQ Plots
Prepare the association results as a CHR/BP/P table, then read skills/data-visualization/omics-plotting/SKILL.md and follow its "Manhattan" and "QQ plot" recipes on the exported CSV (→ figures/manhattan.png, figures/qq.png).
pythonimport pandas as pd # Load GWAS results (PLINK2 linear output) as a CHR/BP/P table for plotting df = pd.read_csv("results/gwas_qt.PHENO1.glm.linear", sep="\t", usecols=["#CHROM", "POS", "ID", "P"]) df.columns = ["CHR", "BP", "SNP", "P"] df = df.dropna(subset=["P"]) df["P"] = pd.to_numeric(df["P"], errors="coerce") df = df[df["P"] > 0].copy() df.to_csv("gwas.csv", index=False) print(f"Variants: {len(df)}") print(f"Genome-wide significant (p<5e-8): {(df['P'] < 5e-8).sum()}") # Render with the omics-plotting SKILL (`skills/data-visualization/omics-plotting/SKILL.md`) "Manhattan" and "QQ plot" recipes (input columns CHR, BP, P).
Key Parameters
| 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) |
Common Recipes
Recipe 1: Relatedness QC and Remove One from Each Related Pair
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"
Recipe 2: Parse GWAS Results and Report Top Hits
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)
Expected Outputs
| 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 |
Troubleshooting
| 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 |
References
- PLINK2 documentation — official command reference and tutorials
- PLINK2 GitHub: chrchang/plink-ng — source code and releases
- Chang CC et al. (2015) "Second-generation PLINK: rising to the challenge of larger and richer datasets" — GigaScience 4:7. DOI:10.1186/s13742-015-0047-8
- GWAS best practices (Marees et al. 2018) — comprehensive QC and analysis guide


