Project: RNA-Seq_yeast
Not connected
Yeast Single-end 0 / 15
Choose mode
Run mode
ℹ️
Using RNAflow as
🖥️
Compute environment
⚡ How would you like to run commands?
✅ Active
Mode A — Run directly from this app
Every code block gets a ▶ Run button. Click it — the command executes automatically, live output streams in, steps tick off. Requires: rnaflow_server.py running in Terminal first.
✅ Active
📋
Mode B — Run on terminal manually
Every code block has a Copy button. Copy → paste into Terminal → run yourself. No server needed. Works offline on any OS.
ℹ️ What is rnaflow_server.py? — setup for your system
🎉 Nothing to do — server starts automatically!
The RNAflow desktop app bundles rnaflow_server.py internally. It starts the moment you open the app and stops when you close it. No Terminal commands are ever needed.
🔒 Runs only on 127.0.0.1:7788 — your machine only. Data never leaves your computer.
🖥️ Compute environment settings
↓ Download from NCBI SRA

Enter SRA accession numbers in the Configure page. RNAflow will download the raw FASTQ files directly from NCBI to your local project folder using fasterq-dump.

💡 Find SRA accessions at ncbi.nlm.nih.gov/sra
Accessions start with SRR, ERR, or DRR
📂 Use files already on this computer

If your FASTQ files are already on disk, set Data source → My own FASTQ files on the Configure page. Every command then reads from your folder instead of downloading.

Files should be named: SampleA.fastq or SampleA_1.fastq / SampleA_2.fastq
Local computer selected. All analysis runs on your machine. Make sure your rnaseq conda environment is active before running commands.
📖 How to use this page
💡
Open your Terminal (Cmd + Space → type Terminal → Enter), then run the check commands below one by one. Compare what you see to the result guide, and use the fix commands if needed.
1️⃣
Run the check command in Terminal
2️⃣
Read the result guide to understand what you found
3️⃣
Run the fix commands if old software is found
🐚 Check 1 — Which shell are you using? Start here

Modern macOS uses zsh. Old Macs use bash. This matters for Conda setup.

bash — check shell
echo $SHELL
You seeMeaningAction
/bin/zsh✅ Modern zsh — perfectNo action needed
/bin/bash⚠️ Old bash shellRun fix below to switch to zsh
bash — fix: switch to zsh (only if you saw /bin/bash)
# Switch to zsh (modern macOS default)
chsh -s /bin/zsh
# Close Terminal completely (Cmd+Q) and reopen it
🐍 Check 2 — Conda / Anaconda / Miniconda installed? Most common problem

Old Anaconda (version 4.x or earlier) causes installation failures. We need Conda 23+ for bioinformatics tools to install correctly.

bash — check conda
# Check if conda exists and what version
conda --version

# Check where it is installed
conda info | grep "base environment"

# Check which type (Anaconda / Miniconda / Miniforge)
conda info | grep "active env location"
You seeMeaningAction
command not found: conda✅ Clean — nothing installedGo straight to Install Tools step
conda 23.x.x or higher✅ Modern version — goodNo action needed
conda 4.x.x or lower❌ Very old — must removeRun the removal commands below
/opt/anaconda3 in path⚠️ Old Anaconda locationRun removal commands below
/opt/miniconda3 in path⚠️ Check version numberIf version < 23, remove and reinstall
(base) at start of promptℹ️ Conda is activeCheck version — ok if 23+

🗑️ Remove old Anaconda/Conda (only run if version was old or causing problems):

⚠️ Only run these if your conda version was 4.x or below, or if you had installation errors. Skip if conda is already version 23+.
bash — step 1: try normal uninstall first
# Try the Anaconda uninstaller (may fail on old versions — that's ok)
conda install anaconda-clean -y
anaconda-clean --yes
bash — step 2: force remove the main folder
# Remove Anaconda (try without sudo first)
rm -rf /opt/anaconda3

# If you get "Permission denied", use sudo:
sudo rm -rf /opt/anaconda3

# Also remove Miniconda if present
rm -rf ~/miniconda3
rm -rf ~/opt/miniconda3
sudo rm -rf /opt/miniconda3

# Remove hidden config files
rm -rf ~/.conda
rm -rf ~/.condarc
bash — step 3: clean up shell config file
# Open your shell config in a text editor
nano ~/.zshrc

# Look for and DELETE these lines (use arrow keys + Ctrl+K to delete a line):
# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
# __conda_setup= ... (several lines)
# <<< conda initialize <<<

# Save: press Ctrl+X → type Y → press Enter
# Then close and reopen Terminal completely (Cmd+Q)
bash — step 4: verify conda is gone
conda --version
# Expected: "zsh: command not found: conda"
# That means it's cleanly removed ✅
🍺 Check 3 — Homebrew installed and working?
bash — check homebrew
brew --version
You seeMeaningAction
Homebrew 4.x.x✅ Installed and workingNo action needed
command not found: brew⚠️ Not installedInstall it in Step 1
Permission errors⚠️ Folder permissions issueRun fix below
bash — fix homebrew permissions (if you saw permission errors)
# Fix ownership of Homebrew folders (use your actual username)
sudo chown -R $(whoami) /usr/local/share/man/man7
chmod u+w /usr/local/share/man/man7

# Fix all Homebrew directory permissions at once
sudo chown -R $(whoami) $(brew --prefix)/*
🔧 Check 4 — Old bioinformatics tools already installed?

Run all of these. If any tool is already installed, check its version against the minimum requirements.

bash — check all RNA tools
# Check each tool — note the version number you see
fastqc --version
fastp --version
hisat2 --version
samtools --version
featureCounts -v
fasterq-dump --version
multiqc --version
ToolMinimum version neededIf version is too old
FastQC0.11.9+Run fix command below
fastp0.20.0+Run fix command below
HISAT22.2.0+Run fix command below
samtools1.15+Run fix command below
featureCounts (subread)2.0.0+Run fix command below
fasterq-dump (sra-tools)3.0+Run fix command below
MultiQC1.12+Run fix command below
bash — fix: remove and reinstall all RNA tools fresh
# Remove old versions from conda environment
conda activate rnaseq
conda remove fastqc fastp hisat2 samtools subread sra-tools multiqc -y

# Reinstall fresh latest versions
conda install -c bioconda -c conda-forge \
  fastqc fastp hisat2 samtools subread sra-tools multiqc salmon -y

# Verify new versions installed
fastqc --version && hisat2 --version && samtools --version && salmon --version && fasterq-dump --version
📊 Check 5 — R version and DESeq2 installed?

Run these in your RStudio console (bottom-left panel where you see the > prompt).

R — check R and packages
# Check R version (need 4.0 or higher)
R.version.string

# Check if DESeq2 is installed
packageVersion("DESeq2")

# Check all needed packages at once
pkgs <- c("DESeq2","ggplot2","pheatmap","RColorBrewer","ggrepel","dplyr")
installed <- pkgs %in% rownames(installed.packages())
data.frame(Package=pkgs, Installed=installed)
You seeMeaningAction
R version 4.x.x✅ GoodNo action needed
R version 3.x.x❌ Too oldDownload R 4.x from cran.r-project.org
DESeq2 version 1.38+✅ GoodNo action needed
Error: no package called 'DESeq2'❌ Not installedRun install commands in Step 1
FALSE in the Installed column❌ Missing packagesRun fix below
R — fix: install or update all missing packages
# Update BiocManager first
install.packages("BiocManager")

# Force reinstall DESeq2 (fixes version conflicts)
BiocManager::install(c("DESeq2", "apeglm", "tximport"), force = TRUE)

# Install all visualization packages
install.packages(c("ggplot2", "pheatmap", "RColorBrewer", "ggrepel", "dplyr"),
                 dependencies = TRUE)
🐍 Check 6 — Python version in RNA-seq environment
bash — check python
# First activate the rnaseq environment
conda activate rnaseq

# Check Python version
python --version

# Check the environment is correct
conda info --envs
You seeMeaningAction
Python 3.10.x or 3.11.x✅ PerfectNo action needed
Python 3.7.x or 3.8.x⚠️ Old — may cause issuesRecreate environment below
conda: command not found❌ Conda not set up yetGo to Step 1 to install
rnaseq not in env list❌ Environment not createdCreate it in Step 1
bash — fix: recreate rnaseq environment with correct Python
# Remove the old environment completely
conda deactivate
conda env remove -n rnaseq -y

# Create fresh with Python 3.10
conda create -n rnaseq python=3.10 -y
conda activate rnaseq

# Reinstall all tools into the fresh environment
conda install -c bioconda -c conda-forge \
  fastqc fastp hisat2 samtools subread sra-tools multiqc salmon -y
What a clean system looks like before starting
bash — final verification — all 8 checks
# Run all checks in one go — paste this entire block
echo "=== Shell ===" && echo $SHELL
echo "=== Conda ===" && conda --version
echo "=== Python ===" && python --version
echo "=== FastQC ===" && fastqc --version
echo "=== HISAT2 ===" && hisat2 --version | head -1
echo "=== Samtools ===" && samtools --version | head -1
echo "=== fasterq-dump ===" && fasterq-dump --version
echo "=== Homebrew ===" && brew --version | head -1
echo "=== All checks done ==="
When everything is clean, you should see something like:
Shell = /bin/zsh · Conda 23.x · Python 3.10.x · FastQC 0.12.x · HISAT2 2.2.x · Samtools 1.17 · fasterq-dump 3.x
💾 Project file New in v3

Your settings are saved in this browser automatically and restored when you come back. Export a project file to archive the exact configuration behind a result, or to hand the analysis to someone else.

📁 Project folder
📁 ~/Desktop/RNA-Seq_yeast
Run at the start of every Terminal session
conda activate rnaseq && cd ~/Desktop/RNA-Seq_yeast
💾 Where is your data? New in v3
🌐
Samples are fetched with fasterq-dump from their SRR/ERR/DRR accessions. Enter the accessions in the Experiment design section below.
🔎 Import samples from a public accession New in v3

Paste a GEO series or SRA/ENA study accession and RNAflow will look up every run in it — no more copying accessions one at a time. Works without the local server.

🔬 Organism & reference genome
📖 How to find genome URLs for any organism
  1. For animals: go to https://www.ensembl.org  |  For plants: go to https://plants.ensembl.org
  2. Search your organism name → click its name in results → click "Download DNA sequence (FASTA)"
  3. Right-click the .dna.toplevel.fa.gz or .dna.primary_assembly.fa.gz file → copy link address
  4. For the GTF: same page → click "Download genes (GTF)" → copy the .gtf.gz file link
  5. Paste both links into the boxes below. The app handles the rest.
Paste Ensembl FTP URL ending in .fa.gz
Paste Ensembl FTP URL ending in .gtf.gz
🧪 Your RNA-seq samples
🔍
Find SRA accessions at https://www.ncbi.nlm.nih.gov/sra — search your paper title or GEO dataset ID. Accessions start with SRR, ERR, or DRR.
🔵 Control — wildtype 3 replicates
🔴 Treatment — snf2_mutant 3 replicates
🍺 Homebrew + Miniforge (Conda)
bash
# 1. Install Homebrew (macOS package manager)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 2. Install Miniforge (Conda for bioinformatics)
brew install miniforge
conda init zsh
# ↑ Close and reopen Terminal after this!

# 3. Create dedicated RNA-seq environment
conda create -n rnaseq python=3.10 -y
conda activate rnaseq

# 4. Install all bioinformatics tools at once
conda install -c bioconda -c conda-forge \
  fastqc fastp hisat2 samtools subread sra-tools multiqc salmon -y

# 5. Verify — all should print version numbers
fastqc --version && hisat2 --version && samtools --version && salmon --version && fasterq-dump --version
📊 R + RStudio packages
⚠️ Download R from cran.r-project.org/bin/macosx and RStudio from posit.co/download/rstudio-desktop first. Then paste this into the RStudio console:
R
install.packages("BiocManager")
BiocManager::install(c("DESeq2", "apeglm", "tximport"))
install.packages(c("ggplot2", "pheatmap", "RColorBrewer", "ggrepel", "dplyr"))
📂 Session startup + create project folder
💡 Run these two commands every time you open a new Terminal session.
bash
...
bash — create folders (run once)
...
⬇️ Download samples from NCBI SRA
⏱️ Each sample takes 3–10 minutes. Run one at a time and wait for the progress bar to complete.
bash
...

Verify downloads:

bash
...
🔬 Run FastQC + MultiQC
bash
...
bash — combine all reports
...
📋 FastQC result guide
ModuleExpected resultIf it fails
Per base sequence quality✅ GreenIncrease trimming quality threshold
Per base sequence content❌ Fail — normal!Already fixed by trimming first 5 bases
Adapter content✅ GreenAdd adapter sequence to fastp
Sequence duplication⚠️ Warning — normal!No action — highly expressed genes cause this
Per sequence GC content✅ GreenRed = possible contamination — investigate
Per tile sequence quality❌ Fail — normal!Machine issue — safe to ignore
✂️ fastp trimming
bash
...
Expected result: ~99.5% reads pass · Zero adapters · Q30 bases >90% after trimming · Each sample finishes in under 10 seconds.

Verify trimmed files exist:

bash
...
🗂️ Download genome + annotation
⚠️ Only needed once per organism. Once downloaded, reuse for all future projects with the same species.
bash
...
🏗️ Build HISAT2 index
⏱️ Build time: Yeast ~2 min · Worm/Fly ~5 min · Mouse/Human ~30–60 min. Build once, reuse forever.
bash
...
📌
Bundled URLs point at Ensembl 116 / Ensembl Genomes 63, verified 2026-08-24. Ensembl retires old releases, so if a download 404s, run the check below and grab the current link from the portals listed here — both URL boxes on the Configure page are editable.

Check your reference URLs before a long download

bash — verify both links resolve
...

Reference genome portals by organism type

Organism groupBest source
Human, Mouse, Rat, Zebrafish, Fly, Worm, Yeasthttps://www.ensembl.org → FTP Download
Arabidopsis, Rice, Maize, Wheat, Soybean, Tomatohttps://plants.ensembl.org → FTP Download
Yarrowia lipolyticahttps://www.ncbi.nlm.nih.gov/datasets/genome/
Any organismhttps://www.ncbi.nlm.nih.gov/genome/ → Download FASTA + GTF
🎯 Align all samples — HISAT2 → sorted BAM
bash
...
Acceptable alignment rates: >80% = OK   >90% = Good   >95% = Excellent. Low rate = check organism/genome match or strandedness setting.
📊 Check mapping rates
bash
...
🔢 featureCounts — all samples in one command
bash
...
💡 Expected % assigned: Yeast 60–70% · Human/Mouse 65–75% · Plants 50–65% — remaining reads fall in intergenic regions, which is normal.

Preview the count table:

bash
...
📊 Complete DESeq2 script
⚠️ Requirement: Minimum 2 biological replicates per condition. 3+ replicates strongly recommended for reliable statistical results.
R — paste entire block into RStudio console
...

Understanding the output columns

ColumnWhat it meansHow to use
log2FoldChangeFold change in log2 scale. Positive = higher in treatment.Direction of change
padjAdjusted p-value after multiple testing correctionAlways use this, never raw pvalue
baseMeanAverage expression level across all samplesFilter very lowly expressed genes

Filter significant genes:

R
# Standard: significant genes (padj < 0.05)
sig <- res_df[res_df$padj < 0.05 & !is.na(res_df$padj), ]

# Strict: significant AND at least 2-fold change
sig_strict <- res_df[res_df$padj < 0.05 & abs(res_df$log2FoldChange) > 1 & !is.na(res_df$padj), ]

# Top 20 most significant
top20 <- head(sig[order(sig$padj), ], 20)
print(top20[, c("gene", "log2FoldChange", "padj")])
🌋 Volcano plot
R
...
🌡️ Heatmap — top 40 genes
R
...
🎯 PCA plot — sample quality check
R
...
Good PCA result: Replicates from the same condition cluster tightly together. The two conditions are far apart on PC1. PC1 explains >50% of total variance.
☑️ Step 1 — What would you like to analyse? Tick one or both — independent selections
🔬
Whole RNA-seq Analysis
Run DESeq2 on all genes in your count matrix.
Produces: full volcano plot, heatmap of top 40 DE genes, PCA.
✓ Ticked by default — standard starting point
🧬
Gene Family Subset
Filter results to specific gene families — transcription factors, kinases, hormone pathways, and 40+ more.
Multi-select: choose as many categories and families as you want.
Tick to reveal family selector below
💡
Whole analysis ticked. You can also tick Gene Family Subset to add focused analysis — both can run together and produce separate output files.
Whole analysis ready — click to generate R scripts
🗂️ Gene family database reference
DatabaseWhat it containsURLBest for
PlantTFDBAll plant TF familiesplanttfdb.gao-lab.orgArabidopsis, rice, maize, wheat, soybean, tomato
iTAKPlant TF + protein kinaseitak.bioinfotoolkits.netAll plant species
AnimalTFDBAnimal TF familiesanimaltfdb.bioinfotoolkits.netHuman, mouse, rat, zebrafish
KEGG PathwayMetabolic & signalling pathwayskegg.jpAll organisms
Pfam / InterProProtein domain familiesebi.ac.uk/interproAny organism
PANTHERProtein families + subfamiliespantherdb.orgHuman, mouse, Drosophila, worm
Ensembl BioMartGene family / Pfam filteringensembl.org/biomartAll Ensembl organisms
Step 8 of 10
🤔 When do you need this?
❌ Single-factor (basic)
All samples processed identically. Only one variable differs. Use the standard DESeq2 page.
✅ Two-factor additive
Samples have a known confound (e.g. sequencing batch, sex, age). Add the second factor to the design to control for it.
🔬 Interaction model
You want to find genes where the treatment effect differs between groups (e.g. drug works differently in males vs females).
⚠️ Multi-factor designs require more replicates. For a 2-factor additive model, aim for ≥ 3 replicates per condition per batch level.
📋 Step 1 — Set up your multi-factor sample metadata

Extend the sample_info data frame from the standard DESeq2 step to include your second factor. Edit the values to match your actual experiment.

R — paste into RStudio, edit to match your experiment
library(DESeq2); library(ggplot2); library(ggrepel)

# ─── Update these paths and names to match your project ───
base_path  <- "~/Desktop/RNA-Seq_yeast"   # your project folder
ctrl_name  <- "wildtype"                   # control condition label
treat_name <- "snf2_mutant"                # treatment condition label
n_ctrl     <- 3                           # number of control replicates
n_treat    <- 3                           # number of treatment replicates
n_total    <- n_ctrl + n_treat

# Load count matrix
counts_raw <- read.table(paste0(base_path, "/counts/gene_counts_all.txt"),
                          header=TRUE, skip=1, row.names=1)
counts <- counts_raw[, (ncol(counts_raw)-n_total+1):ncol(counts_raw)]

# Column names: edit to match your sample order
colnames(counts) <- c("ctrl_1","ctrl_2","ctrl_3","treat_1","treat_2","treat_3")

# ─── Define multi-factor metadata ───────────────────────────────
# EDIT batch/sex/time values to match your actual sample assignments
sample_info <- data.frame(
  condition = factor(c(ctrl_name,ctrl_name,ctrl_name, treat_name,treat_name,treat_name)),
  batch     = factor(c("batch1","batch1","batch2", "batch1","batch2","batch2")),
  # Optionally add more factors:
  # sex  = factor(c("M","F","M", "F","M","F")),
  row.names = colnames(counts)
)
print(sample_info)  # verify it looks correct
Model 1 — Additive: test condition while controlling for batch

The most common multi-factor model. Adds batch to the design formula so DESeq2 accounts for it when estimating the condition effect. Use this when you have a known confound (batch, sex, age, etc.).

💡 Design ~batch + condition means: "find genes that differ by condition, after accounting for batch variation."
R — additive two-factor model
# Additive design: controls for batch, tests condition effect
dds_add <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info,
                                   design=~batch + condition)
dds_add <- dds_add[rowSums(counts(dds_add)) >= 10, ]
dds_add <- DESeq(dds_add)

# Extract condition effect (batch effect is now controlled)
res_add <- results(dds_add,
                    contrast=c("condition", treat_name, ctrl_name),
                    alpha=0.05)
summary(res_add)

# Save results
res_add_df <- as.data.frame(res_add)
res_add_df$gene <- rownames(res_add_df)
res_add_df <- res_add_df[order(res_add_df$padj, na.last=TRUE), ]
write.csv(res_add_df, paste0(base_path, "/results/deseq2_additive_model.csv"), row.names=FALSE)

cat("\nAdditive model results:\n")
cat("  Significant (padj<0.05):", sum(res_add_df$padj < 0.05, na.rm=TRUE), "\n")
cat("  Up in treatment:         ", sum(res_add_df$padj < 0.05 & res_add_df$log2FoldChange > 0, na.rm=TRUE), "\n")
cat("  Down in treatment:       ", sum(res_add_df$padj < 0.05 & res_add_df$log2FoldChange < 0, na.rm=TRUE), "\n")
Compare to basic DESeq2What changes
Design formula~condition~batch + condition
Fold changesAdjusted for batch effect — more accurate
Gene countUsually more significant genes (reduced noise)
InterpretationSame — padj < 0.05, LFC direction still applies
✖️ Model 2 — Interaction: genes where treatment effect differs by group

Use when you want to find genes where the effect of treatment is different depending on another variable (e.g. a drug that works in females but not males). This is more complex — only use if scientifically justified.

R — interaction model
# Interaction model: tests if treatment effect DIFFERS by batch
dds_int <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info,
                                   design=~batch * condition)
dds_int <- dds_int[rowSums(counts(dds_int)) >= 10, ]
dds_int <- DESeq(dds_int)

# List all available result names (choose the interaction term)
resultsNames(dds_int)
# Look for a name like: "batchbatch2.conditiontreat_name"

# Extract the interaction term (genes where batch modifies treatment effect)
interaction_term <- grep("batch.*condition", resultsNames(dds_int), value=TRUE)[1]
res_int <- results(dds_int, name=interaction_term, alpha=0.05)
summary(res_int)

res_int_df <- as.data.frame(res_int)
res_int_df$gene <- rownames(res_int_df)
write.csv(res_int_df, paste0(base_path, "/results/deseq2_interaction.csv"), row.names=FALSE)
cat("Genes with significant interaction:", sum(res_int_df$padj < 0.05, na.rm=TRUE), "\n")
📐 Model 3 — Likelihood Ratio Test (LRT): overall effect of condition

LRT identifies genes that change anywhere across your conditions — useful for time courses or experiments with 3+ condition levels. It compares a full model to a reduced model without condition.

R — LRT (best for time-course and 3+ conditions)
# LRT: finds genes where REMOVING condition significantly worsens the model fit
dds_lrt <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info,
                                   design=~batch + condition)
dds_lrt <- dds_lrt[rowSums(counts(dds_lrt)) >= 10, ]
dds_lrt <- DESeq(dds_lrt, test="LRT", reduced=~batch)  # reduced removes condition

res_lrt <- results(dds_lrt, alpha=0.05)
summary(res_lrt)

res_lrt_df <- as.data.frame(res_lrt)
res_lrt_df$gene <- rownames(res_lrt_df)
res_lrt_df <- res_lrt_df[order(res_lrt_df$padj, na.last=TRUE), ]
write.csv(res_lrt_df, paste0(base_path, "/results/deseq2_LRT.csv"), row.names=FALSE)
cat("Genes significant by LRT (padj<0.05):", sum(res_lrt_df$padj < 0.05, na.rm=TRUE), "\n")
ℹ️ LRT p-values test whether the gene responds to condition at all. LFC values are still reported but are pairwise comparisons — use results(dds_lrt, contrast=...) to get specific pairwise LFCs for significant LRT genes.
🔗 Paired design — matched samples (patient before/after, twin studies)

If each control sample is paired with a treatment sample from the same biological source (e.g. same patient pre/post treatment), use the paired design. This is the most powerful approach for paired data.

R — paired design
# Paired design: each control is matched to a treatment from the same source
sample_info_paired <- data.frame(
  condition = factor(c(ctrl_name,ctrl_name,ctrl_name, treat_name,treat_name,treat_name)),
  patient   = factor(c("P1","P2","P3", "P1","P2","P3")),  # same patient IDs
  row.names = colnames(counts)
)

# Patient goes first — this blocks for individual variation
dds_paired <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info_paired,
                                      design=~patient + condition)
dds_paired <- dds_paired[rowSums(counts(dds_paired)) >= 10, ]
dds_paired <- DESeq(dds_paired)
res_paired <- results(dds_paired, contrast=c("condition", treat_name, ctrl_name), alpha=0.05)
summary(res_paired)
cat("Significant (paired, padj<0.05):", sum(res_paired$padj < 0.05, na.rm=TRUE), "\n")
🔍 Step 1 — Detect batch effects with PCA

Always check for batch effects before correcting. If replicates from the same condition cluster by batch instead of by condition on PCA, you have a batch effect.

R — PCA colored by both condition AND batch
library(DESeq2); library(ggplot2); library(ggrepel)

# Run this AFTER loading counts and sample_info from the DESeq2 step
# (sample_info must have both 'condition' and 'batch' columns)
base_path <- "~/Desktop/RNA-Seq_yeast"

dds_raw <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info, design=~condition)
dds_raw <- dds_raw[rowSums(counts(dds_raw)) >= 10, ]
dds_raw <- estimateSizeFactors(dds_raw)
vst_raw  <- vst(dds_raw, blind=TRUE)

# PCA: color by condition, shape by batch — batch effect = shapes separate
pca_data <- plotPCA(vst_raw, intgroup=c("condition","batch"), returnData=TRUE)
pct_var  <- round(100 * attr(pca_data, "percentVar"))

p_check <- ggplot(pca_data, aes(PC1, PC2, color=condition, shape=batch, label=name)) +
  geom_point(size=4) +
  geom_text_repel(size=3.5, show.legend=FALSE) +
  labs(title="PCA: check for batch effects",
       subtitle="If shapes separate on PC1/PC2 — batch effect present",
       x=paste0("PC1: ", pct_var[1], "% variance"),
       y=paste0("PC2: ", pct_var[2], "% variance")) +
  theme_minimal(base_size=13)
print(p_check)
ggsave(paste0(base_path, "/results/pca_batch_check.png"), p_check, width=9, height=7, dpi=300)
PCA patternInterpretationAction
Conditions separate on PC1, batches overlap✅ No batch effectUse standard DESeq2
Batches separate on PC1 or PC2⚠️ Batch effect presentUse ComBat-seq or additive model
Random — no clear patternℹ️ Low quality / high noiseCheck FastQC reports, consider removing outliers
⚔️ Step 2A — ComBat-seq: correct raw counts (recommended)

ComBat-seq corrects for batch effects directly on raw count data while preserving the count distribution needed by DESeq2. This is the recommended approach for RNA-seq count data.

ComBat-seq vs multi-factor model: ComBat-seq physically removes batch variation from counts. The multi-factor model (Step 11) statistically accounts for it. Both are valid — ComBat-seq is preferred when sharing data or running downstream tools that don't accept multi-factor designs.
R — ComBat-seq batch correction on raw counts
# Install if needed: BiocManager::install("sva")
library(sva); library(DESeq2); library(ggplot2); library(ggrepel)

# batch and group vectors must be in the same order as your count columns
# 1 = batch1, 2 = batch2  |  1 = control, 2 = treatment
batch_vec <- as.integer(sample_info$batch)   # e.g. c(1,1,2, 1,2,2)
group_vec <- as.integer(sample_info$condition) # e.g. c(1,1,1, 2,2,2)

# Apply ComBat-seq: corrects raw counts, preserves count nature
counts_mat       <- as.matrix(counts)
counts_corrected <- ComBat_seq(counts_mat, batch=batch_vec, group=group_vec)

cat("ComBat-seq done. Matrix dimensions:", dim(counts_corrected), "\n")

# Save corrected count matrix
write.csv(as.data.frame(counts_corrected),
          paste0(base_path, "/counts/gene_counts_batch_corrected.csv"))

# Re-run DESeq2 with corrected counts — simple design, no batch term needed
sample_info_simple <- data.frame(
  condition = sample_info$condition,
  row.names = colnames(counts_corrected)
)
dds_bc <- DESeqDataSetFromMatrix(countData=counts_corrected, colData=sample_info_simple,
                                  design=~condition)
dds_bc <- dds_bc[rowSums(counts(dds_bc)) >= 10, ]
dds_bc <- DESeq(dds_bc)
res_bc <- results(dds_bc, contrast=c("condition", treat_name, ctrl_name), alpha=0.05)
summary(res_bc)
res_bc_df <- as.data.frame(res_bc); res_bc_df$gene <- rownames(res_bc_df)
write.csv(res_bc_df, paste0(base_path, "/results/deseq2_combatseq_corrected.csv"), row.names=FALSE)
cat("Significant after ComBat-seq:", sum(res_bc_df$padj < 0.05, na.rm=TRUE), "\n")
📊 Step 2B — limma::removeBatchEffect (for visualization only)

Use removeBatchEffect on VST-normalized data for making PCA plots and heatmaps. Do not use the output as DESeq2 input — it is not count data.

R — limma batch removal for plots
# Install if needed: BiocManager::install("limma")
library(limma); library(DESeq2)

# Get VST-normalized data from your DESeq2 object
vst_data <- vst(dds_bc, blind=FALSE)
mat       <- assay(vst_data)

# Remove batch effect from the VST matrix (visualization only)
mat_corrected <- removeBatchEffect(mat, batch=batch_vec)

# Replace the assay data with corrected values for plotting
assay(vst_data) <- mat_corrected

# PCA after batch removal
pca_after <- plotPCA(vst_data, intgroup="condition") +
  ggtitle("PCA AFTER limma batch removal") +
  theme_minimal(base_size=13)
print(pca_after)
ggsave(paste0(base_path, "/results/pca_after_batch_removal.png"), pca_after, width=8, height=6, dpi=300)
cat("PCA plot saved! Use the corrected matrix for heatmaps and other visualizations.\n")
🔬 Step 2C — SVA: when batch is unknown

Surrogate Variable Analysis (SVA) detects hidden batch-like sources of variation in your data — even when you don't know what they are. Use this when you can't identify specific batch variables.

R — SVA surrogate variable analysis
library(sva); library(DESeq2)

# Normalize raw counts for SVA input
dds_sva <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info, design=~condition)
dds_sva <- dds_sva[rowSums(counts(dds_sva)) >= 10, ]
dds_sva <- estimateSizeFactors(dds_sva)
log_norm <- log2(counts(dds_sva, normalized=TRUE) + 1)

# Define full model (includes condition) and null model (no condition)
mod_full <- model.matrix(~condition, data=colData(dds_sva))
mod_null <- model.matrix(~1,          data=colData(dds_sva))

# Estimate number of surrogate variables
n_sv <- num.sv(log_norm, mod_full, method="leek")
cat("Estimated surrogate variables:", n_sv, "\n")

svobj <- sva(log_norm, mod_full, mod_null, n.sv=n_sv)

# Add surrogate variables to DESeq2 metadata
for (i in 1:n_sv) {
  colData(dds_sva)[[paste0("SV", i)]] <- svobj$sv[, i]
}
sv_terms    <- paste(paste0("SV", 1:n_sv), collapse=" + ")
design_sva  <- as.formula(paste("~", sv_terms, "+ condition"))
design(dds_sva) <- design_sva
dds_sva <- DESeq(dds_sva)
res_sva <- results(dds_sva, contrast=c("condition", treat_name, ctrl_name), alpha=0.05)
summary(res_sva)
cat("Significant after SVA correction:", sum(res_sva$padj < 0.05, na.rm=TRUE), "\n")
🖼️ Step 3 — Compare PCA before vs after batch correction
R — side-by-side PCA comparison
# Install if needed: install.packages("patchwork")
library(patchwork)

# Before: VST from uncorrected counts
dds_before <- DESeqDataSetFromMatrix(countData=counts, colData=sample_info, design=~condition)
dds_before <- dds_before[rowSums(counts(dds_before)) >= 10, ]
dds_before <- estimateSizeFactors(dds_before)
vst_before <- vst(dds_before, blind=TRUE)

# After: VST from ComBat-seq corrected counts
dds_after <- DESeqDataSetFromMatrix(countData=counts_corrected, colData=sample_info_simple, design=~condition)
dds_after <- dds_after[rowSums(counts(dds_after)) >= 10, ]
dds_after <- estimateSizeFactors(dds_after)
vst_after <- vst(dds_after, blind=TRUE)

p_before <- plotPCA(vst_before, intgroup="condition") +
  ggtitle("BEFORE batch correction") + theme_minimal(base_size=12)
p_after  <- plotPCA(vst_after,  intgroup="condition") +
  ggtitle("AFTER batch correction")  + theme_minimal(base_size=12)

ggsave(paste0(base_path, "/results/pca_before_after_batch.png"),
       p_before + p_after, width=14, height=6, dpi=300)
cat("Before/after PCA comparison saved!\n")
Good result: After correction, samples from the same condition cluster together regardless of batch. The two conditions should be clearly separated on PC1.
🗺️ Two complementary approaches
📌 ORA — Over-Representation Analysis
Takes your significant gene list (padj < 0.05) and asks: "Is any GO term or pathway over-represented compared to the genome background?" Fast, simple, widely used.
Best for: clear, well-defined gene lists with strong signal
📈 GSEA — Gene Set Enrichment Analysis
Uses all genes ranked by LFC × significance score. Tests whether a gene set's members are concentrated at the top or bottom of the ranked list. More sensitive — captures subtle pathway-level changes.
Best for: noisy data, modest fold changes, whole-pathway shifts
📦 Install required packages (one-time)
R — install packages
if (!requireNamespace("BiocManager", quietly=TRUE)) install.packages("BiocManager")

# Core enrichment packages
BiocManager::install(c("clusterProfiler", "enrichplot", "ReactomePA", "fgsea"), ask=FALSE)

# Organism annotation databases (install the ones relevant to your organism)
BiocManager::install(c(
  "org.Hs.eg.db",   # Human
  "org.Mm.eg.db",   # Mouse
  "org.Rn.eg.db",   # Rat
  "org.Sc.sgd.db",  # Yeast (S. cerevisiae)
  "org.At.tair.db", # Arabidopsis
  "org.Dm.eg.db",   # Fruit fly
  "org.Ce.eg.db",   # C. elegans
  "org.Dr.eg.db"    # Zebrafish
), ask=FALSE)

install.packages(c("ggplot2", "dplyr"))  # if not already installed
OrganismOrgDb packageKEGG codeReactome organism
Humanorg.Hs.eg.dbhsahuman
Mouseorg.Mm.eg.dbmmumouse
Ratorg.Rn.eg.dbrnorat
Yeastorg.Sc.sgd.dbsceyeast
Arabidopsisorg.At.tair.dbath
Fruit flyorg.Dm.eg.dbdmefly
C. elegansorg.Ce.eg.dbcelworm
Zebrafishorg.Dr.eg.dbdrezebrafish
⚙️ Setup — load results and build gene lists
R — load DESeq2 results and prepare gene lists
library(clusterProfiler); library(enrichplot); library(ggplot2); library(dplyr)
library(org.Sc.sgd.db)  # ← CHANGE to your organism's OrgDb package

# ─── Edit these ───────────────────────────────────────────────────────────
base_path  <- "~/Desktop/RNA-Seq_yeast"
OrgDb_pkg  <- org.Sc.sgd.db  # your organism OrgDb
key_type   <- "GENENAME"      # "SYMBOL" for most; "TAIR" for arabidopsis
kegg_org   <- "sce"           # KEGG organism code
react_org  <- "yeast"         # Reactome organism name
# ──────────────────────────────────────────────────────────────────────────

# Load DESeq2 results
res_df <- read.csv(paste0(base_path, "/results/deseq2_results.csv"))

# 1. Significant gene list for ORA (padj<0.05, |LFC|>1)
sig_genes <- res_df$gene[!is.na(res_df$padj) & res_df$padj < 0.05 &
                          abs(res_df$log2FoldChange) > 1]
cat("Significant genes (ORA input):", length(sig_genes), "\n")

# 2. Ranked gene list for GSEA (all genes, ranked by signed log10 p-value)
res_ranked <- res_df[!is.na(res_df$padj) & !is.na(res_df$log2FoldChange), ]
res_ranked$score <- sign(res_ranked$log2FoldChange) * (-log10(res_ranked$padj + 1e-300))
res_ranked <- res_ranked[order(res_ranked$score, decreasing=TRUE), ]
gene_list <- setNames(res_ranked$score, res_ranked$gene)
cat("Ranked gene list (GSEA input):", length(gene_list), "genes\n")
🔵 GO Enrichment — Over-Representation Analysis (ORA)
R — GO ORA with clusterProfiler
# GO Biological Process ORA
go_bp <- enrichGO(gene         = sig_genes,
                   OrgDb        = OrgDb_pkg,
                   keyType      = key_type,
                   ont          = "BP",        # "BP", "MF", or "CC"
                   pAdjustMethod = "BH",
                   pvalueCutoff = 0.05,
                   qvalueCutoff = 0.20,
                   readable     = TRUE)

go_df <- as.data.frame(go_bp)
cat("GO BP terms enriched:", nrow(go_df), "\n")
write.csv(go_df, paste0(base_path, "/results/GO_BP_ORA.csv"), row.names=FALSE)

# Dot plot (top 20 terms, sized by gene count, coloured by p-value)
p_dot <- dotplot(go_bp, showCategory=20, title="GO Biological Process — ORA") +
  theme_minimal(base_size=12)
ggsave(paste0(base_path, "/results/GO_BP_dotplot.png"), p_dot, width=10, height=10, dpi=300)
cat("GO dotplot saved!\n")

# Also run Molecular Function and Cellular Component
go_mf <- enrichGO(gene=sig_genes, OrgDb=OrgDb_pkg, keyType=key_type, ont="MF",
                   pAdjustMethod="BH", pvalueCutoff=0.05, readable=TRUE)
go_cc <- enrichGO(gene=sig_genes, OrgDb=OrgDb_pkg, keyType=key_type, ont="CC",
                   pAdjustMethod="BH", pvalueCutoff=0.05, readable=TRUE)
write.csv(as.data.frame(go_mf), paste0(base_path, "/results/GO_MF_ORA.csv"), row.names=FALSE)
write.csv(as.data.frame(go_cc), paste0(base_path, "/results/GO_CC_ORA.csv"), row.names=FALSE)
cat("GO MF terms:", nrow(as.data.frame(go_mf)), " | CC terms:", nrow(as.data.frame(go_cc)), "\n")
📈 GSEA — Gene Set Enrichment Analysis (preranked)
R — GSEA with clusterProfiler
# GO GSEA (uses all ranked genes — no threshold cutoff needed)
gsea_bp <- gseGO(geneList      = gene_list,
                  OrgDb         = OrgDb_pkg,
                  keyType       = key_type,
                  ont           = "BP",
                  minGSSize     = 10,
                  maxGSSize     = 500,
                  pvalueCutoff  = 0.05,
                  pAdjustMethod = "BH",
                  verbose       = FALSE,
                  seed          = 42)

gsea_df <- as.data.frame(gsea_bp)
cat("GSEA GO BP terms significant:", nrow(gsea_df), "\n")
cat("  Activated (NES>0):",  sum(gsea_df$NES > 0), "\n")
cat("  Suppressed (NES<0):", sum(gsea_df$NES < 0), "\n")
write.csv(gsea_df, paste0(base_path, "/results/GSEA_GO_BP.csv"), row.names=FALSE)

# Ridge plot — shows distribution of ranked genes within each term
p_ridge <- ridgeplot(gsea_bp, showCategory=15) +
  theme_minimal(base_size=11) +
  labs(title="GSEA GO Biological Process — Ridge Plot")
ggsave(paste0(base_path, "/results/GSEA_ridgeplot.png"), p_ridge, width=11, height=10, dpi=300)

# Enrichment score plot for top terms
gseaplot2(gsea_bp, geneSetID=1:min(3,nrow(gsea_df)),
          title="Top GSEA enrichment scores")
cat("GSEA plots saved!\n")
ℹ️ NES (Normalized Enrichment Score): NES > 0 means the gene set is enriched in upregulated genes. NES < 0 means it's enriched in downregulated genes. Only terms with |NES| > 1 and padj < 0.05 are typically reported.
🗾 KEGG Pathway Analysis
R — KEGG ORA and GSEA
# ─── KEGG ORA ────────────────────────────────────────────────────────────
# For most organisms, clusterProfiler can use gene symbols directly.
# For human/mouse, convert SYMBOL → ENTREZID first (see note below).
kegg_ora <- enrichKEGG(gene          = sig_genes,
                        organism      = kegg_org,
                        pAdjustMethod = "BH",
                        pvalueCutoff  = 0.05)

kegg_df <- as.data.frame(kegg_ora)
cat("KEGG pathways enriched:", nrow(kegg_df), "\n")
write.csv(kegg_df, paste0(base_path, "/results/KEGG_ORA.csv"), row.names=FALSE)

# KEGG bar plot
p_kegg <- barplot(kegg_ora, showCategory=20, title="KEGG Pathway Enrichment") +
  theme_minimal(base_size=12)
ggsave(paste0(base_path, "/results/KEGG_barplot.png"), p_kegg, width=10, height=8, dpi=300)

# ─── KEGG GSEA ───────────────────────────────────────────────────────────
gsea_kegg <- gseKEGG(geneList      = gene_list,
                      organism      = kegg_org,
                      minGSSize     = 10,
                      pvalueCutoff  = 0.05,
                      pAdjustMethod = "BH",
                      verbose       = FALSE)
write.csv(as.data.frame(gsea_kegg), paste0(base_path, "/results/KEGG_GSEA.csv"), row.names=FALSE)
cat("KEGG GSEA terms:", nrow(as.data.frame(gsea_kegg)), "\n")

# Note for human/mouse — must convert gene IDs to ENTREZ first:
# entrez <- bitr(sig_genes, fromType="SYMBOL", toType="ENTREZID", OrgDb=org.Hs.eg.db)$ENTREZID
# Then use entrez as the gene= argument and gene_list with ENTREZ names for GSEA
⚛️ Reactome Pathway Analysis
⚠️ ReactomePA requires Entrez IDs. Use bitr() to convert gene symbols → Entrez IDs. Reactome supports: human, mouse, rat, zebrafish, fly, worm, and yeast.
R — Reactome ORA and GSEA
library(ReactomePA)

# Convert gene names to Entrez IDs (required by ReactomePA)
id_map <- bitr(sig_genes, fromType=key_type, toType="ENTREZID", OrgDb=OrgDb_pkg)
entrez_sig <- id_map$ENTREZID
cat("Mapped to Entrez:", length(entrez_sig), "/", length(sig_genes), "genes\n")

# Reactome ORA
react_ora <- enrichPathway(gene          = entrez_sig,
                             organism      = react_org,
                             pAdjustMethod = "BH",
                             pvalueCutoff  = 0.05,
                             readable      = TRUE)

react_df <- as.data.frame(react_ora)
cat("Reactome pathways enriched:", nrow(react_df), "\n")
write.csv(react_df, paste0(base_path, "/results/Reactome_ORA.csv"), row.names=FALSE)

p_react <- dotplot(react_ora, showCategory=20, title="Reactome Pathway ORA") +
  theme_minimal(base_size=12)
ggsave(paste0(base_path, "/results/Reactome_dotplot.png"), p_react, width=10, height=10, dpi=300)

# Reactome GSEA — convert ranked list to Entrez IDs
id_map_all <- bitr(res_ranked$gene, fromType=key_type, toType="ENTREZID", OrgDb=OrgDb_pkg)
ranked_entrez <- setNames(
  res_ranked$score[match(id_map_all[[key_type]], res_ranked$gene)],
  id_map_all$ENTREZID
)
ranked_entrez <- sort(ranked_entrez, decreasing=TRUE)
ranked_entrez <- ranked_entrez[!duplicated(names(ranked_entrez))]

gsea_react <- gsePathway(geneList=ranked_entrez, organism=react_org,
                           pvalueCutoff=0.05, pAdjustMethod="BH", verbose=FALSE)
write.csv(as.data.frame(gsea_react), paste0(base_path, "/results/Reactome_GSEA.csv"), row.names=FALSE)
cat("Reactome GSEA terms:", nrow(as.data.frame(gsea_react)), "\n")
🎨 Network visualizations — enrichment map & concept network
R — enrichment map and gene-concept network
library(enrichplot)

# ─── Enrichment map: shows overlap between GO terms ───
go_bp_sim <- pairwise_termsim(go_bp)  # compute term similarity
p_emap <- emapplot(go_bp_sim, showCategory=30, layout="kk") +
  ggtitle("GO BP Enrichment Map")
ggsave(paste0(base_path, "/results/GO_enrichment_map.png"), p_emap,
       width=13, height=11, dpi=300)

# ─── Concept network: genes connected to their GO terms ───
# Shows which specific genes drive each enriched term
p_cnet <- cnetplot(go_bp, showCategory=8, colorEdge=TRUE,
                    node_label="gene",
                    foldChange=setNames(res_df$log2FoldChange, res_df$gene)) +
  ggtitle("Gene-Concept Network (top 8 GO terms)") +
  scale_color_gradient2(name="log2FC", low="#58a6ff", mid="white", high="#f0883e")
ggsave(paste0(base_path, "/results/GO_cnetplot.png"), p_cnet,
       width=13, height=11, dpi=300)

# ─── Upset plot: genes shared across multiple GO terms ───
p_upset <- upsetplot(go_bp, n=10)
ggsave(paste0(base_path, "/results/GO_upsetplot.png"), p_upset,
       width=12, height=6, dpi=300)
cat("Network and upset plots saved!\n")
📁 Expected output files in results/
FileContents
GO_BP_ORA.csvGO Biological Process enrichment table (ORA)
GO_MF_ORA.csvGO Molecular Function enrichment table (ORA)
GO_CC_ORA.csvGO Cellular Component enrichment table (ORA)
GO_BP_dotplot.pngDot plot — top 20 GO BP terms, sized by gene count
GSEA_GO_BP.csvGO GSEA results table with NES scores
GSEA_ridgeplot.pngRidge plot of top GSEA terms
KEGG_ORA.csv / KEGG_GSEA.csvKEGG pathway enrichment and GSEA tables
KEGG_barplot.pngKEGG pathway bar chart
Reactome_ORA.csv / Reactome_GSEA.csvReactome pathway enrichment tables
Reactome_dotplot.pngReactome pathway dot plot
GO_enrichment_map.pngNetwork of GO term overlaps
GO_cnetplot.pngGene-concept network — genes to GO terms
GO_upsetplot.pngGenes shared across top GO terms
🖼️ Output browser
🔬 Export to nf-core/rnaseq

RNAflow teaches the pipeline one step at a time, which is the right way to learn it and the wrong way to run it fifty times. nf-core/rnaseq is the community-standard Nextflow workflow — it resumes after failures, parallelises across samples, and records software versions for your methods section. These two files hand your exact configuration over to it.

💡
Install once with conda install -c bioconda nextflow, then run the command below. Nextflow pulls the workflow and containers itself — nothing else to set up.

1 — Samplesheet

samplesheet.csv
...

2 — Write the samplesheet and launch

bash
...
📜 Export as one shell script

Prefer to stay with the tools you have just learned? This bundles every generated step, in order, into a single script you can run unattended or attach to a paper as the exact commands used.

Step complete!
Step complete!