This repository provides a server-ready workflow for batch quality control of GCTA/GEMMA-style GWAS .mlma summary result files across multiple populations and traits.
It is designed for use cases like:
- thousands of
.mlmaresult files - automated Manhattan plot, QQ plot, lambda GC, P-value sanity checks, and QC flagging
For every GWAS result file, the pipeline writes:
- combined Manhattan + QQ plot
- optional compressed cleaned summary table
- per-trait QC metrics
For the whole batch, it writes:
qc_summary.tsv: one row per GWAS resultqc_fail.tsv: results requiring reviewqc_warn.tsv: borderline resultsqc_pass.tsv: results passing automated checksqc_recommendations.tsv: likely problem pattern and suggested adjustment
project/
gwas-results/
pop1/
pop2/
pop3/
manifest.tsv
gwas-mlma-batch-qc/
The manifest controls which files are analyzed.
Create a tab-separated manifest.tsv with these columns:
population trait file
pop1 height /path/to/gwas-results/pop1/height.mlma
pop2 height /path/to/gwas-results/pop2/height.mlma
pop3 height /path/to/gwas-results/pop3/height.mlma
Optional columns are allowed and preserved in the output, for example sample_size, batch, or note.
An example file is provided at examples/manifest.example.tsv.
The recommended server workflow uses the Python fast script.
conda install -c conda-forge numpy scipy matplotlib polarsOptional GPU acceleration for QQ sorting requires a CUDA-matched CuPy build. Example for CUDA 12:
conda install -c conda-forge cupy cuda-version=12.0If polars is unavailable, the script can still run with a slower Python csv fallback, but installing Polars is strongly recommended.
For whitespace-delimited GCTA .mlma files, the fast script uses numpy.loadtxt(usecols=...) to read only the required columns.
Use Python 3.8 or newer.
GPU note: .mlma text parsing is usually the main bottleneck, so GPU mainly helps the QQ sorting step for very large files. It does not remove the need to read the text files from disk.
On most Linux servers:
Rscript -e 'install.packages(c("data.table", "ggplot2"), repos="https://cloud.r-project.org")'No other R packages are required.
The R script is kept as a stable fallback. For large batches, prefer scripts/gwas_mlma_qc_fast.py.
For large batches, use the three-step workflow below. It is much faster than drawing Manhattan plots for every .mlma file.
This reads each .mlma file, calculates QC metrics, and does not draw plots.
Full run:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step1_metrics \
--threads 8 \
--mode metricsPartial run by population:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step1_metrics_C4 \
--threads 4 \
--mode metrics \
--only-pop C4Partial run for a quick first-N test:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step1_metrics_first4 \
--threads 4 \
--mode metrics \
--limit 4For pilot tests on sampled .mlma files, use --assessment-mode sample. This still calculates lambda GC, minimum P value, valid P count, invalid P count, and significant hit count, but it skips final thresholds that require the complete file, such as total SNP count and excess significant hits.
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest_sample100k.tsv \
--outdir qc_sample100k_metrics \
--threads 4 \
--mode metrics \
--assessment-mode sampleTo calculate metrics only and avoid all PASS/WARN/FAIL classification:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest_sample100k.tsv \
--outdir qc_sample100k_calculate \
--threads 4 \
--mode metrics \
--assessment-mode calculateMain outputs:
qc_summary.tsvqc_pass.tsvqc_warn.tsvqc_fail.tsvqc_calculated.tsvqc_quality_solutions.tsv
This draws QQ plots for all manifest rows and classifies the QQ shape.
Full run:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step2_qq \
--threads 4 \
--mode qq \
--qq-max-points 100000 \
--qq-confidence 0.95GPU QQ sorting, if CuPy is installed:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step2_qq_gpu \
--threads 2 \
--mode qq \
--qq-engine gpu \
--qq-max-points 100000Use fewer threads with GPU mode so multiple processes do not fight for the same GPU memory.
When a server has 2 GPUs, split the manifest into two files and bind one Python process to each GPU. This is usually more stable than using many threads with one process.
Create split manifests:
mkdir -p QC_02_2026_results/gpu_split
head -n 1 QC_02_2026_results/manifest.tsv > QC_02_2026_results/gpu_split/manifest_gpu0.tsv
head -n 1 QC_02_2026_results/manifest.tsv > QC_02_2026_results/gpu_split/manifest_gpu1.tsv
awk 'NR==1{next} NR%2==0{print >> "QC_02_2026_results/gpu_split/manifest_gpu0.tsv"} NR%2==1{print >> "QC_02_2026_results/gpu_split/manifest_gpu1.tsv"}' \
QC_02_2026_results/manifest.tsvTest each GPU with a small subset first:
CUDA_VISIBLE_DEVICES=0 python GWAS_QC/scripts/gwas_mlma_qc_fast.py \
--manifest QC_02_2026_results/gpu_split/manifest_gpu0.tsv \
--outdir QC_02_2026_results/test_gpu0_qq \
--threads 1 \
--mode qq \
--assessment-mode full \
--qq-engine gpu \
--qq-max-points 100000 \
--limit 4CUDA_VISIBLE_DEVICES=1 python GWAS_QC/scripts/gwas_mlma_qc_fast.py \
--manifest QC_02_2026_results/gpu_split/manifest_gpu1.tsv \
--outdir QC_02_2026_results/test_gpu1_qq \
--threads 1 \
--mode qq \
--assessment-mode full \
--qq-engine gpu \
--qq-max-points 100000 \
--limit 4Run the full QQ workflow on both GPUs:
mkdir -p QC_02_2026_results/logs
CUDA_VISIBLE_DEVICES=0 python GWAS_QC/scripts/gwas_mlma_qc_fast.py \
--manifest QC_02_2026_results/gpu_split/manifest_gpu0.tsv \
--outdir QC_02_2026_results/step2_qq_gpu0 \
--threads 1 \
--mode qq \
--assessment-mode full \
--qq-engine gpu \
--qq-max-points 100000 \
> QC_02_2026_results/logs/step2_qq_gpu0.log 2>&1 &
CUDA_VISIBLE_DEVICES=1 python GWAS_QC/scripts/gwas_mlma_qc_fast.py \
--manifest QC_02_2026_results/gpu_split/manifest_gpu1.tsv \
--outdir QC_02_2026_results/step2_qq_gpu1 \
--threads 1 \
--mode qq \
--assessment-mode full \
--qq-engine gpu \
--qq-max-points 100000 \
> QC_02_2026_results/logs/step2_qq_gpu1.log 2>&1 &Check progress:
jobs
tail -f QC_02_2026_results/logs/step2_qq_gpu0.log
tail -f QC_02_2026_results/logs/step2_qq_gpu1.logMerge QQ summary tables after both jobs finish:
mkdir -p QC_02_2026_results/step2_qq_gpu_merged
head -n 1 QC_02_2026_results/step2_qq_gpu0/qq_shape_summary.tsv \
> QC_02_2026_results/step2_qq_gpu_merged/qq_shape_summary.tsv
tail -n +2 QC_02_2026_results/step2_qq_gpu0/qq_shape_summary.tsv \
>> QC_02_2026_results/step2_qq_gpu_merged/qq_shape_summary.tsv
tail -n +2 QC_02_2026_results/step2_qq_gpu1/qq_shape_summary.tsv \
>> QC_02_2026_results/step2_qq_gpu_merged/qq_shape_summary.tsv
head -n 1 QC_02_2026_results/step2_qq_gpu0/qc_quality_solutions.tsv \
> QC_02_2026_results/step2_qq_gpu_merged/qc_quality_solutions.tsv
tail -n +2 QC_02_2026_results/step2_qq_gpu0/qc_quality_solutions.tsv \
>> QC_02_2026_results/step2_qq_gpu_merged/qc_quality_solutions.tsv
tail -n +2 QC_02_2026_results/step2_qq_gpu1/qc_quality_solutions.tsv \
>> QC_02_2026_results/step2_qq_gpu_merged/qc_quality_solutions.tsvOptionally collect QQ images into one directory:
mkdir -p QC_02_2026_results/step2_qq_gpu_merged/qq_plots
cp -r QC_02_2026_results/step2_qq_gpu0/qq_plots/* QC_02_2026_results/step2_qq_gpu_merged/qq_plots/
cp -r QC_02_2026_results/step2_qq_gpu1/qq_plots/* QC_02_2026_results/step2_qq_gpu_merged/qq_plots/Recommended resource settings:
- metrics: CPU only,
--threads 4 - QQ GPU: 2 shell jobs, each with
CUDA_VISIBLE_DEVICES=<gpu_id>and--threads 1 - Manhattan: CPU only,
--threads 2to4 - if GPU memory is limited, keep
--qq-max-points 100000
Partial run by population:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step2_qq_C4 \
--threads 4 \
--mode qq \
--only-pop C4Partial run for one trait:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step2_qq_C4_FL1 \
--threads 1 \
--mode qq \
--only-pop C4 \
--only-trait FL1Main outputs:
qq_plots/qq_shape_summary.tsvqc_quality_solutions.tsv
QQ plot style:
- points are downsampled for drawing using
--qq-max-points, but lambda GC and QC metrics use all valid P values - each plot includes a confidence interval ribbon when SciPy is available
- each plot annotates
lambdaGCand valid SNP count
QQ shape classes:
| QQ class | Meaning |
|---|---|
QQ_IDEAL |
QQ body follows the diagonal; acceptable for downstream review |
QQ_GOOD_WITH_TAIL_SIGNAL |
QQ body is acceptable, with strong tail deviation compatible with true loci |
QQ_INFLATED |
global upward deviation; likely structure, relatedness, batch effect, or confounding |
QQ_DEFLATED |
global downward deviation; likely overcorrection or overfitted model |
QQ_NOISY |
irregular curve; check sample size, missingness, convergence, or low-count variants |
After reviewing Step 1 and Step 2, draw Manhattan plots. This mode does not redraw QQ plots.
Full run:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step3_manhattan \
--threads 4 \
--mode manhattan \
--max-plot-points 200000 \
--plot-keep-p 1e-4Partial run by population:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step3_manhattan_C6 \
--threads 4 \
--mode manhattan \
--only-pop C6 \
--max-plot-points 200000 \
--plot-keep-p 1e-4You can also draw one specific population-trait pair:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step3_manhattan_C6_height \
--threads 1 \
--mode manhattan \
--only-pop C6 \
--only-trait height \
--max-plot-points 300000Main outputs:
manhattan_plots/manhattan_plot_summary.tsvqc_summary.tsvqc_quality_solutions.tsv
For faster Manhattan plotting on very large .mlma files, the Python script keeps all points with P <= 1e-4 and samples the remaining points when there are too many:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_step3_manhattan_C6_fastplot \
--threads 4 \
--mode manhattan \
--only-pop C6 \
--max-plot-points 200000 \
--plot-keep-p 1e-4Increase --max-plot-points for publication-style figures. Decrease it for faster visual QC.
Manhattan colors use a colorblind-safe blue/orange palette, with a dark red genome-wide significance line.
The older combined Manhattan + QQ mode is still available if needed:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_legacy_full_C6 \
--threads 4 \
--mode full \
--only-pop C6The R script supports the same three modes:
Rscript scripts/gwas_mlma_qc.R \
--manifest manifest.tsv \
--outdir qc_step1_metrics_R \
--threads 8 \
--mode metricsRscript scripts/gwas_mlma_qc.R \
--manifest manifest.tsv \
--outdir qc_output \
--threads 8 \
--mode fullFor small test runs:
Rscript examples/create_smoke_data.R smoke_data
Rscript scripts/gwas_mlma_qc.R \
--manifest smoke_data/manifest.tsv \
--outdir qc_test \
--threads 2 \
--mode metricsFast Python smoke test:
python examples/create_smoke_data.py smoke_data
python scripts/gwas_mlma_qc_fast.py \
--manifest smoke_data/manifest.tsv \
--outdir qc_test_fast \
--threads 2 \
--mode qqThe script automatically recognizes common .mlma columns:
| Meaning | Accepted names |
|---|---|
| chromosome | Chr, CHR, chrom, chromosome |
| position | bp, BP, pos, position |
| SNP ID | SNP, rs, ID, marker, variant |
| P value | p, P, pval, PVAL, p_value |
| effect | b, BETA, beta, effect |
| standard error | se, SE, stderr |
| allele frequency | Freq, freq, AF, maf, MAF |
If your files use unusual column names, use the explicit options:
python scripts/gwas_mlma_qc_fast.py \
--manifest manifest.tsv \
--outdir qc_output \
--chr-col Chr \
--bp-col bp \
--snp-col SNP \
--p-col pThe defaults are intentionally conservative for sample sizes around 200-540.
| QC status | Rule |
|---|---|
PASS |
no major automated problem detected |
WARN |
borderline lambda GC, few SNPs, many missing P values, or unusual significant count |
FAIL |
severe lambda GC deviation, invalid P values dominate, missing required columns, unreadable file |
Default thresholds:
| Parameter | Default |
|---|---|
| lambda GC fail high | 1.20 |
| lambda GC warn high | 1.10 |
| lambda GC warn low | 0.95 |
| lambda GC fail low | 0.90 |
| minimum SNP count fail | 10000 |
| minimum SNP count warn | 50000 |
| genome-wide significance | 5e-8 |
You can override thresholds:
Rscript scripts/gwas_mlma_qc.R \
--manifest manifest.tsv \
--outdir qc_output \
--mode metrics \
--lambda-warn-high 1.10 \
--lambda-fail-high 1.20 \
--lambda-warn-low 0.95 \
--lambda-fail-low 0.90 \
--min-snps-warn 50000 \
--min-snps-fail 10000| Pattern | Common cause | Recommended adjustment |
|---|---|---|
| QQ plot globally above diagonal, lambda GC high | population structure, relatedness, batch effect, phenotype confounding | add/top up PCA covariates; check GRM/kinship; include batch/sex/age; rerun by population then meta-analyze |
| QQ plot below diagonal, lambda GC low | overcorrection, too many covariates, phenotype residualization too strong | reduce redundant covariates; check kinship and residualization strategy |
| Many significant SNPs across many chromosomes | phenotype or batch artifact | test phenotype against batch/plate/family/sex; inspect outliers; winsorize or transform trait if justified |
| One chromosome or region has broad elevation | map/build mismatch, local genotyping artifact, structural region, low-quality imputation | check genome build, allele coding, INFO/MAF/missingness; rerun excluding problematic low-quality SNPs |
| Very few SNPs or many invalid P values | file truncation, ID mismatch, overly strict filtering, model convergence failures | check input genotype filters, sample ID matching, model logs, and per-file line counts |
| Only small sample population is noisy | low power and unstable estimates | use as sensitivity analysis; prioritize larger groups or meta-analysis |
The file qc_quality_solutions.tsv summarizes the combined quality decision and recommended action.
| Quality class | Interpretation | Main action |
|---|---|---|
PASS_IDEAL |
Numeric QC passes; no major QQ problem detected | Keep for downstream analysis |
PASS_WITH_TAIL_SIGNAL |
QQ body is good with tail signal | Draw full Manhattan + QQ and inspect whether peaks are localized |
WARN_EXCESS_SIGNAL |
Too many significant SNPs | Check phenotype-batch associations and outliers |
WARN_NOISY_QQ |
QQ is irregular | Check sample size, missingness, low MAC variants, and convergence |
FAIL_OR_WARN_INFLATION |
Lambda/QQ inflation | Add/check PCs, kinship, batch covariates, and population-specific analysis |
FAIL_OR_WARN_DEFLATION |
Lambda/QQ deflation | Check overcorrection, residualization, and overfitted mixed model |
WARN_DATA_COMPLETENESS |
Fewer SNPs or more invalid P values than expected | Check filters and logs; may still be usable after review |
FAIL_DATA_COMPLETENESS |
Too few valid SNPs or many invalid P values | Check file completeness, SNP filters, imputation/MAF thresholds, and model logs |
ERROR_INPUT |
File or column problem | Check path, delimiter, and required MLMA columns |
REVIEW |
Borderline mixed signals | Review QQ plus full Manhattan plot |
- Run batch QC for all files.
- Open
qc_summary.tsvand sort bystatus,lambda_gc,n_significant, andn_valid_p. - Review only
FAILandWARNplots first. - Fix upstream model/filtering problems.
- Rerun the failed subset using a smaller manifest.
- Preserve the first and rerun QC outputs for audit.
Initial upload from a server:
git init
git add README.md scripts config examples .gitignore
git commit -m "Add batch GWAS MLMA QC workflow"
git branch -M main
git remote add origin git@github.com:YOUR_ORG/gwas-mlma-batch-qc.git
git push -u origin mainAfter updates:
git add README.md scripts config examples .gitignore
git commit -m "Update GWAS QC workflow"
git push- This pipeline performs summary-statistics QC. It does not replace genotype-level QC.
- For publication-grade interpretation, inspect cohort design, kinship, phenotype transformation, covariates, and genotype filtering logs.
- If ancestry or population structure differs across the 3 groups, analyze each group separately and combine with meta-analysis rather than forcing a single pooled model.