Question: Is Limma's removeBatchEffect() and log2() commutative?
gravatar for Ekarl2
2.2 years ago by
Ekarl220 wrote:

I am working on an RNA-seq dataset and have been using the EdgeR differential expression analysis methods with GLMs. Here, I have specified batch as a factor in the design matrix. The batch factor is uncorrelated with the other experimental factors and each batch is evenly represented among all environments.

But I have been asked to get batch-adjusted RPKM expression values for a select number of genes to make a bar chart (I of course want to use another chart type since bar charts can be deceptive) because if we just show the rpkm-normalized values that are not batch-adjusted, the results might be compromised by batch.

My general approach is as follows. I start with the entire dataset of about 100k genes. I make the design matrix from the experimental factors in accordance with the experimental design, and then define a batch factor. I then put the counts into a DGEList, normalize, log2 transform with rpkm() using effective gene lengths and then use the removeBatchEffect() function.

factorA <- factor(substring(colnames(data), 1, 6))
factorB <- factor(substring(colnames(data), 7, 11))
batch <- factor(substring(colnames(data), 13, 13))

# Making the design matrix. Should only contain factors to be saved.
design <- model.matrix(~factorA*factorB)

# Filtering
keep <- rowSums(cpm(data)>0.1) >= 8
data <- data[keep, ]

# Put counts into DGEList container needed for latter steps.
y <- DGEList(counts=data)

# TMM-normalization.
y <- calcNormFactors(y)

# Convert to RPKM and log2 transformation.
data <- rpkm(y, gene.length=length$effective_length, log=TRUE, prior.count=0.01)

# Remove batch effect.
data <- removeBatchEffect(data, batch=batch, design=design)

The reason for such a shallow filtering and prior count is because those genes that are of interest are very, very lowly expressed, so they might otherwise get removed in the filtering step or given a prior count that, proportionally speaking, would increase their normalized expression value substantially.

So for my questions:

- Can I remove the 2 logarithm and get back non-logarithm batch-adjusted normalized values? In other words, is Limma's removeBatchEffect() and log2() commutative? I am guessing no since log2() is rarely commutative with other operations.

- When running rpkm() as above, is log2() applied before or after rpkm normalization?

- Anything that looks questionable in the above approach I have taken?

ADD COMMENTlink modified 5 months ago by alakatos80 • written 2.2 years ago by Ekarl220
gravatar for Aaron Lun
2.2 years ago by
Aaron Lun20k
Cambridge, United Kingdom
Aaron Lun20k wrote:

There's a way around this that doesn't involve log-transformation. Let's mock up some data first:

grouping <- factor(rep(1:2, each=10))
batch <- factor(rep(1:2, 10))
counts <- matrix(rnbinom(2000, mu=100, size=20), ncol=length(grouping))

Here I'm using a fairly simple set-up, but you should be able to generalize this to your design. Now, let's do:

y <- DGEList(counts)
y <- calcNormFactors(y)

The next step involves fitting a GLM to the counts, using the full design matrix. I'll assume that the dispersion is 0.05, though you can use the estimated dispersion for each gene if you run estimateDisp beforehand.

design.all <- model.matrix(~grouping + batch)
fit <- glmFit(y, design.all, dispersion=0.05)
old.fitted <- fit$fitted.values

We then identify the batch coefficients and recompute the fitted values as if the batch effect were zero. That is:

batch.coefs <- 3 # change according to your design matrix
new.coefs <- fit$unshrunk.coefficients
new.coefs[,batch.coefs] <- 0
new.fitted <- exp(new.coefs %*% t(design.all) + as.matrix(fit$offset))

Finally, we perform quantile-quantile mapping from the old means to the new means:

pseudo.counts <- q2qnbinom(y$counts, old.fitted, new.fitted, dispersion=0.05)

This gives you batch-corrected pseudo-counts that you can use for downstream visualization, e.g., in rpkm with log=FALSE. This is probably better than log-transforming and removing the batch effect with removeBatchEffect, which would be too sensitive to fluctuations at low and zero counts when your prior count is so low.

ADD COMMENTlink modified 5 months ago • written 2.2 years ago by Aaron Lun20k

Thanks for this neat technique!

ADD REPLYlink written 2.2 years ago by Ryan C. Thompson6.8k

This will work even better if you set

contrasts(batch) <- contr.sum(10)

before forming the design matrix. That will ensure that setting the batch effects to zero does not change the size of the counts on average.

ADD REPLYlink modified 5 months ago • written 5 months ago by Gordon Smyth34k
gravatar for alakatos
5 months ago by
United States
alakatos80 wrote:


​I tried your example code at  and recevied an error message.
(I changed desing to design.all)

pseudo.counts <- q2qnbinom(y$counts, old.fitted, new.fitted, dispersion=0.05)
Error in raw.mat[i, , drop = FALSE] : 
  (subscript) logical subscript too long

I am not sure what I was doin wrong.   I just copied the code.

Thank you for you  help.



ADD COMMENTlink written 5 months ago by alakatos80

Please use the "Add comment" button rather than the "Add answer" button, as you're responding to an existing answer rather than adding your own answer. The problem is caused by a series of unfortunate coincidences after updates to the internal edgeR machinery. You can solve this by using as.matrix(new.fitted) instead.

ADD REPLYlink modified 5 months ago • written 5 months ago by Aaron Lun20k
Please log in to add an answer.


Use of this site constitutes acceptance of our User Agreement and Privacy Policy.
Powered by Biostar version 2.2.0
Traffic: 174 users visited in the last hour