From 03cc34fe19d69cecd447efcda3b76c957325ed12 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:37:07 +0100 Subject: [PATCH 01/36] run checks monthly --- .github/workflows/R-CMD-check.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 6117b1c..ad0c65b 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -4,7 +4,7 @@ on: branches: - master schedule: - - cron: '0 8 * * 5' + - cron: '0 8 1 * *' name: R-CMD-check @@ -21,7 +21,6 @@ jobs: config: - { os: windows-latest, r: 'release'} - { os: macOS-latest, r: 'release'} - #- { os: ubuntu-16.04, r: 'release', bioc: 'release', cran: "https://demo.rstudiopm.com/all/__linux__/xenial/latest"} - { os: ubuntu-latest, r: 'release'} env: From 40a360c25786cc73a4f8dd2f1097917fc81ec673 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:37:28 +0100 Subject: [PATCH 02/36] rename branch --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index ad0c65b..ad35fcf 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -2,7 +2,7 @@ on: push: pull_request: branches: - - master + - main schedule: - cron: '0 8 1 * *' From fb162483f6c6c77ff43f3740d09dc3fd6b27ab4c Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:59:29 +0100 Subject: [PATCH 03/36] add basic unit tests --- DESCRIPTION | 4 +++- tests/testthat.R | 12 ++++++++++ tests/testthat/test-equality.R | 42 +++++++++++++++++++++++++++++++++ tests/testthat/test-existence.R | 35 +++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-equality.R create mode 100644 tests/testthat/test-existence.R diff --git a/DESCRIPTION b/DESCRIPTION index 4c63dfc..2680af9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -37,6 +37,8 @@ Imports: magrittr Suggests: knitr, - rmarkdown + rmarkdown, + testthat (>= 3.0.0) VignetteBuilder: knitr URL: https://stopsack.github.io/batchtma, https://github.com/stopsack/batchtma +Config/testthat/edition: 3 diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..ac0e094 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(batchtma) + +test_check("batchtma") diff --git a/tests/testthat/test-equality.R b/tests/testthat/test-equality.R new file mode 100644 index 0000000..6ca325a --- /dev/null +++ b/tests/testthat/test-equality.R @@ -0,0 +1,42 @@ +test_that("equivalent approaches give same result", { + df <- data.frame( + tma = rep(1:2, times = 10), + biomarker = rep(1:2, times = 10) + + runif(max = 5, n = 20), + confounder = rep(0:1, times = 10) + + runif(max = 10, n = 20), + unity = 1 + ) + + df_adj2 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = simple, + suffix = "adj" + ) |> # drop all attributes + data.frame() + + df_adj3 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = standardize, + confounders = unity, + suffix = "adj" + ) |> + data.frame() + + df_adj4 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = ipw, + confounders = unity, + suffix = "adj" + ) |> + data.frame() + + expect_equal(df_adj2, df_adj3) + expect_equal(df_adj2, df_adj4) +}) diff --git a/tests/testthat/test-existence.R b/tests/testthat/test-existence.R new file mode 100644 index 0000000..826d553 --- /dev/null +++ b/tests/testthat/test-existence.R @@ -0,0 +1,35 @@ +test_that("new variables are generated", { + df <- data.frame( + tma = rep(1:2, times = 10), + biomarker = rep(1:2, times = 10) + + runif(max = 5, n = 20), + confounder = rep(0:1, times = 10) + + runif(max = 10, n = 20) + ) + + df_adj5 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = quantreg, + suffix = "adj" + ) + + expect_equal( + object = sum(!is.na(df_adj5$biomarkeradj)), + expected = sum(!is.na(df_adj5$biomarker)) + ) + + df_adj6 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = quantnorm, + suffix = "adj" + ) + + expect_equal( + object = sum(!is.na(df_adj6$biomarkeradj)), + expected = sum(!is.na(df_adj6$biomarker)) + ) +}) From d6d47361ec133017598843eaa5efa21a237fc46c Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:59:49 +0100 Subject: [PATCH 04/36] increase dev version no --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 2680af9..fbffdf3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: batchtma Title: Batch Effect Adjustments -Version: 0.1.7 +Version: 0.1.7-9000 Authors@R: c(person(given = "Konrad", family = "Stopsack", From 705ad41891693bb6b4348e52a67c29bf37723143 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:04:09 +0100 Subject: [PATCH 05/36] move functions to separate .R files --- R/adjust_batch.R | 414 -------------------------------------- R/batch_quantnorm.R | 31 +++ R/batch_rq.R | 103 ++++++++++ R/batchmean_ipw.R | 154 ++++++++++++++ R/batchmean_simple.R | 29 +++ R/batchmean_standardize.R | 74 +++++++ R/factor_drop.R | 14 ++ 7 files changed, 405 insertions(+), 414 deletions(-) create mode 100644 R/batch_quantnorm.R create mode 100644 R/batch_rq.R create mode 100644 R/batchmean_ipw.R create mode 100644 R/batchmean_simple.R create mode 100644 R/batchmean_standardize.R create mode 100644 R/factor_drop.R diff --git a/R/adjust_batch.R b/R/adjust_batch.R index 035cd6f..1b07e0e 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -2,420 +2,6 @@ ## quiets concerns of R CMD check re: the .'s that appear in pipelines if (getRversion() >= "2.15.1") utils::globalVariables(c(".")) -#' Drop empty factor levels -#' -#' @description -#' Avoids predict() issues. This is \code{forcats::fct_drop()} -#' without bells and whistles. -#' -#' @param f factor -#' -#' @return Factor -#' @noRd -factor_drop <- function(f) { - factor_levels <- levels(f) - factor(f, levels = setdiff(factor_levels, factor_levels[table(f) == 0])) -} - - -#' Batch means for approach 2: Unadjusted means -#' -#' @param data Data set -#' @param markers Biomarkers to adjust -#' -#' @importFrom magrittr %>% -#' @importFrom rlang .data -#' @return Tibble of means per marker and batch -#' @noRd -batchmean_simple <- function(data, markers) { - values <- data %>% - dplyr::select(.data$.id, .data$.batchvar, {{ markers }}) %>% - dplyr::group_by(.data$.batchvar) %>% - dplyr::summarize_at( - .vars = dplyr::vars(-.data$.id), - .funs = mean, - na.rm = TRUE - ) %>% - dplyr::mutate_at( - .vars = dplyr::vars(-.data$.batchvar), - .funs = ~ . - mean(., na.rm = TRUE) - ) %>% - tidyr::pivot_longer( - col = c(-.data$.batchvar), - names_to = "marker", - values_to = "batchmean" - ) - return(list(list(values = values, models = NULL))) -} - -#' Batch means for approach 3: Marginal standardization -#' -#' @param data Data set -#' @param markers Vector of variables to batch-adjust -#' @param confounders Confounders: features that differ -#' between batches that should be retained -#' -#' @return Tibble with conditional means per marker and batch -#' @noRd -batchmean_standardize <- function(data, markers, confounders) { - res <- data %>% - tidyr::pivot_longer( - cols = {{ markers }}, - names_to = "marker", - values_to = "value" - ) %>% - dplyr::filter(!is.na(.data$value)) %>% - dplyr::group_by(.data$marker) %>% - tidyr::nest(data = c(-.data$marker)) %>% - dplyr::mutate( - data = purrr::map( - .x = .data$data, - .f = ~ .x %>% - dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) - ), - model = purrr::map( - .x = .data$data, - .f = ~ stats::lm( - formula = stats::as.formula(paste0( - "value ~ .batchvar +", - paste( - confounders, - collapse = " + ", - sep = " + " - ) - )), - data = .x - ) - ), - .batchvar = purrr::map( - .x = .data$data, - .f = ~ .x %>% - dplyr::pull(.data$.batchvar) %>% - levels() - ) - ) - - values <- res %>% - tidyr::unnest(cols = .data$.batchvar) %>% - dplyr::mutate( - data = purrr::map2( - .x = .data$data, - .y = .data$.batchvar, - .f = ~ .x %>% dplyr::mutate(.batchvar = .y) - ), - pred = purrr::map2(.x = .data$model, .y = .data$data, .f = stats::predict) - ) %>% - dplyr::select(.data$marker, .data$.batchvar, .data$pred) %>% - tidyr::unnest(cols = .data$pred) %>% - dplyr::group_by(.data$marker, .data$.batchvar) %>% - dplyr::summarize(batchmean = mean(.data$pred, na.rm = TRUE)) %>% - dplyr::group_by(.data$marker) %>% - dplyr::mutate(markermean = mean(.data$batchmean)) %>% - dplyr::ungroup() %>% - dplyr::transmute( - marker = .data$marker, - .batchvar = .data$.batchvar, - batchmean = .data$batchmean - .data$markermean - ) - return(list(list( - models = res %>% dplyr::ungroup() %>% dplyr::pull("model"), - values = values - ))) -} - -#' Batch means for approach 4: IPW -#' -#' @param data Data set -#' @param markers Variables to batch-adjust -#' @param confounders Confounders: features that differ -#' @param truncate Lower and upper extreme quantiles to -#' truncate stabilized weights at. Defaults to c(0.025, 0.975). -#' -#' @return Tibble of batch means per batch and marker -#' @noRd -batchmean_ipw <- function( - data, - markers, - confounders, - truncate = c(0.025, 0.975) -) { - ipwbatch <- function(data, variable, confounders, truncate) { - data <- data %>% - dplyr::rename(variable = dplyr::one_of(variable)) %>% - dplyr::filter(!is.na(.data$variable)) %>% - dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) - - res <- data %>% - tidyr::nest(data = dplyr::everything()) %>% - dplyr::mutate( - num = purrr::map( - .x = .data$data, - .f = ~ nnet::multinom( - formula = .batchvar ~ 1, - data = .x, - trace = FALSE - ) - ), - den = purrr::map( - .x = .data$data, - .f = ~ nnet::multinom( - formula = stats::as.formula( - paste(".batchvar ~", confounders) - ), - data = .x, - trace = FALSE - ) - ) - ) - - values <- res %>% - dplyr::mutate_at( - .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map(.x = ., .f = stats::predict, type = "probs") %>% - purrr::map(.x = ., .f = tibble::as_tibble) %>% - purrr::map2( - .x = ., - .y = .data$data, - .f = ~ .x %>% - dplyr::mutate(.batchvar = .y %>% - purrr::pluck(".batchvar")) - ) - ) - - # multinom()$fitted.values is just a vector of probabilities for - # the 2nd outcome level if there are only two levels - if (length(levels(factor(data$.batchvar))) == 2) { - values <- values %>% - dplyr::mutate_at( - .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map(.x = ., .f = ~ .x %>% - dplyr::mutate( - probs = dplyr::if_else( - .data$.batchvar == - levels(factor(.data$.batchvar))[1], - true = 1 - .data$value, - false = .data$value - ) - ) %>% - dplyr::pull(.data$probs)) - ) - # otherwise probabilities are a data frame - } else { - values <- values %>% - dplyr::mutate_at( - .vars = dplyr::vars(.data$num, .data$den), - .funs = - ~ purrr::map( - .x = ., - .f = ~ .x %>% - tidyr::pivot_longer( - -.data$.batchvar, - names_to = "batch", - values_to = "prob" - ) %>% - dplyr::filter(.data$batch == .data$.batchvar) %>% - dplyr::pull(.data$prob) - ) - ) - } - - values <- values %>% - tidyr::unnest(cols = c(.data$data, .data$num, .data$den)) %>% - dplyr::mutate( - sw = .data$num / .data$den, - trunc = dplyr::case_when( - .data$sw < stats::quantile(.data$sw, truncate[1]) ~ - stats::quantile(.data$sw, truncate[1]), - .data$sw > stats::quantile(.data$sw, truncate[2]) ~ - stats::quantile(.data$sw, truncate[2]), - TRUE ~ .data$sw - ) - ) - - xlev <- unique(data %>% dplyr::pull(.data$.batchvar)) - - values <- geepack::geeglm( - formula = variable ~ .batchvar, - data = values, - weights = values$trunc, - id = values$.id, - corstr = "independence" - ) %>% - broom::tidy() %>% - dplyr::filter(!stringr::str_detect( - string = .data$term, - pattern = "(Intercept)" - )) %>% - dplyr::mutate(term = as.character( - stringr::str_remove_all( - string = .data$term, - pattern = ".batchvar" - ) - )) %>% - dplyr::full_join(tibble::tibble(term = as.character(xlev)), by = "term") %>% - dplyr::mutate( - estimate = dplyr::if_else( - is.na(.data$estimate), - true = 0, - false = .data$estimate - ), - estimate = .data$estimate - mean(.data$estimate), - marker = variable, - term = .data$term - ) %>% - dplyr::arrange(.data$term) %>% - dplyr::select(.data$marker, .batchvar = .data$term, batchmean = .data$estimate) - list(values = values, models = res %>% dplyr::pull(.data$den)) - } - - purrr::map( - .x = data %>% dplyr::select({{ markers }}) %>% names(), - .f = ipwbatch, - data = data %>% - dplyr::filter(dplyr::across(dplyr::all_of(confounders), ~ !is.na(.x))), - truncate = truncate, - confounders = paste(confounders, sep = " + ", collapse = " + ") - ) -} - - -#' Quantiles for approach 5: Quantile regression -#' -#' @param data Data set -#' @param variable Single variable to batch-adjust -#' @param confounders Confounders: features that differ -#' @param tau Quantiles to use for scaling -#' @param rq_method Algorithmic method to fit quantile regression. -#' -#' @return Tibble of quantiles per batch -#' @noRd -batchrq <- function(data, variable, confounders, tau, rq_method) { - res <- data %>% - dplyr::rename(variable = {{ variable }}) %>% - dplyr::filter(!is.na(.data$variable)) %>% - dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) %>% - tidyr::nest(data = dplyr::everything()) %>% - dplyr::mutate( - un = purrr::map( - .x = .data$data, - .f = ~ quantreg::rq( - formula = variable ~ .batchvar, - data = .x, - tau = tau, - method = rq_method - ) - ), - ad = purrr::map( - .x = .data$data, - .f = ~ quantreg::rq( - formula = stats::reformulate( - response = "variable", - termlabels = c(".batchvar", confounders) - ), - data = .x, - tau = tau, - method = rq_method - ) - ), - .batchvar = purrr::map( - .x = .data$data, - .f = ~ .x %>% - dplyr::pull(.data$.batchvar) %>% - levels() - ) - ) - - values <- res %>% - tidyr::unnest(cols = .data$.batchvar) %>% - dplyr::mutate( - data = purrr::map2( - .x = .data$data, - .y = .data$.batchvar, - .f = ~ .x %>% dplyr::mutate(.batchvar = .y) - ), - un = purrr::map2(.x = .data$un, .y = .data$data, .f = stats::predict), - ad = purrr::map2(.x = .data$ad, .y = .data$data, .f = stats::predict), - un = purrr::map( - .x = .data$un, - .f = tibble::as_tibble, - .name_repair = ~ c("un_lo", "un_hi") - ), - ad = purrr::map( - .x = .data$ad, - .f = tibble::as_tibble, - .name_repair = ~ c("ad_lo", "ad_hi") - ), - all_lo = purrr::map_dbl( - .x = .data$data, - .f = ~ stats::quantile(.x$variable, probs = 0.25) - ), - all_hi = purrr::map_dbl( - .x = .data$data, - .f = ~ stats::quantile(.x$variable, probs = 0.75) - ), - all_iq = .data$all_hi - .data$all_lo - ) %>% - dplyr::select( - .data$.batchvar, - .data$un, - .data$ad, - .data$all_lo, - .data$all_hi, - .data$all_iq - ) %>% - tidyr::unnest(cols = c(.data$un, .data$ad)) %>% - dplyr::group_by(.data$.batchvar) %>% - dplyr::summarize( - un_lo = stats::quantile(.data$un_lo, probs = 0.25), - ad_lo = stats::quantile(.data$ad_lo, probs = 0.25), - un_hi = stats::quantile(.data$un_hi, probs = 0.75), - ad_hi = stats::quantile(.data$ad_hi, probs = 0.75), - all_lo = stats::median(.data$all_lo), - all_iq = stats::median(.data$all_iq) - ) %>% - dplyr::mutate( - un_iq = .data$un_hi - .data$un_lo, - ad_iq = .data$ad_hi - .data$ad_lo, - marker = {{ variable }} - ) - - models <- res %>% dplyr::pull(.data$ad) - return(tibble::lst(values, models)) -} - -# -#' Helper function for approach 6: Quantile normalize -#' -#' @param var Single variable to quantile-normalize -#' @param batch Variable indicating batch -#' -#' @return Tibble of means per batch for one variable -#' @noRd -batch_quantnorm <- function(var, batch) { - tibble::tibble(var, batch) %>% - tibble::rowid_to_column() %>% - tidyr::pivot_wider(names_from = batch, values_from = var) %>% - dplyr::select(-.data$rowid) %>% - dplyr::select_if(~ !all(is.na(.))) %>% - as.matrix() %>% - limma::normalizeQuantiles() %>% - tibble::as_tibble() %>% - dplyr::transmute( - result = purrr::pmap_dbl( - .l = ., - .f = function(...) { - mean(c(...), na.rm = TRUE) - } - ), - result = dplyr::if_else( - is.nan(.data$result), - true = NA_real_, - false = .data$result - ) - ) %>% - dplyr::pull(.data$result) -} - #' Adjust for batch effects #' #' @description diff --git a/R/batch_quantnorm.R b/R/batch_quantnorm.R new file mode 100644 index 0000000..aa8c1bf --- /dev/null +++ b/R/batch_quantnorm.R @@ -0,0 +1,31 @@ +#' Helper function for approach 6: Quantile normalize +#' +#' @param var Single variable to quantile-normalize +#' @param batch Variable indicating batch +#' +#' @return Tibble of means per batch for one variable +#' @noRd +batch_quantnorm <- function(var, batch) { + tibble::tibble(var, batch) %>% + tibble::rowid_to_column() %>% + tidyr::pivot_wider(names_from = batch, values_from = var) %>% + dplyr::select(-.data$rowid) %>% + dplyr::select_if(~ !all(is.na(.))) %>% + as.matrix() %>% + limma::normalizeQuantiles() %>% + tibble::as_tibble() %>% + dplyr::transmute( + result = purrr::pmap_dbl( + .l = ., + .f = function(...) { + mean(c(...), na.rm = TRUE) + } + ), + result = dplyr::if_else( + is.nan(.data$result), + true = NA_real_, + false = .data$result + ) + ) %>% + dplyr::pull(.data$result) +} diff --git a/R/batch_rq.R b/R/batch_rq.R new file mode 100644 index 0000000..f847946 --- /dev/null +++ b/R/batch_rq.R @@ -0,0 +1,103 @@ +#' Quantiles for approach 5: Quantile regression +#' +#' @param data Data set +#' @param variable Single variable to batch-adjust +#' @param confounders Confounders: features that differ +#' @param tau Quantiles to use for scaling +#' @param rq_method Algorithmic method to fit quantile regression. +#' +#' @return Tibble of quantiles per batch +#' @noRd +batchrq <- function(data, variable, confounders, tau, rq_method) { + res <- data %>% + dplyr::rename(variable = {{ variable }}) %>% + dplyr::filter(!is.na(.data$variable)) %>% + dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) %>% + tidyr::nest(data = dplyr::everything()) %>% + dplyr::mutate( + un = purrr::map( + .x = .data$data, + .f = ~ quantreg::rq( + formula = variable ~ .batchvar, + data = .x, + tau = tau, + method = rq_method + ) + ), + ad = purrr::map( + .x = .data$data, + .f = ~ quantreg::rq( + formula = stats::reformulate( + response = "variable", + termlabels = c(".batchvar", confounders) + ), + data = .x, + tau = tau, + method = rq_method + ) + ), + .batchvar = purrr::map( + .x = .data$data, + .f = ~ .x %>% + dplyr::pull(.data$.batchvar) %>% + levels() + ) + ) + + values <- res %>% + tidyr::unnest(cols = .data$.batchvar) %>% + dplyr::mutate( + data = purrr::map2( + .x = .data$data, + .y = .data$.batchvar, + .f = ~ .x %>% dplyr::mutate(.batchvar = .y) + ), + un = purrr::map2(.x = .data$un, .y = .data$data, .f = stats::predict), + ad = purrr::map2(.x = .data$ad, .y = .data$data, .f = stats::predict), + un = purrr::map( + .x = .data$un, + .f = tibble::as_tibble, + .name_repair = ~ c("un_lo", "un_hi") + ), + ad = purrr::map( + .x = .data$ad, + .f = tibble::as_tibble, + .name_repair = ~ c("ad_lo", "ad_hi") + ), + all_lo = purrr::map_dbl( + .x = .data$data, + .f = ~ stats::quantile(.x$variable, probs = 0.25) + ), + all_hi = purrr::map_dbl( + .x = .data$data, + .f = ~ stats::quantile(.x$variable, probs = 0.75) + ), + all_iq = .data$all_hi - .data$all_lo + ) %>% + dplyr::select( + .data$.batchvar, + .data$un, + .data$ad, + .data$all_lo, + .data$all_hi, + .data$all_iq + ) %>% + tidyr::unnest(cols = c(.data$un, .data$ad)) %>% + dplyr::group_by(.data$.batchvar) %>% + dplyr::summarize( + un_lo = stats::quantile(.data$un_lo, probs = 0.25), + ad_lo = stats::quantile(.data$ad_lo, probs = 0.25), + un_hi = stats::quantile(.data$un_hi, probs = 0.75), + ad_hi = stats::quantile(.data$ad_hi, probs = 0.75), + all_lo = stats::median(.data$all_lo), + all_iq = stats::median(.data$all_iq) + ) %>% + dplyr::mutate( + un_iq = .data$un_hi - .data$un_lo, + ad_iq = .data$ad_hi - .data$ad_lo, + marker = {{ variable }} + ) + + models <- res %>% dplyr::pull(.data$ad) + return(tibble::lst(values, models)) +} diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R new file mode 100644 index 0000000..e0d8216 --- /dev/null +++ b/R/batchmean_ipw.R @@ -0,0 +1,154 @@ +#' Batch means for approach 4: IPW +#' +#' @param data Data set +#' @param markers Variables to batch-adjust +#' @param confounders Confounders: features that differ +#' @param truncate Lower and upper extreme quantiles to +#' truncate stabilized weights at. Defaults to c(0.025, 0.975). +#' +#' @return Tibble of batch means per batch and marker +#' @noRd +batchmean_ipw <- function( + data, + markers, + confounders, + truncate = c(0.025, 0.975) +) { + ipwbatch <- function(data, variable, confounders, truncate) { + data <- data %>% + dplyr::rename(variable = dplyr::one_of(variable)) %>% + dplyr::filter(!is.na(.data$variable)) %>% + dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) + + res <- data %>% + tidyr::nest(data = dplyr::everything()) %>% + dplyr::mutate( + num = purrr::map( + .x = .data$data, + .f = ~ nnet::multinom( + formula = .batchvar ~ 1, + data = .x, + trace = FALSE + ) + ), + den = purrr::map( + .x = .data$data, + .f = ~ nnet::multinom( + formula = stats::as.formula( + paste(".batchvar ~", confounders) + ), + data = .x, + trace = FALSE + ) + ) + ) + + values <- res %>% + dplyr::mutate_at( + .vars = dplyr::vars(.data$num, .data$den), + .funs = ~ purrr::map(.x = ., .f = stats::predict, type = "probs") %>% + purrr::map(.x = ., .f = tibble::as_tibble) %>% + purrr::map2( + .x = ., + .y = .data$data, + .f = ~ .x %>% + dplyr::mutate(.batchvar = .y %>% + purrr::pluck(".batchvar")) + ) + ) + + # multinom()$fitted.values is just a vector of probabilities for + # the 2nd outcome level if there are only two levels + if (length(levels(factor(data$.batchvar))) == 2) { + values <- values %>% + dplyr::mutate_at( + .vars = dplyr::vars(.data$num, .data$den), + .funs = ~ purrr::map(.x = ., .f = ~ .x %>% + dplyr::mutate( + probs = dplyr::if_else( + .data$.batchvar == + levels(factor(.data$.batchvar))[1], + true = 1 - .data$value, + false = .data$value + ) + ) %>% + dplyr::pull(.data$probs)) + ) + # otherwise probabilities are a data frame + } else { + values <- values %>% + dplyr::mutate_at( + .vars = dplyr::vars(.data$num, .data$den), + .funs = + ~ purrr::map( + .x = ., + .f = ~ .x %>% + tidyr::pivot_longer( + -.data$.batchvar, + names_to = "batch", + values_to = "prob" + ) %>% + dplyr::filter(.data$batch == .data$.batchvar) %>% + dplyr::pull(.data$prob) + ) + ) + } + + values <- values %>% + tidyr::unnest(cols = c(.data$data, .data$num, .data$den)) %>% + dplyr::mutate( + sw = .data$num / .data$den, + trunc = dplyr::case_when( + .data$sw < stats::quantile(.data$sw, truncate[1]) ~ + stats::quantile(.data$sw, truncate[1]), + .data$sw > stats::quantile(.data$sw, truncate[2]) ~ + stats::quantile(.data$sw, truncate[2]), + TRUE ~ .data$sw + ) + ) + + xlev <- unique(data %>% dplyr::pull(.data$.batchvar)) + + values <- geepack::geeglm( + formula = variable ~ .batchvar, + data = values, + weights = values$trunc, + id = values$.id, + corstr = "independence" + ) %>% + broom::tidy() %>% + dplyr::filter(!stringr::str_detect( + string = .data$term, + pattern = "(Intercept)" + )) %>% + dplyr::mutate(term = as.character( + stringr::str_remove_all( + string = .data$term, + pattern = ".batchvar" + ) + )) %>% + dplyr::full_join(tibble::tibble(term = as.character(xlev)), by = "term") %>% + dplyr::mutate( + estimate = dplyr::if_else( + is.na(.data$estimate), + true = 0, + false = .data$estimate + ), + estimate = .data$estimate - mean(.data$estimate), + marker = variable, + term = .data$term + ) %>% + dplyr::arrange(.data$term) %>% + dplyr::select(.data$marker, .batchvar = .data$term, batchmean = .data$estimate) + list(values = values, models = res %>% dplyr::pull(.data$den)) + } + + purrr::map( + .x = data %>% dplyr::select({{ markers }}) %>% names(), + .f = ipwbatch, + data = data %>% + dplyr::filter(dplyr::across(dplyr::all_of(confounders), ~ !is.na(.x))), + truncate = truncate, + confounders = paste(confounders, sep = " + ", collapse = " + ") + ) +} diff --git a/R/batchmean_simple.R b/R/batchmean_simple.R new file mode 100644 index 0000000..ff8af67 --- /dev/null +++ b/R/batchmean_simple.R @@ -0,0 +1,29 @@ +#' Batch means for approach 2: Unadjusted means +#' +#' @param data Data set +#' @param markers Biomarkers to adjust +#' +#' @importFrom magrittr %>% +#' @importFrom rlang .data +#' @return Tibble of means per marker and batch +#' @noRd +batchmean_simple <- function(data, markers) { + values <- data %>% + dplyr::select(.data$.id, .data$.batchvar, {{ markers }}) %>% + dplyr::group_by(.data$.batchvar) %>% + dplyr::summarize_at( + .vars = dplyr::vars(-.data$.id), + .funs = mean, + na.rm = TRUE + ) %>% + dplyr::mutate_at( + .vars = dplyr::vars(-.data$.batchvar), + .funs = ~ . - mean(., na.rm = TRUE) + ) %>% + tidyr::pivot_longer( + col = c(-.data$.batchvar), + names_to = "marker", + values_to = "batchmean" + ) + return(list(list(values = values, models = NULL))) +} diff --git a/R/batchmean_standardize.R b/R/batchmean_standardize.R new file mode 100644 index 0000000..208b186 --- /dev/null +++ b/R/batchmean_standardize.R @@ -0,0 +1,74 @@ +#' Batch means for approach 3: Marginal standardization +#' +#' @param data Data set +#' @param markers Vector of variables to batch-adjust +#' @param confounders Confounders: features that differ +#' between batches that should be retained +#' +#' @return Tibble with conditional means per marker and batch +#' @noRd +batchmean_standardize <- function(data, markers, confounders) { + res <- data %>% + tidyr::pivot_longer( + cols = {{ markers }}, + names_to = "marker", + values_to = "value" + ) %>% + dplyr::filter(!is.na(.data$value)) %>% + dplyr::group_by(.data$marker) %>% + tidyr::nest(data = c(-.data$marker)) %>% + dplyr::mutate( + data = purrr::map( + .x = .data$data, + .f = ~ .x %>% + dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) + ), + model = purrr::map( + .x = .data$data, + .f = ~ stats::lm( + formula = stats::as.formula(paste0( + "value ~ .batchvar +", + paste( + confounders, + collapse = " + ", + sep = " + " + ) + )), + data = .x + ) + ), + .batchvar = purrr::map( + .x = .data$data, + .f = ~ .x %>% + dplyr::pull(.data$.batchvar) %>% + levels() + ) + ) + + values <- res %>% + tidyr::unnest(cols = .data$.batchvar) %>% + dplyr::mutate( + data = purrr::map2( + .x = .data$data, + .y = .data$.batchvar, + .f = ~ .x %>% dplyr::mutate(.batchvar = .y) + ), + pred = purrr::map2(.x = .data$model, .y = .data$data, .f = stats::predict) + ) %>% + dplyr::select(.data$marker, .data$.batchvar, .data$pred) %>% + tidyr::unnest(cols = .data$pred) %>% + dplyr::group_by(.data$marker, .data$.batchvar) %>% + dplyr::summarize(batchmean = mean(.data$pred, na.rm = TRUE)) %>% + dplyr::group_by(.data$marker) %>% + dplyr::mutate(markermean = mean(.data$batchmean)) %>% + dplyr::ungroup() %>% + dplyr::transmute( + marker = .data$marker, + .batchvar = .data$.batchvar, + batchmean = .data$batchmean - .data$markermean + ) + return(list(list( + models = res %>% dplyr::ungroup() %>% dplyr::pull("model"), + values = values + ))) +} diff --git a/R/factor_drop.R b/R/factor_drop.R new file mode 100644 index 0000000..d663fb3 --- /dev/null +++ b/R/factor_drop.R @@ -0,0 +1,14 @@ +#' Drop empty factor levels +#' +#' @description +#' Avoids predict() issues. This is \code{forcats::fct_drop()} +#' without bells and whistles. +#' +#' @param f factor +#' +#' @return Factor +#' @noRd +factor_drop <- function(f) { + factor_levels <- levels(f) + factor(f, levels = setdiff(factor_levels, factor_levels[table(f) == 0])) +} From 801698146e6dd46df04ac9a0becad15b071217fa Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:09:36 +0100 Subject: [PATCH 06/36] let air reformat code --- R/adjust_batch.R | 80 +++++++++++++++++++++++++++++------------ R/batchmean_ipw.R | 90 +++++++++++++++++++++++++++-------------------- R/plot_batch.R | 24 +++++++++---- 3 files changed, 127 insertions(+), 67 deletions(-) diff --git a/R/adjust_batch.R b/R/adjust_batch.R index 1b07e0e..ff32bd8 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -1,6 +1,8 @@ # as per https://github.com/jennybc/googlesheets/blob/master/R/googlesheets.R: ## quiets concerns of R CMD check re: the .'s that appear in pipelines -if (getRversion() >= "2.15.1") utils::globalVariables(c(".")) +if (getRversion() >= "2.15.1") { + utils::globalVariables(c(".")) +} #' Adjust for batch effects #' @@ -208,10 +210,13 @@ adjust_batch <- function( )) } - if (method %in% c("simple", "quantnorm") & - data %>% - dplyr::select({{ confounders }}) %>% - ncol() > 0) { + if ( + method %in% c("simple", "quantnorm") & + data %>% + dplyr::select({{ confounders }}) %>% + ncol() > + 0 + ) { message(paste0( "Batch effect correction via 'method = ", method, @@ -226,10 +231,14 @@ adjust_batch <- function( # Mean-based methods if (method %in% c("simple", "standardize", "ipw")) { - if (method %in% c("standardize", "ipw") & - data %>% - dplyr::select({{ confounders }}) %>% - ncol() == 0) { + if ( + method %in% + c("standardize", "ipw") & + data %>% + dplyr::select({{ confounders }}) %>% + ncol() == + 0 + ) { message(paste0( "Batch effect correction via 'method = ", method, @@ -254,7 +263,10 @@ adjust_batch <- function( truncate = ipw_truncate ) ) - adjust_parameters <- purrr::map_dfr(.x = res, .f = ~ purrr::pluck(.x, "values")) + adjust_parameters <- purrr::map_dfr( + .x = res, + .f = ~ purrr::pluck(.x, "values") + ) method_indices <- c("simple" = 2, "standardize" = 3, "ipw" = 4) if (suffix == "_adjX") { suffix <- paste0("_adj", method_indices[method[1]]) @@ -281,7 +293,10 @@ adjust_batch <- function( .x = data %>% dplyr::select({{ markers }}) %>% names(), .f = batchrq, data = data %>% - dplyr::filter(dplyr::across(dplyr::all_of({{ confounders }}), ~ !is.na(.x))), + dplyr::filter(dplyr::across( + dplyr::all_of({{ confounders }}), + ~ !is.na(.x) + )), confounders = dplyr::if_else( dplyr::enexpr(confounders) != "", true = paste0( @@ -297,7 +312,10 @@ adjust_batch <- function( tau = quantreg_tau, rq_method = quantreg_method ) - adjust_parameters <- purrr::map_dfr(.x = res, .f = ~ purrr::pluck(.x, "values")) + adjust_parameters <- purrr::map_dfr( + .x = res, + .f = ~ purrr::pluck(.x, "values") + ) if (suffix == "_adjX") { suffix <- "_adj5" } @@ -316,8 +334,12 @@ adjust_batch <- function( dplyr::group_by(.data$marker) %>% dplyr::mutate( value_adjusted = (.data$value - .data$un_lo) / - .data$un_iq * .data$all_iq * (.data$un_iq / .data$ad_iq) + - .data$all_lo - .data$ad_lo + .data$un_lo, + .data$un_iq * + .data$all_iq * + (.data$un_iq / .data$ad_iq) + + .data$all_lo - + .data$ad_lo + + .data$un_lo, marker = paste0(.data$marker, suffix) ) %>% dplyr::select( @@ -348,15 +370,20 @@ adjust_batch <- function( ) %>% dplyr::mutate(marker = paste0(.data$marker, suffix)) %>% dplyr::group_by(.data$marker) %>% - dplyr::mutate(value_adjusted = batch_quantnorm( - var = .data$value, - batch = .data$.batchvar - )) %>% + dplyr::mutate( + value_adjusted = batch_quantnorm( + var = .data$value, + batch = .data$.batchvar + ) + ) %>% dplyr::ungroup() %>% dplyr::select(-.data$value) res <- list(list(res = NULL, models = NULL)) - adjust_parameters <- tibble::tibble(marker = data %>% - dplyr::select({{ markers }}) %>% names()) + adjust_parameters <- tibble::tibble( + marker = data %>% + dplyr::select({{ markers }}) %>% + names() + ) } # Dataset to return @@ -372,12 +399,19 @@ adjust_batch <- function( # Meta-data to return as attribute attr_list <- list( adjust_method = method, - markers = data_orig %>% dplyr::select({{ markers }}) %>% names(), + markers = data_orig %>% + dplyr::select({{ markers }}) %>% + names(), suffix = suffix, - batchvar = data_orig %>% dplyr::select({{ batch }}) %>% names(), + batchvar = data_orig %>% + dplyr::select({{ batch }}) %>% + names(), confounders = dplyr::enexpr(confounders), adjust_parameters = adjust_parameters, - model_fits = purrr::map(.x = res, .f = ~ purrr::pluck(.x, "models")) + model_fits = purrr::map( + .x = res, + .f = ~ purrr::pluck(.x, "models") + ) ) class(attr_list) <- c("batchtma", class(res)) attr(values, which = ".batchtma") <- attr_list diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R index e0d8216..141325f 100644 --- a/R/batchmean_ipw.R +++ b/R/batchmean_ipw.R @@ -9,10 +9,10 @@ #' @return Tibble of batch means per batch and marker #' @noRd batchmean_ipw <- function( - data, - markers, - confounders, - truncate = c(0.025, 0.975) + data, + markers, + confounders, + truncate = c(0.025, 0.975) ) { ipwbatch <- function(data, variable, confounders, truncate) { data <- data %>% @@ -52,8 +52,10 @@ batchmean_ipw <- function( .x = ., .y = .data$data, .f = ~ .x %>% - dplyr::mutate(.batchvar = .y %>% - purrr::pluck(".batchvar")) + dplyr::mutate( + .batchvar = .y %>% + purrr::pluck(".batchvar") + ) ) ) @@ -63,34 +65,35 @@ batchmean_ipw <- function( values <- values %>% dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map(.x = ., .f = ~ .x %>% - dplyr::mutate( - probs = dplyr::if_else( - .data$.batchvar == - levels(factor(.data$.batchvar))[1], - true = 1 - .data$value, - false = .data$value - ) - ) %>% - dplyr::pull(.data$probs)) + .funs = ~ purrr::map( + .x = ., + .f = ~ .x %>% + dplyr::mutate( + probs = dplyr::if_else( + .data$.batchvar == levels(factor(.data$.batchvar))[1], + true = 1 - .data$value, + false = .data$value + ) + ) %>% + dplyr::pull(.data$probs) + ) ) # otherwise probabilities are a data frame } else { values <- values %>% dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), - .funs = - ~ purrr::map( - .x = ., - .f = ~ .x %>% - tidyr::pivot_longer( - -.data$.batchvar, - names_to = "batch", - values_to = "prob" - ) %>% - dplyr::filter(.data$batch == .data$.batchvar) %>% - dplyr::pull(.data$prob) - ) + .funs = ~ purrr::map( + .x = ., + .f = ~ .x %>% + tidyr::pivot_longer( + -.data$.batchvar, + names_to = "batch", + values_to = "prob" + ) %>% + dplyr::filter(.data$batch == .data$.batchvar) %>% + dplyr::pull(.data$prob) + ) ) } @@ -117,17 +120,24 @@ batchmean_ipw <- function( corstr = "independence" ) %>% broom::tidy() %>% - dplyr::filter(!stringr::str_detect( - string = .data$term, - pattern = "(Intercept)" - )) %>% - dplyr::mutate(term = as.character( - stringr::str_remove_all( + dplyr::filter( + !stringr::str_detect( string = .data$term, - pattern = ".batchvar" + pattern = "(Intercept)" ) - )) %>% - dplyr::full_join(tibble::tibble(term = as.character(xlev)), by = "term") %>% + ) %>% + dplyr::mutate( + term = as.character( + stringr::str_remove_all( + string = .data$term, + pattern = ".batchvar" + ) + ) + ) %>% + dplyr::full_join( + tibble::tibble(term = as.character(xlev)), + by = "term" + ) %>% dplyr::mutate( estimate = dplyr::if_else( is.na(.data$estimate), @@ -139,7 +149,11 @@ batchmean_ipw <- function( term = .data$term ) %>% dplyr::arrange(.data$term) %>% - dplyr::select(.data$marker, .batchvar = .data$term, batchmean = .data$estimate) + dplyr::select( + .data$marker, + .batchvar = .data$term, + batchmean = .data$estimate + ) list(values = values, models = res %>% dplyr::pull(.data$den)) } diff --git a/R/plot_batch.R b/R/plot_batch.R index 2bb6b97..0aa1128 100644 --- a/R/plot_batch.R +++ b/R/plot_batch.R @@ -73,8 +73,15 @@ #' color = confounder #' ) + #' ggplot2::labs(y = "Biomarker (variable 'noisy')") -plot_batch <- function(data, marker, batch, color = NULL, maxlevels = 15, title = NULL, ...) { - +plot_batch <- function( + data, + marker, + batch, + color = NULL, + maxlevels = 15, + title = NULL, + ... +) { # Set levels to number of discrete entries for `color` or 101 if NULL. # If distinct count of `color` is only 1, that indicates NULL was passed. nlevels <- dplyr::n_distinct(dplyr::select(data, {{ color }})) @@ -115,15 +122,20 @@ plot_batch <- function(data, marker, batch, color = NULL, maxlevels = 15, title mapping = ggplot2::aes(color = {{ color }}, shape = {{ color }}) ) + ggplot2::scale_shape_manual(name = dplyr::enexpr(color), values = 15:30) + - ggplot2::scale_color_viridis_d(name = dplyr::enexpr(color), option = "cividis") - } - else { + ggplot2::scale_color_viridis_d( + name = dplyr::enexpr(color), + option = "cividis" + ) + } else { myplot + ggplot2::geom_jitter( width = 0.2, height = 0, mapping = ggplot2::aes(color = {{ color }}) ) + - ggplot2::scale_color_viridis_c(name = dplyr::enexpr(color), option = "cividis") + ggplot2::scale_color_viridis_c( + name = dplyr::enexpr(color), + option = "cividis" + ) } } From 814cc01cabca7d81c5507070bc2e6683ec6a9302 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:55:03 +0100 Subject: [PATCH 07/36] switch to native pipe --- DESCRIPTION | 2 + NAMESPACE | 1 - R/adjust_batch.R | 80 +++++++++++++++++++-------------------- R/batch_quantnorm.R | 18 ++++----- R/batch_rq.R | 32 ++++++++-------- R/batchmean_ipw.R | 60 ++++++++++++++--------------- R/batchmean_simple.R | 11 +++--- R/batchmean_standardize.R | 40 ++++++++++---------- R/batchtma.R | 2 +- 9 files changed, 123 insertions(+), 123 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index fbffdf3..a9d369c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,6 +21,8 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.3 biocViews: +Depends: + R (>= 4.1.0) Imports: broom (>= 0.7.0), dplyr (>= 1.0.0), diff --git a/NAMESPACE b/NAMESPACE index 38231f7..078d0a7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,6 +5,5 @@ export(adjust_batch) export(diagnose_models) export(plot_batch) importFrom(ggplot2,labs) -importFrom(magrittr,"%>%") importFrom(rlang,":=") importFrom(rlang,.data) diff --git a/R/adjust_batch.R b/R/adjust_batch.R index ff32bd8..9e58007 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -185,17 +185,17 @@ adjust_batch <- function( ) { method <- as.character(dplyr::enexpr(method)) allmethods <- c("simple", "standardize", "ipw", "quantreg", "quantnorm") - data_orig <- data %>% + data_orig <- data |> dplyr::mutate(.id = dplyr::row_number()) - data <- data_orig %>% - dplyr::rename(.batchvar = {{ batch }}) %>% + data <- data_orig |> + dplyr::rename(.batchvar = {{ batch }}) |> dplyr::mutate( .batchvar = factor(.data$.batchvar), .batchvar = factor_drop(.data$.batchvar) - ) %>% + ) |> dplyr::select(.data$.id, .data$.batchvar, {{ markers }}, {{ confounders }}) - confounders <- data %>% - dplyr::select({{ confounders }}) %>% + confounders <- data |> + dplyr::select({{ confounders }}) |> names() # Check inputs: method and confounders @@ -212,8 +212,8 @@ adjust_batch <- function( if ( method %in% c("simple", "quantnorm") & - data %>% - dplyr::select({{ confounders }}) %>% + data |> + dplyr::select({{ confounders }}) |> ncol() > 0 ) { @@ -225,7 +225,7 @@ adjust_batch <- function( paste(dplyr::enexpr(confounders), sep = ", ", collapse = ", "), "). They will be ignored." )) - data <- data %>% dplyr::select(-dplyr::any_of({{ confounders }})) + data <- data |> dplyr::select(-dplyr::any_of({{ confounders }})) confounders <- NULL } @@ -234,8 +234,8 @@ adjust_batch <- function( if ( method %in% c("standardize", "ipw") & - data %>% - dplyr::select({{ confounders }}) %>% + data |> + dplyr::select({{ confounders }}) |> ncol() == 0 ) { @@ -272,27 +272,27 @@ adjust_batch <- function( suffix <- paste0("_adj", method_indices[method[1]]) } - values <- data %>% - dplyr::select(-dplyr::any_of({{ confounders }})) %>% + values <- data |> + dplyr::select(-dplyr::any_of({{ confounders }})) |> tidyr::pivot_longer( cols = c(-.data$.id, -.data$.batchvar), names_to = "marker", values_to = "value" - ) %>% - dplyr::left_join(adjust_parameters, by = c("marker", ".batchvar")) %>% + ) |> + dplyr::left_join(adjust_parameters, by = c("marker", ".batchvar")) |> dplyr::mutate( value_adjusted = .data$value - .data$batchmean, marker = paste0(.data$marker, suffix) - ) %>% + ) |> dplyr::select(-.data$batchmean, -.data$value) } # Quantile regression if (method == "quantreg") { res <- purrr::map( - .x = data %>% dplyr::select({{ markers }}) %>% names(), + .x = data |> dplyr::select({{ markers }}) |> names(), .f = batchrq, - data = data %>% + data = data |> dplyr::filter(dplyr::across( dplyr::all_of({{ confounders }}), ~ !is.na(.x) @@ -320,7 +320,7 @@ adjust_batch <- function( suffix <- "_adj5" } - values <- data %>% + values <- data |> tidyr::pivot_longer( cols = c( -.data$.id, @@ -329,9 +329,9 @@ adjust_batch <- function( ), names_to = "marker", values_to = "value" - ) %>% - dplyr::left_join(adjust_parameters, by = c("marker", ".batchvar")) %>% - dplyr::group_by(.data$marker) %>% + ) |> + dplyr::left_join(adjust_parameters, by = c("marker", ".batchvar")) |> + dplyr::group_by(.data$marker) |> dplyr::mutate( value_adjusted = (.data$value - .data$un_lo) / .data$un_iq * @@ -341,7 +341,7 @@ adjust_batch <- function( .data$ad_lo + .data$un_lo, marker = paste0(.data$marker, suffix) - ) %>% + ) |> dplyr::select( -dplyr::any_of({{ confounders }}), -.data$value, @@ -361,50 +361,50 @@ adjust_batch <- function( if (suffix == "_adjX") { suffix <- "_adj6" } - values <- data %>% - dplyr::select(-dplyr::any_of({{ confounders }})) %>% + values <- data |> + dplyr::select(-dplyr::any_of({{ confounders }})) |> tidyr::pivot_longer( cols = c(-.data$.id, -.data$.batchvar), names_to = "marker", values_to = "value" - ) %>% - dplyr::mutate(marker = paste0(.data$marker, suffix)) %>% - dplyr::group_by(.data$marker) %>% + ) |> + dplyr::mutate(marker = paste0(.data$marker, suffix)) |> + dplyr::group_by(.data$marker) |> dplyr::mutate( value_adjusted = batch_quantnorm( var = .data$value, batch = .data$.batchvar ) - ) %>% - dplyr::ungroup() %>% + ) |> + dplyr::ungroup() |> dplyr::select(-.data$value) res <- list(list(res = NULL, models = NULL)) adjust_parameters <- tibble::tibble( - marker = data %>% - dplyr::select({{ markers }}) %>% + marker = data |> + dplyr::select({{ markers }}) |> names() ) } # Dataset to return - values <- values %>% + values <- values |> tidyr::pivot_wider( names_from = .data$marker, values_from = .data$value_adjusted - ) %>% - dplyr::select(-.data$.batchvar) %>% - dplyr::left_join(x = data_orig, by = ".id") %>% + ) |> + dplyr::select(-.data$.batchvar) |> + dplyr::left_join(x = data_orig, by = ".id") |> dplyr::select(-.data$.id) # Meta-data to return as attribute attr_list <- list( adjust_method = method, - markers = data_orig %>% - dplyr::select({{ markers }}) %>% + markers = data_orig |> + dplyr::select({{ markers }}) |> names(), suffix = suffix, - batchvar = data_orig %>% - dplyr::select({{ batch }}) %>% + batchvar = data_orig |> + dplyr::select({{ batch }}) |> names(), confounders = dplyr::enexpr(confounders), adjust_parameters = adjust_parameters, diff --git a/R/batch_quantnorm.R b/R/batch_quantnorm.R index aa8c1bf..481c729 100644 --- a/R/batch_quantnorm.R +++ b/R/batch_quantnorm.R @@ -6,14 +6,14 @@ #' @return Tibble of means per batch for one variable #' @noRd batch_quantnorm <- function(var, batch) { - tibble::tibble(var, batch) %>% - tibble::rowid_to_column() %>% - tidyr::pivot_wider(names_from = batch, values_from = var) %>% - dplyr::select(-.data$rowid) %>% - dplyr::select_if(~ !all(is.na(.))) %>% - as.matrix() %>% - limma::normalizeQuantiles() %>% - tibble::as_tibble() %>% + tibble::tibble(var, batch) |> + tibble::rowid_to_column() |> + tidyr::pivot_wider(names_from = batch, values_from = var) |> + dplyr::select(-.data$rowid) |> + dplyr::select_if(~ !all(is.na(.))) |> + as.matrix() |> + limma::normalizeQuantiles() |> + tibble::as_tibble() |> dplyr::transmute( result = purrr::pmap_dbl( .l = ., @@ -26,6 +26,6 @@ batch_quantnorm <- function(var, batch) { true = NA_real_, false = .data$result ) - ) %>% + ) |> dplyr::pull(.data$result) } diff --git a/R/batch_rq.R b/R/batch_rq.R index f847946..a6855b4 100644 --- a/R/batch_rq.R +++ b/R/batch_rq.R @@ -9,11 +9,11 @@ #' @return Tibble of quantiles per batch #' @noRd batchrq <- function(data, variable, confounders, tau, rq_method) { - res <- data %>% - dplyr::rename(variable = {{ variable }}) %>% - dplyr::filter(!is.na(.data$variable)) %>% - dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) %>% - tidyr::nest(data = dplyr::everything()) %>% + res <- data |> + dplyr::rename(variable = {{ variable }}) |> + dplyr::filter(!is.na(.data$variable)) |> + dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) |> + tidyr::nest(data = dplyr::everything()) |> dplyr::mutate( un = purrr::map( .x = .data$data, @@ -38,19 +38,19 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { ), .batchvar = purrr::map( .x = .data$data, - .f = ~ .x %>% - dplyr::pull(.data$.batchvar) %>% + .f = ~ .x |> + dplyr::pull(.data$.batchvar) |> levels() ) ) - values <- res %>% - tidyr::unnest(cols = .data$.batchvar) %>% + values <- res |> + tidyr::unnest(cols = .data$.batchvar) |> dplyr::mutate( data = purrr::map2( .x = .data$data, .y = .data$.batchvar, - .f = ~ .x %>% dplyr::mutate(.batchvar = .y) + .f = ~ .x |> dplyr::mutate(.batchvar = .y) ), un = purrr::map2(.x = .data$un, .y = .data$data, .f = stats::predict), ad = purrr::map2(.x = .data$ad, .y = .data$data, .f = stats::predict), @@ -73,7 +73,7 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { .f = ~ stats::quantile(.x$variable, probs = 0.75) ), all_iq = .data$all_hi - .data$all_lo - ) %>% + ) |> dplyr::select( .data$.batchvar, .data$un, @@ -81,9 +81,9 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { .data$all_lo, .data$all_hi, .data$all_iq - ) %>% - tidyr::unnest(cols = c(.data$un, .data$ad)) %>% - dplyr::group_by(.data$.batchvar) %>% + ) |> + tidyr::unnest(cols = c(.data$un, .data$ad)) |> + dplyr::group_by(.data$.batchvar) |> dplyr::summarize( un_lo = stats::quantile(.data$un_lo, probs = 0.25), ad_lo = stats::quantile(.data$ad_lo, probs = 0.25), @@ -91,13 +91,13 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { ad_hi = stats::quantile(.data$ad_hi, probs = 0.75), all_lo = stats::median(.data$all_lo), all_iq = stats::median(.data$all_iq) - ) %>% + ) |> dplyr::mutate( un_iq = .data$un_hi - .data$un_lo, ad_iq = .data$ad_hi - .data$ad_lo, marker = {{ variable }} ) - models <- res %>% dplyr::pull(.data$ad) + models <- res |> dplyr::pull(.data$ad) return(tibble::lst(values, models)) } diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R index 141325f..3f1c866 100644 --- a/R/batchmean_ipw.R +++ b/R/batchmean_ipw.R @@ -15,13 +15,13 @@ batchmean_ipw <- function( truncate = c(0.025, 0.975) ) { ipwbatch <- function(data, variable, confounders, truncate) { - data <- data %>% - dplyr::rename(variable = dplyr::one_of(variable)) %>% - dplyr::filter(!is.na(.data$variable)) %>% + data <- data |> + dplyr::rename(variable = dplyr::one_of(variable)) |> + dplyr::filter(!is.na(.data$variable)) |> dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) - res <- data %>% - tidyr::nest(data = dplyr::everything()) %>% + res <- data |> + tidyr::nest(data = dplyr::everything()) |> dplyr::mutate( num = purrr::map( .x = .data$data, @@ -43,17 +43,17 @@ batchmean_ipw <- function( ) ) - values <- res %>% + values <- res |> dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map(.x = ., .f = stats::predict, type = "probs") %>% - purrr::map(.x = ., .f = tibble::as_tibble) %>% + .funs = ~ purrr::map(.x = ., .f = stats::predict, type = "probs") |> + purrr::map(.x = ., .f = tibble::as_tibble) |> purrr::map2( .x = ., .y = .data$data, - .f = ~ .x %>% + .f = ~ .x |> dplyr::mutate( - .batchvar = .y %>% + .batchvar = .y |> purrr::pluck(".batchvar") ) ) @@ -62,43 +62,43 @@ batchmean_ipw <- function( # multinom()$fitted.values is just a vector of probabilities for # the 2nd outcome level if there are only two levels if (length(levels(factor(data$.batchvar))) == 2) { - values <- values %>% + values <- values |> dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), .funs = ~ purrr::map( .x = ., - .f = ~ .x %>% + .f = ~ .x |> dplyr::mutate( probs = dplyr::if_else( .data$.batchvar == levels(factor(.data$.batchvar))[1], true = 1 - .data$value, false = .data$value ) - ) %>% + ) |> dplyr::pull(.data$probs) ) ) # otherwise probabilities are a data frame } else { - values <- values %>% + values <- values |> dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), .funs = ~ purrr::map( .x = ., - .f = ~ .x %>% + .f = ~ .x |> tidyr::pivot_longer( -.data$.batchvar, names_to = "batch", values_to = "prob" - ) %>% - dplyr::filter(.data$batch == .data$.batchvar) %>% + ) |> + dplyr::filter(.data$batch == .data$.batchvar) |> dplyr::pull(.data$prob) ) ) } - values <- values %>% - tidyr::unnest(cols = c(.data$data, .data$num, .data$den)) %>% + values <- values |> + tidyr::unnest(cols = c(.data$data, .data$num, .data$den)) |> dplyr::mutate( sw = .data$num / .data$den, trunc = dplyr::case_when( @@ -110,7 +110,7 @@ batchmean_ipw <- function( ) ) - xlev <- unique(data %>% dplyr::pull(.data$.batchvar)) + xlev <- unique(data |> dplyr::pull(.data$.batchvar)) values <- geepack::geeglm( formula = variable ~ .batchvar, @@ -118,14 +118,14 @@ batchmean_ipw <- function( weights = values$trunc, id = values$.id, corstr = "independence" - ) %>% - broom::tidy() %>% + ) |> + broom::tidy() |> dplyr::filter( !stringr::str_detect( string = .data$term, pattern = "(Intercept)" ) - ) %>% + ) |> dplyr::mutate( term = as.character( stringr::str_remove_all( @@ -133,11 +133,11 @@ batchmean_ipw <- function( pattern = ".batchvar" ) ) - ) %>% + ) |> dplyr::full_join( tibble::tibble(term = as.character(xlev)), by = "term" - ) %>% + ) |> dplyr::mutate( estimate = dplyr::if_else( is.na(.data$estimate), @@ -147,20 +147,20 @@ batchmean_ipw <- function( estimate = .data$estimate - mean(.data$estimate), marker = variable, term = .data$term - ) %>% - dplyr::arrange(.data$term) %>% + ) |> + dplyr::arrange(.data$term) |> dplyr::select( .data$marker, .batchvar = .data$term, batchmean = .data$estimate ) - list(values = values, models = res %>% dplyr::pull(.data$den)) + list(values = values, models = res |> dplyr::pull(.data$den)) } purrr::map( - .x = data %>% dplyr::select({{ markers }}) %>% names(), + .x = data |> dplyr::select({{ markers }}) |> names(), .f = ipwbatch, - data = data %>% + data = data |> dplyr::filter(dplyr::across(dplyr::all_of(confounders), ~ !is.na(.x))), truncate = truncate, confounders = paste(confounders, sep = " + ", collapse = " + ") diff --git a/R/batchmean_simple.R b/R/batchmean_simple.R index ff8af67..fe1f6cc 100644 --- a/R/batchmean_simple.R +++ b/R/batchmean_simple.R @@ -3,23 +3,22 @@ #' @param data Data set #' @param markers Biomarkers to adjust #' -#' @importFrom magrittr %>% #' @importFrom rlang .data #' @return Tibble of means per marker and batch #' @noRd batchmean_simple <- function(data, markers) { - values <- data %>% - dplyr::select(.data$.id, .data$.batchvar, {{ markers }}) %>% - dplyr::group_by(.data$.batchvar) %>% + values <- data |> + dplyr::select(.data$.id, .data$.batchvar, {{ markers }}) |> + dplyr::group_by(.data$.batchvar) |> dplyr::summarize_at( .vars = dplyr::vars(-.data$.id), .funs = mean, na.rm = TRUE - ) %>% + ) |> dplyr::mutate_at( .vars = dplyr::vars(-.data$.batchvar), .funs = ~ . - mean(., na.rm = TRUE) - ) %>% + ) |> tidyr::pivot_longer( col = c(-.data$.batchvar), names_to = "marker", diff --git a/R/batchmean_standardize.R b/R/batchmean_standardize.R index 208b186..f12953b 100644 --- a/R/batchmean_standardize.R +++ b/R/batchmean_standardize.R @@ -8,19 +8,19 @@ #' @return Tibble with conditional means per marker and batch #' @noRd batchmean_standardize <- function(data, markers, confounders) { - res <- data %>% + res <- data |> tidyr::pivot_longer( cols = {{ markers }}, names_to = "marker", values_to = "value" - ) %>% - dplyr::filter(!is.na(.data$value)) %>% - dplyr::group_by(.data$marker) %>% - tidyr::nest(data = c(-.data$marker)) %>% + ) |> + dplyr::filter(!is.na(.data$value)) |> + dplyr::group_by(.data$marker) |> + tidyr::nest(data = c(-.data$marker)) |> dplyr::mutate( data = purrr::map( .x = .data$data, - .f = ~ .x %>% + .f = ~ .x |> dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) ), model = purrr::map( @@ -39,36 +39,36 @@ batchmean_standardize <- function(data, markers, confounders) { ), .batchvar = purrr::map( .x = .data$data, - .f = ~ .x %>% - dplyr::pull(.data$.batchvar) %>% + .f = ~ .x |> + dplyr::pull(.data$.batchvar) |> levels() ) ) - values <- res %>% - tidyr::unnest(cols = .data$.batchvar) %>% + values <- res |> + tidyr::unnest(cols = .data$.batchvar) |> dplyr::mutate( data = purrr::map2( .x = .data$data, .y = .data$.batchvar, - .f = ~ .x %>% dplyr::mutate(.batchvar = .y) + .f = ~ .x |> dplyr::mutate(.batchvar = .y) ), pred = purrr::map2(.x = .data$model, .y = .data$data, .f = stats::predict) - ) %>% - dplyr::select(.data$marker, .data$.batchvar, .data$pred) %>% - tidyr::unnest(cols = .data$pred) %>% - dplyr::group_by(.data$marker, .data$.batchvar) %>% - dplyr::summarize(batchmean = mean(.data$pred, na.rm = TRUE)) %>% - dplyr::group_by(.data$marker) %>% - dplyr::mutate(markermean = mean(.data$batchmean)) %>% - dplyr::ungroup() %>% + ) |> + dplyr::select(.data$marker, .data$.batchvar, .data$pred) |> + tidyr::unnest(cols = .data$pred) |> + dplyr::group_by(.data$marker, .data$.batchvar) |> + dplyr::summarize(batchmean = mean(.data$pred, na.rm = TRUE)) |> + dplyr::group_by(.data$marker) |> + dplyr::mutate(markermean = mean(.data$batchmean)) |> + dplyr::ungroup() |> dplyr::transmute( marker = .data$marker, .batchvar = .data$.batchvar, batchmean = .data$batchmean - .data$markermean ) return(list(list( - models = res %>% dplyr::ungroup() %>% dplyr::pull("model"), + models = res |> dplyr::ungroup() |> dplyr::pull("model"), values = values ))) } diff --git a/R/batchtma.R b/R/batchtma.R index 296f59d..6c057da 100644 --- a/R/batchtma.R +++ b/R/batchtma.R @@ -14,7 +14,7 @@ #' #' \code{\link[batchtma]{plot_batch}}: Plot biomarkers by batch #' -#' @docType package +#' @docType _PACKAGE #' @name batchtma #' @seealso \url{https://stopsack.github.io/batchtma/} #' @references From 39acc07c0fb58776a2466d3d8e1aaf8530def1e3 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:55:22 +0100 Subject: [PATCH 08/36] r(e)oxygenize --- DESCRIPTION | 2 +- man/batchtma.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a9d369c..b64d9fa 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,7 +19,7 @@ Description: Different adjustment methods for batch effects in biomarker data, License: GPL-3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.3 biocViews: Depends: R (>= 4.1.0) diff --git a/man/batchtma.Rd b/man/batchtma.Rd index fba57e3..df225bc 100644 --- a/man/batchtma.Rd +++ b/man/batchtma.Rd @@ -1,6 +1,6 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/batchtma.R -\docType{package} +\docType{_PACKAGE} \name{batchtma} \alias{batchtma} \title{batchtma: Methods to address batch effects} From b7c4d37a6e9e74153f7ea773770e5f3c089a9cf6 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:41:49 +0100 Subject: [PATCH 09/36] replace . obj; ~ fn definition; filter(across()) --- DESCRIPTION | 2 +- R/adjust_batch.R | 16 ++--- R/batch_quantnorm.R | 20 +++--- R/batch_rq.R | 79 +++++++++++++++--------- R/batchmean_ipw.R | 124 ++++++++++++++++++++++++-------------- R/batchmean_simple.R | 2 +- R/batchmean_standardize.R | 49 +++++++++------ 7 files changed, 185 insertions(+), 107 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b64d9fa..bd78d79 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -25,7 +25,7 @@ Depends: R (>= 4.1.0) Imports: broom (>= 0.7.0), - dplyr (>= 1.0.0), + dplyr (>= 1.0.4), geepack, ggplot2, limma, diff --git a/R/adjust_batch.R b/R/adjust_batch.R index 9e58007..d766f96 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -265,7 +265,7 @@ adjust_batch <- function( ) adjust_parameters <- purrr::map_dfr( .x = res, - .f = ~ purrr::pluck(.x, "values") + .f = \(x) purrr::pluck(x, "values") ) method_indices <- c("simple" = 2, "standardize" = 3, "ipw" = 4) if (suffix == "_adjX") { @@ -293,10 +293,12 @@ adjust_batch <- function( .x = data |> dplyr::select({{ markers }}) |> names(), .f = batchrq, data = data |> - dplyr::filter(dplyr::across( - dplyr::all_of({{ confounders }}), - ~ !is.na(.x) - )), + dplyr::filter( + dplyr::if_all( + dplyr::all_of({{ confounders }}), + \(x) !is.na(x) + ) + ), confounders = dplyr::if_else( dplyr::enexpr(confounders) != "", true = paste0( @@ -314,7 +316,7 @@ adjust_batch <- function( ) adjust_parameters <- purrr::map_dfr( .x = res, - .f = ~ purrr::pluck(.x, "values") + .f = \(x) purrr::pluck(x, "values") ) if (suffix == "_adjX") { suffix <- "_adj5" @@ -410,7 +412,7 @@ adjust_batch <- function( adjust_parameters = adjust_parameters, model_fits = purrr::map( .x = res, - .f = ~ purrr::pluck(.x, "models") + .f = \(x) purrr::pluck(x, "models") ) ) class(attr_list) <- c("batchtma", class(res)) diff --git a/R/batch_quantnorm.R b/R/batch_quantnorm.R index 481c729..856caf5 100644 --- a/R/batch_quantnorm.R +++ b/R/batch_quantnorm.R @@ -6,17 +6,22 @@ #' @return Tibble of means per batch for one variable #' @noRd batch_quantnorm <- function(var, batch) { - tibble::tibble(var, batch) |> + df <- tibble::tibble(var, batch) |> tibble::rowid_to_column() |> - tidyr::pivot_wider(names_from = batch, values_from = var) |> + tidyr::pivot_wider( + names_from = batch, + values_from = var + ) |> dplyr::select(-.data$rowid) |> - dplyr::select_if(~ !all(is.na(.))) |> + dplyr::select(dplyr::where(\(x) !all(is.na(x)))) |> as.matrix() |> limma::normalizeQuantiles() |> - tibble::as_tibble() |> - dplyr::transmute( + tibble::as_tibble() + + df |> + dplyr::mutate( result = purrr::pmap_dbl( - .l = ., + .l = df, .f = function(...) { mean(c(...), na.rm = TRUE) } @@ -25,7 +30,8 @@ batch_quantnorm <- function(var, batch) { is.nan(.data$result), true = NA_real_, false = .data$result - ) + ), + .keep = "none" ) |> dplyr::pull(.data$result) } diff --git a/R/batch_rq.R b/R/batch_rq.R index a6855b4..268e8bf 100644 --- a/R/batch_rq.R +++ b/R/batch_rq.R @@ -17,30 +17,36 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { dplyr::mutate( un = purrr::map( .x = .data$data, - .f = ~ quantreg::rq( - formula = variable ~ .batchvar, - data = .x, - tau = tau, - method = rq_method - ) + .f = \(x) { + quantreg::rq( + formula = variable ~ .batchvar, + data = x, + tau = tau, + method = rq_method + ) + } ), ad = purrr::map( .x = .data$data, - .f = ~ quantreg::rq( - formula = stats::reformulate( - response = "variable", - termlabels = c(".batchvar", confounders) - ), - data = .x, - tau = tau, - method = rq_method - ) + .f = \(x) { + quantreg::rq( + formula = stats::reformulate( + response = "variable", + termlabels = c(".batchvar", confounders) + ), + data = x, + tau = tau, + method = rq_method + ) + } ), .batchvar = purrr::map( .x = .data$data, - .f = ~ .x |> - dplyr::pull(.data$.batchvar) |> - levels() + .f = \(x) { + x |> + dplyr::pull(.data$.batchvar) |> + levels() + } ) ) @@ -50,27 +56,46 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { data = purrr::map2( .x = .data$data, .y = .data$.batchvar, - .f = ~ .x |> dplyr::mutate(.batchvar = .y) + .f = \(x, y) { + x |> + dplyr::mutate(.batchvar = y) + } + ), + un = purrr::map2( + .x = .data$un, + .y = .data$data, + .f = stats::predict + ), + ad = purrr::map2( + .x = .data$ad, + .y = .data$data, + .f = stats::predict ), - un = purrr::map2(.x = .data$un, .y = .data$data, .f = stats::predict), - ad = purrr::map2(.x = .data$ad, .y = .data$data, .f = stats::predict), un = purrr::map( .x = .data$un, - .f = tibble::as_tibble, - .name_repair = ~ c("un_lo", "un_hi") + .f = \(x) { + tibble::as_tibble( + x, + .name_repair = ~ c("un_lo", "un_hi") + ) + } ), ad = purrr::map( .x = .data$ad, - .f = tibble::as_tibble, - .name_repair = ~ c("ad_lo", "ad_hi") + .f = \(x) { + tibble::as_tibble( + x, + .name_repair = ~ c("ad_lo", "ad_hi") + ) + } ), all_lo = purrr::map_dbl( .x = .data$data, - .f = ~ stats::quantile(.x$variable, probs = 0.25) + .f = \(x) stats::quantile(x$variable, probs = 0.25) ), all_hi = purrr::map_dbl( .x = .data$data, - .f = ~ stats::quantile(.x$variable, probs = 0.75) + .f = \(x) stats::quantile(x$variable, probs = 0.75) ), all_iq = .data$all_hi - .data$all_lo ) |> diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R index 3f1c866..9fcc38b 100644 --- a/R/batchmean_ipw.R +++ b/R/batchmean_ipw.R @@ -25,38 +25,57 @@ batchmean_ipw <- function( dplyr::mutate( num = purrr::map( .x = .data$data, - .f = ~ nnet::multinom( - formula = .batchvar ~ 1, - data = .x, - trace = FALSE - ) + .f = \(x) { + nnet::multinom( + formula = .batchvar ~ 1, + data = x, + trace = FALSE + ) + } ), den = purrr::map( .x = .data$data, - .f = ~ nnet::multinom( - formula = stats::as.formula( - paste(".batchvar ~", confounders) - ), - data = .x, - trace = FALSE - ) + .f = \(x) { + nnet::multinom( + formula = stats::as.formula( + paste(".batchvar ~", confounders) + ), + data = x, + trace = FALSE + ) + } ) ) values <- res |> dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map(.x = ., .f = stats::predict, type = "probs") |> - purrr::map(.x = ., .f = tibble::as_tibble) |> - purrr::map2( - .x = ., - .y = .data$data, - .f = ~ .x |> - dplyr::mutate( - .batchvar = .y |> - purrr::pluck(".batchvar") + .funs = \(num_den) { + purrr::map( + .x = num_den, + .f = \(x) { + stats::predict( + x, + type = "probs" ) - ) + } + ) |> + purrr::map( + .x = _, + .f = tibble::as_tibble + ) |> + purrr::map2( + .x = _, + .y = .data$data, + .f = \(x, y) { + x |> + dplyr::mutate( + .batchvar = y |> + purrr::pluck(".batchvar") + ) + } + ) + } ) # multinom()$fitted.values is just a vector of probabilities for @@ -65,35 +84,43 @@ batchmean_ipw <- function( values <- values |> dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map( - .x = ., - .f = ~ .x |> - dplyr::mutate( - probs = dplyr::if_else( - .data$.batchvar == levels(factor(.data$.batchvar))[1], - true = 1 - .data$value, - false = .data$value - ) - ) |> - dplyr::pull(.data$probs) - ) + .funs = \(num_den) { + purrr::map( + .x = num_den, + .f = \(x) { + x |> + dplyr::mutate( + probs = dplyr::if_else( + .data$.batchvar == levels(factor(.data$.batchvar))[1], + true = 1 - .data$value, + false = .data$value + ) + ) |> + dplyr::pull(.data$probs) + } + ) + } ) # otherwise probabilities are a data frame } else { values <- values |> dplyr::mutate_at( .vars = dplyr::vars(.data$num, .data$den), - .funs = ~ purrr::map( - .x = ., - .f = ~ .x |> - tidyr::pivot_longer( - -.data$.batchvar, - names_to = "batch", - values_to = "prob" - ) |> - dplyr::filter(.data$batch == .data$.batchvar) |> - dplyr::pull(.data$prob) - ) + .funs = \(num_den) { + purrr::map( + .x = num_den, + .f = \(x) { + x |> + tidyr::pivot_longer( + -.data$.batchvar, + names_to = "batch", + values_to = "prob" + ) |> + dplyr::filter(.data$batch == .data$.batchvar) |> + dplyr::pull(.data$prob) + } + ) + } ) } @@ -161,7 +188,12 @@ batchmean_ipw <- function( .x = data |> dplyr::select({{ markers }}) |> names(), .f = ipwbatch, data = data |> - dplyr::filter(dplyr::across(dplyr::all_of(confounders), ~ !is.na(.x))), + dplyr::filter( + dplyr::if_all( + .cols = dplyr::all_of(confounders), + .fns = \(x) !is.na(x) + ) + ), truncate = truncate, confounders = paste(confounders, sep = " + ", collapse = " + ") ) diff --git a/R/batchmean_simple.R b/R/batchmean_simple.R index fe1f6cc..74ee06b 100644 --- a/R/batchmean_simple.R +++ b/R/batchmean_simple.R @@ -17,7 +17,7 @@ batchmean_simple <- function(data, markers) { ) |> dplyr::mutate_at( .vars = dplyr::vars(-.data$.batchvar), - .funs = ~ . - mean(., na.rm = TRUE) + .funs = \(x) x - mean(x, na.rm = TRUE) ) |> tidyr::pivot_longer( col = c(-.data$.batchvar), diff --git a/R/batchmean_standardize.R b/R/batchmean_standardize.R index f12953b..bd49e55 100644 --- a/R/batchmean_standardize.R +++ b/R/batchmean_standardize.R @@ -20,28 +20,34 @@ batchmean_standardize <- function(data, markers, confounders) { dplyr::mutate( data = purrr::map( .x = .data$data, - .f = ~ .x |> - dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) + .f = \(x) { + x |> + dplyr::mutate(.batchvar = factor_drop(.data$.batchvar)) + } ), model = purrr::map( .x = .data$data, - .f = ~ stats::lm( - formula = stats::as.formula(paste0( - "value ~ .batchvar +", - paste( - confounders, - collapse = " + ", - sep = " + " - ) - )), - data = .x - ) + .f = \(x) { + stats::lm( + formula = stats::as.formula(paste0( + "value ~ .batchvar +", + paste( + confounders, + collapse = " + ", + sep = " + " + ) + )), + data = x + ) + } ), .batchvar = purrr::map( .x = .data$data, - .f = ~ .x |> - dplyr::pull(.data$.batchvar) |> - levels() + .f = \(x) { + x |> + dplyr::pull(.data$.batchvar) |> + levels() + } ) ) @@ -51,9 +57,16 @@ batchmean_standardize <- function(data, markers, confounders) { data = purrr::map2( .x = .data$data, .y = .data$.batchvar, - .f = ~ .x |> dplyr::mutate(.batchvar = .y) + .f = \(x, y) { + x |> + dplyr::mutate(.batchvar = y) + } ), - pred = purrr::map2(.x = .data$model, .y = .data$data, .f = stats::predict) + pred = purrr::map2( + .x = .data$model, + .y = .data$data, + .f = stats::predict + ) ) |> dplyr::select(.data$marker, .data$.batchvar, .data$pred) |> tidyr::unnest(cols = .data$pred) |> From 4c3cda0192e4550d91eef1fad3d0c6a1f10dec3d Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:50:27 +0100 Subject: [PATCH 10/36] dots should no longer appear in pipes --- R/adjust_batch.R | 6 ------ 1 file changed, 6 deletions(-) diff --git a/R/adjust_batch.R b/R/adjust_batch.R index d766f96..866a214 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -1,9 +1,3 @@ -# as per https://github.com/jennybc/googlesheets/blob/master/R/googlesheets.R: -## quiets concerns of R CMD check re: the .'s that appear in pipelines -if (getRversion() >= "2.15.1") { - utils::globalVariables(c(".")) -} - #' Adjust for batch effects #' #' @description From ce42136574d3ac832c4ca726db4949bffd9295b3 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:50:50 +0100 Subject: [PATCH 11/36] modernize tidyselect --- R/adjust_batch.R | 73 +++++++++++++++++++++++---------------- R/batch_quantnorm.R | 2 +- R/batch_rq.R | 16 ++++----- R/batchmean_ipw.R | 31 +++++++++-------- R/batchmean_simple.R | 8 ++--- R/batchmean_standardize.R | 12 ++++--- 6 files changed, 80 insertions(+), 62 deletions(-) diff --git a/R/adjust_batch.R b/R/adjust_batch.R index 866a214..b608389 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -187,7 +187,7 @@ adjust_batch <- function( .batchvar = factor(.data$.batchvar), .batchvar = factor_drop(.data$.batchvar) ) |> - dplyr::select(.data$.id, .data$.batchvar, {{ markers }}, {{ confounders }}) + dplyr::select(".id", ".batchvar", {{ markers }}, {{ confounders }}) confounders <- data |> dplyr::select({{ confounders }}) |> names() @@ -219,7 +219,7 @@ adjust_batch <- function( paste(dplyr::enexpr(confounders), sep = ", ", collapse = ", "), "). They will be ignored." )) - data <- data |> dplyr::select(-dplyr::any_of({{ confounders }})) + data <- data |> dplyr::select(!dplyr::any_of({{ confounders }})) confounders <- NULL } @@ -267,24 +267,29 @@ adjust_batch <- function( } values <- data |> - dplyr::select(-dplyr::any_of({{ confounders }})) |> + dplyr::select(!dplyr::any_of({{ confounders }})) |> tidyr::pivot_longer( - cols = c(-.data$.id, -.data$.batchvar), + cols = !c(".id", ".batchvar"), names_to = "marker", values_to = "value" ) |> - dplyr::left_join(adjust_parameters, by = c("marker", ".batchvar")) |> + dplyr::left_join( + adjust_parameters, + by = c("marker", ".batchvar") + ) |> dplyr::mutate( value_adjusted = .data$value - .data$batchmean, marker = paste0(.data$marker, suffix) ) |> - dplyr::select(-.data$batchmean, -.data$value) + dplyr::select(!c("batchmean", "value")) } # Quantile regression if (method == "quantreg") { res <- purrr::map( - .x = data |> dplyr::select({{ markers }}) |> names(), + .x = data |> + dplyr::select({{ markers }}) |> + names(), .f = batchrq, data = data |> dplyr::filter( @@ -318,15 +323,18 @@ adjust_batch <- function( values <- data |> tidyr::pivot_longer( - cols = c( - -.data$.id, - -.data$.batchvar, - -dplyr::any_of({{ confounders }}) + cols = !c( + ".id", + ".batchvar", + dplyr::any_of({{ confounders }}) ), names_to = "marker", values_to = "value" ) |> - dplyr::left_join(adjust_parameters, by = c("marker", ".batchvar")) |> + dplyr::left_join( + adjust_parameters, + by = c("marker", ".batchvar") + ) |> dplyr::group_by(.data$marker) |> dplyr::mutate( value_adjusted = (.data$value - .data$un_lo) / @@ -339,16 +347,18 @@ adjust_batch <- function( marker = paste0(.data$marker, suffix) ) |> dplyr::select( - -dplyr::any_of({{ confounders }}), - -.data$value, - -.data$un_lo, - -.data$un_hi, - -.data$ad_lo, - -.data$ad_hi, - -.data$un_iq, - -.data$ad_iq, - -.data$all_iq, - -.data$all_lo + !c( + dplyr::any_of({{ confounders }}), + "value", + "un_lo", + "un_hi", + "ad_lo", + "ad_hi", + "un_iq", + "ad_iq", + "all_iq", + "all_lo" + ) ) } @@ -358,9 +368,9 @@ adjust_batch <- function( suffix <- "_adj6" } values <- data |> - dplyr::select(-dplyr::any_of({{ confounders }})) |> + dplyr::select(!dplyr::any_of({{ confounders }})) |> tidyr::pivot_longer( - cols = c(-.data$.id, -.data$.batchvar), + cols = !c(".id", ".batchvar"), names_to = "marker", values_to = "value" ) |> @@ -373,7 +383,7 @@ adjust_batch <- function( ) ) |> dplyr::ungroup() |> - dplyr::select(-.data$value) + dplyr::select(!"value") res <- list(list(res = NULL, models = NULL)) adjust_parameters <- tibble::tibble( marker = data |> @@ -385,12 +395,15 @@ adjust_batch <- function( # Dataset to return values <- values |> tidyr::pivot_wider( - names_from = .data$marker, - values_from = .data$value_adjusted + names_from = "marker", + values_from = "value_adjusted" + ) |> + dplyr::select(!".batchvar") |> + dplyr::left_join( + x = data_orig, + by = ".id" ) |> - dplyr::select(-.data$.batchvar) |> - dplyr::left_join(x = data_orig, by = ".id") |> - dplyr::select(-.data$.id) + dplyr::select(!".id") # Meta-data to return as attribute attr_list <- list( diff --git a/R/batch_quantnorm.R b/R/batch_quantnorm.R index 856caf5..c904d4e 100644 --- a/R/batch_quantnorm.R +++ b/R/batch_quantnorm.R @@ -12,7 +12,7 @@ batch_quantnorm <- function(var, batch) { names_from = batch, values_from = var ) |> - dplyr::select(-.data$rowid) |> + dplyr::select(!"rowid") |> dplyr::select(dplyr::where(\(x) !all(is.na(x)))) |> as.matrix() |> limma::normalizeQuantiles() |> diff --git a/R/batch_rq.R b/R/batch_rq.R index 268e8bf..008072a 100644 --- a/R/batch_rq.R +++ b/R/batch_rq.R @@ -51,7 +51,7 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { ) values <- res |> - tidyr::unnest(cols = .data$.batchvar) |> + tidyr::unnest(cols = ".batchvar") |> dplyr::mutate( data = purrr::map2( .x = .data$data, @@ -100,14 +100,14 @@ batchrq <- function(data, variable, confounders, tau, rq_method) { all_iq = .data$all_hi - .data$all_lo ) |> dplyr::select( - .data$.batchvar, - .data$un, - .data$ad, - .data$all_lo, - .data$all_hi, - .data$all_iq + ".batchvar", + "un", + "ad", + "all_lo", + "all_hi", + "all_iq" ) |> - tidyr::unnest(cols = c(.data$un, .data$ad)) |> + tidyr::unnest(cols = c("un", "ad")) |> dplyr::group_by(.data$.batchvar) |> dplyr::summarize( un_lo = stats::quantile(.data$un_lo, probs = 0.25), diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R index 9fcc38b..6bf35ca 100644 --- a/R/batchmean_ipw.R +++ b/R/batchmean_ipw.R @@ -49,7 +49,7 @@ batchmean_ipw <- function( values <- res |> dplyr::mutate_at( - .vars = dplyr::vars(.data$num, .data$den), + .vars = c("num", "den"), .funs = \(num_den) { purrr::map( .x = num_den, @@ -57,16 +57,13 @@ batchmean_ipw <- function( stats::predict( x, type = "probs" - ) + ) |> + tibble::as_tibble() } ) |> - purrr::map( - .x = _, - .f = tibble::as_tibble - ) |> purrr::map2( .x = _, - .y = .data$data, + .y = res$data, .f = \(x, y) { x |> dplyr::mutate( @@ -83,7 +80,7 @@ batchmean_ipw <- function( if (length(levels(factor(data$.batchvar))) == 2) { values <- values |> dplyr::mutate_at( - .vars = dplyr::vars(.data$num, .data$den), + .vars = dplyr::vars("num", "den"), .funs = \(num_den) { purrr::map( .x = num_den, @@ -125,7 +122,7 @@ batchmean_ipw <- function( } values <- values |> - tidyr::unnest(cols = c(.data$data, .data$num, .data$den)) |> + tidyr::unnest(cols = c("data", "num", "den")) |> dplyr::mutate( sw = .data$num / .data$den, trunc = dplyr::case_when( @@ -177,15 +174,21 @@ batchmean_ipw <- function( ) |> dplyr::arrange(.data$term) |> dplyr::select( - .data$marker, - .batchvar = .data$term, - batchmean = .data$estimate + "marker", + .batchvar = "term", + batchmean = "estimate" ) - list(values = values, models = res |> dplyr::pull(.data$den)) + list( + values = values, + models = res |> + dplyr::pull(.data$den) + ) } purrr::map( - .x = data |> dplyr::select({{ markers }}) |> names(), + .x = data |> + dplyr::select({{ markers }}) |> + names(), .f = ipwbatch, data = data |> dplyr::filter( diff --git a/R/batchmean_simple.R b/R/batchmean_simple.R index 74ee06b..5d9fcbe 100644 --- a/R/batchmean_simple.R +++ b/R/batchmean_simple.R @@ -8,19 +8,19 @@ #' @noRd batchmean_simple <- function(data, markers) { values <- data |> - dplyr::select(.data$.id, .data$.batchvar, {{ markers }}) |> + dplyr::select(".id", ".batchvar", {{ markers }}) |> dplyr::group_by(.data$.batchvar) |> dplyr::summarize_at( - .vars = dplyr::vars(-.data$.id), + .vars = dplyr::vars(!".id"), .funs = mean, na.rm = TRUE ) |> dplyr::mutate_at( - .vars = dplyr::vars(-.data$.batchvar), + .vars = dplyr::vars(!".batchvar"), .funs = \(x) x - mean(x, na.rm = TRUE) ) |> tidyr::pivot_longer( - col = c(-.data$.batchvar), + col = !".batchvar", names_to = "marker", values_to = "batchmean" ) diff --git a/R/batchmean_standardize.R b/R/batchmean_standardize.R index bd49e55..02d3067 100644 --- a/R/batchmean_standardize.R +++ b/R/batchmean_standardize.R @@ -16,7 +16,7 @@ batchmean_standardize <- function(data, markers, confounders) { ) |> dplyr::filter(!is.na(.data$value)) |> dplyr::group_by(.data$marker) |> - tidyr::nest(data = c(-.data$marker)) |> + tidyr::nest(data = !"marker") |> dplyr::mutate( data = purrr::map( .x = .data$data, @@ -52,7 +52,7 @@ batchmean_standardize <- function(data, markers, confounders) { ) values <- res |> - tidyr::unnest(cols = .data$.batchvar) |> + tidyr::unnest(cols = ".batchvar") |> dplyr::mutate( data = purrr::map2( .x = .data$data, @@ -68,8 +68,8 @@ batchmean_standardize <- function(data, markers, confounders) { .f = stats::predict ) ) |> - dplyr::select(.data$marker, .data$.batchvar, .data$pred) |> - tidyr::unnest(cols = .data$pred) |> + dplyr::select("marker", ".batchvar", "pred") |> + tidyr::unnest(cols = "pred") |> dplyr::group_by(.data$marker, .data$.batchvar) |> dplyr::summarize(batchmean = mean(.data$pred, na.rm = TRUE)) |> dplyr::group_by(.data$marker) |> @@ -81,7 +81,9 @@ batchmean_standardize <- function(data, markers, confounders) { batchmean = .data$batchmean - .data$markermean ) return(list(list( - models = res |> dplyr::ungroup() |> dplyr::pull("model"), + models = res |> + dplyr::ungroup() |> + dplyr::pull("model"), values = values ))) } From be3cb030e008bea6092f76739783093ea678ce16 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:45:28 +0100 Subject: [PATCH 12/36] _at() -> across() --- R/batchmean_ipw.R | 123 ++++++++++++++++++++++--------------------- R/batchmean_simple.R | 20 ++++--- 2 files changed, 77 insertions(+), 66 deletions(-) diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R index 6bf35ca..05e4dd2 100644 --- a/R/batchmean_ipw.R +++ b/R/batchmean_ipw.R @@ -48,76 +48,81 @@ batchmean_ipw <- function( ) values <- res |> - dplyr::mutate_at( - .vars = c("num", "den"), - .funs = \(num_den) { - purrr::map( - .x = num_den, - .f = \(x) { - stats::predict( - x, - type = "probs" - ) |> - tibble::as_tibble() - } - ) |> - purrr::map2( - .x = _, - .y = res$data, - .f = \(x, y) { - x |> - dplyr::mutate( - .batchvar = y |> - purrr::pluck(".batchvar") - ) + dplyr::mutate( + dplyr::across( + .cols = c("num", "den"), + .fns = \(num_den) { + purrr::map( + .x = num_den, + .f = \(x) { + stats::predict( + x, + type = "probs" + ) |> + tibble::as_tibble() } - ) - } + ) |> + purrr::map2( # .x = _ # would require R 4.2.0 + .y = res$data, + .f = \(x, y) { + x |> + dplyr::mutate( + .batchvar = y |> + purrr::pluck(".batchvar") + ) + } + ) + } + ) ) # multinom()$fitted.values is just a vector of probabilities for # the 2nd outcome level if there are only two levels if (length(levels(factor(data$.batchvar))) == 2) { values <- values |> - dplyr::mutate_at( - .vars = dplyr::vars("num", "den"), - .funs = \(num_den) { - purrr::map( - .x = num_den, - .f = \(x) { - x |> - dplyr::mutate( - probs = dplyr::if_else( - .data$.batchvar == levels(factor(.data$.batchvar))[1], - true = 1 - .data$value, - false = .data$value - ) - ) |> - dplyr::pull(.data$probs) - } - ) - } + dplyr::mutate( + dplyr::across( + .cols = c("num", "den"), + .fns = \(num_den) { + purrr::map( + .x = num_den, + .f = \(x) { + x |> + dplyr::mutate( + probs = dplyr::if_else( + .data$.batchvar == levels(factor(.data$.batchvar))[1], + true = 1 - .data$value, + false = .data$value + ) + ) |> + dplyr::pull(.data$probs) + } + ) + } + ) ) # otherwise probabilities are a data frame } else { values <- values |> - dplyr::mutate_at( - .vars = dplyr::vars(.data$num, .data$den), - .funs = \(num_den) { - purrr::map( - .x = num_den, - .f = \(x) { - x |> - tidyr::pivot_longer( - -.data$.batchvar, - names_to = "batch", - values_to = "prob" - ) |> - dplyr::filter(.data$batch == .data$.batchvar) |> - dplyr::pull(.data$prob) - } - ) - } + dplyr::mutate( + dplyr::across( + .cols = c("num", "den"), + .fns = \(num_den) { + purrr::map( + .x = num_den, + .f = \(x) { + x |> + tidyr::pivot_longer( + cols = c(!".batchvar"), + names_to = "batch", + values_to = "prob" + ) |> + dplyr::filter(.data$batch == .data$.batchvar) |> + dplyr::pull(.data$prob) + } + ) + } + ) ) } diff --git a/R/batchmean_simple.R b/R/batchmean_simple.R index 5d9fcbe..aca2139 100644 --- a/R/batchmean_simple.R +++ b/R/batchmean_simple.R @@ -10,14 +10,20 @@ batchmean_simple <- function(data, markers) { values <- data |> dplyr::select(".id", ".batchvar", {{ markers }}) |> dplyr::group_by(.data$.batchvar) |> - dplyr::summarize_at( - .vars = dplyr::vars(!".id"), - .funs = mean, - na.rm = TRUE + dplyr::summarize( + dplyr::across( + .cols = !".id", + .fns = \(x) mean( + x, + na.rm = TRUE + ) + ) ) |> - dplyr::mutate_at( - .vars = dplyr::vars(!".batchvar"), - .funs = \(x) x - mean(x, na.rm = TRUE) + dplyr::mutate( + dplyr::across( + .cols = !".batchvar", + .fns = \(x) x - mean(x, na.rm = TRUE) + ) ) |> tidyr::pivot_longer( col = !".batchvar", From 70dfc767e04fb544ff3989f10324a5f7001da91c Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:45:55 +0100 Subject: [PATCH 13/36] transmute() has been superseded --- R/batchmean_standardize.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/R/batchmean_standardize.R b/R/batchmean_standardize.R index 02d3067..3d0de84 100644 --- a/R/batchmean_standardize.R +++ b/R/batchmean_standardize.R @@ -75,10 +75,11 @@ batchmean_standardize <- function(data, markers, confounders) { dplyr::group_by(.data$marker) |> dplyr::mutate(markermean = mean(.data$batchmean)) |> dplyr::ungroup() |> - dplyr::transmute( + dplyr::mutate( marker = .data$marker, .batchvar = .data$.batchvar, - batchmean = .data$batchmean - .data$markermean + batchmean = .data$batchmean - .data$markermean, + .keep = "none" ) return(list(list( models = res |> From f7438fe3aeb09b41ae6bd1a3eb44adade3dff8d6 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:47:34 +0100 Subject: [PATCH 14/36] change with roxygen 7.0.0 in 2019 --- R/batchtma.R | 4 +--- man/batchtma.Rd | 12 +++++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/R/batchtma.R b/R/batchtma.R index 6c057da..283fb33 100644 --- a/R/batchtma.R +++ b/R/batchtma.R @@ -14,7 +14,6 @@ #' #' \code{\link[batchtma]{plot_batch}}: Plot biomarkers by batch #' -#' @docType _PACKAGE #' @name batchtma #' @seealso \url{https://stopsack.github.io/batchtma/} #' @references @@ -23,5 +22,4 @@ #' Mucci LA+ (+ equal contribution). Extent, impact, and mitigation of #' batch effects in tumor biomarker studies using tissue microarrays. #' eLife 2021;10:e71265. doi: https://doi.org/10.7554/elife.71265 -NULL -# > NULL +"_PACKAGE" diff --git a/man/batchtma.Rd b/man/batchtma.Rd index df225bc..a889c21 100644 --- a/man/batchtma.Rd +++ b/man/batchtma.Rd @@ -1,7 +1,8 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/batchtma.R -\docType{_PACKAGE} +\docType{package} \name{batchtma} +\alias{batchtma-package} \alias{batchtma} \title{batchtma: Methods to address batch effects} \description{ @@ -31,3 +32,12 @@ eLife 2021;10:e71265. doi: https://doi.org/10.7554/elife.71265 \seealso{ \url{https://stopsack.github.io/batchtma/} } +\author{ +\strong{Maintainer}: Konrad Stopsack \email{stopsack@post.harvard.edu} (\href{https://orcid.org/0000-0002-0722-1311}{ORCID}) + +Authors: +\itemize{ + \item Travis Gerke \email{gerket@mskcc.org} (\href{https://orcid.org/0000-0002-9500-8907}{ORCID}) +} + +} From 206d0ed02d08360329a13a6a78750624cd7ef5bb Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:48:09 +0100 Subject: [PATCH 15/36] %>% pipe and . variables are gone --- DESCRIPTION | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index bd78d79..9d99606 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,8 +35,7 @@ Imports: rlang (>= 0.4.0), stringr, tibble, - tidyr (>= 1.1.0), - magrittr + tidyr (>= 1.1.0) Suggests: knitr, rmarkdown, From d30d5edfa2cfca9a8dd0dc2db4325864ea6693d2 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:54:41 +0100 Subject: [PATCH 16/36] unify syntax --- R/batchmean_ipw.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/batchmean_ipw.R b/R/batchmean_ipw.R index 05e4dd2..5a03b12 100644 --- a/R/batchmean_ipw.R +++ b/R/batchmean_ipw.R @@ -113,7 +113,7 @@ batchmean_ipw <- function( .f = \(x) { x |> tidyr::pivot_longer( - cols = c(!".batchvar"), + cols = !".batchvar", names_to = "batch", values_to = "prob" ) |> From 0c531e1a6607eb4d9b6cc7d68fa53ca278889d21 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:55:11 +0100 Subject: [PATCH 17/36] misnamed argument, apparently inconsequential --- R/batchmean_simple.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/batchmean_simple.R b/R/batchmean_simple.R index aca2139..e19777d 100644 --- a/R/batchmean_simple.R +++ b/R/batchmean_simple.R @@ -26,7 +26,7 @@ batchmean_simple <- function(data, markers) { ) ) |> tidyr::pivot_longer( - col = !".batchvar", + cols = !".batchvar", names_to = "marker", values_to = "batchmean" ) From 4aec1a765b95320e273f5e0616d5234a78480501 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:56:26 +0100 Subject: [PATCH 18/36] add covr action from rifttable --- .github/workflows/R-CMD-check.yaml | 48 ++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index ad35fcf..f6d2dcc 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -100,11 +100,6 @@ jobs: BiocCheck::BiocCheck(".") shell: Rscript {0} - - name: Show testthat output - if: always() - run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true - shell: bash - - name: Upload check results if: failure() uses: actions/upload-artifact@HEAD @@ -112,19 +107,46 @@ jobs: name: ${{ runner.os }}-r${{ matrix.config.r }}-bioc-${{ matrix.config.bioc }}-results path: check + - name: pkgdown dependencies + if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::., any::covr, any::xml2 + needs: website, coverage + - name: Test coverage - if: matrix.config.os == 'macOS-latest' run: | - install.packages("covr") - covr::codecov(token = "${{secrets.CODECOV_TOKEN}}") + cov <- covr::package_coverage( + quiet = FALSE, + clean = FALSE, + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") + ) + print(cov) + covr::to_cobertura(cov) shell: Rscript {0} - - name: pkgdown dependencies - if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' - uses: r-lib/actions/setup-r-dependencies@v2 + - uses: codecov/codecov-action@v5 + with: + # Fail if error if not on PR, or if on PR and token is given + fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} + files: ./cobertura.xml + plugins: noop + disable_search: true + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Show testthat output + if: always() + run: | + ## -------------------------------------------------------------------- + find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 with: - extra-packages: any::pkgdown, local::. - needs: website + name: coverage-test-failures + path: ${{ runner.temp }}/package - name: Build site if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' From c04b3c1ffb9e28c439165c371bb1980c27c630e9 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:57:42 +0100 Subject: [PATCH 19/36] trigger new action --- .github/workflows/R-CMD-check.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index f6d2dcc..d83fe20 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -1,5 +1,7 @@ on: push: + branches: + - dev pull_request: branches: - main From e9e9711cb9c737a98e54ef626fb65b20256723af Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:12:57 +0100 Subject: [PATCH 20/36] give intelligible error for empty method argument --- R/adjust_batch.R | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/R/adjust_batch.R b/R/adjust_batch.R index b608389..5058f44 100644 --- a/R/adjust_batch.R +++ b/R/adjust_batch.R @@ -193,6 +193,14 @@ adjust_batch <- function( names() # Check inputs: method and confounders + if(method[1] == "c") { + stop(paste0( + "No valid argument 'method =' provided. Available methods: ", + paste(allmethods, collapse = ", "), + ".\n", + "See: help(\"adjust_batch\")." + )) + } if (!(method[1] %in% allmethods)) { stop(paste0( "Method '", From 94fcf6df87b28ff23d2de64fe9d132aea5b9142a Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:13:15 +0100 Subject: [PATCH 21/36] add some missing tests --- tests/testthat/test-diagnostics.R | 21 +++++++++++++++ tests/testthat/test-equality.R | 33 +++++++++++++++++++++++ tests/testthat/test-input.R | 45 +++++++++++++++++++++++++++++++ tests/testthat/test-plot.R | 17 ++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 tests/testthat/test-diagnostics.R create mode 100644 tests/testthat/test-input.R create mode 100644 tests/testthat/test-plot.R diff --git a/tests/testthat/test-diagnostics.R b/tests/testthat/test-diagnostics.R new file mode 100644 index 0000000..bad385f --- /dev/null +++ b/tests/testthat/test-diagnostics.R @@ -0,0 +1,21 @@ +test_that("diagnostics run", { + df <- data.frame( + tma = rep(1:2, times = 10), + biomarker = rep(1:2, times = 10) + + runif(max = 5, n = 20), + confounder = rep(0:1, times = 10) + + runif(max = 10, n = 20) + ) + expect_no_error( + object = print( + diagnose_models( + adjust_batch( + data = df, + marker = biomarker, + batch = tma, + method = simple + ) + ) + ) + ) +}) diff --git a/tests/testthat/test-equality.R b/tests/testthat/test-equality.R index 6ca325a..bf9d93c 100644 --- a/tests/testthat/test-equality.R +++ b/tests/testthat/test-equality.R @@ -40,3 +40,36 @@ test_that("equivalent approaches give same result", { expect_equal(df_adj2, df_adj3) expect_equal(df_adj2, df_adj4) }) + + +test_that("multinomial model works", { + df <- data.frame( + tma = rep(1:3, times = 10), + biomarker = rep(1:2, times = 15) + + runif(max = 5, n = 30), + confounder = rep(0:1, times = 15) + + runif(max = 10, n = 30), + unity = 1 + ) + + df_adj2 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = simple, + suffix = "adj" + ) |> # drop all attributes + data.frame() + + df_adj4 <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = ipw, + confounders = unity, + suffix = "adj" + ) |> + data.frame() + + expect_equal(df_adj2, df_adj4) +}) diff --git a/tests/testthat/test-input.R b/tests/testthat/test-input.R new file mode 100644 index 0000000..0207d87 --- /dev/null +++ b/tests/testthat/test-input.R @@ -0,0 +1,45 @@ +test_that("input errors are detected", { + df <- data.frame( + tma = rep(1:2, times = 10), + biomarker = rep(1:2, times = 10) + + runif(max = 5, n = 20), + confounder = rep(0:1, times = 10) + + runif(max = 10, n = 20) + ) + expect_error( + object = adjust_batch( + data = df, + markers = biomarker, + batch = tma + ), + regexp = "No valid argument \'method" + ) + expect_error( + object = adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = inexistant + ), + regexp = "is not implemented" + ) + expect_message( + object = adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = simple, + confounders = confounder + ), + regexp = "does not support adjustment" + ) + expect_message( + object = adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = standardize + ), + regexp = "no valid confounders" + ) +}) diff --git a/tests/testthat/test-plot.R b/tests/testthat/test-plot.R new file mode 100644 index 0000000..1179701 --- /dev/null +++ b/tests/testthat/test-plot.R @@ -0,0 +1,17 @@ +test_that("ggplot runs", { + df <- data.frame( + tma = rep(1:2, times = 10), + biomarker = rep(1:2, times = 10) + + runif(max = 5, n = 20), + confounder = rep(0:1, times = 10) + + runif(max = 10, n = 20) + ) + expect_no_error( + object = plot_batch( + data = df, + marker = biomarker, + batch = tma, + color = confounder + ) + ) +}) From 0a2855c2f7bb6400b5cd0a596ae691b5333e4c40 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:16:07 +0100 Subject: [PATCH 22/36] catch pipe in vignette --- vignettes/batchtma.Rmd | 58 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/vignettes/batchtma.Rmd b/vignettes/batchtma.Rmd index 0d79ea0..300d903 100644 --- a/vignettes/batchtma.Rmd +++ b/vignettes/batchtma.Rmd @@ -63,7 +63,7 @@ df We plot the biomarker values (*y*-axis) by batch (*x*-axis), using the `plot_batch()` function. Color/shape symbolizes which participant/tumor the measurements came from. Boxes span from the 25th to the 75th percentile (interquartile range); thick lines indicate medians; asterisks indicate means. ```{r} -df %>% plot_batch(marker = noisy, batch = batch, color = person) +df |> plot_batch(marker = noisy, batch = batch, color = person) ``` @@ -72,14 +72,14 @@ df %>% plot_batch(marker = noisy, batch = batch, color = person) We add systematic differences between batches such that there is differential measurement error between batches in terms of mean and variance. As shown above, true values were the same beyond random error. ```{r} -df <- df %>% +df <- df |> # Multiply by batch number to differentially change variance by batch, # divide by mean batch number to keep overall variance the same: mutate(noisy_batch = noisy * batchnum / mean(batchnum) + # Similarly, change mean value per batch, keeping overall mean the same: batchnum * 3 - mean(batchnum) * 3) -df %>% plot_batch(marker = noisy_batch, batch = batch, color = person) +df |> plot_batch(marker = noisy_batch, batch = batch, color = person) ``` @@ -90,9 +90,9 @@ df %>% plot_batch(marker = noisy_batch, batch = batch, color = person) `method = simple` calculates the mean for each batch and subtracts the difference between this mean and the grand mean, such that all batches end up having a mean equivalent to the grand mean. Differences in variance between batches will remain, if they exist (as in this example). ```{r} -df %>% +df |> adjust_batch(markers = noisy_batch, batch = batch, - method = simple) %>% + method = simple) |> plot_batch(marker = noisy_batch_adj2, batch = batch, color = person) ``` @@ -102,9 +102,9 @@ df %>% `method = standardize` performs marginal standardization by fitting a linear regression model for biomarker values with `batch` and `confounders` as predictors, and obtains the marginal means per batch if they had the same distribution of `confounders`. Differences between these marginal means and the grand mean are subtracted as in `method = simple`. In this example, the confounder is a random variable, and the results are essentially the same as for `method = simple`. ```{r} -df %>% +df |> adjust_batch(markers = noisy_batch, batch = batch, - method = standardize, confounders = random) %>% + method = standardize, confounders = random) |> plot_batch(marker = noisy_batch_adj3, batch = batch, color = person) ``` @@ -114,9 +114,9 @@ df %>% `method = ipw` predicts the probability of a measurement being from a specific batch, given the `confounders`. Mean differences between batches are obtained from a marginal structural model with stabilized inverse-probability weights and then used as in the two preceding methods. Again, the confounder is merely a random variable in this example. ```{r} -df %>% +df |> adjust_batch(markers = noisy_batch, batch = batch, - method = ipw, confounders = random) %>% + method = ipw, confounders = random) |> plot_batch(marker = noisy_batch_adj4, batch = batch, color = person) ``` @@ -126,9 +126,9 @@ df %>% `method = quantreg`, unlike the three preceding mean-only methods, addresses two distinct properties of batches: the offset values (a lower quantile), potentially reflective of background signal, and an inter-quantile range, potentially reflective of the dynamic range of the measurement. By default, the first and third qua**r**tile are used. ```{r} -df %>% +df |> adjust_batch(markers = noisy_batch, batch = batch, - method = quantreg, confounders = random) %>% + method = quantreg, confounders = random) |> plot_batch(marker = noisy_batch_adj5, batch = batch, color = person) ``` @@ -137,9 +137,9 @@ df %>% `method = quantnorm` performs quantile normalization: values are ranked within each batch, and then each rank is assigned the mean per rank across batches. Quantile normalization ensures that all batches have near-identical biomarker distributions. However, quantile normalization does not allow for accounting for `confounders`. ```{r} -df %>% +df |> adjust_batch(markers = noisy_batch, batch = batch, - method = quantnorm) %>% + method = quantnorm) |> plot_batch(marker = noisy_batch_adj6, batch = batch, color = person) ``` @@ -157,25 +157,25 @@ All following plots show color/symbol shape by `confounder`. ```{r} set.seed(123) # for reproducibility -df <- df %>% +df <- df |> # Make confounder associated with batch: mutate(confounder = round(batchnum + runif(n = 200, max = 2)), # Make biomarker values associated with confounder: noisy_conf = noisy + confounder * 3 - mean(confounder) * 3) -df %>% plot_batch(marker = noisy_conf, batch = batch, color = confounder) +df |> plot_batch(marker = noisy_conf, batch = batch, color = confounder) ``` Second, batch effects for mean and variance are added. ```{r} -df <- df %>% +df <- df |> # Add batch effects to confounded biomarker values: mutate(noisy_conf_batch = noisy_conf * batchnum / mean(batchnum) + batchnum * 3 - mean(batchnum) * 3) -df %>% plot_batch(marker = noisy_conf_batch, batch = batch, color = confounder) +df |> plot_batch(marker = noisy_conf_batch, batch = batch, color = confounder) ``` The following examples show to what extent batch effects can be removed from `noisy_conf_batch` to recover the "ground truth," `noisy_conf`. @@ -186,9 +186,9 @@ The following examples show to what extent batch effects can be removed from `no `method = standardize` reduces batch effects while allowing batch means to differ because of confounders. Batches with higher true biomarker values associated with the confounders, like batch B, retain higher means after batch effect adjustment. ```{r} -df %>% +df |> adjust_batch(markers = noisy_conf_batch, batch = batch, - method = standardize, confounders = confounder) %>% + method = standardize, confounders = confounder) |> plot_batch(marker = noisy_conf_batch_adj3, batch = batch, color = confounder) ``` @@ -198,9 +198,9 @@ df %>% `method = ipw` also reduces batch effects while allowing batch means to differ because of confounders. As with marginal standardization, differences in variance between batches are not addressed. ```{r} -df %>% +df |> adjust_batch(markers = noisy_conf_batch, batch = batch, - method = ipw, confounders = confounder) %>% + method = ipw, confounders = confounder) |> plot_batch(marker = noisy_conf_batch_adj4, batch = batch, color = confounder) ``` @@ -210,9 +210,9 @@ df %>% `method = quantreg` reduces for batch effects of offset and dynamic range separately, accounting for differences in confounders. In the example, batch B with its high levels of the `confounder` retains somewhat higher values and a higher variance, unlike with the two preceding mean-only methods. ```{r} -df %>% +df |> adjust_batch(markers = noisy_conf_batch, batch = batch, - method = quantreg, confounders = confounder) %>% + method = quantreg, confounders = confounder) |> plot_batch(marker = noisy_conf_batch_adj5, batch = batch, color = confounder) ``` @@ -224,9 +224,9 @@ df %>% `method = simple` leads to all batches having the same mean after adjustment. In the example, this method ignores that batch B should have higher values because of higher values of the `confounder`. ```{r} -df %>% +df |> adjust_batch(markers = noisy_conf_batch, batch = batch, - method = simple, confounders = confounder) %>% + method = simple, confounders = confounder) |> plot_batch(marker = noisy_conf_batch_adj2, batch = batch, color = confounder) ``` @@ -236,9 +236,9 @@ df %>% `method = quantreg` also ignores the confounder. ```{r} -df %>% +df |> adjust_batch(markers = noisy_conf_batch, batch = batch, - method = quantnorm, confounders = confounder) %>% + method = quantnorm, confounders = confounder) |> plot_batch(marker = noisy_conf_batch_adj6, batch = batch, color = confounder) ``` @@ -264,7 +264,7 @@ To obtain an overview of model diagnostics, `diagnose_models()` is called on the ```{r} # df2 is the new dataset that also contains "noisy_conf_batch_adj2": -df2 <- df %>% adjust_batch(markers = noisy_conf_batch, batch = batch, +df2 <- df |> adjust_batch(markers = noisy_conf_batch, batch = batch, method = standardize, confounders = confounder) # Show overview of model diagnostics: @@ -287,7 +287,7 @@ Diagnostics for regression models can be performed on fitted models just as for ```{r} tibble(fitted = fitted.values(fit), - residuals = residuals(fit)) %>% + residuals = residuals(fit)) |> ggplot(mapping = aes(x = fitted, y = residuals)) + geom_point() + theme_minimal() From 6c78e36ea61e333fd4e3272bf1a1deaf11999445 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:49:31 +0100 Subject: [PATCH 23/36] match install instructions with index.Rmd --- vignettes/batchtma.Rmd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vignettes/batchtma.Rmd b/vignettes/batchtma.Rmd index 300d903..8015609 100644 --- a/vignettes/batchtma.Rmd +++ b/vignettes/batchtma.Rmd @@ -16,17 +16,17 @@ knitr::opts_chunk$set( # Installation -Batchtma can be installed from [GitHub](https://github.com/) using: +batchtma can be installed from [CRAN](https://cran.r-project.org/) using: -```{r, eval = FALSE} -# install.packages("remotes") # The "remotes" package needs to be installed -remotes::install_github("stopsack/batchtma") +```{r} +install.packages("batchtma") ``` -To have vignettes like this current site be available offline via, *e.g.*, `vignette("batchtma")`, modify the last command: +To install a potentially newer version from [GitHub](https://github.com/), use: -```{r, eval = FALSE} -remotes::install_github("stopsack/batchtma", build_vignettes = TRUE) +```{r} +# install.packages("remotes") # The "remotes" package needs to be installed +remotes::install_github("stopsack/batchtma") ``` From 1f5b45c75ce680d6cb1de70e4ada89b5b0c557ad Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:49:39 +0100 Subject: [PATCH 24/36] reformat code --- vignettes/batchtma.Rmd | 242 ++++++++++++++++++++++++++++++++--------- 1 file changed, 189 insertions(+), 53 deletions(-) diff --git a/vignettes/batchtma.Rmd b/vignettes/batchtma.Rmd index 8015609..3a0c3b9 100644 --- a/vignettes/batchtma.Rmd +++ b/vignettes/batchtma.Rmd @@ -46,16 +46,34 @@ We construct a toy dataset of 10 individuals (*e.g.,* tumors), each with 40 meas set.seed(123) # for reproducibility df <- tibble( # Batches: - batch = rep(paste0("batch", LETTERS[1:4]), times = 100), - batchnum = rep(c(1, 5, 2, 3), times = 100), + batch = rep( + paste0("batch", LETTERS[1:4]), + times = 100 + ), + batchnum = rep( + c(1, 5, 2, 3), + times = 100 + ), # Participants: - person = rep(letters[1:10], each = 40), + person = rep( + letters[1:10], + each = 40 + ), # Instead of a confounder, we will use a random variable for now: - random = runif(n = 400, min = -2, max = 2), + random = runif( + n = 400, + min = -2, + max = 2 + ), # The true (usually unobservable biomarker value): - true = rep(c(2, 2.5, 3, 5, 6, 8, 10, 12, 15, 12), each = 40), + true = rep( + c(2, 2.5, 3, 5, 6, 8, 10, 12, 15, 12), + each = 40 + ), # The observed biomarker value with random error ("noise"): - noisy = true + runif(max = true / 3, n = 400) * 4) + noisy = true + + runif(max = true / 3, n = 400) * 4 +) df ``` @@ -63,7 +81,12 @@ df We plot the biomarker values (*y*-axis) by batch (*x*-axis), using the `plot_batch()` function. Color/shape symbolizes which participant/tumor the measurements came from. Boxes span from the 25th to the 75th percentile (interquartile range); thick lines indicate medians; asterisks indicate means. ```{r} -df |> plot_batch(marker = noisy, batch = batch, color = person) +df |> + plot_batch( + marker = noisy, + batch = batch, + color = person + ) ``` @@ -75,11 +98,19 @@ We add systematic differences between batches such that there is differential me df <- df |> # Multiply by batch number to differentially change variance by batch, # divide by mean batch number to keep overall variance the same: - mutate(noisy_batch = noisy * batchnum / mean(batchnum) + - # Similarly, change mean value per batch, keeping overall mean the same: - batchnum * 3 - mean(batchnum) * 3) + mutate( + noisy_batch = noisy * batchnum / mean(batchnum) + + # Similarly, change mean value per batch, keeping overall mean the same: + batchnum * 3 - + mean(batchnum) * 3 + ) -df |> plot_batch(marker = noisy_batch, batch = batch, color = person) +df |> + plot_batch( + marker = noisy_batch, + batch = batch, + color = person + ) ``` @@ -91,9 +122,16 @@ df |> plot_batch(marker = noisy_batch, batch = batch, color = person) ```{r} df |> - adjust_batch(markers = noisy_batch, batch = batch, - method = simple) |> - plot_batch(marker = noisy_batch_adj2, batch = batch, color = person) + adjust_batch( + markers = noisy_batch, + batch = batch, + method = simple + ) |> + plot_batch( + marker = noisy_batch_adj2, + batch = batch, + color = person + ) ``` @@ -103,9 +141,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_batch, batch = batch, - method = standardize, confounders = random) |> - plot_batch(marker = noisy_batch_adj3, batch = batch, color = person) + adjust_batch( + markers = noisy_batch, + batch = batch, + method = standardize, + confounders = random + ) |> + plot_batch( + marker = noisy_batch_adj3, + batch = batch, + color = person + ) ``` @@ -115,9 +161,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_batch, batch = batch, - method = ipw, confounders = random) |> - plot_batch(marker = noisy_batch_adj4, batch = batch, color = person) + adjust_batch( + markers = noisy_batch, + batch = batch, + method = ipw, + confounders = random + ) |> + plot_batch( + marker = noisy_batch_adj4, + batch = batch, + color = person + ) ``` @@ -127,9 +181,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_batch, batch = batch, - method = quantreg, confounders = random) |> - plot_batch(marker = noisy_batch_adj5, batch = batch, color = person) + adjust_batch( + markers = noisy_batch, + batch = batch, + method = quantreg, + confounders = random + ) |> + plot_batch( + marker = noisy_batch_adj5, + batch = batch, + color = person + ) ``` ## Quantile normalization @@ -138,9 +200,16 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_batch, batch = batch, - method = quantnorm) |> - plot_batch(marker = noisy_batch_adj6, batch = batch, color = person) + adjust_batch( + markers = noisy_batch, + batch = batch, + method = quantnorm + ) |> + plot_batch( + marker = noisy_batch_adj6, + batch = batch, + color = person + ) ``` @@ -159,11 +228,18 @@ All following plots show color/symbol shape by `confounder`. set.seed(123) # for reproducibility df <- df |> # Make confounder associated with batch: - mutate(confounder = round(batchnum + runif(n = 200, max = 2)), - # Make biomarker values associated with confounder: - noisy_conf = noisy + confounder * 3 - mean(confounder) * 3) + mutate( + confounder = round(batchnum + runif(n = 200, max = 2)), + # Make biomarker values associated with confounder: + noisy_conf = noisy + confounder * 3 - mean(confounder) * 3 + ) -df |> plot_batch(marker = noisy_conf, batch = batch, color = confounder) +df |> + plot_batch( + marker = noisy_conf, + batch = batch, + color = confounder + ) ``` @@ -172,10 +248,18 @@ Second, batch effects for mean and variance are added. ```{r} df <- df |> # Add batch effects to confounded biomarker values: - mutate(noisy_conf_batch = noisy_conf * batchnum / mean(batchnum) + - batchnum * 3 - mean(batchnum) * 3) + mutate( + noisy_conf_batch = noisy_conf * batchnum / mean(batchnum) + + batchnum * 3 - + mean(batchnum) * 3 + ) -df |> plot_batch(marker = noisy_conf_batch, batch = batch, color = confounder) +df |> + plot_batch( + marker = noisy_conf_batch, + batch = batch, + color = confounder + ) ``` The following examples show to what extent batch effects can be removed from `noisy_conf_batch` to recover the "ground truth," `noisy_conf`. @@ -187,9 +271,17 @@ The following examples show to what extent batch effects can be removed from `no ```{r} df |> - adjust_batch(markers = noisy_conf_batch, batch = batch, - method = standardize, confounders = confounder) |> - plot_batch(marker = noisy_conf_batch_adj3, batch = batch, color = confounder) + adjust_batch( + markers = noisy_conf_batch, + batch = batch, + method = standardize, + confounders = confounder + ) |> + plot_batch( + marker = noisy_conf_batch_adj3, + batch = batch, + color = confounder + ) ``` @@ -199,9 +291,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_conf_batch, batch = batch, - method = ipw, confounders = confounder) |> - plot_batch(marker = noisy_conf_batch_adj4, batch = batch, color = confounder) + adjust_batch( + markers = noisy_conf_batch, + batch = batch, + method = ipw, + confounders = confounder + ) |> + plot_batch( + marker = noisy_conf_batch_adj4, + batch = batch, + color = confounder + ) ``` @@ -211,9 +311,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_conf_batch, batch = batch, - method = quantreg, confounders = confounder) |> - plot_batch(marker = noisy_conf_batch_adj5, batch = batch, color = confounder) + adjust_batch( + markers = noisy_conf_batch, + batch = batch, + method = quantreg, + confounders = confounder + ) |> + plot_batch( + marker = noisy_conf_batch_adj5, + batch = batch, + color = confounder + ) ``` @@ -225,9 +333,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_conf_batch, batch = batch, - method = simple, confounders = confounder) |> - plot_batch(marker = noisy_conf_batch_adj2, batch = batch, color = confounder) + adjust_batch( + markers = noisy_conf_batch, + batch = batch, + method = simple, + confounders = confounder + ) |> + plot_batch( + marker = noisy_conf_batch_adj2, + batch = batch, + color = confounder + ) ``` @@ -237,9 +353,17 @@ df |> ```{r} df |> - adjust_batch(markers = noisy_conf_batch, batch = batch, - method = quantnorm, confounders = confounder) |> - plot_batch(marker = noisy_conf_batch_adj6, batch = batch, color = confounder) + adjust_batch( + markers = noisy_conf_batch, + batch = batch, + method = quantnorm, + confounders = confounder + ) |> + plot_batch( + marker = noisy_conf_batch_adj6, + batch = batch, + color = confounder + ) ``` @@ -264,8 +388,13 @@ To obtain an overview of model diagnostics, `diagnose_models()` is called on the ```{r} # df2 is the new dataset that also contains "noisy_conf_batch_adj2": -df2 <- df |> adjust_batch(markers = noisy_conf_batch, batch = batch, - method = standardize, confounders = confounder) +df2 <- df |> + adjust_batch( + markers = noisy_conf_batch, + batch = batch, + method = standardize, + confounders = confounder + ) # Show overview of model diagnostics: diagnose_models(df2) @@ -286,9 +415,16 @@ diagnose_models(df2)$adjust_parameters Diagnostics for regression models can be performed on fitted models just as for any other regression model in R. For example, residuals *vs.* fitted can be plotted: ```{r} -tibble(fitted = fitted.values(fit), - residuals = residuals(fit)) |> - ggplot(mapping = aes(x = fitted, y = residuals)) + +tibble( + fitted = fitted.values(fit), + residuals = residuals(fit) +) |> + ggplot( + mapping = aes( + x = fitted, + y = residuals + ) + ) + geom_point() + theme_minimal() ``` From 92d0b18070400463060d6b9fee8c185e4999313b Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:53:05 +0100 Subject: [PATCH 25/36] do not actually install pkg in vignette --- vignettes/batchtma.Rmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vignettes/batchtma.Rmd b/vignettes/batchtma.Rmd index 3a0c3b9..3b5406c 100644 --- a/vignettes/batchtma.Rmd +++ b/vignettes/batchtma.Rmd @@ -18,13 +18,13 @@ knitr::opts_chunk$set( batchtma can be installed from [CRAN](https://cran.r-project.org/) using: -```{r} +```{r install_cran, eval = FALSE} install.packages("batchtma") ``` To install a potentially newer version from [GitHub](https://github.com/), use: -```{r} +```{r install_github, eval = FALSE} # install.packages("remotes") # The "remotes" package needs to be installed remotes::install_github("stopsack/batchtma") ``` From 27390195abbb2c97a87271d690af16e7a1e43983 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:53:37 +0100 Subject: [PATCH 26/36] release v0.2.0 (also fixes #3) --- DESCRIPTION | 2 +- NEWS.md | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9d99606..b8efd68 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: batchtma Title: Batch Effect Adjustments -Version: 0.1.7-9000 +Version: 0.2.0 Authors@R: c(person(given = "Konrad", family = "Stopsack", diff --git a/NEWS.md b/NEWS.md index f0722f3..5102098 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,15 @@ +# batchtma 0.2.0 + +* Braking change: Require R >= 4.1 +* No other user-visible changes. +* Internal changes: + + Replace long-deprecated `filter(across())` with `if_all()` to be {dplyr} + 1.2.0-compatible (#3, thanks to @DavisVaughan). + + Split code into multiple files. + + Use modern tidyselect, pipe, and anonymous functions. + + Add some unit tests. + + # batchtma 0.1.7 * Separately call tidyverse packages @@ -16,7 +28,7 @@ # batchtma 0.1.4 -* Bioconductor limma dependency added for `remotes::install_github()`` +* Bioconductor limma dependency added for `remotes::install_github()` * Code review, updates to curly-curly, and tidyverse style changes, thanks to @tgerke From 2693a83658f71f99745856e7e918468f1a180493 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:54:55 +0100 Subject: [PATCH 27/36] update action version --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index d83fe20..55ff19c 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -64,7 +64,7 @@ jobs: - name: Cache R packages if: runner.os != 'Windows' && matrix.config.image == null - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ${{ env.R_LIBS_USER }} key: ${{ runner.os }}-r-${{ matrix.config.r }}-bioc-${{ matrix.config.bioc }}-${{ hashFiles('depends.Rds') }} From 3d84e30af2b5dc721f7a88330692ff9a1542c947 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:41:54 +0100 Subject: [PATCH 28/36] code formatting only --- index.Rmd | 30 ++++++++++++---- index.md | 59 +++++++++++++++++++++----------- man/figures/index-example-1.png | Bin 25505 -> 25165 bytes man/figures/index-example-2.png | Bin 26483 -> 26740 bytes 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/index.Rmd b/index.Rmd index 11984f3..82826e2 100644 --- a/index.Rmd +++ b/index.Rmd @@ -48,16 +48,34 @@ library(batchtma) Define example data with batch effects: ```{r eval=TRUE} -df <- data.frame(tma = rep(1:2, times = 30), - biomarker = rep(1:2, times = 30) + runif(max = 3, n = 60)) +df <- data.frame( + tma = rep(1:2, times = 30), + biomarker = rep(1:2, times = 30) + + runif(max = 3, n = 60) +) ``` Run the `adjust_batch()` function to adjust for batch effects: ```{r example, eval=TRUE, fig.show="hold", out.width='40%'} -df_adjust <- adjust_batch(data = df, markers = biomarker, batch = tma, method = simple) - -plot_batch(data = df, marker = biomarker, batch = tma, title = "Raw data") -plot_batch(data = df_adjust, marker = biomarker_adj2, batch = tma, title = "Adjusted data") +df_adjust <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = simple +) + +plot_batch( + data = df, + marker = biomarker, + batch = tma, + title = "Raw data" +) +plot_batch( + data = df_adjust, + marker = biomarker_adj2, + batch = tma, + title = "Adjusted data" +) ``` * The ["Get Started"](articles/batchtma.html) vignette has details on how to use the package. diff --git a/index.md b/index.md index 39c9b5e..ca02d24 100644 --- a/index.md +++ b/index.md @@ -4,6 +4,7 @@ # batchtma R package: Methods to address batch effects + The goal of the batchtma package is to provide functions for batch @@ -40,23 +41,41 @@ library(batchtma) Define example data with batch effects: ``` r -df <- data.frame(tma = rep(1:2, times = 30), - biomarker = rep(1:2, times = 30) + runif(max = 3, n = 60)) +df <- data.frame( + tma = rep(1:2, times = 30), + biomarker = rep(1:2, times = 30) + + runif(max = 3, n = 60) +) ``` Run the `adjust_batch()` function to adjust for batch effects: ``` r -df_adjust <- adjust_batch(data = df, markers = biomarker, batch = tma, method = simple) - -plot_batch(data = df, marker = biomarker, batch = tma, title = "Raw data") -plot_batch(data = df_adjust, marker = biomarker_adj2, batch = tma, title = "Adjusted data") +df_adjust <- adjust_batch( + data = df, + markers = biomarker, + batch = tma, + method = simple +) + +plot_batch( + data = df, + marker = biomarker, + batch = tma, + title = "Raw data" +) +plot_batch( + data = df_adjust, + marker = biomarker_adj2, + batch = tma, + title = "Adjusted data" +) ``` - + -- The [“Get Started”](articles/batchtma.html) vignette has details on - how to use the package. +- The [“Get Started”](articles/batchtma.html) vignette has details on + how to use the package. ## Methodology @@ -69,17 +88,17 @@ characteristics that are expected to lead to differences in biomarker levels. They should ideally be retained when performing batch effect adjustments. -| \# | `method =` | Approach | Addressed | Retains “true” between-batch differences | -|-----|---------------|-------------------------------|------------------------|------------------------------------------| -| 1 | — | Unadjusted | — | Yes | -| 2 | `simple` | Simple means | Means | No | -| 3 | `standardize` | Standardized batch means | Means | Yes | -| 4 | `ipw` | Inverse-probability weighting | Means | Yes | -| 5 | `quantreg` | Quantile regression | Low and high quantiles | Yes | -| 6 | `quantnorm` | Quantile normalization | All ranks | No | - -- [“Get Started”](articles/batchtma.html) shows general examples on - the different methods in absence and presence of confounding. +| \# | `method =` | Approach | Addressed | Retains “true” between-batch differences | +|----|----|----|----|----| +| 1 | — | Unadjusted | — | Yes | +| 2 | `simple` | Simple means | Means | No | +| 3 | `standardize` | Standardized batch means | Means | Yes | +| 4 | `ipw` | Inverse-probability weighting | Means | Yes | +| 5 | `quantreg` | Quantile regression | Low and high quantiles | Yes | +| 6 | `quantnorm` | Quantile normalization | All ranks | No | + +- [“Get Started”](articles/batchtma.html) shows general examples on the + different methods in absence and presence of confounding. ## Reference diff --git a/man/figures/index-example-1.png b/man/figures/index-example-1.png index 5e8d0f7378c80450fad5837b7b9220b522ba2959..20477cad4dbe17dd44a1e739765b6fe9ec8197e2 100644 GIT binary patch literal 25165 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 literal 25505 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 diff --git a/man/figures/index-example-2.png b/man/figures/index-example-2.png index 3e841052f1c40cdddf0b6115f4eedbcfbd87316c..093fbf939f11e7a0d5719c42691ec3472dee93f6 100644 GIT binary patch literal 26740 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

H94FycsQSBlJ4-bO5$I!h^{`@| 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 C8i<|% literal 26483 zcmeEuWmJ@J^zAS-h@dD4Qi7C}0*ds2ARwirv_YrR9fCgtq@_!`LAt@FJET#%yX&3- z^?yI#Pj}sQKfoep-g(|9&e?mPecm_s6{Ls=CEMwOS8(~DU zQ|59^cKt5=G4=JUfbA!@b|SZS4mSVy*WNQ>Tps^iJFvJ+_kE;qt91L=Xg>G86vpe} zmp`*_H`4z$Kpb8CR-ogFCAMFW-}ht(|Er*R4BhYZUy2dxm*R+53fdxrxL@l@va@9R z9eo{qjXTY-rHO@Wvoh!}bTd7NmA*-`;Z|0Z0z+4|_r6DoMavqx8aeM&bW-W6s#tU$;Bh z+_P7Yjl}yM%uh~8>Bq>WR=g@+@1b7IoJ^uuV(Ss`d#GN1Fr*P9zBZn6@_q*A@$3@h!KpO=kQ8LG~>I~ix$wc{H;W9Ysi!zB~!y+cJGGI5>aN)lZ#bAz(JpMHnH zy|XuegwfmRua|dpJ7F*=#q;@A`OY|qeLphNj~FXz`*ts$kK||D&o;Bh_kEVS0&bS- zzLj^?;wyV?W^Jz;QJQkt)(>>YTDKj4)|>9j*PG_kJadHWj_EN!s8)J&p0`#Nkdu!6ow3Wuus1T>{H|#Y`ntN9x()aN|itz62zdjj7-tO`K@=0BhvMq~tepAcQ zl5qs_)3sXnc>mDZZ5^|F6j7FNjCsuHNcin(ZxaNF+XfG14CUn!*WhP71jZ{91Qz^+ z0bi8xg^nf2zkfj{Bw+si8Dk9n&+~eNLkI*4AtNEC;)t<0j33jYdR(`7IT=lpFQ;gK3wz<7JWU zR_p23*vf;jHmBrL)|#x}-*A%&F?eaj-4InGi1`@74`dDUB*^*3$jx>+fNor~DD zN}3&-lVw8mzxz?t^aVLv|Ne?^(w7@swL2Akj^9AdiFottFPh7<+&)oyTX#-p0K*FZ zHa()Ju2AC5dFwXe?fRY7@yVHvq{n@oKWIJJzx+o;xWBV-wp++;vq!zZdhzw5C5tyh zLc`V5$)X|8cSB?n-mE^{8tg2w&?+DI7t~)Ksb2epORKMCv3HcMQK%1#p|PY~pfAI! zTUEAP&7oCNpQ}^pnwP_Tna?uRZGTIHt7SZ|_UM|P`)*41!;kcIcnUHy=PXislKc9e z9+F3NYW+6r&A!2rx2J2*npS1x5MxO^`E&e7KvSk_mK=rq!KxI$^~CGtQ2zIi_qP_g ztww|*gZAs1*OMgqJqqj8crf!YoDr=f$Y45trpi^WO9a3^^uUsgVUS?)$eF z_vZ5yW#!~}MV*`Lyf9ln28bS&Ssho{&8OPUw13*JaNe9V;TwA9y<99JL;St@IVR4T z>x0C!A~9h!h-eFEcVP!-y67+(yy)#NOr=8kQZ3QhPq9wR`^&Y8g5!QXNPqcS)%Y-) z_Q>tc`1|Wif_2L>=|56SsnXtGQ*E4J(5rEsy1~!SZ!u70GG*TX{ufNjy~?#lHcbC6 zZe53AJCh&&)ay<7is%0B-*znXQRH6y@xhNFcST~y&T*O`PMwPS&J@|nSG4Ze^Kv}) zHfEn{USd{^W1sa3V%MwD9`QJKn_mQqmz?$`8e!>ijrTSa-qff|P*wiV?X$mz$lZ?wB zsIvN$OScvWqs6pwiD`IuWsWZf;nFzPP26b;QKf*XW3-uSzER>lG*E0NUtY|1CqK+s z@0a6?Kj#ilpCsQ7ky%?Y zJ&q1K%bq_iY$;Wyi8Fo_HsZ`8AH^Fa{*pi@R=`dI^XVdGLTO)@mSl=tB=;178mS#&+CR3v=37iEiQaH5B^0RfT2R1N@?R$?x zqttS>OIj5T6Tof(s96m$x8O_{&;|> z97PdrcHEs18h~woVxcwLqj`ji7RAlkneFUO!R0CjVV5mlZ}$zitjUf6buB8{a88Tr zGrJEkIggVu($1RiZPy-i#Jg=pmTzX~#6{(uW4IfkwQ#hr)n?}zQqt>B!Sj7!!bNDk znMV>ATf;l}bAwCzk2$-g%4HE!2{SXJmjoB(%$8=;;w?2%JYjv|4I9inMy@O>sf48dj`IN9{IaL%{G&Yt4e2BM`Q8@MwyBcslyF^B%P(e# z%ug|?7K0_&3u&L!z6UK}2qxFItJU~%IL%^OaW66DbZV(@N^SROO*=Q~RbtHK9Qo}Q zo)uXv>XP$JTMwMGSkCTL6g||x^(tihep9Rno~-RPHj9?-UhS6fRF~yy^{KXa+Urk7 zt5%PXXXKF1(YoHPlD=e-399>rri8g+@rZ6c-|0Wmnj{l7vaI;ZIYoPk&wk;djSo)5 zbc~Hconq46cM~OpmOP~;wYyW7`RB6>fUPdz0aqxkN+jUD+E8e z5k35@%yJQQvt1lA)y|s*>~H;1eH-5|TSbUTIlE}@`<r3Uxs#o6ttlkg zs#{Io;-y@?b%S4a-{7Rc+92gd@{eK1@pp+{xj4@T@{}>V-4FL72j|uG+=}^&OP-m| zC+Mn6jha!=I8B_(c=$1qf2rRn{-zt39sS`e8t3|=Yo@!lbt+|$uYPW1q)oJm9`lM7 zau#On_8nyj@MSP9Ue08FN&&IouyLnX%PO*5vDo0DmQtntqA3oC#*awvUEIGp9#567 zTD8rd)$JN1&$9kr`eK5US$l$*rgM9En0waGh3z4mwiMM2rEgTxG^o^+QF_I$a!&6S z%qWq2!W_b{Ql)6Dd(JgkWe#y&GU5TpO?ZPZ8+EDBBe0rzBr4GZL%UVD!&u)LQG`d{&rqwLNZkO}+ zQf$~rZP&z2H_!LW<`hkVG_I+!4kJ#6qQbWhOFW`lTcSi#lr^2%a+hz%{adIH1XpML zXT4r#bpn`Z;V`213B56U;hyvSUWlRyzN6Xk(V^jpLaaSYW%9b?+*>KUaN!4 z0J6sXEVY0Nq||wr7Scb{_BtMcVSKPqZhZmEpGNo^Hw{6}K_g+_|0i%$K|7Jl9+2edgEZB^6IWOAGAbTDoB zuxz=F!#ZP6zP*(JIK_R@dbGyPdcY*jU?47>2(obC8&c-R|A;QHSyTE;p6rM^8}v$M zSv!#51~|cOF~F-VO>}ot3X=KZ)t7|ix7z*%lHY8BqBY^ub9fTE36QcAIpP5KyoWED zS7)h{;WLA3NsJ^;_nB8dY^pC$#_1{vL)kTyDj+Ovn+J=YPJ|F^9fzZ$n&8WSoANg^ z3PCCAlI!<(J2@)Vg_VxkS-Qsl{?ErV7TdPl5Kp8~29Gr~C5bc@{t}Gj(GN zN?XhN4iaZd%n*%8`5T^7=bg^yyG!l5wU9F4o}7fT`*nMM&AG-UXHe^tP2rQKxB4%> zSj78!hwkRh9_^C*AH)x8z9%8en9x`De2Q&O;VGaN;x~8~<^APB(%pvESiuyT5GKQG zOoRIVey34r>^YE}fMaCY$DM(NysqnLl=kb(D|rWh!KJc0BW~L|d}QI=r<7^zHpbmg zg1L?L&lHe{``dipKVf%F%*!5hNYOUT5h>_kc_z1&@iaW*=|If0W!sXd>qU|>D>QR^2;g4(133lZ z1Po-{swYG`k+7&{hF)c2!if<}OS-0({jlzq?&-RbSi$r;_96PD=){pH>}HNtIE7O( zv+KdEh2#0ty=8x%4pFRZ5lD`;M)wTH--IRyrahTTHLgzOGl{iSt_;m)V(Z~Fn-ZR^S!|k5=ZV6B zQJkTxIz2CR+Frm_9{xv6yCmp3*PXGpSUP%deX7N65X}wfhh9vV{yA58c-eZjB6K@N z3Qj+$9Udw=Ng%wA$>|W2>KG?$N1d{uIuj>Mr%ds_606WuKxapS@Lq9{^m{9yYDC&D~34n zG13Uzal2=4N*|rv>~GHYA`|6ZfUXi+tT`%W&mN;W(ZSAmKra)`In>xOwlT)P0SV!` z9_>ySnD=wb$jGe5N@|Ii_2pWmif7&b@WM(EsOeZO?Dzwwlhtw59P7?dPTgFC=^$~7 z8%%Z+^(Et=eD+XNGfE>e+jh1SW6;S9fjou?DO8Jpaw6g`-UjAdWZHA@XlI5jilNjrjj%R$`_V{qyS0Q{NeuK>QG&l$mBLN@=%W_q# zD8$@;{6b;TAzIYKeRinahNiXgg97X$J7=O`4|Hts%EzYhI9Tce_UYa|czqsv@Xi~uNd_EL*Z7Q-D$EnvIivN@WDRx+C7MMb;pZ}~7M z@DR+L!K4T|yF>4!?uqEGv{P(AZ>2UuwBXY<6fbbwaK1MuuJp|XRH0!@b2IWthkApo z3TN-?G8&DPF0t~%$sBX?g6tJ_^!kWI051dL$x*apoRk+(hx-5|Ewn*QVTZQ|`*K#W z1Q<;|V}^hwP`*_!h(6V*III!Zb$Om^gOr=O95%$l8-Xz%N4rh}R{7qgR-=~4eAO&9 z?7zXa--L6xdM-7Ahw5p zc}48o8o#&RCMxTVL-ad@B6p4el8U+t!^;zO15X_l-a^G|e823~E7zg9o~+5ath}>7 zVO>Z0a=ZYyE7sWyR`0;C^0W%aLN_f2RC=>D7<#hQw?~_8u*As#M%1e;ygtdQs2yZi znj$%o{b5LM6|d#c?PK{87ZLsXKsl#QlhE%0wDBf_mr3wcJ;~bdr~D-Lx{YT{`w{R$ z6ysrq!!l=Dti2|c^(V%2D7y2=in~?0RmpCwhsq7XjE8yyOXUi5@i!p0UsLAiUixB& z^#Z>gNh$Ygo8)y;9dI5-0r72!`Xm-!>6*ovvsJV|*q@tVk<@#8ZXI^_gxliZKjyWb zkUkDCaTx)W1tW42uC$#E{xc#3uL|qd9lHOJ0}Z2;cpo^3oGWF!i1>yAO?tvE8T!gz z@kG&Jy(Q5 zAG@DN_>6F5z7`{NGz?i`UvL!E_n_cNW}3PCTxD?;M6q$&-=({Sq;%EHi91s7M(~c);T&%}`NQN( z$fCl#yJ^oOgGgXSu3q4m%Ed0iMyY>k*HVH=b1_Z6K4u;{y@se3P9xRVlIfG{Ra1=h z4M21&{W|w2L>GAq6i-pbSj8@s+b<4aFt$Ikg=4wFx~5z{8K%`-U4joUGEf#yht`DriXi1f*3hcoC7TXehcC%NCQ z03v?Eu>!EMA<`($WG`)AVD<7X^VfD{LN>y&fuf9z-*_fd61*VPd(l+A^+KO=*V2KI zkdPMIy>MM;ncS+i#!K3-AenM8SSb8y~FIn)?g4#_1zF!U&t=q zc;XXjs(|}p?%sWn+0N{1Q_aX>?0E_lj!?zVdmN0@cGkGLbXD9hw>Ic_6A8G9gx1TT zDXf9kOTy627eMR4@RyvP`+4gIHPVj(q>OFPB8+*&up`gsujQ;fkAM`p%c#C+dT&cPGPf z%^l_lW9Rn(Th&{Ool6U2?c8NTyendvMl0J?EccV8&vl6O>niY#I3}Gz(-Fvq?7Y|e zOvZC7=B{xSN>nWfq^$yDrg)~SGnw=Bhks)2D@*ACkBAG}fU?Snm+8@z#8EoIYP7~0 zXnWICOSJsx#=ZB~oYFTwvd$%ldFy|BdvUFSIbJ$LDW$m$GKmhU>-W1IyXtS85Sg|5 zAV@y9#WkH)7lbncH(Rfe0Pju|O`gGbJy`v*HXZK~)v+vqkilIDtYReTGKnB#e@M62 zVgyNZMCngbfd!;XD2(SdZucDHL46bCdP3&8G*Ug-e3;&{c=ZpGh08A!e4QYxYTelQ zOvojT&brQ{{Lkl)n$NxuIlufvt4^~;hBjwGiR_4zp`tt`4Lbf6qlM+ms9PxM!(Ibi`izcq(J#p<*>{ zCI**~0+b_J@x&Dm#XmmzR*;jJj+;>7zqB#iHTjNjr2ZBu#i%q0>Pf%3EvIiH?a#N5 zD$5I4;OWRNsPIfMmLflCvRb=vZb<=%MIF%a)_F<7*c^^y8JZFO462gIoAZ!#vabk~hqsS20uKzd98 z+M`g<4e;_L{-`2v5TnxMB7a{)5sb5YK?Kw_Y>8srU7IA;*^}?9q2bnlT}U8id5VRf zm7$uaFzm`m`yIUMppqh~K6=}6B>`WgSo-*AyVhW;Ig*n_XXI^y@=Ifn#k`umduO45 zP#W_5kK(|O_q7U}_tnA5E#w2zosBL(p1^Cn|<{RZI%&We097bq2lR(&+c_c~>{ z-h*Do(sUS8wfghpQ|nLI^ zee2D5Tr}7cb<<2@-z$U!MJIwBdJzyHsQ%8o4pP4r^8Vs+hK%TI-umv|z-%QD5Sm3G zi^yo<6kq%%KMQf408gaYT*{v+2sUPUMnvehBqY!{-cM;{dRjY*faI>)-X#O(@>hi6 z|Bh76R5?dfVtdptU^n;rtaRsK3pT-pdT!?VzPz>z5hit+)6bL}O;g?iPJW&HL`=MH z76~#{ko9Dv!L|Qd73IYdUOT|1zPH7({SZqq?=L8DTz!JZL9Q#GaK+TqjXRUylpij| z%UclH4O$55ToB*?rXi#CnyX@lK0`e(uK0}<78Tn`1a@zw2yu_R3Q78}(qzGzP`Yq) ztlK>?idVWztqUq9gF@5gO(U4ZLUbSw2d_;w$)e@)a35?!vWcau3-3JTv4O?0+1AR zXhVFaXHV!#pCty0xo`z^3zu%p@{>r0{wBl#*f%mWHQ`Tg^z;;lt1Lbso`8*Y%2!wX zAORa?M!h%*rReOoMFQMXN;r`4-wEVFYcLt61aD9BFMRuX2QV_tCW`a)8t_qVc-M*3 ziLxpn5EmG>P+p$$W8Wo=5pwFRI)E~SMcC$sN5J$JDGN}YDxemI#mxJY{#0o7VdOk~ z++()@3ZA=-@7b7(&H73nFVzP#21Z28Xoc}`aI_3GClO&oUb_mhC1j-9h17E(-@P98 zlOyPh-KlmXAetiPjnmr5TaFfywAoR#_sP{cCI8k_u@+cR z2-6>$R}}wEi!TIA9qsZ9M1Q9xKMAIV+U%v!znU^98L*G%lS;h*ro|to#XkGun$O>9 zDSHUx`SxD6_`iK-ybkQywSd*f2-r3HYxv5< z6&)nc5F7x#bOXMK?9GxYoibubt0=(j{R%YeeP)YP{}fk^C~XwsQaY*_S+W0SS~ z`1tr@qFNT0J!u4NS(G;0oJjCUB0z4z7a%0(G_RQfy+CqfI|me0jQ!QmZb+#Bnwxfu zHNg+)0d^(TeSG>uXrC_)vs5f_)1heU z+r*~pJey+u>m_+3NCp!7n5OsShxsVVo2C=TVM+03bRx#>bmeoI zS;-E|BQ*&_9WUwEPbMRmbd0QOKC@d8yVE@yB7d{W7+S}%wzf`Pv>Z*)A+gm}rtZjA z_L!MD0#>OCl{glGz39{U;HiUUj8QBr9~b~8hl3|pwTJep<6r|BZ!qEF{n?F8#kbcY5(9|{{AS>NQe8%4E30F2!!XS1C<_x=SFdv>_?TvV4+^ zhw;6g<>Iymuo#}-qXXX=PIrtwN&QD`6{}yfRd4q`WN@%j;PS9*6nt$?o9P*hvrm^; zd!&dFOOpvQBSs{sJ2hu6T}Kr)p?EuE_-*tXLs`-3gVYrY#~hU5zhWR_(s^xq4CxG#{IoiB(H;(L7?28Gw5f>H$8xx$M=jEA z^1Zr*y04(^OrY^fhDk@Fq~YunErZZzMXNxl>+j1)v!#p9Mh@sqOH!1q&TN3ih0HWm^&0kb?h(*g&{UX3$}AS)GKL+ z`HP`NO^V->2_0R*NkYN5PU#gvh(&}l>rQ{n^l_`L=MJ&(RoK>{X1V2e-S-#yj!S`D zFkl@WtShcF-s|%XP}7rp*?x0sQ&zjbz@WuOr`V)E@JgbxBoqki%8?+G@f_zJ$|ek( zf$?FsmUXEuOFhqW3c1zxnsekq%Ds!n@3HemgMV^_e~2w=UFh=Uq)J16^qR1_Sq zrE~3V60y21OEQ%>RS=`6IQ@#RzdXyn)Rh)6T&Qp)L`PDDy)j^x_ns}UO34zAURUr_ zrP2?D2LM5 z=f$0DpPz>0UcOv458XkI^Bgo15LZz@M~_A`<>$&ad$lAzKrjh(@k+p$(Qz0yc+m#2 zze!bHhjD*JL8EE@l>B2DogFd|iv0gyJLYi(~>@Ts1o~e7c5PWv2UF*ES@=qUp(t^Qv zP~cKU;bNN{D=vPiGHRpg$OztGek$;6%rnMisXQ^DqE>;a_VE{-j5v|Z(7zzdi4>30 zb1tW0!Lo8+Pi^{Ew+VI8w+%4DZFqpO8eIgNUBI_g+%h3mOD z@aD+0ldhhW&>%19pNH#|fGDq)ryDz);CI<}RpMD)Bt4Zv8<@qQ|?$PQ|wSYgPmm47!)s&S z#Pn}G&9I8}(*3Dge(vRAr2E@5{sEMz5%lyxeT>Hl4gf@#_YkN1mGVu;`(MQW%(|x(;T}M?p=>R;+;qc^a%&$?;ebJ>{t4;zB^~H5n1S zEU_YP6*C1f0+OWo7^V2&Tx{6b?RJkJpuG3TisV&~=X1exF6FyFwOJT@x6E|0YC0IW z3|SR)GP^uv3hMQJ3r|Z~o~UGOTl>ovyq*1KMYa1`c{P-?vd=`Y#Lwdm;C3IMsc7r~ z8r#waxL>j_qNky9v1}v4qvk}DL|@^_)LO4ssKCY8o8MwMzkX=49Ch0sCb>eQ>=mem z%F#BV8~AVs*QCE7C6ag*79$X-=zw!HCk0Cm~cpL|7!R)6n$^xqK7s z_$cyNd&`xvPpi#jeOu|MdW`g3OO;%0ejb8{Qg!o+5RFoRMRVKDsRXP&FvnV8O>khH z>&-Dc3NW4jueD1SCc+AG_;-OOsqeOM9lvPDcc^Y`@v|s9L?>t}EC71AZLV-a+GDTO z$WUVJIQJRWEB{p;Sk#)et3lOI)3~Q%(*wjsBma??Ft;=?DF}&Z-w(PdsI9gCLKp0u zXdng!NO1y`9YN~iU}<#m$Y7DlQ%!N%_tzM~{1Yri&VDyUk#IY<0h`BW&}tPMMiN^TsK^J@Sy)Q3nqMRIti?PSoYF`C@JEN$y{OjP<7YIAjDuz*HY*OMJTWlogDDiC>gPn>{*s~KUPeZS zhJ_)Nyd`ADz>uj_?QF+0Ftq2Jta`g$EU;D_wE!d$WmMEV-J+jaUi2FL*u$ z{MNyu=OR%eOm{cyn&gXzbl6ctKON_i3w!BXcDOK<`nza(*EGicAVvGv+4bo#5B!e2 zA-p$}M8UTTa&98ab>|IZp5dU2u5&3xv{N>1!sOqwrd&-e_NUe#0DB zZ~JL!xYB6gI}TPA6Gu0OCb!RGXyHllB8ny7Xb0l! z#H-`FYZRU>OOVcGf1$8-1Y&Fi@q56al2v{KMqM5&pCR?MRK*D(?k|f3f;#w!7}P-q zu7sTUy^y|SHsZO@TmwY@2;RIXDq1Tq?co;z4u<6-pj76+xHEsF)2XZ1-%rN-epqjF z`e!q&sw=F{ z87W=o>O?_LC{sk?63vaqfWc94;j>A+G7i;$EkC?&TmX;Km0RJ~VRg-sJd)mZvYh2# zH-0r*EERjqK$(XQyePJo#5ecnt74rkF`Ea?G_;fd(V9=i00G#(Mhs%Q@4b7 z#=Nazr$yhuDi;nrJoma7fnl8etvv227VI|;NCP*$-EHwTsijFwJI9o#TYJQCp?df% z{guIhRcj1CTIDKK83B9lB%qm=BI3({xu}s9_mp4_Byn42N8^XEKA=m7P_AH8Dlyk6 zlqeCDV0jshj!ufP0+WWgjsKyTFsRECec6pkPHKDUV(A_$85sJyTI^h9W5`0huO?}+ zVXOh@Oumgbqw>m`P`Feq9VedQuHW@f)8I2s0nf&g*oUjR7h&I9v?pM&(UfoXdA=}U z4h#(q&7$Ldx{|Su2u7D_?_+QHT3Nnm_xqH_87(hfhmX91olxO73<;GnP3?Xx*^AB8 z*!znUH~5em*dHn#mRoHJUbM+wg%9_-j+ITPlIB2YFBmt*b4L5@$9E`cjC6NIEuh5C zJU!9EySo5}IRs{_Hj~`Ccuht_s^kjHE??w+fx_|f6HDJ}8>scDwc>iANQ(`Hd^#kO z(q}H=&M{rL$KA6p+VJRX-j)&g2%W3;pXebTiW7D*Y+RA&8@?LCtk_x*CqW(kQ5Gd%BmH(|h z<9J2O${%_DW!5{$E4T>);g9V@M(56ZUBw!r%eR?UAeF$Gx9kKW-CyHYNkN@w&^v~9 z_)mTL1!w&HVFTx^XYzWhAXjB+Kh)@GNZXf$_FmrlIU1v7RjqooH_z44)5kkIN~J?2 zgV_ls)#&(zpP+pG;4bR_l`ou&ydvcfwYzs_cgt@hKa$JD~4Zch-JB4VZL{akrnZxr(*GL}HD|r0Dx3 z9q+!E0X3)AY{{I_df?1gch_Eazwfp*U={^X)$Xo(@tO+W0ObeV*{hpuYS}B)dGkVM zY?49rlCFaV&WXo*=t7+*V>{S*Sam9F{XJ|p)b@elSy3B)*Pa=TiJ^Q|CcMlr{-y;B zwrp{O-ZWE}R)BL>0%)siUr7}s+&@$dJ%Xx^H<`SZ#nH+a;>1!+5Yo@t>WxUy=^~Bd z-AZN@^OG_Cd3S(#Chs2RX_pt23*YiMa<$pl^?rc1(2%_K+@hf6)|YPA+4TL06F<-+ zwOsPh!v;+Sj;3b94bLMLNb$;N3&0D~S>dJ-h<34`(=@?_7SC=drOvEQNR%c=CQlXX{G+FuExjqSJJz2F~gT@+fIo&vjKY zMB`9!TvR}WkBzCnvS^}J7JV)tNu8s%<8xejv`*BF?6lIU*jgwu>B^Rmq_GT>jW#V+#TWsA8oM(aAqu>(vy z(vO*m$uKJM4P7^d-964@W^{r%^z3nugA{f&8gzUtCpCsWj~Nvh_^3CnXBag5D zGx@6vEOnzXC}P2?#!IfIdJ071Iib(rGR<3^Egk2$;v%#(SlU+TVN>K`oiEaejs_w* zxz-!7kWH}bF>7W^*9H6jqOAjw=B8-4dPrEjKCM^w zi?(cQDUzu7Z>vi#vfQuh^3W>CB08GVXC{qb)WD9{nDn9JWxMENvAzcTDs|=zVBI+? z=)*ztyiTwPb%?4#Z5J4K_XEdjm+jV%-;39aMpCr_lWoj&eDO=|BVNc3E=iw55a%(8 zoLNv_|9}&(=8rGVf$t(RH}ppLXfRkOXN8YhFmk1I{xioO230mTOIG14sE_EzOahaFo#Bggl+ONv`fgLG8w$=5K3yLQ#nm3$;lDp8^uRKW`JgA zv|l^V$DP;;GgE>Xq^6u-GO*!6Zpb+}Sa0J7aY~RpqzdX4SlAn3^WVtz%td%M2^F`O zhVO_}y4Uz$y75`=V`%h6|7^N$%7_iHJ-u?3w@06iE2W-C^LoeJq!xPC4UCDRQ3`QF zCODv=&F8H_vyFiUj)i-#r>fS?;!Tz9o}<`agg-m+69(ih664+~Dr}iSei-6Pa5J$}P_!`rsCG8y`r{Wd0QL-VB4V&8`7F)Dc6ydf%XK>Y2*fw7 z{BaT?mT*EW*@OJm00QR(zyV%^PR37574w|C;46EP`#a(I+-BH|9@ETY8Cwb0a_h+j z%R5IxoC(8st)t?(E(PIy*sD}PgK)eV?bzOfI<+@;L*D-H+gb5!?hQQIgGrh$(kfY= zpU&E(h+R~0a=mjo(6;>X;Tfx+`HyMZzW>rCgS>SJuWQkMHIOD*+b;}V5S^*=mw7Jw zxK|Q}wF+-*lX}isKiZ@9J0YHrXfa|xNF1k%f~jSzAr#XlG5(!eQ<}$?T@NZ&fHki{ zU>n_)1=zY}e%9K}RjrT_D;k@*kNjB&hMG=!y7W1&dq6XAf&_B>aAF0JN215O0cZia zypiOSQEu7K{wbtUoKq_FlW1uc89!c+2^Pi@C#}C$hqoxb)hI*PSrR0or?A_$p>;DLdx}zvDb&e>g+uSSoG0VXEWmZ$rALLp)(W&dj8{>r8SyZHn_>ev;D5`EdCH(U|FQR9Xukdot(r7%o(Ipr#Thbe}NIZtZSk$yzuHKFkOF7$IvsjRw z^8(P-dnja98diCqpd!;8Eq0+>ou+crTRoFxYRilrgw7*>;bFz%szmqN zUA%f|(w!dE9C72z_P{`Hz@Dr$?2i*M(}7?H&!w6{GwCi7t5Wi76=)=2XGxOv2`0gl zqD!uSkQVe1%rou%x2c}^i=DiuZWk#K=m?@`D;FMP{!29GXf;X?8QhxhYuhl(P)_ro zxy8l^#yL=)bHar{9>N5gOL*v!i+P5tYDK(IKNr(iEbK&zrJ^HILS#<1;}_dqh_M%M zjkx3!`oO0T@3t&z-*z_Yn}D%6d5q(9tV6SPrNGBqr^2t7G@%cb=iF-%GnCSIxprLM zB)q(H&%#jPmX_DEBofIton=u!PWC=)){{9|`vzKS9w~eA zjk+qgiXOWb-fxD3Y$=jw_eS+R(jE;Gd$er#-kE~?%g_M#Fl@7tEl(Oef8g%AV$VUl z3)Nf_tfGKzFsMtW>2@W1pV|Df0CL&cU&i)g2roy1Z0+(+_NxiUuZTfXr*xzN|1Hj$ zOC)XP&n>WC5TNA<3lMlqn}E-m^}gUz&y7KAzUYRw0HL+lLePjK#`QTG25E&sbi3HB z1&kqRQniK-LJ-szV@<5VTyz(8*~j+n)#GGYF@@+`Egqz8=knj9q}@<Tzvbwwzce=lX%Qe}-4qZ9J_`c-08?+IU3;j;@+=+=ZQapy`$1P0QY zbR#oArRCbMOoS*#O&tPZRZo?Rd=j<0c{ywg_n$JffNgtC;*hF@@(nL8XkF91_!yN0 z==Vm_LKmv-OuHDQbVi6o0Tiyu@<#&akKnco&35a_{~Q~#_DAbL&v*|})6J0#z(slE z`}d^9>i`r25T$zzb=1Ved%P>!AD_H*1vX+0z6BX@q%CNQti41a`SE7L>(LFYA_psX zc-uc>S+ElZqrL07#6WB2 zksxy4DioUuF9e(#iBLJV5(1^dKau}%qiYgvh;-tF)>>#b=sbhk1Iq}t>RE2zllaN* z6{v|SmaY(Q)G&%QNVRXrkU9z(DX|oE&oQf~zFVh(kVo1!9}^Gt%;M({>M4;Q zFFq_WkK9=3S4P`%fFT6->AJ{gs$`rWlzVm)0vO#3Y4wLd<|6OjTBh;}`tEJG$aNS)?^+?~0lESK@PY^=`5t3)#o+FDz72O&{ z3eHAS`!CU@H1q|iJVk(b8-8Q3SZ#=icrAjCrzeZKlUE0EV=^Nx?tHACI_}{x6vaxC zJDlDX(^ihWC=G7Ktbrz4bS*_fj&1^B3$|NB*4FIY=GXn%DnI`_SwA}rqN$DC46ockR&GUOGimmxwD-JtIv~GocHRm=BLp;b#n4%>ef$<0 zKKhvg^uG$DVci_dP7_zHb`hh>G)9jWKGZiuKw)WsD8MeDA$)p>z+29|0!+(y(%>AeH;J!b zA`tZL=w|_xHi9@oS9`GL>j$7ym_8KjE@;w^hd+}L=QrGKtKtM=DueCI`glXAU=}%ij1^P>^g zgfg6@m4R7fylAb^M*AJ;EgnDPvUVQ&<9%!ib@!c8Q&J4d&mN$gTz;5ZeSDyKni#Fg zq!koy*%%X3a7%104YNU>;1lJiZJ@y`)1pg8sO&qS%K+$Bl14(VDmriko}<3V=dh&h z1Z6pN33y2@(e|YNi|50d5dT3S9G#|*PB_%CTX*^cLxd7URC!#3ShfX7Y`mY0<3-$# z9PY;b^Ns-Fl%Pu{yDy9u=+__`L>u7RjRm(a340C3UOCC z%1(jj@XG&w9e-3iVxw}ohG*R~7Q{cpPN(BHwufA5AH=ao_dXsC{u9s92l_6cD`W5n zwj`rl2x=k;&7tv-B7iSXgHS}4?A9O44-|PT&ZH}>rYoxk4RMhag{fyheC$pU`WT8; z{iZX#@_%a$D7sQ4bUAf`Y`u#H&!MX5Iy_phCHI)va$Q(HgGO`5l*53xa5voSO*j7b!deO*aaReM-vcXzj3T6^z(rCua-d|$|Ul_=>O zwaSQACpqdhmRrPC}*|k=-@1*lB@I16p3=z=h$;zHFxS9OodB+B}rd#}w36+>-tR&vW%E?Find zQge9P0m>)g{-@lSA5SYu?6vxaF6>zj?JU)~C~&>MP9NO*QC# zPEOfAN8B)i92hMFpYm3#K9vnQFXG+Pnz9pbD#{NF{K+IqY?8~|0$RQ$mUAyyk228q zNd1QM89J3E$A(fO(4swX{iI`@HJM5SkNaKSF(JCI{G3mJj^Ogks4lB=IPtHmJQXSn z!RK5bAHnlo2L*P|HiPd773SDJ_c+UR2Q$R`lA($ZLeGy=u3$d6*#7d;26Lr;6GFv` zBa~H{sZyxY-9@eRt+t*q!E4kREP!OcW@|Za(D~0Z+OD)gH@U^0y=G|^6dRVKGLSh4 zl{B`sH6wj(kA^bGuUOFGcsJWNroI?CQ0@UPnMBMn@Pucwt9L%L_{Dk&>bI=z`H%1P zg9ilNySDMZYBn`G8KSGkQqx@+d>#VB*!8g7`4STSv;vJ52`voUFr4a~%z6B-e!(+x zXT|CakkD!wUgNf3k*#t87h@aWr|5j}{#*GsG1n4G6N)77?7Qr8r8qzm_>b2hLb*x4 z<`gqL3ju3jv}qTn0lE!QBeSJ8eV|V^{AbhcojMQNzwOVSZfK{aGWo<+EDUOi;GO~} zfnz;L^5VBKEB{tUtH5Qf6sQhd`fkx?~(lZw{bf08dylv z*R0o!@r2;%6oyK(06ZgQBKJ3D8Gv-z@N$0RKOG#3jsRSV4^We#9pq*6xs?*=GC!13 z0jmjNSbh@BYdyu_Cx0=9@`#i=ZREe+@EB(Bi&hCR{P^=8PtPP6BDId2KFyz)P4X-b z?tXpFkBZ^ss_@MpZH%QJl6l(Wp8VOF|wmaYU&bthaB*e)nx+vbeMy96Q%MB^?1m$WN2fzN{*%~^sO;M#~|9! za`NDrpZQ{7-l5XwE%*)UeBFW2wE({8%iv^ty9gh>- zZ9}gd&f&>C0)~+~+2t{vBo)vIwL8!?{&u%^!mRDyL>C+ewhN(*hhcP2RyG8j8QA9I zf4Io$BZ{EX3f*tu`))3Odx4c6>h{5q+qIy+LmsqlbN+NjhpwUsVZ((Kd{un}-k?q> zdfTw+Vzz^N)(D`Fs!!C21X{UQja97t8Zcf*Jn6AGw&@Jorz%H>E{9mnIc|6qX z+a6OW{&kW8V?_cld{q={>r_9XvJJ0ui?&rR*`?{`X9TBGOd*7ivGVmms$kg$S zf42N3LfT5EtF_2GVNkZluIUyNe6jPWNVS`tYoBe@4Yxsb9~~|5J@KYWde+s}!a#1E zVx_*ti42yyl;!Qnu`eDhm2KPIQ3mlaM&YXkdO6-;6Iqu9{GIEXoG@w8gX;UfQ?&VH zFtqbpAw!9ijP&Jlw9NJ26z##UtI{<5!IPt!O&@gppgru>QMMN~*Z@V+kKJfzF=?A) zx~P%E#qm7a{b*#NsDgsRy+LDV28iO^XBb`tIlP&4nfNxA9r5`+w_fMNJ#C%zN>@x! z2+JX-o%2*I&+Qw=8`UxbPbTbkxdl)g-um^9&t00dxBm&Zcl=HX#IZpU7LzrOAz7!N z1-)S|8S{@4cWe*#x9m*c>-HuJqFOm!fp^z!Zw7le_j;e(?DX>XH?9f#rqvP+I-!e? zrEl1Kx5xl2&yPJGf!cILGY}8T(^30o<-Plz z$9L?Ms*v=J(S2asgNXu6NBkN$<+fci{kq1R8IBTjcj{@J6e;cRw55##1Fr+>-*NzY zrQbdefJGtVmWkK|ks^WBUZE{~HTFKEdK=Sev*^Z`zkW!}mtbjU=hW=(pan1H{K-fl z<}J~$E)!`|R5Lm*MIv+a%FsY}ymea-y5^)G{LAiiOk9EHz+pitLb|bh;ck5jTbDk3 z{LF4W^PYVIOZ4-+sA$*Z{NN*X^v(0+#6+pJ(g|~wF1Y2s%ul59&Wn3S;LVy3J3mXIY8RiJ z@RFy5crJ#mLCebP(vd#yTEE;AAx3cuD^~1IyrI_@} zT?MviB%HqACTzkr-c1S_hYolVM+q=yREavDrB|j8F^kDe zlLN+u?WawT*FMo*4%&2)$ylv0wLr2Jof#<=Go8n)p=>vsp`9};XumX3$I(9;qpQh6 z4f=8Ais$-r{?s<=7&`14*bS+M47G43zJEy1XMh0ZQ>*Yj?;EWUh zLtfLXp^8>6C_GnV;CTUcd@T@(T<6mX{NRjpVnauV-E*ni!Qc1DcbDa>pb#db(x~V} zjMbF2*9_xfCqDQwLe;0g<-jwLDc4=>3HfyEpUGWPku7CfJS>!{PU7y>noVzGS@|@e zaRc;?R}Oib3<6#LGubMl;_dainHsJYu^b)u*3OF?&HR&AuRCJet+DIby3zHzoffKd zkc6>CDxhr{s&>5eXtiX+)4zg^uSJece095G$dSLW2|j)?4nLNO^P_$rVY7Go9R5~j z%pB;(nR1aRb1>&#Ng9Pgg^xGS@o_=c4s6s)$B{&x94aWoZl+fsRP(n(;SEY@&pT(G zz8T$2z9srYUcy??ZW59P}7G5VBlGy*DQteGkj78pvL{(|8X>Nc{s6`iZp6++g%HJ#l5z;F|NaO<{U-UNPalbH z>MaBwg23|OHHF2P5Aj0i*kOu*7bMZbs3qaqVH;cieDn5OM_4l+|6!5_dP;(*R^E2~I~-$BpCRrciyaaqhXAu^wIfUN~n{lo&bm1uFr zTJKNEwd5fJ`(mgRICmyvvE%Ugw&eUWqo|~?4N-j>=y8JnO_PUF=q+9&tuvE(MYif; zT_jGV@vhFxy>inP8o$WK1t7JjWpXV0WT$gFg}RKsYMvKF{e$b-VI!|njz1@qA|rG2 zeNG>hrXEs>G$O+9Y1}n0QK)N-+e-XYKop4so?8nJP9G9*0(2b)Z{pKrrmpmjRwne} z_d7OwL^=ZN)CdXc1z-{AHCy1r$N|P5Cdjhp-@5p20cyjB9hBtLm8`CCqNA%iLPyC; zU!W?|n$&y}iQBs=OSJSu(4pQa{u-?j!Eq)i@>AzemZ)D|R7Qj{H$&yP5(v^f@)mjh z6E!d<3~Y;;F~Nz((Y%Pc|MdYjT=kB@y+#@CM6X7{0rB_ionq&KpolfqcQ*I4U;|{X zBf@^1;e?b`4r0&iN$)3l@;gNK8@Lk;F(B(7t0((^zrR*Qs>KG1JCEv*M-bxs*jk;p0`2r~qtD$g2rgfngIe3OtNtTC5y9lc`u5c-*6xu{-kNOSL@ZBi@dCw z<+X!enLbElpOP1kr9{WwwR*XbcReS17WfX;ibWP4C~Opzm1|4J-gVw8h3kX;?TK+; zqOKxES#bJJ393J7FN5A$ug6=fqNzAsHS-hk>K%ZCA78&kRG}-k=e63s1dJ*Hth7fO zUG3EtjegQ@=*q{XrTu1*;I#ZDdC%HUgWe?c$F$ zyvB}6qXic>N?Pc0yc-%J%whmEjIEO1psz$FMFpTBeehqW}>2Kja)tK*LQqC zNTR7JVL%$@yu9-06{#hXiHI0<8AZ&nZ+Q5_JVc$imnLfZrJzpIY&P+T|M-5E|DA{_ z+Y;ol`O+JpKJkvK-`|8o@~rYTh{20PN#1w*oXBr$*~i2Xs9V4cT^LZ0Q{@LAsh?bR z-F1GdP(v8J8^834p9Qv>GQW0Ue)c@=Du(SBg02HQb()y?`r(*Mv}E}f_&L4L-Db=e zm?5Hr<0mnL?wY-RsI53(7Nyhzb!V@fdpE)HbD+riulu!|4#8)se+>_crBF*`>@l{~ z@O@@llyNE6cjE1hCy!>+;96B0)m7_GGR4vvr!Lk@p|hHM)Zr|)>dvL@3;pJNM@LLX z_Hm@Tl@tYglAqaZ1$^T;lM|%>Mg=*xr&o(AfYe5+Z{7<*EZt8Q)*5)#z8jar-F zb-SoAMn>cd3|z*2iExAWNr8&1KpGGF>f-csh*Sl`MU)Jw%C+a|M3_`1MH7Q5QXGD^ zGqaqUO&k&#G&VdN>`;f|bk>WtC3{{6n-Irc(%Pdt&B83P2{4ya|LPKXXEUMTr|K>O zDm;`el{+d!q%XWRpOh}7HXu#4zF%v-=|)EQkaC_F(msqhk2+KLk&S@8BS^K|WD-@B z9+BB9)tHUo5Am+HR^{I~eqBJ$mn)0N0GR(A%WTosW=oq~n_46arqGD=V$bP0GcYA< z?6qV&)#{nq#oLvTo;^Ri%#HsU81|YVh+G`6Nztd)NW3r~C4!6Z(a4#xhC0bc|CgK5 zld&%gI8SIs*2ptFb0_p+=a)W8ho;3>z^OneVGB@=JUN`kU;kPv6ds{DZ!>V_ zLG_;K9Z{ian`=p6sNQpP3QjZAu~H+ZHKM%rPE!rQRKB)w=l___hx1uBstGO(t&CyB zljf|-jb&ZI&flX$>k2Il*;gyee`byI8`|56g%UENVaghh3qkYiA=>`me%Uziayp!6 zPd^eS47Iyw{z75@7YwzLGHv|Tc@t7znZ%?#{4gC^sF_UhU4MP}c&d&hn+%BAlBvyx zJ2`flBH(3OUJj&px=0aF`b%);FVxkDnHXhFnHf5d|C_v81_EoS}8$Ta+=lU0e_Pdrm5@AAhh+m{Yl-i$PkkN_YBi` zNm`(C+@|EOHz0}4qN}=A*ymVes1q75oY>|N^G7)!YFit$d?wvv3Gwj?C}oH*dG~tA zhcvLkFFjp2R(zxiqAk;n8WbK)rBNyuRaez(Xq~oWyp-k)WQRDPu@dkc&)GWhRZ>;W zRO}OC7cZ%+kUF$gB{C+c#nGcP!+f$eUQwoiejc}c=sNWw=s|*E`DW! z%8SXPXX()3oTUb4Te+%pnI$UXfml5&F$=sV{N}i_F7Gv+Ld0qvmA$6mQo8g?1!PbS z2Xk}tBp`S%e*W4wkF6?n=BV<_#M6Bq8{^4`gb0nIAs!^=F9!$_=k)A&I_<8wp`nSz zPT2>`{*4@hw$Q!of0La~xESabAVA9kW%-^&B(lYm@Gt+b*X$BEG&14%c_jGR@LnU`Hc%M1J zN6Ji)!Cgj;yzz+VQbF}i5)hnqMr9so-f($SCWe~?+P-suE|^9I9&d>!XfYv$5KtY=Ak27^gn=`+||@**jP} zqo%(O9wg;cZ8zq)>KFYveldw!pO=<=! zfLMC;x*Dt@`D&}N+`%mKdttm>yoRpf-E^F&%*}$5kwf?U`gV<^&pOiRuQVL*5~b->hr&-6Pe zLwuV!@l9+AMdLG0gW>Iw5VV!o8|K?x>BBm$U*4m#Jx@W+v1rxBexf==aXutWaNk`dBl{=c7{Hmm5D2+$)V6Z>?jXqGr#p_okaM%*rrHL z&3(WCI)vgv(&xDDOe3p~AxDUnhqV;hP~>EQ1cjDqo~y{^`_7SlV%RfDdB9;@j`Drn SjQ{R}Vx(`rwLs7P(0>3qUHqK@ From 6ab14b9b124704bd498dd6a96e5058a9537e6b60 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:46:13 +0100 Subject: [PATCH 29/36] add codecov token --- .github/workflows/R-CMD-check.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 55ff19c..88ca8eb 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -10,6 +10,8 @@ on: name: R-CMD-check +permissions: read-all + jobs: R-CMD-check: runs-on: ${{ matrix.config.os }} @@ -30,6 +32,7 @@ jobs: CRAN: ${{ matrix.config.cran }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: - name: Check out repo From 9deeb2b59a4109425a8109e843bebca0bccdc50a Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:08:41 +0100 Subject: [PATCH 30/36] test coverage only once --- .github/workflows/R-CMD-check.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 88ca8eb..2a55f0c 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -120,6 +120,7 @@ jobs: needs: website, coverage - name: Test coverage + if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' run: | cov <- covr::package_coverage( quiet = FALSE, @@ -131,6 +132,7 @@ jobs: shell: Rscript {0} - uses: codecov/codecov-action@v5 + if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' with: # Fail if error if not on PR, or if on PR and token is given fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} From 2caceb741255f3daf8bf12cee5ed3f779361c5bd Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:12:39 +0100 Subject: [PATCH 31/36] allow push to gh-pages to address https://github.com/stopsack/batchtma/actions/runs/22286391193/job/64465534971 --- .github/workflows/R-CMD-check.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 2a55f0c..a17909e 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -10,7 +10,8 @@ on: name: R-CMD-check -permissions: read-all +permissions: + contents: write jobs: R-CMD-check: From 86ae31275637e69f9b554c445a70d7554bde4a61 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:13:02 +0100 Subject: [PATCH 32/36] mac is not supported, ubuntu is default --- .github/workflows/R-CMD-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a17909e..acf0449 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -157,12 +157,12 @@ jobs: path: ${{ runner.temp }}/package - name: Build site - if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' + if: github.event_name == 'push' && matrix.config.os == 'ubuntu-latest' run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) shell: Rscript {0} - name: Deploy to GitHub pages 🚀 - if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' + if: github.event_name == 'push' && matrix.config.os == 'ubuntu-latest' uses: JamesIves/github-pages-deploy-action@v4.4.1 with: clean: false From 5df538200e6d46a712bfa02275423d47351a26c6 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:17:14 +0100 Subject: [PATCH 33/36] bring all extra dependencies to Ubuntu run --- .github/workflows/R-CMD-check.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index acf0449..1367289 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -114,14 +114,14 @@ jobs: path: check - name: pkgdown dependencies - if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' + if: github.event_name == 'push' && matrix.config.os == 'ubuntu-latest' uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: any::pkgdown, local::., any::covr, any::xml2 needs: website, coverage - name: Test coverage - if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' + if: github.event_name == 'push' && matrix.config.os == 'ubuntu-latest' run: | cov <- covr::package_coverage( quiet = FALSE, @@ -133,7 +133,7 @@ jobs: shell: Rscript {0} - uses: codecov/codecov-action@v5 - if: github.event_name == 'push' && matrix.config.os == 'macOS-latest' + if: github.event_name == 'push' && matrix.config.os == 'ubuntu-latest' with: # Fail if error if not on PR, or if on PR and token is given fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} From 105652b867e09b830192e4f03800b4fe1006ea36 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:26:13 +0100 Subject: [PATCH 34/36] typo --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 5102098..8c4ea3b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # batchtma 0.2.0 -* Braking change: Require R >= 4.1 +* Breaking change: Require R >= 4.1 * No other user-visible changes. * Internal changes: + Replace long-deprecated `filter(across())` with `if_all()` to be {dplyr} From 2e87fcea5ba5c9b3ee9d64abc31b27979bf52ca3 Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:32:15 +0100 Subject: [PATCH 35/36] only update pkgdown from main --- .github/workflows/R-CMD-check.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 1367289..36cf2ff 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -1,7 +1,5 @@ on: push: - branches: - - dev pull_request: branches: - main @@ -162,7 +160,7 @@ jobs: shell: Rscript {0} - name: Deploy to GitHub pages 🚀 - if: github.event_name == 'push' && matrix.config.os == 'ubuntu-latest' + if: github.ref != 'refs/heads/dev' && matrix.config.os == 'ubuntu-latest' uses: JamesIves/github-pages-deploy-action@v4.4.1 with: clean: false From b0116b5887a460f45bf41565e1f08ad7ee684afb Mon Sep 17 00:00:00 2001 From: "Konrad H. Stopsack" <35557324+stopsack@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:03:43 +0100 Subject: [PATCH 36/36] only update pkgdown on push to main --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 36cf2ff..261a519 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -160,7 +160,7 @@ jobs: shell: Rscript {0} - name: Deploy to GitHub pages 🚀 - if: github.ref != 'refs/heads/dev' && matrix.config.os == 'ubuntu-latest' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.config.os == 'ubuntu-latest' uses: JamesIves/github-pages-deploy-action@v4.4.1 with: clean: false