Macatawa Data: Differential Abundance Analysis using DESeq2

Introduction

In the Lake Macatawa PERMANOVA tutorial, we determined that there was a statistically significant difference between the bacterial community compositions in the summer and winter (water temperature seasons) at the lake sites in the Macatawa watershed. We will now use a DESeq2 analysis to identify particular OTUs that have significantly different abundances between the two seasons.

We will start by following a similar approach to the one we took in the Lake Erie DESeq2 tutorial. However, this time we will also create a visualization that shows the variation in the relative abundances of the OTUs that we identify using the differential abundance analysis. The visualization allows us to further explore patterns between the abundances of these OTUs and other environmental variables.

Setup and reading in the clean data

Let’s start by cleaning up our work space and loading the required packages.

rm(list = ls())
gc()
          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
Ncells  582183 31.1    1328658   71         NA   669442 35.8
Vcells 1075568  8.3    8388608   64     102400  1851737 14.2
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.4.4     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(DESeq2)
Loading required package: S4Vectors
Loading required package: stats4
Loading required package: BiocGenerics

Attaching package: 'BiocGenerics'

The following objects are masked from 'package:lubridate':

    intersect, setdiff, union

The following objects are masked from 'package:dplyr':

    combine, intersect, setdiff, union

The following objects are masked from 'package:stats':

    IQR, mad, sd, var, xtabs

The following objects are masked from 'package:base':

    anyDuplicated, aperm, append, as.data.frame, basename, cbind,
    colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find,
    get, grep, grepl, intersect, is.unsorted, lapply, Map, mapply,
    match, mget, order, paste, pmax, pmax.int, pmin, pmin.int,
    Position, rank, rbind, Reduce, rownames, sapply, setdiff, sort,
    table, tapply, union, unique, unsplit, which.max, which.min


Attaching package: 'S4Vectors'

The following objects are masked from 'package:lubridate':

    second, second<-

The following objects are masked from 'package:dplyr':

    first, rename

The following object is masked from 'package:tidyr':

    expand

The following object is masked from 'package:utils':

    findMatches

The following objects are masked from 'package:base':

    expand.grid, I, unname

Loading required package: IRanges

Attaching package: 'IRanges'

The following object is masked from 'package:lubridate':

    %within%

The following objects are masked from 'package:dplyr':

    collapse, desc, slice

The following object is masked from 'package:purrr':

    reduce

Loading required package: GenomicRanges
Loading required package: GenomeInfoDb
Loading required package: SummarizedExperiment
Loading required package: MatrixGenerics
Loading required package: matrixStats

Attaching package: 'matrixStats'

The following object is masked from 'package:dplyr':

    count


Attaching package: 'MatrixGenerics'

The following objects are masked from 'package:matrixStats':

    colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
    colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
    colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
    colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
    colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
    colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
    colWeightedMeans, colWeightedMedians, colWeightedSds,
    colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
    rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
    rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
    rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
    rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
    rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
    rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
    rowWeightedSds, rowWeightedVars

Loading required package: Biobase
Welcome to Bioconductor

    Vignettes contain introductory material; view with
    'browseVignettes()'. To cite Bioconductor, see
    'citation("Biobase")', and for packages 'citation("pkgname")'.


Attaching package: 'Biobase'

The following object is masked from 'package:MatrixGenerics':

    rowMedians

The following objects are masked from 'package:matrixStats':

    anyMissing, rowMedians

Now we will read in the clean data. We will not be using ordination coordinates, so we can use the version of the metadata that do not include them.

macatawa <- readRDS("clean_data/macatawa.rds")
mac_tax <- readRDS("clean_data/macatawa_tax.rds")
mac_meta <- readRDS("clean_data/macatawa_meta.rds")

Add mTEC categories and year to metadata

As we did in the PERMAOVA tutorial, we will use the cut function to categorize mTEC levels into three groups. We will also add a year column to the metadata. We may find either of these variables useful for plotting.

library(lubridate)
mac_meta <- mac_meta %>%
  mutate(mTEC_cat = mTEC %>% cut(breaks = c(0,300,1000,Inf), labels = c("belowTC", "TCtoPC", "abovePC"), include.lowest = TRUE)) %>%
  mutate(Year = year(CollectionDate) %>% as_factor())

Filter to remove samples with a small number of reads (<10k) and to retain samples from the summer and winter water temperature seasons

We will remove any samples with a sequencing depth of less than 10,000 reads. We also filter to retain only the sites and seasons we want for our comparison.

mac_meta <- mac_meta %>% 
  filter(sequencingDepth >= 10000) %>%
  filter(SiteType == "Lake") %>%
  filter(WaterTempSeason4 == "SUM" | WaterTempSeason4 == "WINT")
mac_samples <- mac_meta %>% pull(SampleID)
macatawa <- macatawa %>% filter(SampleID %>% is.element(mac_samples))

Compute relative abundances

We will compute the relative abundances of OTUs within each sample. This will be useful for plotting later. We leave the data in a long format, making the next step easier.

macatawa <- macatawa %>%
  pivot_longer(cols = starts_with("Otu"),
               names_to = "OTU",
               values_to = "Abundance") %>%
  group_by(SampleID) %>%
  mutate(Rel_Abundance = Abundance/sum(Abundance, na.rm=TRUE)) %>%
  ungroup

Pre-filtering the data by removing OTUs with very few reads from the data, calculating relative abundances

We will filter our data to only retain OTUs with at least 10 reads. We’ll start by creating a vector that lists all of the OTUs that have at least 10 reads in the filtered data.

mac_otus <- macatawa %>%
  group_by(OTU) %>%
  summarize(Total_abundance = sum(Abundance, na.rm=TRUE)) %>%
  filter(Total_abundance >= 10) %>%
  pull(OTU)

Now we remove any OTUs that have less than 10 reads from the taxonomy table and the shared table by only keeping the ones in the list. At the same time, we will also drop the two extra columns that we won’t use (label and numOtus).

macatawa <- macatawa %>%
  filter(OTU %>% is.element(mac_otus)) %>%
  select(-c(label,numOtus))
mac_tax <- mac_tax %>% filter(OTU %>% is.element(mac_otus))

Next we widen the shared data table. We create two versions–one with abundances and one with relative abundances. We will use the one with abundances for the DESeq2 analysis, and we will use the one with relative abundances for plotting.

macatawa_rel <- macatawa %>% select(-Abundance) %>% pivot_wider(names_from = OTU, values_from = Rel_Abundance)
macatawa <- macatawa %>% select(-Rel_Abundance) %>% pivot_wider(names_from = OTU, values_from = Abundance)

Creating a DESeqDataSet object

Recall that to create a DESeqDataSet appropriate for analysis using DESeq2, we need to start by creating a count matrix for the samples we are interested in.

We start by reducing the metadata to two variables–SampleID and WaterTempSeason4–the only variables we need for the DESeq2 analysis (since we are comparing summer to winter samples). Next we ensure sure that the samples in the metadata are ordered the same as in the shared table by joining the two data sets.

mac_meta_wts <- mac_meta %>%
  select(SampleID,WaterTempSeason4)
macatawa_wts <- macatawa %>%
  inner_join(mac_meta_wts, by = "SampleID")

Let’s create the count matrix. We’ll print the first 2 rows to the screen.

mac_count <- macatawa_wts %>%
  select(starts_with("Otu")) %>%
  as.matrix(labels = TRUE)
row.names(mac_count) <- macatawa_wts %>% pull(SampleID)
mac_count <- mac_count %>% t()
head(mac_count,2)
          10_1_73 10_1_74 10_1_75 10_1_83 10_1_88 10_1_89 10_1_90 10_2_73
Otu000001    3151    3656    2304     811    2253    2129    1457    1173
Otu000002    3714    3856    2452     661    3854    1835     849     798
          10_2_78 10_2_79 10_2_80 10_2_88 10_2_93 10_2_94 10_3_70 10_3_78
Otu000001    1591    2088    1948    1906    1148    1156     740     374
Otu000002    1808    2515    1597     523     681     562     369     223
          10_3_82 10_3_83 10_3_84 10_3_91 10_4_74 10_4_75 10_4_82 10_4_86
Otu000001     634    1562    2542    1770    2654    2774    1949    2401
Otu000002    2617    2062    1443     727    1863    1820    1683    1016
          10_4_92 11_1_72 11_1_79 11_4_04 11_4_05 11_4_06 11_4_14 11_4_70
Otu000001    3140    3077    2325    1281    1166     570     479      91
Otu000002     946    1378     732     699     233      42      71       9
          12_1_02 12_1_12 12_1_17 12_1_18 12_1_19 12_1_27 12_1_33 12_1_34
Otu000001     272    1242     260     899     233     165     322     458
Otu000002       6      39       6       7       1       5       4      13
          12_1_35 12_1_43 12_1_48 12_1_49 12_1_50 12_1_57 12_2_16 12_2_17
Otu000001     196     259     730     223     268     416     365     454
Otu000002      21       1       1       0       3       3       3       3
          12_2_18 12_2_26 12_2_33 12_2_34 12_2_35 12_2_43 12_2_49 12_2_50
Otu000001     657     145     202      67     108     111     340     212
Otu000002      55       2       6       4       8       5     418     247
          12_2_51 12_2_59 12_3_05 12_3_06 12_3_07 12_4_05 12_4_06 12_4_07
Otu000001     188     264     363     719     182     781    1594     228
Otu000002       1       3    1455    2229      43     757    1220     297
          12_4_14 12_4_19 12_4_20 12_4_28 12_4_32 12_4_33 12_4_40 14_1_04
Otu000001     469    1506    1834     786    4047    4623    1727    3656
Otu000002     158    1796     534     266    4348    2317     677    7492
          14_1_25 14_3_02 14_3_13 14_3_14 14_3_21 14_3_25 14_4_05 15_1_72
Otu000001    1341     125      50      37     364      50     149    1430
Otu000002     763      24       0       0       2       5       1     536
          15_1_73 15_2_12 15_2_13 15_2_14 15_2_22 15_2_23 15_2_24 15_2_32
Otu000001     601     305      62     154     455    1002     967    2139
Otu000002     723       1       2       0       2       7      16     719
          15_2_33 15_2_34 15_2_42 15_2_43 15_2_44 15_3_64 15_3_65 15_4_58
Otu000001     430     495    2288     627     229      56      83     904
Otu000002       1       5     690       8      24       2      52     196
          15_4_63 15_4_64 15_4_72 15_4_73 15_4_74 1_1_34 1_1_79 1_3_33 1_3_34
Otu000001    3495    2126    2648    3594    4179    113    213    496    570
Otu000002    1434    2349     268     653     893      3     15    368    306
          1_3_54 1_3_55 1_3_62 1_3_74 2_2_77 2_2_78 2_2_79 2_2_87 2_3_47 2_3_48
Otu000001    510   1108    672   1625    114     86    105    101   1105   2716
Otu000002   1204   1180    496   1012      4      1      4     14   1068   2098
          2_3_49 2_3_90 3_1_30 3_1_48 3_1_49 3_1_57 3_2_06 3_2_09 3_2_11 3_2_19
Otu000001   1819    669   1240   1445    973    842    490    183    180    175
Otu000002   1205   1207    525    283    177    332     12     11     11      4
          3_2_22 3_2_23 3_2_24 3_2_32 3_2_36 3_2_37 3_2_38 3_2_46 3_2_50 3_2_51
Otu000001    347    323    280    222    259    340    653    794    281    556
Otu000002     25     14     12     26      4     27     17     24      1      6
          3_2_59 3_2_64 3_2_65 3_2_66 3_2_77 3_2_78 3_2_87 3_2_89 3_2_90 3_2_91
Otu000001    411    377    217    232     63    127    150     85    100    121
Otu000002      3     11      2      6      0      3      7      2      4      5
          3_3_05 3_3_09 3_3_10 3_3_11 3_3_19 3_3_38 3_3_39 3_3_40 3_3_48 3_3_53
Otu000001    292     77     65    140    122    107     50     53    137    134
Otu000002      1      0      1      3      1      1      1      0      2      1
          3_3_54 3_3_55 3_3_68 3_3_69 3_3_70 3_3_78 3_3_83 3_3_84 3_3_93 3_4_04
Otu000001     57     72     60    103     56    270     59     81     73     55
Otu000002      1      0      2      0      0      8      3      0      0      3
          3_4_05 3_4_06 3_4_14 5_1_12 5_1_18 5_1_28 5_1_34 5_1_35 5_1_36 5_1_44
Otu000001     44     33    218    120     83     24     71     95    113     57
Otu000002      0      7      6     10      0      0      0      2      3      4
          6_1_71 6_1_72 6_1_73 6_1_81 6_2_87 6_2_88 6_2_89 6_3_71 6_3_76 6_3_77
Otu000001    115     79    118    181    944    615    642    394    398    603
Otu000002      1      4      2      1    646    466    225    152    357    224
          6_3_78 6_3_86 6_3_91 6_4_63 6_4_68 6_4_72 6_4_73 6_4_81 6_4_87 6_4_88
Otu000001    231    652    568    771   1008   2762   1141   1006   1128    962
Otu000002    155    166     72    927    465   1909    960    654    654    582
          6_4_89 7_1_02 7_1_08 7_1_15 7_1_21 7_1_22 7_1_23 7_1_31 7_1_64 7_1_65
Otu000001   1126   1241   1479   1003    858    828    907   1474   1091    974
Otu000002    591    671    319    409    878    514    441    401    543    557
          7_1_66 7_1_74 7_2_27 7_4_81 7_4_91 8_1_71 8_1_72 8_1_73 8_1_81 8_1_87
Otu000001    946    887    523    364    179    552    722    288    550    569
Otu000002    396    405    668      1      0      8     21     23     20     54
          8_1_88 8_1_89 8_2_58 8_2_59 8_2_60 8_2_68 8_2_84 8_2_90 8_2_91 8_2_92
Otu000001    572    432    410    329    458    292    402    300    209    153
Otu000002     60     30     31     17     18     14      9      6      0      3
          8_3_33 8_3_40 8_3_41 8_3_49 8_3_55 8_3_56 8_3_57 8_3_65 8_3_71 8_3_72
Otu000001    250     84    125    179    145    266    196     84    161    299
Otu000002     13      0      4      3      0      3      1      0      1      2
          8_3_79 8_3_85 8_3_86 8_3_87 8_4_01 8_4_06 8_4_07 8_4_08 8_4_16 8_4_21
Otu000001    153    132    247    180    142    192    162    172    532     63
Otu000002      2      0      0      2      2      0      0      4      5      0
          8_4_22 8_4_23 8_4_31 8_4_40 8_4_41 8_4_42 8_4_50 8_4_56 8_4_57 8_4_64
Otu000001     39     39     17     90     39     60    120     60     33     32
Otu000002      0      2      0      1      3      3      0      5      1      0
          8_4_69 9_1_05 9_1_06 9_1_07 9_1_15 9_1_19 9_1_20 9_1_21 9_1_29 9_2_41
Otu000001     72    139    146     85     74    216     34     90     49    353
Otu000002      0      0      1      8      0      5      4      2      2      6
          9_2_80 9_2_81 9_2_82 9_2_90 9_3_72 9_3_73 9_3_74 9_3_82 9_3_87 9_3_88
Otu000001    607    571    369    529   1355    923    614    429   1515   1559
Otu000002    529    882    183    120   1632   1076    130    157   2045   1969
          9_3_89 9_4_93
Otu000001   1219    395
Otu000002   1450    194

Next we will separate the correctly ordered metadata columns from the joined data.

mac_meta_wts <- macatawa_wts %>%
  select(!starts_with("Otu"))
mac_meta_wts
# A tibble: 277 × 2
   SampleID WaterTempSeason4
   <chr>    <chr>           
 1 10_1_73  WINT            
 2 10_1_74  WINT            
 3 10_1_75  WINT            
 4 10_1_83  WINT            
 5 10_1_88  WINT            
 6 10_1_89  WINT            
 7 10_1_90  WINT            
 8 10_2_73  WINT            
 9 10_2_78  WINT            
10 10_2_79  WINT            
# ℹ 267 more rows

Now we can create the DESeqDataSet object.

mac_dds <- DESeqDataSetFromMatrix(countData = mac_count,
                                      colData = mac_meta_wts,
                                      design = ~ WaterTempSeason4)
Warning in DESeqDataSet(se, design = design, ignoreRank): some variables in
design formula are characters, converting to factors
mac_dds
class: DESeqDataSet 
dim: 9700 277 
metadata(1): version
assays(1): counts
rownames(9700): Otu000001 Otu000002 ... Otu359288 Otu368899
rowData names(0):
colnames(277): 10_1_73 10_1_74 ... 9_3_89 9_4_93
colData names(2): SampleID WaterTempSeason4

Finally, we will identify which level of the WaterTempSeason4 variable should be considered the reference level (e.g., the control). We will choose SUM as our reference level.

mac_dds$WaterTempSeason4 <- relevel(mac_dds$WaterTempSeason4, ref = "SUM")

DESeq2 Analysis

Now that we have our data in the correct form, we can run the differential abundance analysis. First we use the DESeq.

mac_dds <- mac_dds %>% DESeq(test="Wald", fitType="parametric")
estimating size factors
estimating dispersions
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
fitting model and testing
-- replacing outliers and refitting for 582 genes
-- DESeq argument 'minReplicatesForReplace' = 7 
-- original counts are preserved in counts(dds)
estimating dispersions
fitting model and testing

Then we use the results function and print some of the results. We format the results as a tibble. Refer to the Lake Erie DESeq2 tutorial, which discusses how to interpret the results table.

mac_dds_res <- mac_dds %>% results(cooksCutoff = FALSE, tidy = TRUE) %>%
  as_tibble() %>%
  dplyr::rename(OTU = row) #annoyingly DESeq2 has rename, too
mac_dds_res
# A tibble: 9,700 × 7
   OTU       baseMean log2FoldChange lfcSE    stat    pvalue      padj
   <chr>        <dbl>          <dbl> <dbl>   <dbl>     <dbl>     <dbl>
 1 Otu000001   566.           2.22   0.114  19.5   1.38e- 84 3.26e- 83
 2 Otu000002   402.           4.89   0.300  16.3   1.13e- 59 1.62e- 58
 3 Otu000003    65.2          0.606  0.102   5.92  3.24e-  9 1.19e-  8
 4 Otu000004    40.4          1.47   0.215   6.81  9.63e- 12 4.06e- 11
 5 Otu000005    40.5          1.35   0.185   7.32  2.57e- 13 1.16e- 12
 6 Otu000006   282.           6.61   0.250  26.4   1.25e-153 1.03e-151
 7 Otu000007    16.0         -0.206  0.243  -0.850 3.96e-  1 5.04e-  1
 8 Otu000008     4.07        -2.50   0.228 -10.9   7.11e- 28 5.25e- 27
 9 Otu000009   179.           4.38   0.189  23.2   7.62e-119 3.31e-117
10 Otu000010    96.6          0.0947 0.172   0.549 5.83e-  1 6.85e-  1
# ℹ 9,690 more rows

Let’s see how many OTUs were significantly different between groups. We will use a significance level of 0.001 for this analysis (instead of 0.05). 1,558 of them were significantly different at this significance level.

mac_dds_res %>% dplyr::count(padj < 0.001)
# A tibble: 3 × 2
  `padj < 0.001`     n
  <lgl>          <int>
1 FALSE           2311
2 TRUE            1558
3 NA              5831

Instead of testing whether abundances are significantly different between the two seasons, we can test whether their differences are significantly larger than a threshold that we determine. We will start by exploring whether there are any OTUs that are at least twice as abundant in one season than in the other (i.e., OTUs that have a log2 fold change value that is significantly larger than 1 or less than -1).

mac_dds_res_lfc1 <- mac_dds %>% 
  results(lfcThreshold = 1,
          altHypothesis = "greaterAbs", 
          cooksCutoff = FALSE, 
          tidy = TRUE) %>%
  as_tibble() %>%
  dplyr::rename(OTU = row) #annoyingly DESeq2 has rename, too
mac_dds_res_lfc1
# A tibble: 9,700 × 7
   OTU       baseMean log2FoldChange lfcSE  stat    pvalue      padj
   <chr>        <dbl>          <dbl> <dbl> <dbl>     <dbl>     <dbl>
 1 Otu000001   566.           2.22   0.114 10.7  7.54e- 27 1.11e- 25
 2 Otu000002   402.           4.89   0.300 13.0  2.14e- 38 4.52e- 37
 3 Otu000003    65.2          0.606  0.102  0    1   e+  0 1   e+  0
 4 Otu000004    40.4          1.47   0.215  2.16 3.05e-  2 1.25e-  1
 5 Otu000005    40.5          1.35   0.185  1.91 5.64e-  2 2.21e-  1
 6 Otu000006   282.           6.61   0.250 22.4  3.40e-111 3.91e-109
 7 Otu000007    16.0         -0.206  0.243  0    1   e+  0 1   e+  0
 8 Otu000008     4.07        -2.50   0.228 -6.57 5.14e- 11 4.39e- 10
 9 Otu000009   179.           4.38   0.189 17.9  1.46e- 71 6.64e- 70
10 Otu000010    96.6          0.0947 0.172  0    1   e+  0 1   e+  0
# ℹ 9,690 more rows

There are many OTUs (1,048) with log fold change values significantly beyond 1.

mac_dds_res_lfc1 %>% dplyr::count(padj < 0.001)
# A tibble: 3 × 2
  `padj < 0.001`     n
  <lgl>          <int>
1 FALSE           4701
2 TRUE            1048
3 NA              3951

Exploring the effect of log fold change threshold on the number of significant OTUs

Although our analysis has reduced the number of OTUs considerably (from over 300,000 to about 1,000), sometimes we want to be even more selective. For example, we may want to focus on the 100 most important OTUs. The heatmap visualization we will produce later in this tutorial works best with about 30 OTUs.

Let’s explore the effect of the lcfThreshold on the number of OTUs that are identified as having log fold change significantly beyond the threshold. Of course, we expect the number of OTUs to decrease as the threshold increases.

The following code calculates the DESeq2 results for different log fold change thresholds between 0 and 10. We use a loop to perform this repetitive computation (101 different results calculations).

lfc_thresh <- seq(0,10, by = 0.1)
sig_count <- rep(NA, length(lfc_thresh))

for( i in 1:length(lfc_thresh)  ){
  lfct <- lfc_thresh[i]
  mac_dds_res_lfct <- mac_dds %>% 
  results(lfcThreshold = lfct,
          altHypothesis = "greaterAbs", 
          cooksCutoff = FALSE, 
          tidy = TRUE) %>%
  as_tibble() %>%
  dplyr::rename(OTU = row)
  sc <- mac_dds_res_lfct %>% 
    dplyr::count(padj < 0.001) %>%
    filter(`padj < 0.001`)
  if( dim(sc)[1] > 0){ sig_count[i] <- sc$n}
}

# create a tibble to hold the results of the analysis
lfc_thresh_count <- tibble(lfc_thresh = lfc_thresh, sig_count = sig_count)

We’ll plot the results, using a log scale for the vertical axis. Note that the horizontal axis is already using a log scale in some sense.

ggplot(data = lfc_thresh_count, mapping = aes(x = lfc_thresh, y = sig_count)) + 
  geom_point() +
  scale_y_continuous(trans = 'log2') +
  xlab("Log fold change threshold") +
  ylab("Number of significant OTUs") +
  theme_minimal()

We can see, for example, that if we use a log fold chance threshold of 7.4, there will be about 30 significant OTUs. Since this is a reasonable number for plotting, we will adopt a log fold change of 7.4 as our threshold for the rest of this analysis.

mac_dds_res_lfc7_4 <- mac_dds %>% 
  results(lfcThreshold = 7.4,
          altHypothesis = "greaterAbs", 
          cooksCutoff = FALSE, 
          tidy = TRUE) %>%
  as_tibble() %>%
  dplyr::rename(OTU = row) #annoyingly DESeq2 has rename, too
mac_dds_res_lfc7_4
# A tibble: 9,700 × 7
   OTU       baseMean log2FoldChange lfcSE  stat pvalue  padj
   <chr>        <dbl>          <dbl> <dbl> <dbl>  <dbl> <dbl>
 1 Otu000001   566.           2.22   0.114     0      1     1
 2 Otu000002   402.           4.89   0.300     0      1     1
 3 Otu000003    65.2          0.606  0.102     0      1     1
 4 Otu000004    40.4          1.47   0.215     0      1     1
 5 Otu000005    40.5          1.35   0.185     0      1     1
 6 Otu000006   282.           6.61   0.250     0      1     1
 7 Otu000007    16.0         -0.206  0.243     0      1     1
 8 Otu000008     4.07        -2.50   0.228     0      1    NA
 9 Otu000009   179.           4.38   0.189     0      1     1
10 Otu000010    96.6          0.0947 0.172     0      1     1
# ℹ 9,690 more rows
mac_dds_res_lfc7_4 %>% dplyr::count(padj < 0.001)
# A tibble: 3 × 2
  `padj < 0.001`     n
  <lgl>          <int>
1 FALSE            456
2 TRUE              29
3 NA              9215

Let’s filter the results table to only keep the significant OTUs (\(\alpha = 0.001\)).

mac_dds_res_lfc7_4_sig <- mac_dds_res_lfc7_4 %>% filter(padj < 0.001) 
mac_dds_res_lfc7_4_sig
# A tibble: 29 × 7
   OTU       baseMean log2FoldChange lfcSE  stat   pvalue     padj
   <chr>        <dbl>          <dbl> <dbl> <dbl>    <dbl>    <dbl>
 1 Otu000027    148.            9.23 0.371  4.94 7.99e- 7 1.84e- 5
 2 Otu000052    552.           -8.78 0.229 -6.03 1.63e- 9 6.10e- 8
 3 Otu000056    489.           -9.12 0.277 -6.19 6.20e-10 2.73e- 8
 4 Otu000069    460.           -9.36 0.362 -5.42 5.97e- 8 1.70e- 6
 5 Otu000077     90.5           9.90 0.402  6.22 4.92e-10 2.39e- 8
 6 Otu000105    413.           -9.64 0.250 -8.96 3.17e-19 7.69e-17
 7 Otu000139     50.1          -9.02 0.256 -6.33 2.53e-10 1.36e- 8
 8 Otu000164    128.           -9.25 0.236 -7.86 3.95e-15 4.79e-13
 9 Otu000254     48.8          -9.08 0.278 -6.05 1.44e- 9 5.82e- 8
10 Otu000303     84.1          -9.94 0.374 -6.80 1.04e-11 8.40e-10
# ℹ 19 more rows

Filtering data to retain selected OTUs

Now we will create versions of the data that just include the selected OTUs. These will be used for plotting later.

otus_to_keep <- mac_dds_res_lfc7_4_sig %>% pull(OTU)

macatawa_rel_select <- macatawa_rel %>%
  select(SampleID, all_of(otus_to_keep))

#use a join instead of a filter so these are in the same order as results table
mac_tax_select <- mac_tax %>% 
  right_join(mac_dds_res_lfc7_4_sig %>% select(OTU), by = "OTU")

Let’s make a histogram of the relative abundances of the remaining OTUs across all of the samples in our filtered data. This will help us set up our color scale for our plot.

Note that the largest relative abundance is about 0.15, and that there are a lot of very small relative abundances.

macatawa_rel_select_long <- macatawa_rel_select %>%
  pivot_longer(cols = starts_with("Otu"),
               names_to = "OTU",
               values_to = "Rel_Abundance")
ggplot(data = macatawa_rel_select_long, mapping = aes(x = Rel_Abundance)) +
  geom_histogram(boundary = 0, bins = 50) +
  scale_y_sqrt()

Heatmap

We have identified 29 important OTUs that exhibit large differences in abundances between the summer and winter seasons. Now let’s create a visualization that shows these differences. We will create a clustered heatmap using the pheatmap package. By annotating the axes of the plot we can also explore relationships between relative abundance and other environmental variables for these OTUs.

We need to define color schemes for our plot. Since we are not using ggplot, we will be doing this a bit differently than we have in the past.

For the heatmap itself, each cell on the plot corresponds to a single OTU in a single sample. The cells are arranged in a grid and colored according to relative abundance. We observed in the histogram above that there are many very small relative abundances. We will color any cell white that corresponds to a relative abundance below 0.001. Beyond that we will use a roughly even discrete color scale to indicate other abundances. We will use 7 levels between 0.001 and 0.16. Darker greys correspond with higher relative abundances.

#colors (greyscale) to use for heatmap itself
breaklist <- c(c(0,0.001),seq(1,7)/7*0.16)
colorlist <-c("white", "grey90","grey75", "grey60", "grey45", "grey30", "grey15", "grey0")
breaklist
[1] 0.00000000 0.00100000 0.02285714 0.04571429 0.06857143 0.09142857 0.11428571
[8] 0.13714286 0.16000000

Next we will create color scales for some of the variables we might choose to annotate our plots. If you are interested in using a variable that is not listed here, you can easily create your own custom color scale.

There are 6 phyla represented by the 29 OTUs we have selected.

mac_tax_select %>% 
  pull(Phylum) %>% 
  unique()
[1] "Proteobacteria"    "Actinobacteriota"  "Cyanobacteria"    
[4] "Bacteroidota"      "Verrucomicrobiota" "Planctomycetota"  

I created a color scale for these 6 phyla. If you are conducting a different analysis that results in different phyla, you will need to change this list or remove the phylum annotation altogether from the plot.

ann_colors = list(
  mTEC_cat = c(belowTC = "#7570b3", TCtoPC = "#1b9e77", abovePC ="#d95f02"),
  WaterTempSeason4 = c(SPR="#00AAB7", SUM="#7F9C00", FALL="darkorange", WINT="#C86DD7"),
  CalendarSeason = c(SPR="#00AAB7", SUM="#7F9C00", FALL="darkorange", WINT="#C86DD7"),
  SiteType = c(Lake="#1b9e77", Stream="#d95f02", WWF="magenta"),
  Site = c(YC="darkred", LS="red3", KP="red", DD="tomato", BLB="sienna1", GS="orange", WWF="wheat4", PCS="darkslategray3", NB="cyan", AL="midnightblue", MCR="slateblue3", SBK="steelblue2", PTC="mediumspringgreen", UM="chartreuse", NRD="darkgreen"),
  Month = c("January"="#9DA7EC", "February"="#65B3E5", "March"="#00BDD1", "April"="#00C1B6", "May"="#29C196", "June"="#6ABD74", "July"="#96B659", "August"="#B8AD50", "September"="#D2A362", "October"="#E49881", "November"="#ED90A4", "December"="#C699E7"),
  Year = c("2016"="#00AAB7", "2017"="#7F9C00", "2018"="darkorange", "2019"="#C86DD7", "2020"="olivedrab1", "2021"="lavender", "2022"="burlywood", "2023"="mediumpurple4"),
  BaseflowCondition_Locmin = c(BASE="#1b9e77", QUICK="#d95f02"),
  BaseflowCondition_Eckhardt = c(BASE="#1b9e77", QUICK="#d95f02"),
  Phylum = c(Actinobacteriota="#440154ff", Bacteroidota = "darkmagenta", Cyanobacteria = "#95d840ff", Planctomycetota = "#33638dff", Proteobacteria ="#1f968bff", Verrucomicrobiota = "#fde725ff" )
)

Next we select the variables we will use to annotate the columns of the plot (samples). We can use any categorical variable for this.

# data frame for column annotation
mac_col_annotate <- mac_meta %>% 
  select(SampleID, WaterTempSeason4, Site, Month, Year, BaseflowCondition_Eckhardt) %>%
  column_to_rownames("SampleID")

Then we create a tibble for our row (OTU) annotation. In this case we will only use phylum. We also create a vector that contains labels for the rows. We will construct labels that list both the OTU label and the family that the OTU belongs to. You could easily modify the code to label the rows using, for example, OTU and order.

#data frame for row annotation
mac_row_annotate <- mac_tax_select %>%
  select(OTU, Phylum) %>%
  column_to_rownames("OTU")

# labels for rows
row_labels <- mac_tax_select %>% mutate(row_label = paste(OTU, Family, sep="_")) %>% pull(row_label)

Our final setup step before plotting is to create a matrix (rather than a tibble) of relative abundances. This is required when using the pheatmap function.

# heat map function expects a matrix, let's create it
otu_mat <- macatawa_rel_select %>% 
  column_to_rownames("SampleID") %>%
  as.matrix() %>% t()

Let’s create the plot! (You will need to install the pheatmap library if you have not already done so.) We will save the plot as a pdf in our outputs folder.

Samples and OTUs that are similar to each other (with respect to variation in relative abundances) are positioned near each other along the plot axes (clustering). The plot itself is a grid with each cell colored according to the corresponding relative abundance.

The columns (samples) are annotated above the main plot according to the variables and color scales we chose. We can use the annotation to identify patterns that relate relative abundances for the selected OTUs to variables in the metadata. We can clearly see the relationship between relative abundance and water temperature season for these OTUs. Can you describe this pattern? Are there other patterns that you see?

library(pheatmap)
# create the heat map
heatmap <- pheatmap(otu_mat, color = colorlist, breaks = breaklist,
         annotation_col = mac_col_annotate, annotation_row = mac_row_annotate, annotation_colors = ann_colors,
         labels_row = row_labels,
         main = "Heatmap of Relative Abundance. OTUs chosen by Water Temperature Season (Winter vs. Summer) DESeq2 results\n Lake Sites Only\n Log2 Fold Change = Beyond 7.4 (Significance Level = 0.001)",
         fontsize_col = 5, drop_levels = TRUE)

heatmap

ggsave("outputs/heatmap_DESeq2_Lake_Winter_vs_Summer_WTS4_LFC_7_4.pdf", plot = heatmap, width=18, height=14)