Skip to content

Commit 0ea51c1

Browse files
authored
Merge pull request #633 from remlapmot/v0-6-16
TwoSampleMR 0.6.16
2 parents 6268c2c + 936fccd commit 0ea51c1

File tree

9 files changed

+215
-72
lines changed

9 files changed

+215
-72
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: TwoSampleMR
22
Title: Two Sample MR Functions and Interface to MRC Integrative
33
Epidemiology Unit OpenGWAS Database
4-
Version: 0.6.15
4+
Version: 0.6.16
55
Authors@R: c(
66
person("Gibran", "Hemani", , "[email protected]", role = c("aut", "cre"),
77
comment = c(ORCID = "0000-0003-0920-1055")),

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# TwoSampleMR v0.6.16
2+
3+
(Release date 2025-06-05)
4+
5+
* Added weak instrument adjustment estimate and intercept to returned output of `mr_grip()` (thanks @fdudbridge)
6+
* Amended `mr_scatter_plot()` to correctly plot the intercept from `mr_grip()` (thanks @fdudbridge)
7+
18
# TwoSampleMR v0.6.15
29

310
(Release date 2025-05-01)

R/mr-grip.R

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
#' \item{b}{MR estimate}
1919
#' \item{se}{Standard error of MR estimate}
2020
#' \item{pval}{p-value of MR estimate}
21-
#' \item{Q, Q_df, Q_pval}{Heterogeneity stats}
22-
#' \item{b.wi}{MR estimate adjusting for weak instruments}
23-
#' \item{se.wi}{Standard error adjusting for weak instruments}
24-
#' \item{pval.wi}{p-value adjusting for weak instruments}
21+
#' \item{b_i}{Intercept}
22+
#' \item{se_i}{Standard error of intercept}
23+
#' \item{pval_i}{p-value of intercept}
24+
#' \item{b.adj}{MR estimate adjusting for weak instruments}
25+
#' \item{se.adj}{Standard error adjusting for weak instruments}
26+
#' \item{pval.adj}{p-value adjusting for weak instruments}
2527
#' \item{mod}{Summary of regression}
2628
#' \item{dat}{Original data used for MR-GRIP}
2729
#' }
@@ -34,10 +36,13 @@ mr_grip <- function(b_exp, b_out, se_exp, se_out, parameters) {
3436
b = NA,
3537
se = NA,
3638
pval = NA,
39+
b_i = NA,
40+
se_i = NA,
41+
pval_i = NA,
42+
b.adj = NA,
43+
se.adj = NA,
44+
pval.adj = NA,
3745
nsnp = NA,
38-
Q = NA,
39-
Q_df = NA,
40-
Q_pval = NA,
4146
mod = NA,
4247
smod = NA,
4348
dat = NA
@@ -56,24 +61,54 @@ mr_grip <- function(b_exp, b_out, se_exp, se_out, parameters) {
5661
)
5762
grip_out <- b_out * b_exp
5863
grip_exp <- b_exp^2
59-
# GRIP regression. Includes intercept. Weights designed to replicate IVW under no intercept.
60-
mod <- stats::lm(grip_out ~ grip_exp, weights = 1 / (grip_exp * se_out^2))
64+
grip_weights <- 1 / (b_exp^2 * se_out^2)
65+
66+
# GRIP regression. Includes intercept. Weights under NOME assumption.
67+
mod <- stats::lm(grip_out ~ grip_exp, weights = grip_weights)
6168
smod <- summary(mod)
6269
b <- stats::coefficients(smod)[2, 1]
6370
se <- stats::coefficients(smod)[2, 2]
64-
b.wi <- NA
65-
se.wi <- NA
66-
pval.wi <- NA
67-
pval <- 2 * stats::pt(abs(b / se), length(b_exp) - 2L, lower.tail = FALSE)
71+
b_i <- stats::coefficients(smod)[1, 1]
72+
se_i <- stats::coefficients(smod)[1, 2]
73+
74+
# Weak instrument adjustment
75+
grip_weights <- 1 / (se_out^2)
76+
numer <- sum(grip_weights) *
77+
sum(grip_weights * b_out * b_exp * (b_exp^2 - 3 * se_exp^2)) -
78+
sum(grip_weights * b_out * b_exp) * sum(grip_weights * (b_exp^2 - se_exp^2))
79+
denom <- sum(grip_weights) *
80+
sum(grip_weights * (b_exp^4 - 6 * b_exp^2 * se_exp^2 + 3 * se_exp^4)) -
81+
(sum(grip_weights * (b_exp^2 - se_exp^2)))^2
82+
b.adj <- numer / denom
83+
84+
var_out <- mean((b_out - (b * grip_exp + b_i) / b_exp)^2) * b_exp^2
85+
numer <- sum(grip_weights)^2 *
86+
sum(grip_weights^2 * var_out * (b_exp^2 - 3 * se_exp^2)^2) +
87+
sum(grip_weights^2 * var_out) * sum(grip_weights * (b_exp^2 - se_exp^2))^2 -
88+
2 *
89+
sum(grip_weights) *
90+
sum(grip_weights * (b_exp^2 - se_exp^2)) *
91+
sum(grip_weights^2 * (b_exp^2 - se_exp^2) * var_out)
92+
se.adj <- sqrt(numer) / denom
93+
94+
pval <- 2 * stats::pt(abs(b / se), length(b_exp) - 2, lower.tail = FALSE)
95+
pval_i <- 2 *
96+
stats::pt(abs(b_i / se_i), length(b_exp) - 2, lower.tail = FALSE)
97+
pval.adj <- 2 *
98+
stats::pt(abs(b.adj / se.adj), length(b_exp) - 2, lower.tail = FALSE)
6899
return(list(
69100
b = b,
70101
se = se,
71102
pval = pval,
72-
b.wi = b.wi,
73-
se.wi = se.wi,
74-
pval.wi = pval.wi,
103+
b_i = b_i,
104+
se_i = se_i,
105+
pval_i = pval_i,
106+
b.adj = b.adj,
107+
se.adj = se.adj,
108+
pval.adj = pval.adj,
75109
nsnp = length(b_exp),
76-
mod = smod,
110+
mod = mod,
111+
smod = smod,
77112
dat = dat
78113
))
79114
}

R/mr.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ mr_method_list <- function()
231231
obj = "mr_grip",
232232
name = "MR GRIP",
233233
PubmedID = "",
234-
Description = "Allele coding invariant regression",
234+
Description = "Allele coding invariant MR-Egger regression",
235235
use_by_default = FALSE,
236236
heterogeneity_test = FALSE
237237
)

R/scatterplot.R

Lines changed: 111 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,123 @@
1-
#' Create scatter plot with lines showing the causal estimate for different MR tests
1+
#' Create scatter plot with fitted lines showing the causal effect estimate for different MR estimators
22
#'
3-
#' Requires dev version of ggplot2
3+
#' Create scatter plot with fitted lines showing the causal effect estimate for different MR estimators.
44
#'
55
#' @param mr_results Output from [mr()].
66
#' @param dat Output from [harmonise_data()].
77
#' @export
88
#' @return List of plots
9-
mr_scatter_plot <- function(mr_results, dat)
10-
{
11-
# dat <- subset(dat, paste(id.outcome, id.exposure) %in% paste(mr_results$id.outcome, mr_results$id.exposure))
12-
mrres <- plyr::dlply(dat, c("id.exposure", "id.outcome"), function(d)
13-
{
14-
d <- plyr::mutate(d)
15-
if(nrow(d) < 2 | sum(d$mr_keep) == 0)
16-
{
17-
return(blank_plot("Insufficient number of SNPs"))
18-
}
19-
d <- subset(d, mr_keep)
20-
index <- d$beta.exposure < 0
21-
d$beta.exposure[index] <- d$beta.exposure[index] * -1
22-
d$beta.outcome[index] <- d$beta.outcome[index] * -1
23-
mrres <- subset(mr_results, id.exposure == d$id.exposure[1] & id.outcome == d$id.outcome[1])
24-
mrres$a <- 0
25-
if("MR Egger" %in% mrres$method)
26-
{
27-
temp <- mr_egger_regression(d$beta.exposure, d$beta.outcome, d$se.exposure, d$se.outcome, default_parameters())
28-
mrres$a[mrres$method == "MR Egger"] <- temp$b_i
29-
}
9+
mr_scatter_plot <- function(mr_results, dat) {
10+
# dat <- subset(dat, paste(id.outcome, id.exposure) %in% paste(mr_results$id.outcome, mr_results$id.exposure))
11+
mrres <- plyr::dlply(dat, c("id.exposure", "id.outcome"), function(d) {
12+
d <- plyr::mutate(d)
13+
if (nrow(d) < 2 | sum(d$mr_keep) == 0) {
14+
return(blank_plot("Insufficient number of SNPs"))
15+
}
16+
d <- subset(d, mr_keep)
17+
index <- d$beta.exposure < 0
18+
d$beta.exposure[index] <- d$beta.exposure[index] * -1
19+
d$beta.outcome[index] <- d$beta.outcome[index] * -1
20+
mrres <- subset(
21+
mr_results,
22+
id.exposure == d$id.exposure[1] & id.outcome == d$id.outcome[1]
23+
)
24+
mrres$a <- 0
25+
if ("MR Egger" %in% mrres$method) {
26+
temp <- mr_egger_regression(
27+
d$beta.exposure,
28+
d$beta.outcome,
29+
d$se.exposure,
30+
d$se.outcome,
31+
default_parameters()
32+
)
33+
mrres$a[mrres$method == "MR Egger"] <- temp$b_i
34+
}
3035

31-
if("MR Egger (bootstrap)" %in% mrres$method)
32-
{
33-
temp <- mr_egger_regression_bootstrap(d$beta.exposure, d$beta.outcome, d$se.exposure, d$se.outcome, default_parameters())
34-
mrres$a[mrres$method == "MR Egger (bootstrap)"] <- temp$b_i
35-
}
36+
if ("MR Egger (bootstrap)" %in% mrres$method) {
37+
temp <- mr_egger_regression_bootstrap(
38+
d$beta.exposure,
39+
d$beta.outcome,
40+
d$se.exposure,
41+
d$se.outcome,
42+
default_parameters()
43+
)
44+
mrres$a[mrres$method == "MR Egger (bootstrap)"] <- temp$b_i
45+
}
3646

37-
ggplot2::ggplot(data=d, ggplot2::aes(x=beta.exposure, y=beta.outcome)) +
38-
ggplot2::geom_errorbar(ggplot2::aes(ymin=beta.outcome-se.outcome, ymax=beta.outcome+se.outcome), colour="grey", width=0) +
39-
ggplot2::geom_errorbarh(ggplot2::aes(xmin=beta.exposure-se.exposure, xmax=beta.exposure+se.exposure), colour="grey", height=0) +
40-
ggplot2::geom_point() +
41-
ggplot2::geom_abline(data=mrres, ggplot2::aes(intercept=a, slope=b, colour=method), show.legend=TRUE) +
42-
ggplot2::scale_colour_manual(values=c("#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99", "#b15928")) +
43-
ggplot2::labs(colour="MR Test", x=paste("SNP effect on", d$exposure[1]), y=paste("SNP effect on", d$outcome[1])) +
44-
ggplot2::theme(legend.position="top", legend.direction="vertical") +
45-
ggplot2::guides(colour=ggplot2::guide_legend(ncol=2))
46-
})
47-
mrres
47+
if ("MR GRIP" %in% mrres$method) {
48+
temp <- mr_grip(
49+
d$beta.exposure,
50+
d$beta.outcome,
51+
d$se.exposure,
52+
d$se.outcome,
53+
default_parameters()
54+
)
55+
# keep intercept at 0 because plot on gd versus gp axes
56+
# mrres$a[mrres$method == "MR GRIP"] <- temp$b_i
57+
msgtxt <- paste0("Strictly, it is only valid to view the MR-GRIP estimate on the standard MR scatter plot axes when the intercept is zero. The estimated intercept for this model is: ", signif(temp$b_i))
58+
message(msgtxt)
59+
}
60+
61+
ggplot2::ggplot(
62+
data = d,
63+
ggplot2::aes(x = beta.exposure, y = beta.outcome)
64+
) +
65+
ggplot2::geom_errorbar(
66+
ggplot2::aes(
67+
ymin = beta.outcome - se.outcome,
68+
ymax = beta.outcome + se.outcome
69+
),
70+
colour = "grey",
71+
width = 0
72+
) +
73+
ggplot2::geom_errorbarh(
74+
ggplot2::aes(
75+
xmin = beta.exposure - se.exposure,
76+
xmax = beta.exposure + se.exposure
77+
),
78+
colour = "grey",
79+
height = 0
80+
) +
81+
ggplot2::geom_point() +
82+
ggplot2::geom_abline(
83+
data = mrres,
84+
ggplot2::aes(intercept = a, slope = b, colour = method),
85+
show.legend = TRUE
86+
) +
87+
ggplot2::scale_colour_manual(
88+
values = c(
89+
"#a6cee3",
90+
"#1f78b4",
91+
"#b2df8a",
92+
"#33a02c",
93+
"#fb9a99",
94+
"#e31a1c",
95+
"#fdbf6f",
96+
"#ff7f00",
97+
"#cab2d6",
98+
"#6a3d9a",
99+
"#ffff99",
100+
"#b15928"
101+
)
102+
) +
103+
ggplot2::labs(
104+
colour = "MR Estimate",
105+
x = paste("SNP effect on", d$exposure[1]),
106+
y = paste("SNP effect on", d$outcome[1])
107+
) +
108+
ggplot2::theme(legend.position = "top", legend.direction = "vertical") +
109+
ggplot2::guides(colour = ggplot2::guide_legend(ncol = 2))
110+
})
111+
mrres
48112
}
49113

50114

51-
blank_plot <- function(message)
52-
{
53-
ggplot2::ggplot(data.frame(a=0,b=0,n=message)) +
54-
ggplot2::geom_text(ggplot2::aes(x=a,y=b,label=n)) +
55-
ggplot2::labs(x=NULL,y=NULL) +
56-
ggplot2::theme(axis.text=ggplot2::element_blank(), axis.ticks=ggplot2::element_blank())
115+
blank_plot <- function(message) {
116+
ggplot2::ggplot(data.frame(a = 0, b = 0, n = message)) +
117+
ggplot2::geom_text(ggplot2::aes(x = a, y = b, label = n)) +
118+
ggplot2::labs(x = NULL, y = NULL) +
119+
ggplot2::theme(
120+
axis.text = ggplot2::element_blank(),
121+
axis.ticks = ggplot2::element_blank()
122+
)
57123
}

man/mr_grip.Rd

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/mr_scatter_plot.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test_mr.R

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,14 @@ test_that("mr_grip()", {
5858
expect_equal(ncol(res6), 9L)
5959
expect_equal(res6[1, "b"], 0.490, tolerance = 1e-3)
6060
})
61+
62+
test_that("mr_grip() not from mr()", {
63+
tst <- mr_grip(dat$beta.exposure, dat$beta.outcome, dat$se.exposure, dat$se.outcome)
64+
expect_equal(tst$b, 0.49, tol = 1e-4)
65+
expect_equal(tst$se, 0.0947, tol = 1e-4)
66+
expect_equal(tst$b_i, -3.38e-5, tol = 1e-6)
67+
expect_equal(tst$se_i, 5.66e-5, tol = 1e-6)
68+
expect_equal(tst$b.adj, 0.44736, tol = 1e-4)
69+
expect_equal(tst$se.adj, 0.16389, tol = 1e-4)
70+
expect_equal(tst$nsnp, 79L)
71+
})

tests/testthat/test_plots.R

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,32 @@ context("plots")
22

33
load(system.file("extdata", "test_commondata.RData", package="TwoSampleMR"))
44

5-
test_that("scatter plot", {
5+
test_that("MR scatter plot for mr_ivw", {
66
# dat <- make_dat("ieu-a-2", "ieu-a-7")
77
m <- mr(dat, method_list="mr_ivw")
88
p <- mr_scatter_plot(m, dat)
99
expect_true(is.list(p))
10-
expect_true(length(p) == 1)
10+
expect_true(length(p) == 1L)
11+
})
12+
13+
test_that("Scatter plot for default set of estimates", {
14+
# dat <- make_dat("ieu-a-2", "ieu-a-7")
15+
m2 <- mr(dat)
16+
p2 <- mr_scatter_plot(m2, dat)
17+
expect_true(is.list(p2))
18+
expect_true(length(p2) == 1L)
19+
})
20+
21+
test_that("Scatter plot for mr_grip", {
22+
m3 <- mr(dat, method_list = "mr_grip")
23+
p3 <- mr_scatter_plot(m3, dat)
24+
expect_true(is.list(p3))
25+
expect_true(length(p3) == 1L)
26+
})
27+
28+
test_that("A second scatter plot for mr_grip", {
29+
m4 <- mr(dat2, method_list = "mr_grip")
30+
p4 <- mr_scatter_plot(m4, dat2)
31+
expect_true(is.list(p4))
32+
expect_true(length(p4) == 4L)
1133
})

0 commit comments

Comments
 (0)