Mendelian randomization / TwoSampleMR

How to do Mendelian randomization: from GWAS data to sensitivity analyses

This page is for medical graduate students writing two-sample MR papers with TwoSampleMR. It follows the order of the analysis: how to argue the three core assumptions, where to get GWAS data, how to select instruments, how to harmonise alleles, how to run and interpret the main and sensitivity analyses, and what reviewers usually ask. Every command was run locally on public data (LDL-C to coronary disease); software versions and database status were checked on 2026-10-10.

Short answer

The standard two-sample Mendelian randomization workflow is: take SNPs with P<5×10⁻⁸ from the exposure GWAS, LD-clump them at r²=0.001 and 10,000 kb (locally with plink and a 1000 Genomes reference panel when not using OpenGWAS), align effect alleles and handle palindromic SNPs with harmonise_data, use multiplicative random-effects IVW as the main analysis, check pleiotropy with MR-Egger, weighted median, weighted mode and MR-PRESSO, report heterogeneity and direction with Cochran's Q, leave-one-out and the Steiger test, and argue the three core assumptions as STROBE-MR requires. Since May 2024 OpenGWAS requires a JWT token valid for 14 days; summary statistics from GWAS Catalog, FinnGen and Pan-UKB can be downloaded directly and analysed entirely on your own machine.

Workflow

Functions and defaults were checked against TwoSampleMR 0.7.12 (released 2026-10-08) and ieugwasr 1.2.0.

StepTwoSampleMR / toolKey parameters (defaults)Report in the paper
1 Exposure instrumentsextract_instruments (token required); offline: format_datap1 = 5e-8GWAS source, sample size, ancestry, threshold and rationale
2 LD clumpingclump_data / ieugwasr::ld_clumpclump_r2 = 0.001, clump_kb = 10000, pop = "EUR"Reference panel and population; SNP counts before and after clumping
3 Instrument strengthCompute F and R² yourselfF = (β/SE)²F per SNP, total R²
4 Outcome dataextract_outcome_data (token required); offline: format_dataproxies = TRUE, rsq = 0.8, maf_threshold = 0.3Missing SNPs, proxy SNPs
5 Allele harmonisationharmonise_dataaction = 2 (palindromic SNPs with EAF 0.42–0.58 removed)Switched and removed SNP counts from attr(dat, "log")
6 Main and sensitivity analysesmr, mr_heterogeneity, mr_pleiotropy_test, mr_leaveoneout, directionality_test, MRPRESSO::mr_pressoIVW uses multiplicative random effects; MR-PRESSO NbDistribution = 1000Estimates, ORs and 95% CIs for each method; Q, intercept, PRESSO global test
7 Plotsmr_scatter_plot, mr_forest_plot, mr_leaveoneout_plot, mr_funnel_plot—Scatter plot, single-SNP forest plot, leave-one-out plot, funnel plot

Core assumptions

Only relevance can be tested directly. Independence and exclusion restriction can be checked only in part, and the paper has to argue them with sensitivity analyses and biological knowledge. STROBE-MR item 5 asks for all three to be stated explicitly in the methods.

Of the 20 STROBE-MR items (JAMA 2021 statement and BMJ explanation), reviewers most often check: item 5, stating the three core assumptions; item 7, how the assumptions were assessed; item 9, software, package versions and settings, and whether the protocol was preregistered; item 10d, for two-sample MR, the similarity of the exposure and outcome samples and the number of overlapping participants; item 11, results on an interpretable scale (for example OR per 1 SD); and item 13, sensitivity analyses and tests of direction. Writing item by item avoids most comments about incomplete methods.

The 2023 update of the Burgess et al. MR guidelines also recommends a positive control outcome: an outcome for which the causal effect is established, used to check that the instruments reproduce it. The LDL-C to coronary disease analysis on this page is itself a recognised positive control and is a good way to check that a pipeline works.

AssumptionMeaningAvailable checksHow to argue it in the paper
RelevanceInstruments are strongly associated with the exposureP<5e-8; per-SNP F and total R²; overall FReport R² and F; when the threshold is relaxed, report results at 5e-8 as a sensitivity analysis
IndependenceInstruments are unrelated to confoundersNot directly testable; whether both GWAS adjusted for principal components; same ancestry for both samplesState the ancestry and PC adjustment of both GWAS; for socially patterned exposures consider within-family MR
Exclusion restrictionInstruments affect the outcome only through the exposureMR-Egger intercept, Cochran's Q, MR-PRESSO global and outlier tests, weighted median and mode compared with IVWConsistent direction across methods with different pleiotropy assumptions; stable results after removing outliers or known pleiotropic loci

Data sources

The table reflects access checks on 2026-10-10. In the methods, state for each source: dataset ID or file name, version, ancestry, sample size, genome build and effect allele column.

Direction of bias from sample overlap: without overlap, weak instrument bias pulls the estimate towards zero and does not create false positives; with overlap, the bias points towards the observational association and grows linearly with the overlap fraction (Burgess et al. 2023 guidelines). With F around 30, even complete overlap gives a relative bias of only about 1/F, roughly 3%. The usual way to avoid it is to take the exposure from European consortia or UKB and the outcome from FinnGen; when overlap cannot be avoided, report its extent and correct with methods such as MRlap.

SourceStatus in 2026-10Practical notesCommon mistakes
IEU OpenGWASSince 2024-05-01 most API requests need a JWT token, valid for 14 days; every user tier has 100,000 credits per 10 minutes, new accounts start in Trial and must upgrade to Standard as the account page instructs; website VCF downloads are limited to 20 datasets per 24 hours, with links valid for 2 hours; gwas.mrcieu.ac.uk now redirects to opengwas.io/tophits costs 1 credit per dataset with precomputed clumping and 30 when new clumping is needed; /ld/clump costs 12 per call; /gwasinfo/files costs 50. Estimate credits before batch runs over dozens of exposuresRepeated 429 responses can block the account and IP for up to a week; an expired token returns 401
FinnGenR13 public on 2026-06-02: 500,186 participants, 2,755 endpoints, over 21 million variants; GRCh38; alt is the effect alleleThe official route is a form that returns download instructions; the manifest (finngen_R13_manifest.tsv) lists case and control counts and the https URL of each endpoint. Files are tabix-indexed, so you can fetch only the positions you needfinn-b-* in OpenGWAS is the older R5; Finnish LD differs from other European populations; newer large consortium meta-analyses may include FinnGen, so check overlap when using it as the outcome
UK Biobank (Neale lab round 2)361,194 participants, 4,203 phenotypes; Hail linear regressionBinary phenotypes were also fitted with linear regression, so β is on the probability scale; convert to an approximate logOR before MR: β/[u(1−u)], where u is the case fraction; variant IDs are chr:pos:ref:alt and need variants.tsv.bgz for rsIDs; GRCh37Exposure and outcome both from UKB means complete sample overlap
Pan-UKBResults per ancestry group (EUR, CSA, AFR, EAS, AMR, MID); GRCh37The P column is −log10 P (neglog10_pval_EUR), so read it with log_pval = TRUE; drop variants with low_confidence_EUR = TRUE firstTreating neglog10 values as P values makes every threshold filter wrong
GWAS Catalog summary statisticsDirectly downloadable over FTP; harmonised files are *.h.tsv.gz, GRCh38, with hm_ columns in one orientationPrefer hm_rsid, hm_effect_allele and hm_beta from the harmonised file; column names in original files vary, so read the readme firstMixing hm_beta with the original beta column gives inconsistent directions

OpenGWAS

Since 0.6.0 (2024-04), TwoSampleMR depends on the ieugwasr release with the new authentication system, and since 0.6.20 it requires ieugwasr ≥ 1.1.0. Older versions cannot connect even with a token.

Measured here (ieugwasr 1.2.0): without a token, extract_instruments returns the 401 message in the first row; ld_clump without bfile goes through the API and also needs a token. The variable name IUEUGWAS_TOKEN that circulates in Chinese posts is wrong; ieugwasr only reads OPENGWAS_JWT.

Set and check the tokenr
# 1. Sign in at https://api.opengwas.io/profile and generate a token (valid for 14 days)
# 2. Add it to ~/.Renviron (or .Renviron in the project directory); the variable must be OPENGWAS_JWT
usethis::edit_r_environ()
# Add one line to the opened file: OPENGWAS_JWT=eyJhbGciOi... (no quotes, end with a newline)

# 3. Restart R and check
ieugwasr::get_opengwas_jwt()   # a long string means the token was read
ieugwasr::user()               # account info means the token works; 401 means expired or mis-copied

# 4. The current argument name is opengwas_jwt; access_token from older tutorials no longer exists
exp <- TwoSampleMR::extract_instruments("ieu-a-300", p1 = 5e-8, r2 = 0.001, kb = 10000)
Error messageCauseFix
Status code from OpenGWAS API: 401 … From 1st May 2024 you must provide a token (JWT)No token was read, or the token is older than 14 daysGenerate a new token, put it in .Renviron, restart R and check with user()
unused argument (access_token = NULL)Code copied from tutorials written before 2023; the argument is now opengwas_jwtRemove access_token; the token is read from the environment variable
Error in if (nrow(d) == 0) return(NULL) : argument is of length zeroThe API returned nothing: token failure, wrong dataset ID, or no SNPs at the thresholdRun user() to rule out the token, then check the ID with gwasinfo(id)
429 Too Many RequestsCredits for the 10-minute window are used upWait for the time in the Retry-After header; move batch jobs to local files

Offline data

The offline route suits batch analyses and papers that must be reproducible: data files, reference panel and scripts stay local, and results do not change when the OpenGWAS database is updated.

Found in this page's test: the GLGC 2013 LDL-C file contains three P values below the smallest double (1.24e-652, 6.56e-397, 3.85e-326), so fread reads the whole P column as text. pval < 5e-8 then compares strings: 2,435,913 of 2,437,751 rows "pass", clumping yields 1,837 instruments, and nothing raises an error. Check the P column with class() after reading; as.numeric turns those three values into 0, and format_data then truncates them at min_pval = 1e-200.

When format_data meets duplicated rsIDs it keeps the first row and warns. Multi-allelic sites in files such as FinnGen produce duplicated rsIDs (rs7534572 in this test), so match ref/alt to the exposure alleles before calling format_data.

Read text summary statistics, GWAS Catalog harmonised files and IEU VCFsr
library(TwoSampleMR); library(data.table)   # fread on .gz files also needs R.utils

## A. Exposure: text summary statistics from the authors' site or GWAS Catalog (GLGC 2013 LDL-C)
ldl <- fread("jointGwasMc_LDL.txt.gz")
setnames(ldl, c("P-value", "Freq.A1.1000G.EUR"), c("pval", "eaf"))
class(ldl$pval)              # measured here: "character", because the file contains values such as 1.24e-652
ldl[, pval := as.numeric(pval)]                 # without conversion, pval < 5e-8 is a string comparison
ldl[, `:=`(A1 = toupper(A1), A2 = toupper(A2))] # GLGC alleles are lower case; A1 is the effect allele
exp_dat <- format_data(as.data.frame(ldl[pval < 5e-8]), type = "exposure",
  snp_col = "rsid", beta_col = "beta", se_col = "se", eaf_col = "eaf",
  effect_allele_col = "A1", other_allele_col = "A2", pval_col = "pval", samplesize_col = "N")

## B. Outcome: GWAS Catalog harmonised file (*.h.tsv.gz, GRCh38, hm_ columns share one orientation)
# Keep only instrument rows with awk first: about 80 s for the 370 MB file, so R never reads 8.6 million rows
#   gzip -dc GCST003116.h.tsv.gz | awk -F'\t' 'NR==FNR{a[$1];next} FNR==1 || ($2 in a)' snps.txt - > cad_subset.tsv
cad <- fread("cad_subset.tsv")
out_dat <- format_data(as.data.frame(cad), type = "outcome", snps = exp_dat$SNP,
  snp_col = "hm_rsid", beta_col = "hm_beta", se_col = "standard_error",
  eaf_col = "hm_effect_allele_frequency", effect_allele_col = "hm_effect_allele",
  other_allele_col = "hm_other_allele", pval_col = "p_value", chr_col = "hm_chrom", pos_col = "hm_pos")

## C. IEU OpenGWAS VCF: FORMAT is ES:SE:LP:AF:SS:ID, LP is -log10(P), the effect allele is ALT
#   bcftools query -f '%ID\t%CHROM\t%POS\t%ALT\t%REF\t[%ES]\t[%SE]\t[%LP]\t[%AF]\t[%SS]\n' ieu-a-300.vcf.gz > ieu-a-300.tsv
v <- fread("ieu-a-300.tsv", col.names = c("SNP","chr","pos","effect_allele","other_allele","beta","se","lp","eaf","samplesize"))
exp_vcf <- format_data(as.data.frame(v), type = "exposure", pval_col = "lp", log_pval = TRUE)

## D. Pan-UKB: the P column is neglog10_pval_EUR, so also use log_pval = TRUE; alt is the effect allele, GRCh37
FinnGen: fetch only the instrument positions with remote tabixbash
# FinnGen files are tabix-indexed: fetch only the instrument positions instead of the full 810 MB file
# regions_hg38.tsv has two columns: chromosome (1-23, no "chr") and GRCh38 position,
# e.g. from hm_chrom and hm_pos in the GWAS Catalog harmonised file
tabix -h -R regions_hg38.tsv \
  https://storage.googleapis.com/finngen-public-data-r13/summary_stats/finngen_R13_I9_CHD.gz \
  > finngen_chd_subset.tsv
# Columns: #chrom pos ref alt rsids nearest_genes pval mlogp beta sebeta af_alt ... (alt is the effect allele)
# Measured here: 77 positions, 4 min 26 s, 79 rows returned (including multi-allelic sites); a .tbi file is left in the working directory

Instruments

Fix the threshold, clumping parameters and reference panel before looking at results, and state them in the methods.

What F > 10 means: in one-sample settings or with sample overlap, F around 10 corresponds to a 2SLS bias of about 10% of the observational bias; it is a rule of thumb and does not guarantee no bias. The per-SNP F is approximately z², so at P<5e-8 F automatically exceeds 29.7, at P<5e-6 it exceeds 20.8, and at P<1e-5 it exceeds 19.5. After selecting at these thresholds, "all SNPs have F > 10" adds no information; total R², overall F and a comparison of weak SNPs when the threshold is relaxed are more useful.

The formula R² = 2·EAF·(1−EAF)·β² holds only for β in standard deviation units, and β must be squared. Some Chinese tutorials drop the square or plug in β in mg/dL, which puts R² off by orders of magnitude. The code above includes SE and N and does not depend on the units.

Local clumping: plink + 1000 Genomes reference panelr
# Reference panel: http://fileserve.mrcieu.ac.uk/ld/1kg.v3.tgz (1.57 GB, EUR/EAS/AFR/AMR/SAS)
# Extract only the population you need: tar -xzf 1kg.v3.tgz EUR.bed EUR.bim EUR.fam
# plink 1.9 from conda (bioconda::plink) or genetics.binaRies::get_plink_binary()
library(ieugwasr)
# First count significant SNPs missing from the panel: ld_clump drops them silently
bim <- data.table::fread("ref/EUR.bim", select = 2)
miss <- exp_dat[!exp_dat$SNP %in% bim$V2, ]
nrow(miss); min(miss$pval.exposure)   # measured here: 132 SNPs, smallest P 4.3e-127

clumped <- ld_clump(
  dplyr::tibble(rsid = exp_dat$SNP, pval = exp_dat$pval.exposure, id = exp_dat$id.exposure),
  clump_kb = 10000, clump_r2 = 0.001, clump_p = 1,
  bfile = "ref/EUR",                  # file prefix, without .bed
  plink_bin = Sys.which("plink"))
exp_dat <- exp_dat[exp_dat$SNP %in% clumped$rsid, ]
F statistic and R²r
# Per-SNP F statistic (approximation)
exp_dat$F <- (exp_dat$beta.exposure / exp_dat$se.exposure)^2

# Variance explained R2 (form that includes SE and N; for a continuous trait with beta in SD units)
b <- exp_dat$beta.exposure; s <- exp_dat$se.exposure
f <- exp_dat$eaf.exposure;  N <- exp_dat$samplesize.exposure
exp_dat$R2 <- 2*b^2*f*(1-f) / (2*b^2*f*(1-f) + 2*s^2*N*f*(1-f))

# Overall F: k SNPs, total R2
k <- nrow(exp_dat); R2 <- sum(exp_dat$R2, na.rm = TRUE)
F_all <- R2 * (median(N) - k - 1) / (k * (1 - R2))
# Measured here (79 SNPs): per-SNP F min 27.8, median 58.4; total R2 0.085; overall F 201
SettingCommon valueBasisRisks and results from this page
P threshold5e-8Genome-wide significanceLDL-C here: 3,078 SNPs, 79 after clumping
Relaxed P threshold5e-6 or 1e-5 (1e-5 is common for exposures with few hits, such as gut microbiota)With fewer than 3 SNPs at 5e-8, MR-Egger and similar sensitivity analyses are impossibleWeaker instruments, stronger winner's curse, more pleiotropic SNPs. Here 5e-6 gave 114 SNPs after clumping, and the 36 added SNPs had a minimum F of 20.2. Report the 5e-8 result alongside
Clumping r²0.001Default in TwoSampleMR and ieugwasr; keeps instruments approximately independentAt 0.01 or higher, SNPs are correlated and you need correlated IVW with an LD matrix (MendelianRandomization::mr_ivw(correl = TRUE))
Clumping window10,000 kbDefaultA small window keeps correlated SNPs in long-range LD regions
Reference panel1000 Genomes EUR (503 people, 8,550,156 variants)Matches the exposure GWAS ancestry; use EAS for East Asian dataSNPs absent from the panel are dropped. Here 132 of 3,060 significant SNPs were missing, 38 of them with P<1e-20

Allele harmonisation

Harmonisation makes the exposure and outcome β refer to the same effect allele. In this test, 36 of 79 SNPs needed their alleles switched; without harmonisation nearly half the SNPs would point the wrong way.

Palindromic SNPs have A/T or C/G alleles; flipping strand leaves the alleles unchanged, so only frequencies reveal the orientation. The tolerance in the harmonise_data source is 0.08, which gives the 0.42–0.58 window. Removed SNPs are not deleted from the data frame, only flagged with mr_keep = FALSE; mr() ignores them, but if you export supplementary tables without filtering, the SNP count will not match nsnp in the results.

Proxies: extract_outcome_data defaults to proxies = TRUE and rsq = 0.8 and aligns proxy alleles through LD automatically, which relies on the OpenGWAS API. Offline, find proxies in the reference panel with plink and use the in-phase output to map the alleles.

Find proxy SNPs locallybash
# When an instrument is missing from the outcome, find proxies with r2 >= 0.8 in the local panel
plink --bfile ref/EUR --r2 in-phase with-freqs \
  --ld-snp rs646776 --ld-window-kb 500 --ld-window 99999 --ld-window-r2 0.8 \
  --out proxy_rs646776
# The PHASE column, e.g. CG/TA, means allele C of the target SNP sits on the same haplotype as allele G of the proxy
# When using the proxy's outcome effect, map its effect allele back to the target SNP's allele with this pairing
# Measured here: about 5 s per SNP; 10 proxies for rs646776 (including rs12740374 at r2 = 1)
actionBehaviourWhen to useResult here (outcome CARDIoGRAMplusC4D)
1Assumes both datasets are on the forward strand; no inference for palindromic SNPsBoth datasets already harmonised to the reference forward strand (e.g. GWAS Catalog harmonised files and FinnGen)All 79 kept
2 (default)Infers strand for palindromic SNPs from allele frequencies; palindromic SNPs with EAF between 0.42 and 0.58 cannot be inferred and get mr_keep = FALSEMost situations77 kept, rs2954029 and rs964184 removed; the same instruments lost none against the FinnGen outcome
3Removes all palindromic SNPsOutcome lacks EAF, or you want the most conservative sensitivity analysis76 kept

Main and sensitivity analyses

The methods make different assumptions about pleiotropy. IVW is the main analysis; the others check whether its conclusion holds under different assumptions.

MR-PRESSO run time grows with the number of SNPs and NbDistribution. With 77 SNPs here, NbDistribution = 1000 took 66 seconds and warned "Outlier test unstable … The current precision is <0.077"; 2000 took 138 seconds and the warning disappeared. The outlier P values are Bonferroni-corrected for the number of SNPs, so set NbDistribution to at least 20 times the SNP count.

The Burgess et al. guidelines note that outlier-removal methods such as MR-PRESSO have very high false positive rates when several instruments are invalid. Treat the outlier-corrected result as a sensitivity analysis and report it next to the uncorrected IVW.

Main analysis, sensitivity analyses and plotsr
dat <- harmonise_data(exp_dat, out_dat, action = 2)
attr(dat, "log")                      # counts of switched and removed SNPs, for the supplement
dat <- dat[dat$mr_keep, ]             # removed rows stay in the data frame; filter before exporting tables

res <- mr(dat, method_list = c("mr_ivw", "mr_ivw_fe", "mr_egger_regression",
                               "mr_weighted_median", "mr_weighted_mode"))
generate_odds_ratios(res)
mr_heterogeneity(dat)                 # Cochran's Q
mr_pleiotropy_test(dat)               # MR-Egger intercept
Isq(dat$beta.exposure, dat$se.exposure)   # I2GX; below 0.9 MR-Egger suffers regression dilution
loo <- mr_leaveoneout(dat); ss <- mr_singlesnp(dat)

# Steiger: for a binary outcome compute r.outcome first, otherwise it is approximated as continuous
dat$r.outcome  <- get_r_from_lor(dat$beta.outcome, dat$eaf.outcome,
                                 ncase = 60801, ncontrol = 123504, prevalence = 0.05)
dat$r.exposure <- get_r_from_bsen(dat$beta.exposure, dat$se.exposure, dat$samplesize.exposure)
directionality_test(dat)

# MR-PRESSO: NbDistribution at least 20 times the number of SNPs, otherwise the outlier test lacks precision
library(MRPRESSO)
nb <- max(1000, ceiling(nrow(dat) * 20 / 1000) * 1000)
pr <- mr_presso(BetaOutcome = "beta.outcome", BetaExposure = "beta.exposure",
  SdOutcome = "se.outcome", SdExposure = "se.exposure", data = dat,
  OUTLIERtest = TRUE, DISTORTIONtest = TRUE, NbDistribution = nb, seed = 2026)

# Plots
mr_scatter_plot(res, dat); mr_forest_plot(ss); mr_leaveoneout_plot(loo); mr_funnel_plot(ss)
MethodValid whenImplementation in TwoSampleMRWhat to report
IVW (multiplicative random effects)All SNPs valid, or pleiotropy averages to zeromr_ivw: SE divided by min(1, residual SE), widened under heterogeneity and never narrower than fixed effectsMain β, OR and 95% CI
IVW (fixed effects)All SNPs valid and no heterogeneitymr_ivw_fe: SE divided by the residual SESupplementary when Q is not significant; CIs too narrow under heterogeneity
MR-EggerInSIDE: pleiotropy independent of instrument strengthmr_egger_regression; at least 3 SNPsSlope and intercept; with I²GX < 0.9 there is regression dilution, so apply SIMEX or interpret cautiously
Weighted medianMore than 50% of the weight from valid SNPsmr_weighted_median, 1,000 bootstrap replicatesWhether the direction agrees with IVW
Weighted modeThe largest weighted group of SNPs is validmr_weighted_modeAs above; conservative, with wide CIs
MR-PRESSOOutlying SNPs are the source of pleiotropyAt least 4 SNPs; NbDistribution must exceed the SNP count; warns Outlier test unstable when SNPs/NbDistribution exceeds 0.05Global test P, outlier SNPs, corrected estimate and distortion test P
Cochran's Q—mr_heterogeneity gives Q for IVW and EggerQ, df, P; I² = (Q − df)/Q
Leave-one-out—mr_leaveoneoutWhether any single removal crosses zero; limited value with many SNPs
Steiger directionalitySNPs explain more variance in the exposure than in the outcomedirectionality_test; for a binary outcome compute r.outcome with get_r_from_lor firstWhether the direction is correct and its P; steiger_filtering removes SNPs pointing the wrong way

Interpretation

MR-Egger and the mode estimator have less power than IVW and wider confidence intervals, so a non-significant result from them alone is common. Look at direction and point estimates first, then P values.

SituationInterpretationHow to report
IVW significant; other methods agree in direction but are not significantUsually low power; here the Egger SE (0.078) was about 1.5 times the IVW SE (0.051)State that all methods agree in direction with similar point estimates; keep IVW as the main conclusion
Egger intercept P<0.05Directional pleiotropy; IVW is biasedLead with Egger, weighted median and mode; identify and discuss outlier SNPs; downgrade the conclusion to suggestive
Q significant, intercept not significantHeterogeneity, possibly balanced pleiotropyUse multiplicative random-effects IVW (TwoSampleMR default); report Q and I²
MR-PRESSO corrected estimate differs (distortion test P<0.05)Outliers materially change the estimateReport both; describe known associations at the outlier loci
Methods disagree in directionThe causal effect cannot be determinedCheck allele harmonisation and units; make no causal claim
Only 1–3 SNPsOnly the Wald ratio or IVW is possible; Egger needs at least 3 SNPs and MR-PRESSO at least 4State that sensitivity analyses are limited; consider a relaxed threshold as a supplementary analysis or a larger exposure GWAS

Tested on this page

Conditions: macOS arm64, 8 cores, 16 GB RAM; R 4.5.3, TwoSampleMR 0.7.12, ieugwasr 1.2.0, MRPRESSO 1.0, MendelianRandomization 0.10.0, meta 8.5.0, PLINK 1.9, htslib 1.24. Exposure: GLGC 2013 LDL-C (2,437,751 SNPs, N up to about 173,000). Outcome: CARDIoGRAMplusC4D 2015 (GWAS Catalog GCST003116 harmonised file, 60,801 cases and 123,504 controls), replicated with FinnGen R13 I9_CHD (90,714 cases and 409,472 controls). No OpenGWAS token was used.

These results show three things. First, with a highly significant Q test, the fixed-effect IVW SE is only 46% of the random-effects SE and the P values differ by 54 orders of magnitude, so reporting fixed effects overstates precision. Second, the MR-PRESSO outliers include known pleiotropic loci such as SH2B3 (rs3184504) and ABO (rs579459); removing them moved the estimate from 0.412 to 0.437, the distortion test was not significant, and the conclusion stands. Third, the same instruments gave ORs of 1.51 and 1.32 in the two outcome sources, with a heterogeneity P of 0.022 between them; FinnGen's I9_CHD is a register-based "major coronary heart disease event" endpoint in a Finnish population, and both the outcome definition and the population change the effect size.

Combine estimates from two outcome sources and draw a forest plotr
# Combine IVW estimates for one exposure from two independent outcome sources (meta 8.5)
library(meta)
m <- metagen(TE = c(0.4123, 0.2741), seTE = c(0.0511, 0.0318),
             studlab = c("CARDIoGRAMplusC4D 2015", "FinnGen R13 I9_CHD"),
             sm = "OR", common = TRUE, random = TRUE)
forest(m)
# Measured here: common effect OR 1.37 (1.30-1.44); random effects 1.40 (1.22-1.60); Q = 5.28, P = 0.022, I2 = 81%
  1. 01

    Selection and clumping

    P<5e-8 gave 3,078 SNPs, 3,060 after de-duplication; local plink clumping (r²=0.001, 10,000 kb, 1000G EUR) took 3 seconds and kept 79. Reading and clumping took 22 seconds in total, with peak memory around 2 GB.

  2. 02

    Instrument strength

    Per-SNP F minimum 27.8, median 58.4; total R² 0.085; overall F 201.

  3. 03

    Harmonisation

    harmonise_data(action = 2) switched alleles for 36 SNPs and removed 2 palindromic SNPs with intermediate frequency, leaving 77.

  4. 04

    Main and sensitivity analyses

    IVW gave an OR for coronary disease of 1.51 per 1 SD higher LDL-C; Egger intercept P = 0.13; Q test highly significant; MR-PRESSO found 8 outliers and the estimate barely changed after removing them. Leave-one-out estimates ranged from 0.386 to 0.455, with a largest P of 1.4×10⁻¹³. Steiger: SNP R² 0.083 for the exposure and 0.0058 for the outcome, direction correct.

  5. 05

    Replication in an independent outcome

    Remote tabix on FinnGen R13 fetched the 77 positions in 4 min 26 s; IVW OR 1.32, same direction, smaller effect.

MethodSNPsβ (SE)OR (95% CI)P
IVW (multiplicative random effects)770.412 (0.051)1.51 (1.37–1.67)6.7×10⁻¹⁶
IVW (fixed effects)770.412 (0.023)1.51 (1.44–1.58)7.6×10⁻⁷⁰
MR-Egger770.503 (0.078)1.65 (1.42–1.93)1.0×10⁻⁸
Weighted median770.396 (0.044)1.49 (1.36–1.62)3.1×10⁻¹⁹
Weighted mode770.540 (0.079)1.72 (1.47–2.01)2.1×10⁻⁹
MR-PRESSO outlier-corrected (8 removed)690.437 (0.032)1.55 (1.45–1.65)6.4×10⁻²¹
FinnGen R13 replication (IVW)770.274 (0.032)1.32 (1.24–1.40)7.4×10⁻¹⁸
Heterogeneity: IVW Q = 363.8 (df 76, P = 3.8×10⁻³⁹, I² = 79.1%); Egger intercept −0.0070 (SE 0.0046, P = 0.13); I²GX 0.986 (TwoSampleMR Isq) and 97.9% (MendelianRandomization, different weighting); MR-PRESSO global test P < 5×10⁻⁴, distortion test P = 0.44.

Common mistakes

Wrong effect allele column

Effect allele columns by source: A1 in GLGC 2013; alt in FinnGen, Pan-UKB and Neale; hm_effect_allele in GWAS Catalog harmonised files; ALT in IEU VCFs. Taking ref as the effect allele flips the sign of β relative to the harmonised outcome and reverses the causal estimate.

Different populations for exposure and outcome

With instruments from Europeans and an outcome from East Asians, LD structure and allele frequencies differ, and both palindromic inference and proxies go wrong. The reference panel must also match the exposure GWAS ancestry; use the EAS panel for East Asian data.

Interpreting ORs for a binary exposure

When the exposure is a disease, the MR estimate is the effect per unit increase in the log odds of the exposure. Multiply by ln2 = 0.693 to get the effect per doubling of the odds of the exposure (Burgess and Labrecque 2018). Writing "people with the disease have X times the risk" is a misinterpretation.

Binary β from a linear model

Neale lab UKB results use linear regression for binary phenotypes, so β is on the probability scale. Used directly as a logOR, it gives ORs close to 1. Convert approximately with β/[u(1−u)], where u is the case fraction.

Multiple testing

When testing several exposures or outcomes, apply a Bonferroni correction for the number of tests (for 20 exposures the threshold is 0.05/20 = 0.0025) or report FDR. Describe results between 0.05 and the corrected threshold as suggestive.

Passing data through Excel

Tidying GWAS results in Excel and saving as CSV can lose or zero β and SE values when the column format is wrong, and truncates very small P values. Read and write the original files with R or the command line throughout.

Reviewer comments

ConcernAnalysis to respond withWhat goes into the paper
Horizontal pleiotropyMR-Egger intercept, MR-PRESSO global test, weighted median and mode; look up known associations at outlier loci, remove them and rerunConsistent direction across methods; results after removing known pleiotropic loci; multivariable MR for the main pleiotropic pathway if needed
Weak instrumentsTotal R², overall F; compare with the 5e-8 result when the threshold is relaxedF per SNP and total R²; note that without overlap weak instrument bias is towards the null
Sample overlapCompare cohort lists of both GWAS; replicate with a non-overlapping outcome source; correct with MRlapNumber or fraction of overlapping participants; replication in an independent source
Population stratificationWhether both GWAS adjusted for principal components; same ancestry for exposure and outcomeNumber of PCs and ancestry; for socially patterned exposures, discuss within-family MR
Reverse causationSteiger directionality test; reverse MRSteiger results and estimates after steiger_filtering
Public data only, little noveltyPositive control outcome; replication in an independent outcome source; comparison with RCT or observational evidenceForest plot of replication results; comparison with existing evidence

Chinese community

The items below come from CSDN. Only posts with original error messages or the author's own tests, and consistent with official documentation or this page's tests, are included. Many 2026 "pitfall guide" posts are templated text; some give the token variable as IUEUGWAS_TOKEN or claim that action = 2 "keeps all SNPs", and both contradict the source code.

401 errors and token reset

One author (November 2025) recorded the "Status code from OpenGWAS API: 401" error; the fix was to register, put OPENGWAS_JWT in .Renviron, and reset the token on the account page when it expires. This matches the official 14-day validity, and this page reproduced the same error.

Column names and paths for local clumping

One author (April 2024) switched to local plink clumping after repeated online timeouts and noted that the data frame columns must be renamed rsid and pval and that bfile takes the reference file prefix. This matches the ieugwasr documentation and passed this page's test.

Same data and code, different P values from the paper

One author (June 2023, over 9,000 reads) reproduced an MR paper with the data and code supplied by its authors: ORs and confidence intervals were close but P values differed. OpenGWAS datasets and package versions change, so state package versions and download dates and keep the data files you used. The access_token argument in that post's code now raises an error.

Saving from Excel to CSV made every F value the same

One author (April 2023) found identical F values for all SNPs and, after two days, traced it to numeric columns in Excel's "General" format turning into 0 when saved as CSV. The R² formula in that post omits the square of β; use the formula on this page.

Meaning of the IEU VCF fields

One author (June 2023, 27,000 reads) listed the FORMAT fields of IEU VCFs: ES is β, SE the standard error, LP −log10(P), AF the effect allele frequency. This matches the GWAS-VCF specification; exporting with bcftools query is faster and uses less memory than reading the whole file with vcfR.

Hand it to an agent

You can describe the analysis in one sentence and let Scientify's science agent run it on a cloud computer.

Example instruction: "Run a two-sample MR with GLGC 2013 LDL-C summary statistics as the exposure and coronary artery disease from GWAS Catalog GCST003116 as the outcome: P<5e-8, local clumping with 1000G EUR (r²=0.001, 10,000 kb), compute F and R², harmonise with action=2, run IVW, Egger, weighted median, weighted mode, MR-PRESSO, Q test, leave-one-out and Steiger, replicate with FinnGen R13 I9_CHD, and write the methods paragraph and result tables following STROBE-MR."

The agent installs TwoSampleMR, MRPRESSO and plink on the cloud computer, downloads the summary statistics and reference panel, checks the P column type, effect allele columns and genome build, runs selection, clumping, harmonisation and all analyses, and produces result tables, scatter, forest, leave-one-out and funnel plots, and a draft methods paragraph. The workspace keeps scripts, parameters, logs and data files for reproduction; the agent reviews the results adversarially, for example checking the number of switched SNPs after harmonisation, whether the choice of fixed or random effects matches the Q test, and whether outlier SNPs are known pleiotropic loci. The task keeps running after you shut down your machine.

You still need to check: the ancestry and sample overlap of the exposure and outcome GWAS, whether thresholds and methods were fixed before seeing results, whether the effect scale of a binary exposure was converted correctly, and whether the wording of the conclusion matches the sensitivity analyses.

Sources

FAQ

Can I do Mendelian randomization without an OpenGWAS token?

Yes. Download summary statistics from GWAS Catalog, FinnGen, Pan-UKB or the original paper's site, read them with format_data, and clump locally with plink and a 1000 Genomes reference panel; harmonise_data, mr and the sensitivity analyses need no token. The LDL-C to coronary disease test on this page used no token.

Too few instruments at P<5e-8: can I relax to 5e-6?

Yes, as the main or a supplementary analysis, but give the reason in the methods and also report the result at 5e-8. The added SNPs are weaker and more affected by winner's curse and pleiotropy. SNPs selected at 5e-6 automatically have F above about 20.8, so "F > 10" does not show that relaxing is safe.

Should IVW use fixed or random effects?

TwoSampleMR's mr_ivw defaults to multiplicative random effects, which equals fixed effects when heterogeneity is small and widens the CI when it is large. Report random effects when the Q test is significant; in this page's test the SEs were 0.051 and 0.023, so fixed effects would overstate precision.

If the MR-Egger intercept is not significant, is there no pleiotropy?

No. The intercept test has low power, detects only directional pleiotropy and relies on the InSIDE assumption. Report the Q test, MR-PRESSO global test, weighted median and mode alongside it, and describe outlier SNPs.

Should harmonise_data use action 1, 2 or 3?

Usually the default 2, which infers palindromic SNPs from allele frequencies and removes those with EAF between 0.42 and 0.58. Use 1 when both datasets are already harmonised to the forward strand; use 3 when the outcome has no frequencies or for the most conservative analysis.

Can both exposure and outcome come from UK Biobank?

You can, but the samples overlap completely and weak instrument bias then points towards the observational association. The bias is small when instruments are strong (F above about 30). A safer approach is to replicate with a non-overlapping outcome source such as FinnGen and report the overlap in the paper.

Hand your Mendelian randomization analysis to Scientify

The science agent runs this page's workflow on an isolated cloud computer: downloading summary statistics and the reference panel, local clumping, allele harmonisation, main and all sensitivity analyses, replication in an independent outcome and plots, keeping all scripts, parameters and logs. New users get 5 USD of free credit.