Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Remove batch effects from RNA-seq data using ComBat, ComBat-Seq, limma removeBatchEffect, and SVA for unknown batch variables. Use when correcting batch effects in expression data.
.claude/skills/bio-differential-expression-batch-correction/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✓ | ▲ Improved | — | — |
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✓→✓ | = Same ✓ | — | — |
| case-04 | ✗→✗ | = Same ✗ | — | — |
Reference examples tested with: DESeq2 1.42+, ggplot2 3.5+, limma 3.58+, scanpy 1.10+
Before using code patterns, verify installed versions match. If versions differ:
packageVersion('<pkg>') then ?function_name to verify parametersIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Goal: Remove batch effects from raw count data while preserving biological group differences.
Approach: Apply ComBat-Seq's negative binomial regression to adjust counts, keeping the integer nature of the data.
"Remove batch effects from my RNA-seq counts" → Adjust raw count matrix for known batch labels using negative binomial modeling, preserving biological condition effects.
rlibrary(sva) # counts: raw count matrix (genes x samples) # batch: vector of batch labels # group: vector of biological condition (optional, to preserve) corrected_counts <- ComBat_seq(counts = as.matrix(counts), batch = batch, group = condition, full_mod = TRUE) # Result is batch-corrected count matrix # Use for visualization, clustering, but NOT for DE (use design formula instead)
Goal: Remove batch effects from normalized (log-transformed or TPM) expression data.
Approach: Apply parametric empirical Bayes adjustment to normalized expression while protecting biological covariates.
rlibrary(sva) # For normalized expression (log-transformed, TPM, etc.) # NOT for raw counts # Create model matrix mod <- model.matrix(~ condition, data = metadata) mod0 <- model.matrix(~ 1, data = metadata) # Run ComBat corrected_expr <- ComBat(dat = as.matrix(normalized_expr), batch = metadata$batch, mod = mod, par.prior = TRUE)
Goal: Produce batch-corrected expression values for visualization while preserving group differences.
Approach: Regress out the batch effect from normalized expression using limma's linear model.
rlibrary(limma) # For visualization/clustering only # Preserves group differences while removing batch design <- model.matrix(~ condition, data = metadata) corrected_expr <- removeBatchEffect(normalized_expr, batch = metadata$batch, design = design) # For PCA, heatmaps, etc.
Goal: Account for batch effects during DE testing without modifying the count data.
Approach: Include batch as a covariate in the DESeq2 design formula so batch variance is modeled, not removed.
rlibrary(DESeq2) # Include batch in design formula - preferred for DE analysis dds <- DESeqDataSetFromMatrix(countData = counts, colData = metadata, design = ~ batch + condition) # Batch is modeled, not removed # DE results are adjusted for batch dds <- DESeq(dds) res <- results(dds, contrast = c('condition', 'treatment', 'control'))
Goal: Discover and correct for unknown sources of variation (hidden batch effects).
Approach: Estimate surrogate variables from the residual variation not explained by the biological model.
"Correct for unknown batch effects in my expression data" → Estimate latent surrogate variables capturing unwanted variation, then include them as covariates in the DE model.
rlibrary(sva) # When batch is unknown, estimate surrogate variables mod <- model.matrix(~ condition, data = metadata) mod0 <- model.matrix(~ 1, data = metadata) # Estimate number of surrogate variables n_sv <- num.sv(normalized_expr, mod, method = 'leek') # Estimate surrogate variables svobj <- sva(normalized_expr, mod, mod0, n.sv = n_sv) # Add SVs to design for DE design_with_sv <- cbind(mod, svobj$sv)
Goal: Integrate surrogate variables into DESeq2 to adjust for hidden confounders during DE testing.
Approach: Estimate SVs from normalized counts, add them to colData, and update the design formula.
rlibrary(DESeq2) library(sva) # Normalize for SV estimation dds <- DESeqDataSetFromMatrix(countData = counts, colData = metadata, design = ~ condition) dds <- estimateSizeFactors(dds) norm_counts <- counts(dds, normalized = TRUE) # Estimate SVs mod <- model.matrix(~ condition, data = metadata) mod0 <- model.matrix(~ 1, data = metadata) svobj <- sva(norm_counts, mod, mod0) # Add SVs to colData for (i in seq_len(ncol(svobj$sv))) { colData(dds)[[paste0('SV', i)]] <- svobj$sv[, i] } # Update design sv_formula <- as.formula(paste('~', paste(paste0('SV', 1:ncol(svobj$sv)), collapse = ' + '), '+ condition')) design(dds) <- sv_formula # Run DESeq2 dds <- DESeq(dds)
Goal: Confirm batch effect removal by comparing PCA plots before and after correction.
Approach: Run PCA on pre- and post-correction expression, coloring points by batch and condition.
rlibrary(ggplot2) # PCA before correction pca_before <- prcomp(t(normalized_expr), scale. = TRUE) pca_df <- data.frame(PC1 = pca_before$x[, 1], PC2 = pca_before$x[, 2], batch = metadata$batch, condition = metadata$condition) p1 <- ggplot(pca_df, aes(PC1, PC2, color = batch, shape = condition)) + geom_point(size = 3) + ggtitle('Before Correction') # PCA after correction pca_after <- prcomp(t(corrected_expr), scale. = TRUE) pca_df_after <- data.frame(PC1 = pca_after$x[, 1], PC2 = pca_after$x[, 2], batch = metadata$batch, condition = metadata$condition) p2 <- ggplot(pca_df_after, aes(PC1, PC2, color = batch, shape = condition)) + geom_point(size = 3) + ggtitle('After Correction') library(patchwork) p1 + p2
Goal: Measure the proportion of variance attributable to batch versus biological condition.
Approach: Correlate principal components with batch and condition labels, or use PVCA.
r# PVCA - Principal Variance Component Analysis library(pvca) # Proportion of variance explained by batch vs condition pvcaObj <- pvcaBatchAssess(normalized_expr, metadata, threshold = 0.6, theInteractionTerms = c('batch', 'condition')) # Or manual approach pca <- prcomp(t(normalized_expr), scale. = TRUE) variance_explained <- summary(pca)$importance[2, 1:5] # Correlation of PCs with batch cor(pca$x[, 1], as.numeric(as.factor(metadata$batch)))
Goal: Integrate single-cell data from multiple batches into a shared embedding.
Approach: Apply Harmony to PCA embeddings to iteratively remove batch effects while preserving cell-type structure.
rlibrary(harmony) library(Seurat) # For single-cell data with multiple batches seurat_obj <- RunHarmony(seurat_obj, group.by.vars = 'batch', reduction = 'pca', dims.use = 1:30) # Use harmony reduction for downstream seurat_obj <- RunUMAP(seurat_obj, reduction = 'harmony', dims = 1:30) seurat_obj <- FindNeighbors(seurat_obj, reduction = 'harmony', dims = 1:30)
r# DON'T use batch-corrected values for: # - Differential expression (use design formula instead) # - Count-based methods expecting raw/normalized counts # DO use batch-corrected values for: # - Visualization (PCA, UMAP, heatmaps) # - Clustering # - Machine learning features # - Cross-study comparisons
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.