Analyzes time-series and longitudinal RNA-seq for differential expression and trajectory structure. Covers DESeq2 LRT with reduced models, time as factor vs continuous vs natural splines, maSigPro (Nueda 2014 for RNA-seq), ImpulseDE2 with explicit impulse-model failure modes, DREAM for repeated measures via linear mixed models, pseudoreplication avoidance, conditional vs marginal modeling, and trajectory clustering with DPGP, Mfuzz (with Schwämmle 2010 fuzzifier estimation), and splines+k-mea...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add FridrichMethod/awesome-skills --skill timeseries-de --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Timeseries De?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fridrichmethod-timeseries-de)More formats (shields.io, HTML) on the badges page.
---
name: bio-differential-expression-timeseries-de
description: Analyzes time-series and longitudinal RNA-seq for differential expression and trajectory structure. Covers DESeq2 LRT with reduced models, time as factor vs continuous vs natural splines, maSigPro (Nueda 2014 for RNA-seq), ImpulseDE2 with explicit impulse-model failure modes, DREAM for repeated measures via linear mixed models, pseudoreplication avoidance, conditional vs marginal modeling, and trajectory clustering with DPGP, Mfuzz (with Schwämmle 2010 fuzzifier estimation), and splines+k-means. Use when modeling time-course or longitudinal expression, choosing factor vs spline, handling repeated measures from the same subject, avoiding pseudoreplication, clustering temporal trajectories, or selecting between dedicated time-course tools and pairwise+LRT.
tool_type: r
primary_tool: DESeq2
---
## Version Compatibility
Reference examples tested with: DESeq2 1.42+, edgeR 4.0+, limma 3.58+, splines (base R), maSigPro 1.74+, ImpulseDE2 1.10+ (Bioconductor archive; verify availability), variancePartition / dream 1.32+, Mfuzz 2.62+, TCseq 1.26+, ggplot2 3.5+, pheatmap 1.0+
Before using code patterns, verify installed versions match. If versions differ:
- R: `packageVersion('<pkg>')` then `?function_name` to verify parameters
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
# Time-Series Differential Expression
**"Find genes that change over time"** -> Define what "change" means -- any non-zero time effect (LRT), a smooth nonlinear trend (splines), a transient impulse (ImpulseDE2), or differing trajectories between groups (interaction LRT) -- and choose the model that asks that specific question while handling repeated-measures correctly.
## The Single Most Important Modern Insight -- Most dedicated time-course tools UNDERPERFORM pairwise + LRT on short series
Spies, Renz, Beyer, Ciaudo 2019 *Brief Bioinform* 20:288 benchmarked dedicated time-course tools (ImpulseDE2, splineTC, maSigPro, EBSeqHMM, TimeReg) against naive DESeq2/edgeR pairwise comparisons + LRT for omnibus, on simulated and real time-courses. Finding: **on short series (<8 time points), naive pairwise pattern-of-significance OUTPERFORMS dedicated TC tools** because of high false-positive rates in the latter. The exception is ImpulseDE2, which holds up better than the others -- IF its impulse assumption (rise-then-plateau or fall-then-plateau) actually fits the biology.
For most experimental time courses (3-6 time points, common in pharmacology and developmental biology), the right tool is DESeq2 with `test='LRT'` and a sensible reduced model. Reserve splines for >5 evenly-spaced time points; reserve ImpulseDE2 for monotonic-then-saturating dynamics; reserve DREAM for repeated measures.
A second insight that is constantly violated: pseudoreplication. If 3 subjects each contribute 4 time points (12 samples), the effective sample size for testing TIME effects is closer to 3, not 12 -- the within-subject observations are not independent. Treating them as independent inflates type-I error dramatically. Either include subject as a fixed effect, use DREAM (mixed model), or collapse to per-subject means (loses time info).
## Algorithmic Taxonomy
| Method | What it tests | Best for | Failure mode |
|--------|---------------|----------|--------------|
| DESeq2 LRT (`test='LRT'`, `reduced=`) | Joint hypothesis: dropped terms are jointly zero | Default for "any time effect"; multi-group time interaction | Reports LFC of last coefficient, not omnibus -- use padj only |
| DESeq2 + splines (`ns(time, df=3)`) | Smooth nonlinear time effect | 5+ time points, smooth dynamics, multi-group interaction | Spline df > unique time points / 2 overfits |
| maSigPro (Nueda 2014 RNA-seq update) | Polynomial regression on time per group | Multi-group time-course, regression-style hypotheses | Polynomial assumption can be wrong; less popular than DESeq2 LRT |
| ImpulseDE2 (Fischer, Theis, Yosef 2018) | Constant vs monotonic vs impulse trajectories | Monotonic-then-saturating or impulse-like responses | Fails on oscillatory, multi-phase, monotonic-non-asymptotic; sensitive to noise on short series |
| DREAM (Hoffman, Roussos 2021) | Per-gene linear mixed model with random subject | Repeated measures with >2 time points per subject | Slower; requires variancePartition stack; `ddf='adaptive'` default (Kenward-Roger for n<=20, Satterthwaite otherwise) |
| voom + duplicateCorrelation | Single average within-subject correlation | Technical reps within bio reps, paired pre/post | Single correlation across all genes is approximation |
| edgeR LRT with subject as factor | Subject as fixed effect | Small subject count, simple design | Wastes df; can't handle continuous time well |
| TCseq | Spline-based DE + fuzzy clustering | Combined pipeline for DE + cluster discovery | Single-tool dependency; verify maintenance |
## Decision Tree by Scenario
| Scenario | Recommended approach | Why |
|----------|---------------------|-----|
| 2-3 discrete time points, independent samples per time | DESeq2 LRT, time as factor, `reduced = ~1` | Too few points for splines |
| 5+ time points, single group, smooth dynamics | DESeq2 LRT with `ns(time, df=3)`, `reduced = ~1` | Splines capture nonlinearity efficiently |
| 5+ time points, two groups, "do trajectories differ?" | LRT with `~ treatment * ns(time, df=3)` vs `reduced = ~ treatment + ns(time, df=3)` | Tests the interaction (treatment-specific time response) |
| Same subject sampled over time (longitudinal) | DREAM (random subject) OR DESeq2 with subject fixed effect | Mandatory: pseudoreplication otherwise |
| Monotonic-then-saturating biology expected | ImpulseDE2 | Built for the assumption |
| Circadian or cell-cycle (cyclical) | Fourier basis (`fda::create.fourier.basis`) or dedicated tools (JTK_CYCLE, MetaCycle) | Splines can't represent periodicity |
| Short series (<8 time points) | DESeq2 pairwise + LRT (Spies 2019 finding) | Dedicated TC tools have high FPR on short series |
| Want to cluster trajectories after DE | DPGP (nonparametric), Mfuzz (with Schwämmle 2010 m estimation), splines + k-means | Standardize per gene first |
| Multi-batch time course | Add batch to design; test the interaction term | Standard DESeq2 / edgeR pattern |
## DESeq2 LRT for Time -- The Canonical Pattern
**Goal:** Test whether time has ANY effect (omnibus), or whether time trajectories differ between groups (interaction).
**Approach:** Specify a full design including the time terms; specify a reduced design dropping those terms; LRT compares.
```r
library(DESeq2)
dds <- DESeqDataSetFromMatrix(counts, colData, design = ~ time)
dds <- DESeq(dds, test = 'LRT', reduced = ~ 1)
res <- results(dds)
```
The LRT p-value tests "any difference among time points". The `log2FoldChange` column reports the LAST coefficient in `resultsNames(dds)` -- NOT the omnibus effect. Use padj only from LRT results; extract individual Wald per time point for effect sizes.
Interaction (treatment-specific time response):
```r
dds <- DESeqDataSetFromMatrix(counts, colData,
design = ~ treatment + time + treatment:time)
dds <- DESeq(dds, test = 'LRT', reduced = ~ treatment + time)
res_interaction <- results(dds)
```
This tests "does the time trajectory differ between treatment groups?" -- a different question than "is there a time effect" or "is there a treatment effect".
## Time as Factor vs Continuous vs Spline
| Encoding | Assumption | df spent | When |
|----------|------------|----------|------|
| Factor (`time` as factor) | No structure; each level independent | (n_levels - 1) | Few discrete time points, irregular spacing, interest in specific pairwise comparisons |
| Continuous (`as.numeric(time)`) | Linear effect on log expression | 1 | Linear biology, well-spaced time points -- often wrong |
| Natural spline (`ns(time, df=k)`) | Smooth nonlinear; k basis functions | k | Dense time courses (5+ points), smooth biology |
Rule of thumb: `df <= unique_time_points / 2`.
```r
library(splines)
dds <- DESeqDataSetFromMatrix(counts, colData,
design = ~ treatment * ns(time, df = 3))
dds <- DESeq(dds, test = 'LRT', reduced = ~ treatment + ns(time, df = 3))
```
## limma-voom + Splines (Modern Alternative)
```r
library(limma)
library(edgeR)
library(splines)
y <- DGEList(counts = counts)
y <- normLibSizes(y)
keep <- filterByExpr(y, group = metadata$treatment)
y <- y[keep, , keep.lib.sizes = FALSE]
design <- model.matrix(~ treatment * ns(time, df = 3), data = metadata)
v <- voom(y, design, plot = TRUE)
fit <- lmFit(v, design)
fit <- eBayes(fit, robust = TRUE)
interaction_cols <- grep(':ns\\(time', colnames(design))
tt <- topTable(fit, coef = interaction_cols, number = Inf)
```
Tests the joint significance of all interaction spline coefficients.
## DREAM for Repeated Measures
**Goal:** Properly model longitudinal data where the same subject is sampled multiple times.
**Approach:** Linear mixed model per gene with subject as a random intercept (or slope), via `variancePartition::dream`. Uses voom-weighted linear regression internally. Default `ddf='adaptive'` uses Kenward-Roger for n <= 20 samples (the typical longitudinal-DE regime) and Satterthwaite otherwise; force either explicitly with `ddf='Kenward-Roger'` or `ddf='Satterthwaite'`.
```r
library(variancePartition)
library(edgeR)
library(BiocParallel)
y <- DGEList(counts = counts)
y <- normLibSizes(y)
keep <- filterByExpr(y, group = metadata$treatment)
y <- y[keep, , keep.lib.sizes = FALSE]
formula <- ~ treatment + time + treatment:time + (1 | subject)
vobj <- voomWithDreamWeights(y, formula, metadata)
fitmm <- dream(vobj, formula, metadata)
fitmm <- eBayes(fitmm)
tt <- topTable(fitmm, coef = grep(':time', colnames(coefficients(fitmm))),
number = Inf)
```
When to use DREAM over `~ subject + treatment + time` (subject as fixed effect):
- More than 2 time points per subject (more random-effect signal to estimate)
- Many subjects (random effects more parsimonious than fixed)
- Random slopes needed (e.g., `(1 + time | subject)`)
- Multiple random effects (e.g., `(1 | donor) + (1 | batch)`)
`duplicateCorrelation` (`limma`) is the older approximate alternative -- assumes a SINGLE within-subject correlation across all genes. Adequate for technical replicates within biological replicates; less so for proper longitudinal data with multiple time points.
## maSigPro (Nueda 2014 for RNA-seq)
```r
library(maSigPro)
edesign <- data.frame(
Time = metadata$time,
Replicate = metadata$replicate,
Control = as.numeric(metadata$treatment == 'Control'),
Treatment = as.numeric(metadata$treatment == 'Treatment')
)
rownames(edesign) <- metadata$sample
design <- make.design.matrix(edesign, degree = 3)
fit <- p.vector(norm_counts, design, Q = 0.05, MT.adjust = 'BH')
tstep <- T.fit(fit, step.method = 'backward', alfa = 0.05)
sigs <- get.siggenes(tstep, rsq = 0.6, vars = 'groups')
see.genes(sigs$sig.genes, show.fit = TRUE, dis = design$dis,
cluster.method = 'hclust', k = 9)
```
CITATION: Nueda MJ, Tarazona S, Conesa A (2014) "Next maSigPro: updating maSigPro bioconductor package for RNA-seq time series." *Bioinformatics* 30(18):2598-2602. The earlier Conesa et al. 2006 *Bioinformatics* 22:1096 is the microarray-era original. Cite 2014 for RNA-seq use; many secondary refs miscite 2006.
## ImpulseDE2
**Goal:** Detect transient impulse-like expression patterns (rise then decay, or constant-then-monotonic).
**Approach:** Fits constant, monotonic, and impulse (6-parameter sigmoid: baseline, peak, post-peak baseline, rise rate, decay rate) models per gene; selects the best-fitting and tests for differential dynamics.
```r
library(ImpulseDE2)
dfAnnotation <- data.frame(
Sample = colnames(counts),
Time = metadata$time,
Condition = metadata$condition,
Batch = metadata$batch
)
imp <- runImpulseDE2(
matCountData = as.matrix(counts),
dfAnnotation = dfAnnotation,
boolCaseCtrl = TRUE,
vecConfounders = c('Batch'),
scaNProc = 4
)
sig <- imp$dfImpulseDE2Results[imp$dfImpulseDE2Results$padj < 0.05, ]
```
The impulse model FAILS on:
- Oscillatory expression (circadian, cell cycle) -- model can't represent periodicity
- Monotonic-but-non-asymptotic responses (linear increases that don't plateau)
- Multi-phase responses (rise-fall-rise)
For oscillatory biology, use Fourier basis or dedicated tools (JTK_CYCLE, MetaCycle). For non-asymptotic monotonic, splines fit better.
NOTE: ImpulseDE2 was removed from Bioconductor at the 3.13 release (May 2021); last hosted version was 3.10 -- install from the BiocArchive (pin to Bioc 3.10) or the YosefLab GitHub mirror. The Spies 2019 benchmark showed ImpulseDE2 holds up on longer time courses (~8+ time points) but is sensitive to noise on short series; below ~8 time points, DESeq2 pairwise + LRT typically outperforms.
## Trajectory Clustering of DE Genes
**Goal:** After identifying DE-over-time genes, group them by trajectory shape.
**Approach:** Standardize per gene (subtract mean, divide by SD per gene) -- otherwise clusters reflect mean level, not shape. Then apply DPGP (nonparametric), Mfuzz (fuzzy c-means), or splines + k-means.
```r
library(Mfuzz)
eset <- ExpressionSet(assayData = as.matrix(norm_counts[sig_genes, ]))
eset_std <- standardise(eset)
m <- mestimate(eset_std)
cl <- mfuzz(eset_std, c = 9, m = m)
mfuzz.plot(eset_std, cl, mfrow = c(3, 3))
```
The Mfuzz fuzzifier `m` is critical -- too low gives crisp clusters (loses fuzzy advantage); too high collapses everything. `mestimate()` implements Schwämmle & Jensen 2010 *Bioinformatics* 26:2841 to estimate `m` from the data.
CITATION CARE: the Mfuzz PACKAGE paper is Kumar L, Futschik ME (2007) *Bioinformation* 2(1):5-7 (the journal is *Bioinformation*, NOT *Bioinformatics*). The Schwämmle 2010 paper is the fuzzifier-estimation methodology paper, *Bioinformatics*. Many references confuse the two.
For DPGP (Dirichlet Process Gaussian Process; nonparametric in cluster number AND trajectory shape):
CITATION: McDowell IC, Manandhar D, Vockley CM, Schmid AK, Reddy TE, Engelhardt BE (2018) "Clustering gene expression time series data using an infinite Gaussian process mixture model." *PLoS Comput Biol* 14(1):e1005896. CITATION CARE: this is *PLoS Comp Biol*, NOT *Genome Research* (a common miscitation).
Splines + k-means (fast alternative):
```r
library(splines)
spline_coefs <- t(apply(norm_counts[sig_genes, ], 1, function(x) {
fit <- lm(x ~ ns(metadata$time, df = 4))
coef(fit)
}))
km <- kmeans(scale(spline_coefs), centers = 6)
```
## Time as the Only Variable (Developmental Series)
When time is the sole variable (e.g., embryonic development series), the DE question becomes "which genes change across the trajectory":
```r
dds <- DESeqDataSetFromMatrix(counts, colData, design = ~ time)
dds <- DESeq(dds, test = 'LRT', reduced = ~ 1)
res <- results(dds)
```
Caveats:
- Reference time point biases the reported LFC (it's the LFC at the LAST time point vs reference)
- For cyclical biology (circadian, cell cycle), use periodic basis functions
## Per-Method Failure Modes
### Pseudoreplication -- treated 12 samples from 3 subjects as 12 independent
**Trigger:** 3 subjects x 4 time points = 12 samples; vanilla DESeq2 with `~ time`; many DE genes.
**Mechanism:** Within-subject observations are correlated; treating them as independent inflates effective sample size from 3 to 12 in the inference, deflating standard errors.
**Symptom:** p-value histogram anti-conservative; many false-positive DE genes; replication fails.
**Fix:** Include subject in design (`~ subject + time`) OR use DREAM with random subject. Collapsing to per-subject means is OK but loses time info.
### LRT reports the wrong LFC
**Trigger:** `DESeq(dds, test='LRT', reduced=~1)` on a 5-time-point study; user reports the `log2FoldChange` column as "the time effect".
**Mechanism:** LRT padj is the omnibus joint test. The LFC reported is for the LAST coefficient in `resultsNames(dds)`, one specific level-vs-reference comparison.
**Symptom:** A 5-time-point factor produces one LFC per gene; reviewer asks "the effect of which time?"
**Fix:** Treat LRT padj as a screen for "any change". For effect sizes, extract Wald coefficients per time point via `results(dds, name='time_T2_vs_T0')` etc.
### ImpulseDE2 reports noise as "impulse"
**Trigger:** Short series (4-5 time points), oscillatory or non-monotonic biology; ImpulseDE2 flags many "impulse" genes that look like noise on inspection.
**Mechanism:** Impulse model has 6 parameters; on short series, easy to fit by chance. For oscillatory data, model is wrong.
**Symptom:** Validation orthogonal data shows the "impulse" genes are not actually transient; replication low.
**Fix:** Use DESeq2 LRT + spline interaction for short or non-impulse data. Reserve ImpulseDE2 for cases where biology is known to be monotonic-then-asymptotic (immune response, cytokine release).
### Wrong spline df
**Trigger:** 4 time points; user sets `ns(time, df = 5)`; bizarre fits per gene.
**Mechanism:** df > number of unique time points produces overfitting; spline basis is rank-deficient.
**Symptom:** Errors about singular fits; or apparently-clean fits that don't generalize.
**Fix:** `df <= unique_time_points / 2`. For 4 time points, df = 2 (or use factor encoding instead).
### Trajectory clusters dominated by expression level
**Trigger:** Mfuzz / k-means clustering of trajectories; clusters separate high- vs low-expressed genes rather than shape patterns.
**Mechanism:** Forgot to standardize per gene; absolute levels dominate distance computations.
**Symptom:** Clusters labeled by mean expression, not trajectory shape.
**Fix:** `standardise()` in Mfuzz, or `scale()` per gene before k-means. The point of trajectory clustering is shape, not magnitude.
### maSigPro miscited as 2006
**Trigger:** Methods section cites "Conesa 2006 maSigPro" for an RNA-seq analysis.
**Mechanism:** The 2006 Conesa paper is the original microarray maSigPro. The 2014 Nueda paper is the RNA-seq update with NB GLM.
**Symptom:** Reviewer asks for the RNA-seq citation specifically.
**Fix:** Cite Nueda MJ, Tarazona S, Conesa A (2014) *Bioinformatics* 30(18):2598-2602 for RNA-seq use.
## Common errors
| Error / symptom | Cause | Fix |
|-----------------|-------|-----|
| `singular fit` from spline model | df > unique time points | Reduce df or use factor encoding |
| LRT p-values numerically identical across genes | Reduced model matches full model (no effect tested) | Verify `reduced` actually drops the term of interest |
| ImpulseDE2 not installable from Bioconductor | Removed at Bioconductor 3.13 (May 2021); last hosted version 3.10 | Install from BiocArchive (pin Bioc 3.10) or YosefLab GitHub mirror |
| Mfuzz clusters dominated by mean level | Forgot standardisation | Use `standardise()` before `mfuzz()` |
| Trajectory plot shows flat lines | Counts not log-transformed before clustering | Use `cpm(y, log=TRUE)` or `vst()` then standardize |
| DREAM very slow | Large gene set with mixed model per gene | Filter to DE-over-time first (LRT screen), then DREAM on the subset |
## References
- Love MI, Huber W, Anders S. 2014. Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2. *Genome Biol* 15(12):550. doi:10.1186/s13059-014-0550-8
- Nueda MJ, Tarazona S, Conesa A. 2014. Next maSigPro: updating maSigPro bioconductor package for RNA-seq time series. *Bioinformatics* 30(18):2598-2602. doi:10.1093/bioinformatics/btu333
- Conesa A, Nueda MJ, Ferrer A, Talón M. 2006. maSigPro: a method to identify significantly differential expression profiles in time-course microarray experiments. *Bioinformatics* 22(9):1096-1102. doi:10.1093/bioinformatics/btl056
- Fischer DS, Theis FJ, Yosef N. 2018. Impulse model-based differential expression analysis of time course sequencing data. *Nucleic Acids Res* 46(20):e119. doi:10.1093/nar/gky675
- Hoffman GE, Roussos P. 2021. dream: powerful differential expression analysis for repeated measures designs. *Bioinformatics* 37(2):192-201. doi:10.1093/bioinformatics/btaa687
- Hoffman GE, Schadt EE. 2016. variancePartition: interpreting drivers of variation in complex gene expression studies. *BMC Bioinformatics* 17:483. doi:10.1186/s12859-016-1323-z
- Spies D, Renz PF, Beyer TA, Ciaudo C. 2019. Comparative analysis of differential gene expression tools for RNA sequencing time course data. *Brief Bioinform* 20(1):288-298. doi:10.1093/bib/bbx115
- Kumar L, Futschik ME. 2007. Mfuzz: a software package for soft clustering of microarray data. *Bioinformation* 2(1):5-7.
- Schwämmle V, Jensen ON. 2010. A simple and fast method to determine the parameters for fuzzy c-means cluster analysis. *Bioinformatics* 26(22):2841-2848. doi:10.1093/bioinformatics/btq534
- McDowell IC, Manandhar D, Vockley CM, Schmid AK, Reddy TE, Engelhardt BE. 2018. Clustering gene expression time series data using an infinite Gaussian process mixture model. *PLoS Comput Biol* 14(1):e1005896. doi:10.1371/journal.pcbi.1005896
- Law CW, Chen Y, Shi W, Smyth GK. 2014. voom: precision weights unlock linear model analysis tools for RNA-seq read counts. *Genome Biol* 15(2):R29. doi:10.1186/gb-2014-15-2-r29
## Related Skills
- deseq2-basics - LRT mechanics; design formulas
- edger-basics - voom + splines pattern
- de-results - LRT padj interpretation; pseudoreplication detection
- de-visualization - Per-gene trajectories; heatmap with time order
- batch-correction - Time-batch confounding; multi-batch time courses
- expression-matrix/metadata-joins - Subject as covariate; repeated measures designs
- pathway-analysis/go-enrichment - Functional analysis of trajectory clusters
- temporal-genomics/circadian-rhythms - Circadian-specific detection (JTK_CYCLE, MetaCycle)
- temporal-genomics/temporal-clustering - Standalone trajectory clustering methods
- temporal-genomics/trajectory-modeling - GAM trajectory fitting
- temporal-genomics/temporal-grn - Dynamic gene regulatory network inference
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!