Skip to content

Commit 07311dd

Browse files
authored
Merge pull request #669 from remlapmot/devel-2026-02
TwoSampleMR 0.6.30
2 parents bd4d9d5 + b1f4c70 commit 07311dd

24 files changed

Lines changed: 465 additions & 281 deletions

.github/workflows/check-full.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ jobs:
6161
shell: bash
6262
run: echo 'options(pkg.sysreqs_db_update_timeout = as.difftime(59, units = "secs"))' >> ~/.Rprofile
6363

64+
- name: Install gettext system dependency on macOS for robustbase package build from source
65+
if: runner.os == 'macOS'
66+
run: |
67+
brew install gettext
68+
mkdir -p ~/.R
69+
echo "CPPFLAGS=-I$(brew --prefix gettext)/include" >> ~/.R/Makevars
70+
echo "LDFLAGS=-L$(brew --prefix gettext)/lib" >> ~/.R/Makevars
71+
6472
- uses: r-lib/actions/setup-r-dependencies@v2
6573
with:
6674
extra-packages: >

DESCRIPTION

Lines changed: 1 addition & 2 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.6.29
4+
Version: 0.6.30
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")),
@@ -43,7 +43,6 @@ Imports:
4343
MRMix,
4444
MRPRESSO,
4545
pbapply,
46-
plyr,
4746
psych,
4847
RadialMR,
4948
reshape2,

NEWS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
# TwoSampleMR v0.6.30
2+
3+
(Release date 2026-02-06)
4+
5+
* Vectorised `mr_egger_regression_bootstrap()`
6+
* Vectorised `weighted_median_bootstrap()`
7+
* Deleted duplicated `weighted_median()` function
8+
* Replace **plyr** function calls with **data.table** function calls
9+
- `plyr::rbind.fill(...)` to `data.table::rbindlist(..., fill = TRUE, use.names = TRUE)`
10+
- `plyr::ddply(dat, cols, func)` to `lapply()` over unique combinations + `data.table::rbindlist()`
11+
- Added `data.table::setDF()` calls to convert back to data.frame for compatibility
12+
- And removed **plyr** from Imports list
13+
* In `flip_alleles()` use `chartr()` instead of 4 `gsub()` calls
14+
* In `random_string()` use single call to `sample()` instead of n calls
15+
* Optimized `mr_mode()`
16+
* Replaced `apply(..., any(is.na()))` with `complete.case()`
17+
* Optimized the `mr()` function
18+
* Optimized the `Optimize get_r_from_lor()` function
19+
* Optimized the `mr_rucker_bootstrap()` and `mr_rucker_jackknife_internal()` functions
20+
* Replaced `sapply()` with `vapply()` in several cases
21+
* Optimized the `simple_cap()` function
22+
* And a few other minor optimizations
23+
124
# TwoSampleMR v0.6.29
225

326
(Release date 2025-12-16)

R/add_rsq.r

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@
1111
#' @return data frame
1212
add_rsq <- function(dat) {
1313
if ("id.exposure" %in% names(dat)) {
14-
dat <- plyr::ddply(dat, c("id.exposure"), function(x) {
15-
add_rsq_one(x, "exposure")
14+
ids <- unique(dat$id.exposure)
15+
results <- lapply(ids, function(id) {
16+
add_rsq_one(dat[dat$id.exposure == id, ], "exposure")
1617
})
18+
dat <- data.table::rbindlist(results, fill = TRUE, use.names = TRUE)
19+
data.table::setDF(dat)
1720
}
1821
if ("id.outcome" %in% names(dat)) {
19-
dat <- plyr::ddply(dat, c("id.outcome"), function(x) {
20-
add_rsq_one(x, "outcome")
22+
ids <- unique(dat$id.outcome)
23+
results <- lapply(ids, function(id) {
24+
add_rsq_one(dat[dat$id.outcome == id, ], "outcome")
2125
})
26+
dat <- data.table::rbindlist(results, fill = TRUE, use.names = TRUE)
27+
data.table::setDF(dat)
2228
}
2329
return(dat)
2430
}
@@ -261,29 +267,22 @@ get_r_from_lor <- function(
261267
ncontrol <- rep(ncontrol, length(lor))
262268
}
263269

264-
nsnp <- length(lor)
265-
r <- array(NA, nsnp)
266-
for (i in 1:nsnp) {
267-
if (model == "logit") {
268-
ve <- pi^2 / 3
269-
} else if (model == "probit") {
270-
ve <- 1
271-
} else {
272-
stop("Model must be probit or logit")
273-
}
274-
popaf <- get_population_allele_frequency(
275-
af[i],
276-
ncase[i] / (ncase[i] + ncontrol[i]),
277-
exp(lor[i]),
278-
prevalence[i]
279-
)
280-
vg <- (lor[i])^2 * popaf * (1 - popaf)
281-
r[i] <- vg / (vg + ve)
282-
if (correction) {
283-
r[i] <- r[i] / 0.58
284-
}
285-
r[i] <- sqrt(r[i]) * sign(lor[i])
270+
if (model == "logit") {
271+
ve <- pi^2 / 3
272+
} else if (model == "probit") {
273+
ve <- 1
274+
} else {
275+
stop("Model must be probit or logit")
276+
}
277+
278+
prop <- ncase / (ncase + ncontrol)
279+
popaf <- get_population_allele_frequency(af, prop, exp(lor), prevalence)
280+
vg <- lor^2 * popaf * (1 - popaf)
281+
r <- vg / (vg + ve)
282+
if (correction) {
283+
r <- r / 0.58
286284
}
285+
r <- sqrt(r) * sign(lor)
287286
return(r)
288287
}
289288

R/backward_compatibility.R

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ ids_new_to_old <- function(id) {
44

55
ids_new_to_old2 <- function(id) {
66
id <- gsub("IEU-a-", "", id)
7-
id <- gsub("-a-", "-a:", id)
8-
id <- gsub("-b-", "-b:", id)
9-
id <- gsub("-c-", "-c:", id)
10-
id <- gsub("-d-", "-d:", id)
7+
id <- gsub("-([a-d])-", "-\\1:", id)
118
return(id)
129
}
1310

R/enrichment.R

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ enrichment_method_list <- function() {
3434
)
3535
)
3636
a <- lapply(a, as.data.frame)
37-
a <- plyr::rbind.fill(a)
37+
a <- data.table::rbindlist(a, fill = TRUE, use.names = TRUE)
38+
data.table::setDF(a)
3839
a <- as.data.frame(lapply(a, as.character), stringsAsFactors = FALSE)
3940
return(a)
4041
}
@@ -50,9 +51,10 @@ enrichment_method_list <- function() {
5051
#' @export
5152
#' @return data frame
5253
enrichment <- function(dat, method_list = enrichment_method_list()$obj) {
53-
res <- plyr::ddply(dat, c("id.exposure", "id.outcome"), function(x1) {
54-
# message("Performing enrichment analysis of '", x$id.exposure[1], "' on '", x$id.outcome[1], "'")
55-
54+
methl <- enrichment_method_list()
55+
combos <- unique(dat[, c("id.exposure", "id.outcome")])
56+
results <- lapply(seq_len(nrow(combos)), function(i) {
57+
x1 <- dat[dat$id.exposure == combos$id.exposure[i] & dat$id.outcome == combos$id.outcome[i], ]
5658
x <- subset(x1, !is.na(pval.outcome))
5759
if (nrow(x) == 0) {
5860
message(
@@ -67,16 +69,20 @@ enrichment <- function(dat, method_list = enrichment_method_list()$obj) {
6769
res <- lapply(method_list, function(meth) {
6870
get(meth)(x$pval.outcome)
6971
})
70-
methl <- enrichment_method_list()
7172
enrichment_tab <- data.frame(
73+
id.exposure = x1$id.exposure[1],
74+
id.outcome = x1$id.outcome[1],
7275
outcome = x$outcome[1],
7376
exposure = x$exposure[1],
7477
method = methl$name[methl$obj %in% method_list],
75-
nsnp = sapply(res, function(x) x$nsnp),
76-
pval = sapply(res, function(x) x$pval)
78+
nsnp = vapply(res, function(x) x$nsnp, numeric(1)),
79+
pval = vapply(res, function(x) x$pval, numeric(1)),
80+
stringsAsFactors = FALSE
7781
)
7882
enrichment_tab <- subset(enrichment_tab, !is.na(pval))
7983
return(enrichment_tab)
8084
})
85+
res <- data.table::rbindlist(results, fill = TRUE, use.names = TRUE)
86+
data.table::setDF(res)
8187
return(res)
8288
}

R/eve.R

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,10 +327,13 @@ mr_wrapper_single <- function(dat, parameters = default_parameters()) {
327327
#' @export
328328
#' @return list
329329
mr_wrapper <- function(dat, parameters = default_parameters()) {
330-
plyr::dlply(dat, c("id.exposure", "id.outcome"), function(x) {
330+
combos <- unique(dat[, c("id.exposure", "id.outcome")])
331+
res <- lapply(seq_len(nrow(combos)), function(i) {
332+
x <- dat[dat$id.exposure == combos$id.exposure[i] & dat$id.outcome == combos$id.outcome[i], ]
331333
message("Performing MR analysis of '", x$id.exposure[1], "' on '", x$id.outcome[1], "'")
332334
d <- subset(x, mr_keep)
333-
o <- mr_wrapper_single(d, parameters = parameters)
334-
o
335+
mr_wrapper_single(d, parameters = parameters)
335336
})
337+
names(res) <- paste(combos$id.exposure, combos$id.outcome, sep = ".")
338+
res
336339
}

R/forest_plot2.R

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ format_mr_results <- function(
101101

102102
# Fill in missing values
103103
exps <- unique(dat$exposure)
104-
dat <- plyr::ddply(dat, c("outcome"), function(x) {
105-
x <- plyr::mutate(x)
106-
nc <- ncol(x)
104+
outcomes <- unique(dat$outcome)
105+
results <- lapply(outcomes, function(out_val) {
106+
x <- dat[dat$outcome == out_val, ]
107107
missed <- exps[!exps %in% x$exposure]
108108
if (length(missed) >= 1) {
109109
out <- unique(x$outcome)
@@ -116,10 +116,13 @@ format_mr_results <- function(
116116
sample_size = n,
117117
stringsAsFactors = FALSE
118118
)
119-
x <- plyr::rbind.fill(x, md)
119+
x <- data.table::rbindlist(list(x, md), fill = TRUE, use.names = TRUE)
120+
data.table::setDF(x)
120121
}
121122
return(x)
122123
})
124+
dat <- data.table::rbindlist(results, fill = TRUE, use.names = TRUE)
125+
data.table::setDF(dat)
123126
# dat <- dplyr::group_by(dat, outcome) %>%
124127
# dplyr::do({
125128
# x <- .
@@ -162,11 +165,8 @@ format_mr_results <- function(
162165
#' @keywords internal
163166
#' @return Character or array of character
164167
simple_cap <- function(x) {
165-
sapply(x, function(x) {
166-
x <- tolower(x)
167-
s <- strsplit(x, " ")[[1]]
168-
paste(toupper(substring(s, 1, 1)), substring(s, 2), sep = "", collapse = " ")
169-
})
168+
x <- tolower(x)
169+
gsub("\\b(\\w)", "\\U\\1", x, perl = TRUE)
170170
}
171171

172172
#' Trim function to remove leading and trailing blank spaces

R/format_mr_results2.R

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -146,32 +146,9 @@ combine_all_mrresults <- function(
146146
het <- het[, c("id.exposure", "id.outcome", "method", "Q", "Q_df", "Q_pval")]
147147

148148
# Convert all factors to character
149-
# lapply(names(Res), FUN=function(x) class(Res[,x]))
150-
Class <- unlist(lapply(names(res), FUN = function(x) class(res[, x])))
151-
if (any(Class == "factor")) {
152-
Pos <- which(unlist(lapply(names(res), FUN = function(x) class(res[, x]))) == "factor")
153-
for (i in seq_along(Pos)) {
154-
res[, Pos[i]] <- as.character(res[, Pos[i]])
155-
}
156-
}
157-
158-
# lapply(names(Het), FUN=function(x) class(Het[,x]))
159-
Class <- unlist(lapply(names(het), FUN = function(x) class(het[, x])))
160-
if (any(Class == "factor")) {
161-
Pos <- which(unlist(lapply(names(het), FUN = function(x) class(het[, x]))) == "factor")
162-
for (i in seq_along(Pos)) {
163-
het[, Pos[i]] <- as.character(het[, Pos[i]])
164-
}
165-
}
166-
167-
# lapply(names(Sin), FUN=function(x) class(Sin[,x]))
168-
Class <- unlist(lapply(names(sin), FUN = function(x) class(sin[, x])))
169-
if (any(Class == "factor")) {
170-
Pos <- which(unlist(lapply(names(sin), FUN = function(x) class(sin[, x]))) == "factor")
171-
for (i in seq_along(Pos)) {
172-
sin[, Pos[i]] <- as.character(sin[, Pos[i]])
173-
}
174-
}
149+
res[] <- lapply(res, function(x) if (is.factor(x)) as.character(x) else x)
150+
het[] <- lapply(het, function(x) if (is.factor(x)) as.character(x) else x)
151+
sin[] <- lapply(sin, function(x) if (is.factor(x)) as.character(x) else x)
175152

176153
sin <- sin[grep("[:0-9:]", sin$SNP), ]
177154
sin$method <- "Wald ratio"
@@ -185,10 +162,11 @@ combine_all_mrresults <- function(
185162
names(sin)[names(sin) == "method"] <- "Method"
186163

187164
res <- merge(res, het, by = c("id.outcome", "id.exposure", "Method"), all.x = TRUE)
188-
res <- plyr::rbind.fill(
189-
res,
190-
sin[, c("exposure", "outcome", "id.exposure", "id.outcome", "SNP", "b", "se", "pval", "Method")]
165+
res <- data.table::rbindlist(
166+
list(res, sin[, c("exposure", "outcome", "id.exposure", "id.outcome", "SNP", "b", "se", "pval", "Method")]),
167+
fill = TRUE, use.names = TRUE
191168
)
169+
data.table::setDF(res)
192170

193171
if (ao_slc) {
194172
ao <- available_outcomes()

R/harmonise.R

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ harmonise_data <- function(exposure_dat, outcome_dat, action = 2) {
9696
) -
9797
nrow(x)
9898

99-
x$mr_keep[apply(x[, mr_cols], 1, function(y) any(is.na(y)))] <- FALSE
99+
x$mr_keep[!complete.cases(x[, mr_cols])] <- FALSE
100100
attr(x, "log")[["total_variants"]] <- nrow(x)
101101
attr(x, "log")[["total_variants_for_mr"]] <- sum(x$mr_keep)
102102
attr(x, "log")[["proxy_variants"]] <- ifelse(
@@ -114,8 +114,10 @@ harmonise_data <- function(exposure_dat, outcome_dat, action = 2) {
114114
# return(x)
115115
# })
116116

117-
jlog <- plyr::rbind.fill(lapply(fix.tab, function(x) attr(x, "log")))
118-
fix.tab <- plyr::rbind.fill(fix.tab)
117+
jlog <- data.table::rbindlist(lapply(fix.tab, function(x) attr(x, "log")), fill = TRUE, use.names = TRUE)
118+
data.table::setDF(jlog)
119+
fix.tab <- data.table::rbindlist(fix.tab, fill = TRUE, use.names = TRUE)
120+
data.table::setDF(fix.tab)
119121
attr(fix.tab, "log") <- jlog
120122

121123
# fix.tab <- harmonise_make_snp_effects_positive(fix.tab)
@@ -171,12 +173,9 @@ check_palindromic <- function(A1, A2) {
171173

172174

173175
flip_alleles <- function(x) {
174-
x <- toupper(x)
175-
x <- gsub("C", "g", x)
176-
x <- gsub("G", "c", x)
177-
x <- gsub("A", "t", x)
178-
x <- gsub("T", "a", x)
179-
return(toupper(x))
176+
# Use chartr for efficient single-pass character substitution
177+
# This is much faster than 4 sequential gsub calls
178+
chartr("ACGTacgt", "TGCAtgca", x)
180179
}
181180

182181

@@ -680,12 +679,16 @@ harmonise <- function(dat, tolerance, action) {
680679
action
681680
)
682681

683-
jlog <- plyr::rbind.fill(
684-
as.data.frame(attr(d22, "log"), stringsAsFactors = FALSE),
685-
as.data.frame(attr(d21, "log"), stringsAsFactors = FALSE),
686-
as.data.frame(attr(d12, "log"), stringsAsFactors = FALSE),
687-
as.data.frame(attr(d11, "log"), stringsAsFactors = FALSE)
682+
jlog <- data.table::rbindlist(
683+
list(
684+
as.data.frame(attr(d22, "log"), stringsAsFactors = FALSE),
685+
as.data.frame(attr(d21, "log"), stringsAsFactors = FALSE),
686+
as.data.frame(attr(d12, "log"), stringsAsFactors = FALSE),
687+
as.data.frame(attr(d11, "log"), stringsAsFactors = FALSE)
688+
),
689+
fill = TRUE, use.names = TRUE
688690
)
691+
data.table::setDF(jlog)
689692
jlog <- cbind(
690693
data.frame(
691694
id.exposure = dat$id.exposure[1],

0 commit comments

Comments
 (0)