Repository: fmicompbio/footprintR
Pull request #48: General function to find differential windows for scanning framework
Author: login: csoneson
Created: 2025-05-15T19:29:07Z | Merged: 2025-05-16T06:53:06Z

The name (`getDifferentialWindows`) and interface are up for discussion


Diff:
diff --git a/DESCRIPTION b/DESCRIPTION
index 68091095..afdda0d6 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -55,6 +55,7 @@ Suggests:
     e1071,
     edgeR,
     knitr,
+    limma,
     MatrixGenerics,
     R.utils,
     rmarkdown,
diff --git a/NAMESPACE b/NAMESPACE
index 65b6c7fd..ba62ff1d 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -15,6 +15,7 @@ export(filterReads)
 export(filterReadsBam)
 export(flattenReadLevelAssay)
 export(getAnchorRegions)
+export(getDifferentialWindows)
 export(getDifferentiallyModifiedWindows)
 export(getRangesWithAssayValues)
 export(phasingScoreFourier)
diff --git a/R/scanGenome.R b/R/scanGenome.R
index 14ac4c45..5f193b28 100644
--- a/R/scanGenome.R
+++ b/R/scanGenome.R
@@ -727,6 +727,127 @@ getDifferentiallyModifiedWindows <- function(se,
     return(gr)
 }
 
+#' Perform differential analysis on the rows of an assay
+#'
+#' Given a \code{SummarizedExperiment} with at least one assay, use either
+#' the likelihood ratio framework of \code{edgeR} or the linear model
+#' framework of \code{limma} to fit a model for each row of the specified
+#' assay and extract statistics corresponding to a specific contrast.
+#'
+#' @param se \code{SummarizedExperiment}, for example returned by
+#'     \code{phasingScoreFourier}.
+#' @param assayName Character scalar that gives the assay name in \code{se}
+#'     containing the values to test.
+#' @param designMatrix Numeric matrix providing the design matrix for the
+#'     statistical modeling. Must have row names, corresponding to the sample
+#'     (column) names of \code{se}.
+#' @param contrast Numeric vector providing the contrast to test.
+#' @param method Character scalar giving the method to use. Currently
+#'     supported methods are "edgeR" and "limma".
+#' @param verbose Logical scalar. If \code{TRUE}, report on progress.
+#'
+#' @author Charlotte Soneson
+#'
+#' @returns The \code{\link[GenomicRanges]{GRanges}} object constructed from
+#'     the \code{\link[edgeR]{topTags}} or \code{\link[limma]{topTable}} output
+#'     obtained for the statistical analysis, with an additional summary column
+#'     named "dirNegLog10PValue" (the sign of the logFC multiplied with the
+#'     -log10(PValue)).
+#'
+#' @examples
+#' library(GenomicRanges)
+#' library(SummarizedExperiment)
+#' modbamfiles <- system.file("extdata",
+#'                            c("6mA_1_10reads.bam", "6mA_1_10reads.bam",
+#'                              "6mA_2_10reads.bam", "6mA_2_10reads.bam"),
+#'                            package = "footprintR")
+#' rng <- GRanges("chr1", IRanges(6940000, 6943600))
+#' windowStep <- 180
+#' windowSize <- 4 * windowStep
+#' s <- seq(start(rng), end(rng) - windowSize + 1, by = windowStep)
+#' windowgr <- GRanges(seqnames = seqnames(rng),
+#'                     ranges = IRanges(start = s, width = windowSize))
+#' se <- readModBam(bamfiles = modbamfiles, regions = rng, level = "summary",
+#'                  modbase = "a", trim = TRUE,
+#'                  BPPARAM = BiocParallel::SerialParam())
+#' seFourier <- phasingScoreFourier(se = se, gr = windowgr, numCoef = 5)
+#' seFourier$group <- c("group1", "group1", "group2", "group2")
+#' mm <- model.matrix(~ group, data = colData(seFourier))
+#' gr <- getDifferentialWindows(seFourier, assayName = "phasingScoreAbs",
+#'                              designMatrix = mm,
+#'                              contrast = c(0, 1),
+#'                              method = "limma",
+#'                              verbose = TRUE)
+#' class(gr)
+#' head(gr)
+#'
+#' @importFrom SummarizedExperiment assayNames assay rowRanges
+#' @importFrom GenomicRanges GRanges
+#' @importFrom S4Vectors mcols<- DataFrame
+#' @importFrom methods as
+#' @importFrom cli cli_abort
+#'
+#' @export
+getDifferentialWindows <- function(se,
+                                   assayName = "phasingScoreAbs",
+                                   designMatrix,
+                                   contrast,
+                                   method = "limma",
+                                   verbose = FALSE) {
+    # check arguments
+    .assertVector(x = se, type = "SummarizedExperiment")
+    .assertScalar(x = assayName, type = "character",
+                  validValues = assayNames(se))
+    .assertVector(x = designMatrix, type = "matrix")
+    .assertVector(x = rownames(designMatrix), type = "character")
+    stopifnot(all(rownames(designMatrix) == colnames(se)))
+    .assertVector(x = contrast, type = "numeric", len = ncol(designMatrix))
+    .assertScalar(x = verbose, type = "logical")
+    .assertScalar(x = method, type = "character",
+                  validValues = c("edgeR", "limma"))
+    .assertPackagesAvailable(pkgs = method)
+
+    if (nrow(se) > 0) {
+        if (method == "edgeR") {
+            dgeL <- edgeR::DGEList(counts = assay(se, assayName),
+                                   genes = as.data.frame(unname(rowRanges(se))))
+            dgeL <- edgeR::estimateDisp(y = dgeL, design = designMatrix)
+            fit <- edgeR::glmFit(y = dgeL, design = designMatrix)
+            tst <- edgeR::glmLRT(
+                glmfit = fit,
+                contrast = contrast
+            )
+
+            # coerce topTags to GRanges
+            tt <- edgeR::topTags(object = tst, n = Inf, sort.by = "none")
+            tt$table$dirNegLog10PValue <-
+                sign(tt$table$logFC) * -log10(tt$table$PValue)
+            gr <- as(tt$table, "GRanges")
+        } else {
+            fit <- limma::lmFit(object = assay(se, assayName),
+                                design = designMatrix)
+            fit <- limma::contrasts.fit(fit, contrast = contrast)
+            fit <- limma::eBayes(fit)
+
+            # coerce topTable to GRanges
+            tt <- limma::topTable(fit, number = Inf, sort.by = "none")
+            tt$dirNegLog10PValue <-
+                sign(tt$logFC) * -log10(tt$P.Value)
+            gr <- as(cbind(as.data.frame(unname(rowRanges(se))), tt), "GRanges")
+        }
+    } else {
+        gr <- GRanges()
+        mcols(gr) <- DataFrame(logFC = numeric(0),
+                               AveExpr = numeric(0),
+                               t = numeric(0),
+                               P.Value = numeric(0),
+                               adj.P.Val = numeric(0),
+                               B = numeric(0),
+                               dirNegLog10PValue = numeric(0))
+    }
+    return(gr)
+}
+
 #' Create a \code{GRanges} from a \code{SummarizedExperiment}.
 #'
 #' Create a \code{GRanges} object from \code{rowRanges} and \code{assay} of
diff --git a/man/getDifferentialWindows.Rd b/man/getDifferentialWindows.Rd
new file mode 100644
index 00000000..056582ed
--- /dev/null
+++ b/man/getDifferentialWindows.Rd
@@ -0,0 +1,77 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/scanGenome.R
+\name{getDifferentialWindows}
+\alias{getDifferentialWindows}
+\title{Perform differential analysis on the rows of an assay}
+\usage{
+getDifferentialWindows(
+  se,
+  assayName = "phasingScoreAbs",
+  designMatrix,
+  contrast,
+  method = "limma",
+  verbose = FALSE
+)
+}
+\arguments{
+\item{se}{\code{SummarizedExperiment}, for example returned by
+\code{phasingScoreFourier}.}
+
+\item{assayName}{Character scalar that gives the assay name in \code{se}
+containing the values to test.}
+
+\item{designMatrix}{Numeric matrix providing the design matrix for the
+statistical modeling. Must have row names, corresponding to the sample
+(column) names of \code{se}.}
+
+\item{contrast}{Numeric vector providing the contrast to test.}
+
+\item{method}{Character scalar giving the method to use. Currently
+supported methods are "edgeR" and "limma".}
+
+\item{verbose}{Logical scalar. If \code{TRUE}, report on progress.}
+}
+\value{
+The \code{\link[GenomicRanges]{GRanges}} object constructed from
+the \code{\link[edgeR]{topTags}} or \code{\link[limma]{topTable}} output
+obtained for the statistical analysis, with an additional summary column
+named "dirNegLog10PValue" (the sign of the logFC multiplied with the
+-log10(PValue)).
+}
+\description{
+Given a \code{SummarizedExperiment} with at least one assay, use either
+the likelihood ratio framework of \code{edgeR} or the linear model
+framework of \code{limma} to fit a model for each row of the specified
+assay and extract statistics corresponding to a specific contrast.
+}
+\examples{
+library(GenomicRanges)
+library(SummarizedExperiment)
+modbamfiles <- system.file("extdata",
+                           c("6mA_1_10reads.bam", "6mA_1_10reads.bam",
+                             "6mA_2_10reads.bam", "6mA_2_10reads.bam"),
+                           package = "footprintR")
+rng <- GRanges("chr1", IRanges(6940000, 6943600))
+windowStep <- 180
+windowSize <- 4 * windowStep
+s <- seq(start(rng), end(rng) - windowSize + 1, by = windowStep)
+windowgr <- GRanges(seqnames = seqnames(rng),
+                    ranges = IRanges(start = s, width = windowSize))
+se <- readModBam(bamfiles = modbamfiles, regions = rng, level = "summary",
+                 modbase = "a", trim = TRUE,
+                 BPPARAM = BiocParallel::SerialParam())
+seFourier <- phasingScoreFourier(se = se, gr = windowgr, numCoef = 5)
+seFourier$group <- c("group1", "group1", "group2", "group2")
+mm <- model.matrix(~ group, data = colData(seFourier))
+gr <- getDifferentialWindows(seFourier, assayName = "phasingScoreAbs",
+                             designMatrix = mm,
+                             contrast = c(0, 1),
+                             method = "limma",
+                             verbose = TRUE)
+class(gr)
+head(gr)
+
+}
+\author{
+Charlotte Soneson
+}


Reviews:
user: login: mbstadler
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T05:40:16Z
user: login: mbstadler
body: This looks great! I also like the name  - I had come to the same one yesterday ;-)
I also think the general parameterization (design matrix and contrast) is great!
I can't wait to start the scanning...
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T05:43:11Z
user: login: csoneson
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T05:53:51Z

Review comments:
diff_hunk: @@ -727,6 +727,131 @@ getDifferentiallyModifiedWindows <- function(se,
     return(gr)
 }
 
+#' Perform differential analysis on the rows of an assay
+#'
+#' Given a \code{SummarizedExperiment} with at least one assay, use either
+#' the likelihood ratio framework of \code{edgeR} or the linear model
+#' framework of \code{limma} to fit a model for each row of the specified
+#' assay and extract statistics corresponding to a specific contrast.
+#'
+#' @param se \code{SummarizedExperiment}, for example returned by
+#'     \code{phasingScoreFourier}.
+#' @param assayName Character scalar that gives the assay name in \code{se}
+#'     containing the values to test.
+#' @param designMatrix Numeric matrix providing the design matrix for the
+#'     statistical modeling. Must have row names, corresponding to the sample
+#'     (column) names of \code{se}.
+#' @param contrast Numeric vector providing the contrast to test.
+#' @param method Character scalar giving the method to use. Currently
+#'     supported methods are "edgeR" and "limma".
+#' @param verbose Logical scalar. If \code{TRUE}, report on progress.
+#'
+#' @author Charlotte Soneson
+#'
+#' @returns The \code{\link[GenomicRanges]{GRanges}} object constructed from
+#'     the \code{\link[edgeR]{topTags}} or \code{\link[limma]{topTable}} output
+#'     obtained for the statistical analysis, with an additional summary column
+#'     named "dirNegLog10PValue" (the sign of the logFC multiplied with the
+#'     -log10(PValue)).
+#'
+#' @examples
+#' library(GenomicRanges)
+#' library(SummarizedExperiment)
+#' modbamfiles <- system.file("extdata",
+#'                            c("6mA_1_10reads.bam", "6mA_1_10reads.bam",
+#'                              "6mA_2_10reads.bam", "6mA_2_10reads.bam"),
+#'                            package = "footprintR")
+#' rng <- GRanges("chr1", IRanges(6940000, 6943600))
+#' windowStep <- 180
+#' windowSize <- 4 * windowStep
+#' s <- seq(start(rng), end(rng) - windowSize + 1, by = windowStep)
+#' windowgr <- GRanges(seqnames = seqnames(rng),
+#'                     ranges = IRanges(start = s, width = windowSize))
+#' se <- readModBam(bamfiles = modbamfiles, regions = rng, level = "summary",
+#'                  modbase = "a", trim = TRUE,
+#'                  BPPARAM = BiocParallel::SerialParam())
+#' seFourier <- phasingScoreFourier(se = se, gr = windowgr, numCoef = 5)
+#' seFourier$group <- c("group1", "group1", "group2", "group2")
+#' mm <- model.matrix(~ group, data = colData(seFourier))
+#' gr <- getDifferentialWindows(seFourier, assayName = "phasingScoreAbs",
+#'                              designMatrix = mm,
+#'                              contrast = c(0, 1),
+#'                              method = "limma",
+#'                              verbose = TRUE)
+#' class(gr)
+#' head(gr)
+#'
+#' @importFrom SummarizedExperiment assayNames assay rowRanges
+#' @importFrom GenomicRanges GRanges
+#' @importFrom S4Vectors mcols<- DataFrame
+#' @importFrom methods as
+#' @importFrom cli cli_abort
+#'
+#' @export
+getDifferentialWindows <- function(se,
+                                   assayName = "phasingScoreAbs",
+                                   designMatrix,
+                                   contrast,
+                                   method = "limma",
+                                   verbose = FALSE) {
+    # check arguments
+    .assertVector(x = se, type = "SummarizedExperiment")
+    .assertScalar(x = assayName, type = "character",
+                  validValues = assayNames(se))
+    .assertVector(x = designMatrix, type = "matrix")
+    .assertVector(x = rownames(designMatrix), type = "character")
+    stopifnot(all(rownames(designMatrix) == colnames(se)))
+    .assertVector(x = contrast, type = "numeric", len = ncol(designMatrix))
+    .assertScalar(x = verbose, type = "logical")
+    .assertScalar(x = method, type = "character",
+                  validValues = c("edgeR", "limma"))
+    if (method == "edgeR") {
+        .assertPackagesAvailable(pkgs = "edgeR")
+    } else if (method == "limma") {
+        .assertPackagesAvailable(pkgs = "limma")
+    }
path: R/scanGenome.R
user: login: mbstadler
body: I guess this could also be just:
`.assertPackagesAvailable(pkgs = method)`
created_at: 2025-05-16T05:40:16Z
author_association: COLLABORATOR
diff_hunk: @@ -727,6 +727,131 @@ getDifferentiallyModifiedWindows <- function(se,
     return(gr)
 }
 
+#' Perform differential analysis on the rows of an assay
+#'
+#' Given a \code{SummarizedExperiment} with at least one assay, use either
+#' the likelihood ratio framework of \code{edgeR} or the linear model
+#' framework of \code{limma} to fit a model for each row of the specified
+#' assay and extract statistics corresponding to a specific contrast.
+#'
+#' @param se \code{SummarizedExperiment}, for example returned by
+#'     \code{phasingScoreFourier}.
+#' @param assayName Character scalar that gives the assay name in \code{se}
+#'     containing the values to test.
+#' @param designMatrix Numeric matrix providing the design matrix for the
+#'     statistical modeling. Must have row names, corresponding to the sample
+#'     (column) names of \code{se}.
+#' @param contrast Numeric vector providing the contrast to test.
+#' @param method Character scalar giving the method to use. Currently
+#'     supported methods are "edgeR" and "limma".
+#' @param verbose Logical scalar. If \code{TRUE}, report on progress.
+#'
+#' @author Charlotte Soneson
+#'
+#' @returns The \code{\link[GenomicRanges]{GRanges}} object constructed from
+#'     the \code{\link[edgeR]{topTags}} or \code{\link[limma]{topTable}} output
+#'     obtained for the statistical analysis, with an additional summary column
+#'     named "dirNegLog10PValue" (the sign of the logFC multiplied with the
+#'     -log10(PValue)).
+#'
+#' @examples
+#' library(GenomicRanges)
+#' library(SummarizedExperiment)
+#' modbamfiles <- system.file("extdata",
+#'                            c("6mA_1_10reads.bam", "6mA_1_10reads.bam",
+#'                              "6mA_2_10reads.bam", "6mA_2_10reads.bam"),
+#'                            package = "footprintR")
+#' rng <- GRanges("chr1", IRanges(6940000, 6943600))
+#' windowStep <- 180
+#' windowSize <- 4 * windowStep
+#' s <- seq(start(rng), end(rng) - windowSize + 1, by = windowStep)
+#' windowgr <- GRanges(seqnames = seqnames(rng),
+#'                     ranges = IRanges(start = s, width = windowSize))
+#' se <- readModBam(bamfiles = modbamfiles, regions = rng, level = "summary",
+#'                  modbase = "a", trim = TRUE,
+#'                  BPPARAM = BiocParallel::SerialParam())
+#' seFourier <- phasingScoreFourier(se = se, gr = windowgr, numCoef = 5)
+#' seFourier$group <- c("group1", "group1", "group2", "group2")
+#' mm <- model.matrix(~ group, data = colData(seFourier))
+#' gr <- getDifferentialWindows(seFourier, assayName = "phasingScoreAbs",
+#'                              designMatrix = mm,
+#'                              contrast = c(0, 1),
+#'                              method = "limma",
+#'                              verbose = TRUE)
+#' class(gr)
+#' head(gr)
+#'
+#' @importFrom SummarizedExperiment assayNames assay rowRanges
+#' @importFrom GenomicRanges GRanges
+#' @importFrom S4Vectors mcols<- DataFrame
+#' @importFrom methods as
+#' @importFrom cli cli_abort
+#'
+#' @export
+getDifferentialWindows <- function(se,
+                                   assayName = "phasingScoreAbs",
+                                   designMatrix,
+                                   contrast,
+                                   method = "limma",
+                                   verbose = FALSE) {
+    # check arguments
+    .assertVector(x = se, type = "SummarizedExperiment")
+    .assertScalar(x = assayName, type = "character",
+                  validValues = assayNames(se))
+    .assertVector(x = designMatrix, type = "matrix")
+    .assertVector(x = rownames(designMatrix), type = "character")
+    stopifnot(all(rownames(designMatrix) == colnames(se)))
+    .assertVector(x = contrast, type = "numeric", len = ncol(designMatrix))
+    .assertScalar(x = verbose, type = "logical")
+    .assertScalar(x = method, type = "character",
+                  validValues = c("edgeR", "limma"))
+    if (method == "edgeR") {
+        .assertPackagesAvailable(pkgs = "edgeR")
+    } else if (method == "limma") {
+        .assertPackagesAvailable(pkgs = "limma")
+    }
path: R/scanGenome.R
user: login: csoneson
body: Oh yes, good catch 🙂 Fixed
created_at: 2025-05-16T05:53:50Z
author_association: COLLABORATOR

PR comments:
user: login: codecov-commenter
created_at: 2025-05-15T19:48:55Z
body: ## [Codecov](https://app.codecov.io/gh/fmicompbio/footprintR/pull/48?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Attention: Patch coverage is `0%` with `41 lines` in your changes missing coverage. Please review.
> Project coverage is 99.00%. Comparing base [(`bbf47c0`)](https://app.codecov.io/gh/fmicompbio/footprintR/commit/bbf47c0953056c65141b8290ea82e58542f6de46?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`b3155d4`)](https://app.codecov.io/gh/fmicompbio/footprintR/commit/b3155d410462ab09fdb8431e18ccd19ef22b54e8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

| [Files with missing lines](https://app.codecov.io/gh/fmicompbio/footprintR/pull/48?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Patch % | Lines |
|---|---|---|
| [R/scanGenome.R](https://app.codecov.io/gh/fmicompbio/footprintR/pull/48?src=pr&el=tree&filepath=R%2FscanGenome.R&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Ui9zY2FuR2Vub21lLlI=) | 0.00% | [41 Missing :warning: ](https://app.codecov.io/gh/fmicompbio/footprintR/pull/48?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff               @@
##           count-pairs      #48      +/-   ##
===============================================
- Coverage       100.00%   99.00%   -1.00%     
===============================================
  Files               29       29              
  Lines             4094     4135      +41     
===============================================
  Hits              4094     4094              
- Misses               0       41      +41     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/fmicompbio/footprintR/pull/48?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: csoneson
created_at: 2025-05-16T06:52:58Z
body: I'll merge this now - will also add some tests shortly but that can be done in the `count-pairs` branch. 
author_association: COLLABORATOR