set.seed(1234)
# Sample size
n <- 1000000
# Simulate a genotype
g <- rbinom(n, 2, 0.5)
# Try effect of SNP on the exposure on the liability scale (not the risk difference scale)
b_gx <- 0.8
# Generate liability for X
x_liability <- g * b_gx
# Generate X using logistic transformation
x <- rbinom(n, 1, plogis(x_liability))Background
If we perform 2 sample MR analysis where exposure and outcome are both binary, and both are estimated on the log odds scale, the effect estimate will be in different units when comparing against observational logistic regression. In the latter case, the outcome is on the logistic scale but the exposure is on the risk difference scale.
So the solution is to convert the MR estimate by transforming the SNP-exposure estimate on the risk difference scale, or to convert the observational estimate with the inverse.
Example
Note that the risk difference scale gives a different estimate to the logistic scale. Here’s the risk difference scale:
summary(lm(x ~ g))$coef[2][1] 0.1655918
Here’s the logistic scale, which matches the simulated beta:
summary(glm(x ~ g, family = "binomial"))$coef[2][1] 0.7983852
Now make X causal for Y. The observational logistic regression estimate matches the simulation.
b_xy <- 0.1
y <- rbinom(n, 1, plogis(x * b_xy))
# y <- as.integer(x * b_xy + rlogis(n)>0) # note this is equivalent to above
glm(y ~ x, family = "binomial")$coef[2] x
0.09324593
Let’s make the MR estimate
bgx <- glm(x ~ g, family = "binomial")$coef[2]
bgy <- glm(y ~ g, family = "binomial")$coef[2]
bgy/bgx g
0.02376076
This doesn’t match the simulated beta. That’s because we’re asking “how much does Y change per liability change in X”, rather than “how much does Y change per unit change in X”. So we need to change bgx to be in unit change scale (risk difference scale).
px <- mean(x)
scale_factor <- px * (1-px)
bgx_transform <- bgx * scale_factor
bgy / bgx_transform g
0.1089
Or we can convert the observational estimate onto the same scale as the naive MR estimate (effect on log-odds of Y per unit change in log-odds of X), which should match bgy/bgx:
obs_risk_diff <- glm(y ~ x, family = "binomial")$coef[2]
obs_liability_scale <- obs_risk_diff * scale_factor
obs_liability_scale x
0.02034522
Note that the transformation assumes that the disease isn’t rare and the effect sizes are small.
sessionInfo()R version 4.6.1 (2026-06-24)
Platform: aarch64-apple-darwin23
Running under: macOS Sequoia 15.2
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Europe/London
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] htmlwidgets_1.6.4 compiler_4.6.1 fastmap_1.2.0 cli_3.6.6
[5] tools_4.6.1 htmltools_0.5.9 otel_0.2.0 yaml_2.3.12
[9] rmarkdown_2.31 knitr_1.51 jsonlite_2.0.0 xfun_0.57
[13] digest_0.6.39 rlang_1.3.0 evaluate_1.0.5