Skip to content

Commit 48bed40

Browse files
authored
Merge pull request #679 from remlapmot/devel-2026-04-30
TwoSampleMR 0.7.5
2 parents bf5a8c2 + f155c75 commit 48bed40

22 files changed

Lines changed: 104 additions & 57 deletions

.Rbuildignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ cache$
2121
^inst/sandpit$
2222
^air\.toml$
2323
^vignettes/rf\.rdata$
24+
^\.positai$
25+
^\.claude$

.github/workflows/check-full.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
- {os: ubuntu-latest, r: '4.3.2'}
3939
- {os: ubuntu-latest, r: 'oldrel-3'}
4040
- {os: ubuntu-latest, r: 'oldrel-4'}
41+
- {os: ubuntu-latest, r: 'oldrel-5'}
4142
- {os: macos-14, r: 'release'}
4243
# - {os: ubuntu-24.04-arm, r: 'release', rspm: 'no' }
4344

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ vignettes/mr_report.md
2626
vignettes/figure/*.png
2727
vignettes/*.html
2828
rf.rdata
29+
.positai

DESCRIPTION

Lines changed: 4 additions & 4 deletions
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.7.4
4+
Version: 0.7.5
55
Authors@R: c(
66
person("Gibran", "Hemani", , "g.hemani@bristol.ac.uk", role = c("aut", "cre"),
77
comment = c(ORCID = "0000-0003-0920-1055")),
@@ -20,7 +20,7 @@ Authors@R: c(
2020
)
2121
Description: A package for performing Mendelian randomization using GWAS
2222
summary data. It uses the IEU OpenGWAS database
23-
<https://gwas.mrcieu.ac.uk/> to automatically obtain data, and a wide
23+
<https://opengwas.io> to automatically obtain data, and a wide
2424
range of methods to run the analysis.
2525
License: MIT + file LICENSE
2626
URL: https://github.com/MRCIEU/TwoSampleMR,
@@ -68,6 +68,6 @@ Remotes:
6868
MRPRESSO=rondolab/MR-PRESSO,
6969
qingyuanzhao/mr.raps,
7070
WSpiller/RadialMR
71+
Config/roxygen2/markdown: TRUE
72+
Config/roxygen2/version: 8.0.0
7173
Encoding: UTF-8
72-
Roxygen: list(markdown = TRUE)
73-
RoxygenNote: 7.3.3

NEWS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# TwoSampleMR v0.7.5
2+
3+
(Release date 2026-05-04)
4+
5+
* Amend <https://gwas.mrcieu.ac.uk> URLs to <https://opengwas.io/>
6+
* Bump version of roxygen2 to 8.0.0 and regenerate documentation
7+
* Replace `array(1:nexp)` placeholder allocations with `numeric(nexp)`/`integer(nexp)` in multivariable MR functions
8+
* Replace `1:n` with `seq_len(n)` in loop indices across `singlesnp`, `leaveoneout`, `rucker`, `mr_mode`, and `multivariable_mr` for empty-vector safety
9+
* Pre-generate jackknife resample indices in `mr_rucker_jackknife_internal` and switch to `lapply`, matching the bootstrap function pattern
10+
* Vectorise `get_population_allele_frequency` by inlining the quadratic-root logic from `contingency()` and picking the valid root with vectorised constraint checks
11+
* Skip `mr_wald_ratio` inside `mr()` when more than one SNP is provided and other methods are also requested, suppressing the multi-SNP warning for default method runs while preserving the warning when `mr_wald_ratio` is requested on its own
12+
113
# TwoSampleMR v0.7.4
214

315
(Release date 2026-04-02)

R/add_rsq.r

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -347,13 +347,35 @@ allele_frequency <- function(g) {
347347
get_population_allele_frequency <- function(af, prop, odds_ratio, prevalence) {
348348
stopifnot(length(af) == length(odds_ratio))
349349
stopifnot(length(prop) == length(odds_ratio))
350-
for (i in seq_along(odds_ratio)) {
351-
co <- contingency(af[i], prop[i], odds_ratio[i])
352-
af_controls <- co[1, 2] / (co[1, 2] + co[2, 2])
353-
af_cases <- co[1, 1] / (co[1, 1] + co[2, 1])
354-
af[i] <- af_controls * (1 - prevalence[i]) + af_cases * prevalence[i]
350+
eps <- 1e-15
351+
a <- odds_ratio - 1
352+
b <- (af + prop) * (1 - odds_ratio) - 1
353+
c_ <- odds_ratio * af * prop
354+
355+
z <- numeric(length(odds_ratio))
356+
linear <- abs(a) < eps
357+
z[linear] <- -c_[linear] / b[linear]
358+
359+
quad <- !linear
360+
if (any(quad)) {
361+
d <- pmax(0, b[quad]^2 - 4 * a[quad] * c_[quad])
362+
sqrt_d <- sqrt(d)
363+
two_a <- 2 * a[quad]
364+
z_pos <- (-b[quad] + sqrt_d) / two_a
365+
z_neg <- (-b[quad] - sqrt_d) / two_a
366+
af_q <- af[quad]
367+
prop_q <- prop[quad]
368+
tol <- -1e-7
369+
valid_pos <- z_pos >= tol &
370+
(prop_q - z_pos) >= tol &
371+
(af_q - z_pos) >= tol &
372+
(1 + z_pos - af_q - prop_q) >= tol
373+
z[quad] <- ifelse(valid_pos, z_pos, z_neg)
355374
}
356-
return(af)
375+
376+
af_controls <- (af - z) / (1 - prop)
377+
af_cases <- z / prop
378+
return(af_controls * (1 - prevalence) + af_cases * prevalence)
357379
}
358380

359381

R/leaveoneout.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ mr_leaveoneout <- function(dat, parameters = default_parameters(), method = mr_i
4444
return(d)
4545
}
4646
if (nsnp > 2) {
47-
l <- lapply(1:nsnp, function(i) {
47+
l <- lapply(seq_len(nsnp), function(i) {
4848
with(
4949
x,
5050
method(beta.exposure[-i], beta.outcome[-i], se.exposure[-i], se.outcome[-i], parameters)

R/mr.R

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,21 @@ mr <- function(
7272
} else {
7373
message("Analysing '", exp_id, "' on '", out_id, "'")
7474
}
75-
res <- lapply(method_list, function(meth) {
75+
keep <- if (nrow(x) > 1 && length(method_list) > 1) {
76+
method_list != "mr_wald_ratio"
77+
} else {
78+
rep(TRUE, length(method_list))
79+
}
80+
methods_to_run <- method_list[keep]
81+
res <- lapply(methods_to_run, function(meth) {
7682
get(meth)(x$beta.exposure, x$beta.outcome, x$se.exposure, x$se.outcome, parameters)
7783
})
7884
mr_tab <- data.frame(
7985
id.exposure = exp_id,
8086
id.outcome = out_id,
8187
outcome = x$outcome[1],
8288
exposure = x$exposure[1],
83-
method = method_names,
89+
method = method_names[keep],
8490
nsnp = vapply(res, function(x) x$nsnp, numeric(1)),
8591
b = vapply(res, function(x) x$b, numeric(1)),
8692
se = vapply(res, function(x) x$se, numeric(1)),

R/mr_mode.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ mr_mode <- function(dat, parameters = default_parameters(), mode_method = "all")
7878
#Set up a matrix to store the results from each bootstrap iteration
7979
beta.boot <- matrix(nrow = nboot, ncol = length(beta_Mode.in))
8080

81-
for (i in 1:nboot) {
81+
for (i in seq_len(nboot)) {
8282
BetaIV.boot <- BetaIV.boot_mat[i, ]
8383
BetaIV.boot_NOME <- BetaIV.boot_NOME_mat[i, ]
8484

R/multivariable_mr.R

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -498,15 +498,15 @@ mv_residual <- function(
498498
pval.exposure <- mvdat$exposure_pval
499499

500500
nexp <- ncol(beta.exposure)
501-
effs <- array(1:nexp)
502-
se <- array(1:nexp)
503-
pval <- array(1:nexp)
504-
nsnp <- array(1:nexp)
501+
effs <- numeric(nexp)
502+
se <- numeric(nexp)
503+
pval <- numeric(nexp)
504+
nsnp <- integer(nexp)
505505
marginal_outcome <- matrix(0, nrow(beta.exposure), ncol(beta.exposure))
506506
p <- list()
507507
nom <- colnames(beta.exposure)
508508
nom2 <- mvdat$expname$exposure[match(nom, mvdat$expname$id.exposure)]
509-
for (i in 1:nexp) {
509+
for (i in seq_len(nexp)) {
510510
# For this exposure, only keep SNPs that meet some p-value threshold
511511
index <- pval.exposure[, i] < pval_threshold
512512

@@ -607,15 +607,15 @@ mv_multiple <- function(
607607
w <- 1 / mvdat$outcome_se^2
608608

609609
nexp <- ncol(beta.exposure)
610-
effs <- array(1:nexp)
611-
se <- array(1:nexp)
612-
pval <- array(1:nexp)
613-
nsnp <- array(1:nexp)
610+
effs <- numeric(nexp)
611+
se <- numeric(nexp)
612+
pval <- numeric(nexp)
613+
nsnp <- integer(nexp)
614614
# marginal_outcome <- matrix(0, nrow(beta.exposure), ncol(beta.exposure))
615615
p <- list()
616616
nom <- colnames(beta.exposure)
617617
nom2 <- mvdat$expname$exposure[match(nom, mvdat$expname$id.exposure)]
618-
for (i in 1:nexp) {
618+
for (i in seq_len(nexp)) {
619619
# For this exposure, only keep SNPs that meet some p-value threshold
620620
index <- pval.exposure[, i] < pval_threshold
621621

@@ -704,15 +704,15 @@ mv_basic <- function(mvdat, pval_threshold = 5e-8) {
704704
pval.exposure <- mvdat$exposure_pval
705705

706706
nexp <- ncol(beta.exposure)
707-
effs <- array(1:nexp)
708-
se <- array(1:nexp)
709-
pval <- array(1:nexp)
710-
nsnp <- array(1:nexp)
707+
effs <- numeric(nexp)
708+
se <- numeric(nexp)
709+
pval <- numeric(nexp)
710+
nsnp <- integer(nexp)
711711
marginal_outcome <- matrix(0, nrow(beta.exposure), ncol(beta.exposure))
712712
p <- list()
713713
nom <- colnames(beta.exposure)
714714
nom2 <- mvdat$expname$exposure[match(nom, mvdat$expname$id.exposure)]
715-
for (i in 1:nexp) {
715+
for (i in seq_len(nexp)) {
716716
# For this exposure, only keep SNPs that meet some p-value threshold
717717
index <- pval.exposure[, i] < pval_threshold
718718

@@ -772,15 +772,15 @@ mv_ivw <- function(mvdat, pval_threshold = 5e-8) {
772772
w <- 1 / mvdat$outcome_se^2
773773

774774
nexp <- ncol(beta.exposure)
775-
effs <- array(1:nexp)
776-
se <- array(1:nexp)
777-
pval <- array(1:nexp)
778-
nsnp <- array(1:nexp)
775+
effs <- numeric(nexp)
776+
se <- numeric(nexp)
777+
pval <- numeric(nexp)
778+
nsnp <- integer(nexp)
779779
# marginal_outcome <- matrix(0, nrow(beta.exposure), ncol(beta.exposure))
780780
p <- list()
781781
nom <- colnames(beta.exposure)
782782
nom2 <- mvdat$expname$exposure[match(nom, mvdat$expname$id.exposure)]
783-
for (i in 1:nexp) {
783+
for (i in seq_len(nexp)) {
784784
# For this exposure, only keep SNPs that meet some p-value threshold
785785
index <- pval.exposure[, i] < pval_threshold
786786

0 commit comments

Comments
 (0)