This report analyzes the E-MTAB-9838 Arabidopsis
thaliana RNA-seq data: untreated controls (no), flg22
treatment (flg22), and AtPep1 treatment
(pep1), with three samples per treatment. The plots are
shown inside this HTML report, so they will also be
visible when the HTML report is published to RPubs.
Before knitting: Extract all files from the
accompanying ZIP archive into one folder. Open this .Rmd
file from that extracted folder in RStudio and click
Knit. Both TSV input files must be in the same folder
as this .Rmd file. Do not run setwd() inside
the report.
The chunk below is not run during knitting. Remove
the leading # and run the commands in the RStudio Console
if packages are missing.
# install.packages("BiocManager")
# BiocManager::install(c("DESeq2", "EnhancedVolcano", "org.At.tair.db",
# "clusterProfiler", "enrichplot"),
# update = FALSE, ask = FALSE)
# install.packages(c("ggplot2", "knitr", "rmarkdown"))
The raw count table contains two gene-information columns followed by
nine sample count columns. Gene.ID contains Arabidopsis
TAIR locus identifiers.
counts <- read.delim(counts_file, check.names = TRUE)
# R changes the original column names "Gene ID" and "Gene Name"
# to "Gene.ID" and "Gene.Name" when check.names = TRUE.
rownames(counts) <- counts$Gene.ID
# Keep gene names in a separate table.
genes <- counts[, c("Gene.ID", "Gene.Name"), drop = FALSE]
# Keep all nine sample columns as a matrix.
counts2 <- as.matrix(counts[, !(names(counts) %in% c("Gene.ID", "Gene.Name"))])
# Check that counts are unique, numeric, nonmissing, nonnegative integers.
stopifnot(!anyDuplicated(rownames(counts2)))
stopifnot(!anyDuplicated(colnames(counts2)))
stopifnot(is.numeric(counts2), !anyNA(counts2))
stopifnot(all(counts2 >= 0))
stopifnot(all(counts2 == floor(counts2)))
storage.mode(counts2) <- "integer"
cat("Genes:", nrow(counts2), "\n")
## Genes: 32833
cat("Samples:", ncol(counts2), "\n")
## Samples: 9
knitr::kable(head(genes, 5), caption = "Examples of Arabidopsis gene identifiers")
| Gene.ID | Gene.Name | |
|---|---|---|
| AT1G01010 | AT1G01010 | NAC001 |
| AT1G01020 | AT1G01020 | ARV1 |
| AT1G01030 | AT1G01030 | NGA3 |
| AT1G01040 | AT1G01040 | DCL1 |
| AT1G01046 | AT1G01046 | MIR838A |
metadata <- read.delim(metadata_file, check.names = TRUE)
rownames(metadata) <- metadata$Run
# Use only the stimulus variable.
metadata <- metadata[, "Sample.Characteristic.stimulus.", drop = FALSE]
colnames(metadata) <- "treatment"
# Rename the treatments consistently; no unknown values are silently renamed.
metadata$treatment[metadata$treatment == "none"] <- "no"
metadata$treatment[metadata$treatment == "AtPep1"] <- "pep1"
stopifnot(all(metadata$treatment %in% c("no", "flg22", "pep1")))
knitr::kable(metadata, caption = "Sample treatment metadata")
| treatment | |
|---|---|
| ERR4901839 | no |
| ERR4901840 | no |
| ERR4901841 | no |
| ERR4901842 | flg22 |
| ERR4901843 | flg22 |
| ERR4901844 | flg22 |
| ERR4901845 | pep1 |
| ERR4901846 | pep1 |
| ERR4901847 | pep1 |
# Every count-matrix sample must exist in the metadata.
stopifnot(all(colnames(counts2) %in% rownames(metadata)))
# Arrange the metadata rows in exactly the same order as the count columns.
metadata <- metadata[colnames(counts2), , drop = FALSE]
stopifnot(identical(colnames(counts2), rownames(metadata)))
cat("Sample alignment correct:",
identical(colnames(counts2), rownames(metadata)), "\n")
## Sample alignment correct: TRUE
metadata$treatment <- factor(metadata$treatment,
levels = c("no", "flg22", "pep1"))
# There should be three samples in each treatment group.
print(table(metadata$treatment))
##
## no flg22 pep1
## 3 3 3
dds <- DESeqDataSetFromMatrix(
countData = counts2,
colData = metadata,
design = ~ treatment
)
# Exclude genes with 10 or fewer total counts across the nine samples.
dds <- dds[rowSums(counts(dds)) > 10, ]
cat("Genes retained after filtering:", nrow(dds), "\n")
## Genes retained after filtering: 21786
dds <- DESeq(dds)
print(resultsNames(dds))
## [1] "Intercept" "treatment_flg22_vs_no" "treatment_pep1_vs_no"
Positive log2 fold change means higher expression in the first treatment named in the contrast.
res_flg22 <- results(dds, contrast = c("treatment", "flg22", "no"), alpha = 0.05)
res_pep1 <- results(dds, contrast = c("treatment", "pep1", "no"), alpha = 0.05)
res_pep1_vs_flg22 <- results(dds, contrast = c("treatment", "pep1", "flg22"),
alpha = 0.05)
cat("flg22 versus control:\n")
## flg22 versus control:
print(summary(res_flg22, alpha = 0.05))
##
## out of 21786 with nonzero total read count
## adjusted p-value < 0.05
## LFC > 0 (up) : 2785, 13%
## LFC < 0 (down) : 1205, 5.5%
## outliers [1] : 0, 0%
## low counts [2] : 423, 1.9%
## (mean count < 2)
## [1] see 'cooksCutoff' argument of ?results
## [2] see 'independentFiltering' argument of ?results
##
## NULL
cat("\npep1 versus control:\n")
##
## pep1 versus control:
print(summary(res_pep1, alpha = 0.05))
##
## out of 21786 with nonzero total read count
## adjusted p-value < 0.05
## LFC > 0 (up) : 3075, 14%
## LFC < 0 (down) : 2131, 9.8%
## outliers [1] : 0, 0%
## low counts [2] : 423, 1.9%
## (mean count < 2)
## [1] see 'cooksCutoff' argument of ?results
## [2] see 'independentFiltering' argument of ?results
##
## NULL
cat("\npep1 versus flg22:\n")
##
## pep1 versus flg22:
print(summary(res_pep1_vs_flg22, alpha = 0.05))
##
## out of 21786 with nonzero total read count
## adjusted p-value < 0.05
## LFC > 0 (up) : 1818, 8.3%
## LFC < 0 (down) : 2170, 10%
## outliers [1] : 0, 0%
## low counts [2] : 2957, 14%
## (mean count < 10)
## [1] see 'cooksCutoff' argument of ?results
## [2] see 'independentFiltering' argument of ?results
##
## NULL
CSV files are saved to Arabidopsis_DESeq2_results when
that folder is writable. Saving CSV files is separate
from showing graphs in HTML.
save_csv(data.frame(Gene.ID = rownames(res_flg22), as.data.frame(res_flg22)),
"flg22_vs_none.csv")
save_csv(data.frame(Gene.ID = rownames(res_pep1), as.data.frame(res_pep1)),
"pep1_vs_none.csv")
save_csv(data.frame(Gene.ID = rownames(res_pep1_vs_flg22),
as.data.frame(res_pep1_vs_flg22)),
"pep1_vs_flg22.csv")
Change only active_comparison to one of the three names
shown below, then knit the report again.
active_comparison <- "flg22_vs_none"
# Other choices: "pep1_vs_none" or "pep1_vs_flg22"
results_list <- list(
flg22_vs_none = res_flg22,
pep1_vs_none = res_pep1,
pep1_vs_flg22 = res_pep1_vs_flg22
)
stopifnot(active_comparison %in% names(results_list))
result <- results_list[[active_comparison]]
cat("Selected comparison:", active_comparison, "\n")
## Selected comparison: flg22_vs_none
No extra TAIR column is needed. The Gene.ID
column already contains TAIR locus identifiers.
result_df <- as.data.frame(result)
result_df$Gene.ID <- rownames(result_df)
result_df$Gene.Name <- genes$Gene.Name[match(result_df$Gene.ID, genes$Gene.ID)]
result_df <- result_df[, c("Gene.ID", "Gene.Name", "baseMean",
"log2FoldChange", "lfcSE", "stat", "pvalue", "padj")]
save_csv(result_df, paste0("annotated_", active_comparison, ".csv"))
knitr::kable(head(result_df, 10), digits = 3,
caption = "Preview of the selected DESeq2 results")
| Gene.ID | Gene.Name | baseMean | log2FoldChange | lfcSE | stat | pvalue | padj | |
|---|---|---|---|---|---|---|---|---|
| AT1G01010 | AT1G01010 | NAC001 | 103.723 | -0.230 | 0.186 | -1.231 | 0.218 | 0.470 |
| AT1G01020 | AT1G01020 | ARV1 | 125.826 | -0.295 | 0.170 | -1.734 | 0.083 | 0.258 |
| AT1G01030 | AT1G01030 | NGA3 | 20.010 | -0.184 | 0.395 | -0.465 | 0.642 | 0.831 |
| AT1G01040 | AT1G01040 | DCL1 | 351.590 | -0.205 | 0.128 | -1.606 | 0.108 | 0.306 |
| AT1G01046 | AT1G01046 | MIR838A | 2.946 | -0.474 | 0.984 | -0.482 | 0.630 | 0.825 |
| AT1G01050 | AT1G01050 | PPA1 | 828.007 | 0.037 | 0.109 | 0.335 | 0.737 | 0.882 |
| AT1G01060 | AT1G01060 | LHY | 881.313 | 0.284 | 0.181 | 1.568 | 0.117 | 0.322 |
| AT1G01070 | AT1G01070 | 40.907 | -0.075 | 0.264 | -0.286 | 0.775 | 0.903 | |
| AT1G01080 | AT1G01080 | 949.753 | 0.174 | 0.089 | 1.960 | 0.050 | 0.181 | |
| AT1G01090 | AT1G01090 | PDH-E1 ALPHA | 1891.917 | -0.026 | 0.090 | -0.285 | 0.776 | 0.904 |
genes_to_check <- c("AT3G45640", # MPK3
"AT2G43790", # MPK6
"AT5G46330", # FLS2
"AT1G73080", # PEPR1
"AT2G38470") # WRKY33
immune_genes <- result_df[result_df$Gene.ID %in% genes_to_check, ]
knitr::kable(immune_genes, digits = 3,
caption = "Differential expression of selected immune-response genes")
| Gene.ID | Gene.Name | baseMean | log2FoldChange | lfcSE | stat | pvalue | padj | |
|---|---|---|---|---|---|---|---|---|
| AT1G73080 | AT1G73080 | PEPR1 | 1617.871 | 2.301 | 0.100 | 23.001 | 0.000 | 0.000 |
| AT2G38470 | AT2G38470 | WRKY33 | 10129.680 | 3.779 | 0.164 | 23.085 | 0.000 | 0.000 |
| AT2G43790 | AT2G43790 | MPK6 | 476.651 | 0.074 | 0.101 | 0.731 | 0.465 | 0.709 |
| AT3G45640 | AT3G45640 | MPK3 | 7652.691 | 3.376 | 0.148 | 22.837 | 0.000 | 0.000 |
| AT5G46330 | AT5G46330 | FLS2 | 583.786 | 1.195 | 0.101 | 11.778 | 0.000 | 0.000 |
Important: This function produces a plot directly in the knitted HTML.
plotCounts(dds, gene = "AT3G45640", intgroup = "treatment")
The key fix is to call plotMA()
directly. Do not wrap it in pdf() /
dev.off(), which would send the figure to a file rather
than to the HTML.
plotMA(result, main = active_comparison, ylim = c(-5, 5))
EnhancedVolcano() creates a ggplot object. Calling
print(volcano_plot) displays it in the knitted HTML. Genes
without symbols use Gene.ID as labels.
volcano_df <- result_df[is.finite(result_df$log2FoldChange), ]
plot_labels <- volcano_df$Gene.Name
missing_labels <- is.na(plot_labels) | trimws(plot_labels) == ""
plot_labels[missing_labels] <- volcano_df$Gene.ID[missing_labels]
# Missing adjusted p-values are shown as nonsignificant in the visualization.
volcano_df$plot_padj <- volcano_df$padj
volcano_df$plot_padj[is.na(volcano_df$plot_padj)] <- 1
volcano_df$plot_padj <- pmax(volcano_df$plot_padj, .Machine$double.xmin)
volcano_plot <- EnhancedVolcano(
volcano_df,
lab = plot_labels,
x = "log2FoldChange",
y = "plot_padj",
pCutoff = 0.05,
FCcutoff = 1,
title = active_comparison
)
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## ℹ The deprecated feature was likely used in the EnhancedVolcano package.
## Please report the issue to the authors.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: The `size` argument of `element_line()` is deprecated as of ggplot2 3.4.0.
## ℹ Please use the `linewidth` argument instead.
## ℹ The deprecated feature was likely used in the EnhancedVolcano package.
## Please report the issue to the authors.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
print(volcano_plot)
PCA includes all nine samples, irrespective of the comparison selected in Step 9.
vsd <- vst(dds, blind = FALSE)
pca_plot <- plotPCA(vsd, intgroup = "treatment")
print(pca_plot)
Use a data frame (sig_df) for tables
and a vector of gene IDs (sig_genes) for
enrichment analysis.
sig_rows <- !is.na(result_df$padj) & result_df$padj < 0.05
sig_df <- result_df[sig_rows, , drop = FALSE]
sig_df <- sig_df[order(sig_df$padj), , drop = FALSE]
sig_df <- sig_df[!duplicated(sig_df$Gene.ID), , drop = FALSE]
top20 <- head(sig_df[, c("Gene.ID", "Gene.Name", "log2FoldChange", "padj")], 20)
knitr::kable(top20, digits = 4,
caption = "Top 20 significant genes by adjusted p-value")
| Gene.ID | Gene.Name | log2FoldChange | padj | |
|---|---|---|---|---|
| AT1G61340 | AT1G61340 | 5.1209 | 0 | |
| AT1G65390 | AT1G65390 | PP2A5 | 5.0324 | 0 |
| AT1G80840 | AT1G80840 | WRKY40 | 4.6888 | 0 |
| AT3G21070 | AT3G21070 | NADK1 | 3.4850 | 0 |
| AT3G57530 | AT3G57530 | CPK32 | 3.5035 | 0 |
| AT4G29780 | AT4G29780 | 5.4790 | 0 | |
| AT2G40000 | AT2G40000 | HSPRO2 | 3.9985 | 0 |
| AT1G56250 | AT1G56250 | VBF | 6.9428 | 0 |
| AT5G15870 | AT5G15870 | 3.7547 | 0 | |
| AT2G26190 | AT2G26190 | IQM4 | 3.4541 | 0 |
| AT4G01250 | AT4G01250 | WRKY22 | 4.9642 | 0 |
| AT4G24570 | AT4G24570 | PUMP4 | 5.3029 | 0 |
| AT3G02840 | AT3G02840 | 7.3515 | 0 | |
| AT1G09940 | AT1G09940 | HEMA2 | 3.3827 | 0 |
| AT1G72520 | AT1G72520 | LOX4 | 5.1681 | 0 |
| AT1G21110 | AT1G21110 | IGMT3 | 4.2942 | 0 |
| AT3G18710 | AT3G18710 | PUB29 | 4.4350 | 0 |
| AT4G31550 | AT4G31550 | WRKY11 | 3.9659 | 0 |
| AT1G23710 | AT1G23710 | 4.9187 | 0 | |
| AT1G21120 | AT1G21120 | 5.8244 | 0 |
sig_genes <- unique(sig_df$Gene.ID)
background <- unique(result_df$Gene.ID[!is.na(result_df$padj)])
cat("Significant genes in", active_comparison, ":", length(sig_genes), "\n")
## Significant genes in flg22_vs_none : 3990
A few TAIR IDs may fail annotation mapping; that is typically a warning, not a reason to stop the report. If no enriched terms are returned, the report displays a message rather than an empty plot.
ego <- NULL
if (length(sig_genes) > 0) {
ego <- tryCatch(
enrichGO(
gene = sig_genes,
universe = background,
OrgDb = org.At.tair.db,
keyType = "TAIR",
ont = "BP",
pAdjustMethod = "BH",
pvalueCutoff = 0.05,
qvalueCutoff = 0.05,
readable = TRUE
),
error = function(e) {
cat("GO enrichment unavailable:", conditionMessage(e), "\n")
NULL
}
)
}
## Warning in bitr(gene, fromType = fromType, toType = "ENTREZID", OrgDb = OrgDb):
## 1.8% of input gene IDs are fail to map...
## Warning in bitr(gene, fromType = fromType, toType = "ENTREZID", OrgDb = OrgDb):
## 3.74% of input gene IDs are fail to map...
if (!is.null(ego) && nrow(as.data.frame(ego)) > 0) {
save_csv(as.data.frame(ego), paste0("GO_BP_", active_comparison, ".csv"))
go_plot <- enrichplot::dotplot(ego, showCategory = 20)
print(go_plot) # Required to show the graph inside the knitted HTML.
} else {
cat("No significant GO Biological Process terms were found.\n")
}
KEGG access may require an internet connection while knitting. If KEGG is unavailable, this section prints an explanation and the rest of the report still completes.
ekegg <- NULL
if (length(sig_genes) > 0) {
ekegg <- tryCatch(
enrichKEGG(
gene = sig_genes,
universe = background,
organism = "ath",
keyType = "kegg",
pAdjustMethod = "BH",
pvalueCutoff = 0.05
),
error = function(e) {
cat("KEGG enrichment unavailable:", conditionMessage(e), "\n")
NULL
}
)
}
if (!is.null(ekegg) && nrow(as.data.frame(ekegg)) > 0) {
save_csv(as.data.frame(ekegg), paste0("KEGG_", active_comparison, ".csv"))
kegg_plot <- barplot(ekegg, showCategory = 20)
print(kegg_plot) # Required to show the graph inside the knitted HTML.
} else {
cat("No significant KEGG pathways were found, or the service was unavailable.\n")
}
geneList <- result$stat
names(geneList) <- rownames(result)
geneList <- geneList[is.finite(geneList)]
geneList <- sort(geneList, decreasing = TRUE)
cat("Genes in the ranked list:", length(geneList), "\n")
## Genes in the ranked list: 21786
The Direction column names the treatment in the
numerator of the selected comparison.
Not_Significant includes genes with missing adjusted
p-values. No fold-change cutoff is applied in this classification.
final_report <- result_df[, c("Gene.ID", "Gene.Name", "baseMean",
"log2FoldChange", "lfcSE", "stat",
"pvalue", "padj")]
sig <- !is.na(final_report$padj) & final_report$padj < 0.05
treatment <- strsplit(active_comparison, "_vs_")[[1]][1]
final_report$Direction <- ifelse(
sig & !is.na(final_report$log2FoldChange) & final_report$log2FoldChange > 0,
paste0("Up_in_", treatment),
ifelse(
sig & !is.na(final_report$log2FoldChange) & final_report$log2FoldChange < 0,
paste0("Down_in_", treatment),
"Not_Significant"
)
)
file_name <- paste0("DESeq2_", active_comparison, "_results.csv")
save_csv(final_report, file_name)
cat("Direction counts:\n")
## Direction counts:
print(table(final_report$Direction))
##
## Down_in_flg22 Not_Significant Up_in_flg22
## 1205 17796 2785
knitr::kable(head(final_report, 10), digits = 5,
caption = "Preview of the final differential expression table")
| Gene.ID | Gene.Name | baseMean | log2FoldChange | lfcSE | stat | pvalue | padj | Direction | |
|---|---|---|---|---|---|---|---|---|---|
| AT1G01010 | AT1G01010 | NAC001 | 103.72261 | -0.22961 | 0.18647 | -1.23137 | 0.21819 | 0.46988 | Not_Significant |
| AT1G01020 | AT1G01020 | ARV1 | 125.82629 | -0.29510 | 0.17023 | -1.73355 | 0.08300 | 0.25784 | Not_Significant |
| AT1G01030 | AT1G01030 | NGA3 | 20.00956 | -0.18391 | 0.39533 | -0.46522 | 0.64177 | 0.83141 | Not_Significant |
| AT1G01040 | AT1G01040 | DCL1 | 351.58964 | -0.20521 | 0.12775 | -1.60642 | 0.10818 | 0.30594 | Not_Significant |
| AT1G01046 | AT1G01046 | MIR838A | 2.94594 | -0.47444 | 0.98384 | -0.48223 | 0.62964 | 0.82506 | Not_Significant |
| AT1G01050 | AT1G01050 | PPA1 | 828.00725 | 0.03659 | 0.10909 | 0.33546 | 0.73727 | 0.88235 | Not_Significant |
| AT1G01060 | AT1G01060 | LHY | 881.31253 | 0.28402 | 0.18114 | 1.56794 | 0.11689 | 0.32156 | Not_Significant |
| AT1G01070 | AT1G01070 | 40.90688 | -0.07545 | 0.26369 | -0.28611 | 0.77479 | 0.90339 | Not_Significant | |
| AT1G01080 | AT1G01080 | 949.75274 | 0.17419 | 0.08887 | 1.96004 | 0.04999 | 0.18101 | Not_Significant | |
| AT1G01090 | AT1G01090 | PDH-E1 ALPHA | 1891.91692 | -0.02564 | 0.09009 | -0.28457 | 0.77597 | 0.90399 | Not_Significant |
All figures above are rendered directly in the knitted HTML and should be visible in RPubs after you use RStudio’s Publish option on the HTML report. RPubs displays the report and embedded plots; it does not host the separately exported CSV files from your computer.
cat("Selected comparison:", active_comparison, "\n")
## Selected comparison: flg22_vs_none
cat("HTML report includes: MPK3 counts, MA plot, volcano plot, PCA plot,\n")
## HTML report includes: MPK3 counts, MA plot, volcano plot, PCA plot,
cat("and GO/KEGG plots when enriched terms are available.\n")
## and GO/KEGG plots when enriched terms are available.
cat("Local CSV output folder:", output_dir, "\n")
## Local CSV output folder: Arabidopsis_DESeq2_results
cat("Output folder writable:", can_save, "\n")
## Output folder writable: TRUE