small-rna-seq/differential-mirna/SKILL.md
Tests miRNAs for differential expression with DESeq2 or edgeR using small-RNA-aware normalization and filtering. Use when deciding which normalization survives a library dominated by a few hyper-abundant miRNAs (compositional fragility); choosing DESeq2 vs edgeR vs a compositional method; setting a lower prefilter than mRNA; handling biofluid data with no endogenous normalizer; or remembering that RPM is for display and TDMD can make a miRNA drop without transcriptional repression.
npx skillsauth add GPTomics/bioSkills bio-small-rna-seq-differential-mirnaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: DESeq2 1.42+, edgeR 4.0+, apeglm 1.24+, EnhancedVolcano 1.20+, pheatmap 1.0.12+, ggplot2 3.5+
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.
"Find differentially expressed miRNAs between my conditions" -> Test a raw miRNA count matrix for expression changes, accounting for the compositional fragility that makes miRNA normalization harder than mRNA.
DESeq2::DESeq() or edgeR::glmQLFTest() on RAW miRNA countsA miRNA library is not a gently varying pool of thousands of features like an mRNA library. A handful of tissue-dominant miRNAs can be more than half of all reads, and the expressed repertoire is only hundreds to low-thousands of miRNAs. Two consequences follow, and they matter more than the choice of DE engine. First, global-scaling normalizers (DESeq2 median-of-ratios, edgeR TMM) assume most features are not differentially expressed and the count distribution is roughly symmetric; when one dominant miRNA shifts between conditions it absorbs the size factor and distorts every other miRNA's normalized value, manufacturing phantom changes. The normalization choice genuinely changes which miRNAs are called DE (Garmire 2012; Tam 2015) - so filter low-count noise FIRST, inspect whether a few miRNAs dominate, and report the normalizer. Second, empirical-Bayes dispersion shrinkage borrows strength across features, so with only hundreds of miRNAs the prior is estimated from a small, noisy population and is weaker than on ~20k genes; apeglm LFC shrinkage matters more for the many low-count miRNAs.
Two reframes prevent classic mistakes. RPM is for display and cross-sample viewing, never for testing - hand RAW counts to DESeq2/edgeR, which model the count distribution themselves. And a miRNA going DOWN does not necessarily mean transcriptional repression: target-directed miRNA degradation (TDMD, via ZSWIM8) lets a highly complementary target trigger decay of the miRNA itself (Han 2020; Shi 2020), so interpret a drop as a change in steady-state level, not automatically as reduced biogenesis.
A third decision is the level of testing. Mature-miRNA-level DE answers "which miRNAs changed" with good power; isomiR-level DE is sparser (more features and zeros, weaker per-feature power, heavier multiplicity), and 5' isomiRs shift the seed and can move OPPOSITE to the canonical mature form - so never silently sum 5' isomiRs into the mature count. Collapse to mature for the standard question; test at isomiR resolution only when isomiR identity is the biology.
| Method | Normalization assumption | Best when | Fails when | |--------|--------------------------|-----------|------------| | DESeq2 (median-of-ratios) | most features stable; symmetric | balanced designs, no single runaway miRNA | one miRNA dominates and shifts (compositional) | | edgeR TMM (glmQLF) | most features stable; trimmed mean | similar to DESeq2; flexible GLM | strong composition shift; default 30%/5% trim built for thousands of mRNAs | | upper-quartile / quantile / Lowess | rank/quantile-based | skewed miRNA distributions (often better-behaved per Garmire) | when the global shape itself is the biology | | spike-in (cel-miR-39) | external technical scale | biofluids with no endogenous reference; controls extraction | does not correct ligation bias or biological composition | | RUVg (RUVSeq) | unwanted variation from control miRNAs | hidden batch/technical structure global scaling misses | controls poorly chosen | | CLR + ALDEx2 (compositional) | treat counts as compositional | as a sensitivity analysis when a few miRNAs dominate | still blind to a global pool shift; more conservative |
When a perturbation moves the WHOLE pool (e.g. Dicer/Drosha loss), every internal normalizer - including CLR - forces the average change to zero and is blind to it; only external spike-ins or cell-number normalization detect a global shift (Lovén 2012).
Goal: Read raw miRNA counts and build sample metadata for testing.
Approach: Load the miRge3/miRDeep2 count CSV (raw, not RPM) and define the condition factor.
library(DESeq2)
counts <- read.csv('miR.Counts.csv', row.names = 1) # RAW counts, not RPM
coldata <- data.frame(
condition = factor(c('control', 'control', 'treated', 'treated')),
row.names = colnames(counts))
Goal: Identify miRNAs that change between conditions with small-RNA-aware filtering and shrinkage.
Approach: Build a DESeqDataSet from rounded raw counts, prefilter at a lower threshold than mRNA, run DESeq2, then shrink LFCs with apeglm for the many low-count miRNAs.
dds <- DESeqDataSetFromMatrix(
countData = round(counts), # DESeq2 needs integers
colData = coldata,
design = ~ condition)
# Lower prefilter than mRNA: miRNA libraries have fewer total counts, and most
# miRBase entries are near-zero noise. Justify the threshold; do not test everything.
keep <- rowSums(counts(dds)) >= 10
dds <- dds[keep, ]
dds <- DESeq(dds)
# Inspect for compositional risk: a size factor far from 1, or one miRNA that is a
# large fraction of reads, is a warning that median-of-ratios may be distorted.
sizeFactors(dds)
res <- results(dds, contrast = c('condition', 'treated', 'control'))
# apeglm shrinks via a named coef; for an arbitrary/multi-level contrast not expressible
# as one coef, use type = 'ashr' instead.
res_shrunk <- lfcShrink(dds, coef = 'condition_treated_vs_control', type = 'apeglm')
res_shrunk <- res_shrunk[order(res_shrunk$padj), ]
Goal: Test the same data with edgeR's quasi-likelihood GLM as a cross-check.
Approach: Build a DGEList, filter with filterByExpr, TMM-normalize, estimate dispersion, and run the QL F-test.
library(edgeR)
dge <- DGEList(counts = round(counts), group = coldata$condition)
keep <- filterByExpr(dge, group = coldata$condition) # pass group or it treats all samples as one
dge <- dge[keep, , keep.lib.sizes = FALSE]
dge <- calcNormFactors(dge) # TMM
design <- model.matrix(~ condition, data = coldata)
dge <- estimateDisp(dge, design)
fit <- glmQLFit(dge, design)
qlf <- glmQLFTest(fit, coef = 2)
res_edger <- topTags(qlf, n = Inf)$table # edgeR uses $FDR, not $padj
Goal: Avoid calling low-count miRNAs DE on the strength of unstable fold-changes.
Approach: Filter on shrunk LFC and FDR, but always inspect base mean / CPM, because a significant LFC on a ~5-count miRNA is almost always noise.
sig <- subset(as.data.frame(res_shrunk), padj < 0.05 & abs(log2FoldChange) > 1)
sig$baseMean <- res_shrunk[rownames(sig), 'baseMean'] # keep expression level visible
sig <- sig[order(sig$padj), ]
Goal: Show the result with a volcano plot and a heatmap of significant miRNAs.
Approach: Use EnhancedVolcano on the shrunk results and a variance-stabilized, row-scaled pheatmap.
library(EnhancedVolcano); library(pheatmap)
EnhancedVolcano(res_shrunk, lab = rownames(res_shrunk),
x = 'log2FoldChange', y = 'padj', pCutoff = 0.05, FCcutoff = 1,
title = 'Differential miRNA expression')
# vst() subsets 1000 genes to fit the dispersion trend and ERRORS on miRNA-sized data
# (hundreds of features) - use the full varianceStabilizingTransformation instead.
vsd <- varianceStabilizingTransformation(dds, blind = FALSE)
mat <- assay(vsd)[rownames(sig), , drop = FALSE]
pheatmap(t(scale(t(mat))), annotation_col = coldata['condition'],
show_rownames = nrow(mat) < 50)
| Symptom | Cause | Fix |
|---------|-------|-----|
| Everything looks DE in one direction | One dominant miRNA shifted and distorted the size factors | Filter first; inspect sizeFactors; try upper-quartile/quantile or remove the runaway from size-factor estimation |
| Inflated significance on tiny miRNAs | RPM (or unfiltered low counts) fed to the test | Use RAW counts and a lower prefilter; report baseMean for every call |
| filterByExpr warns "all samples one group" | group/design not passed | filterByExpr(dge, group = coldata$condition) |
| edgeR results have no padj column | edgeR names the FDR column FDR | Use topTags(...)$table$FDR, not $padj |
| Biofluid DE driven by a few samples | hemolysis/batch confound; no endogenous normalizer | Add cel-miR-39 spike-in normalization; flag hemolysis (miR-451a:miR-23a-3p); model batch |
| A known miRNA "down" but its gene is unchanged | TDMD (target-driven degradation), not transcription | Interpret as steady-state change; check pri/pre-miRNA or ZSWIM8 context before claiming repression |
| vst() errors "less than 'nsub' rows" | vst() subsets 1000 genes; miRNA datasets have only hundreds | Use varianceStabilizingTransformation(dds, blind=FALSE) (full VST) instead of vst() |
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.