Skip to contents

This guide explains how to perform some common microbial sequencing count data analysis tasks using tidytacos. We will illustrate those using a dataset with human microbiome samples from the upper respiratory tract (URT), taken from this paper by De Boeck et al. It contains nose as well as nasopharynx samples. Most samples were taken using a swab method, but a minority was taking with the aspirate method.

Philosophy of the package

A tidytacos object is simply a list of three tables:

  • counts: These are the counts of reads for each taxon (OTU/ASV/phylotype) in each sample. Each row represents such a read count.
  • samples: This table contains the sample metadata. Each row represents a sample.
  • taxa: This table contains the taxonomy and other metadata for the taxa. Each row represents a taxon.

The package is called tidytacos because each of the tables is tidy: every row represents an observation and every column a variable (more on data tidying can be found at https://r4ds.hadley.nz/data-tidy.html#sec-tidy-data). For this quick start guide, we assume you are familiar with the tidyverse (especially dplyr and ggplot).

The main differences with the popular phyloseq package are:

  • The abundance table of tidytacos is in long format, while the otu_table object of phyloseq is in wide (matrix) format. A tidy read count table is more compact for very sparse data (such as microbiome data), and also easier to handle and visualize within the tidyverse framework.
  • All tables in tidytacos are data frames (technically tibbles). In phyloseq, each table has its own special data type (i.e. the classes “otu_table”, “tax_table” and “sample_data”). This makes it sometimes difficult to access the data directly and do simple stuff with it. Also, the otu_table and tax_table classes are based on the matrix type, while the sample_data class is based on the dataframe type. This also makes data manipulation sometimes unstraightforward.
  • In a phyloseq otu table, sometimes the rows are the taxa and sometimes the columns. This can easily lead to errors.

Setting up

In case you haven’t installed tidytacos yet, it can be installed using devtools:

install.packages("devtools")
devtools::install_github("LebeerLab/tidytacos")

For this guide, we only need to load two packages: tidytacos (of course) and the tidyverse set of packages.

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.5.1      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(tidytacos)
#> 
#> Attaching package: 'tidytacos'
#> 
#> The following object is masked from 'package:dplyr':
#> 
#>     everything
#> 
#> The following object is masked from 'package:tidyr':
#> 
#>     everything

The first step will usually be the conversion our data into a usable tidytacos format. If, for example, you used DADA2 as explained here, you can hand off the data to tidytacos using from_dada(). You can also convert a phyloseq object to a tidytacos object using the from_phyloseq()

function. More options to import and convert your data can be found here.

A tidytacos object is read and stored as three sparse tables (counts-, taxa- and samples.csv). The sample table needs atleast a sample_id column, the taxa table needs a taxon_id column with atleast one other rank name column and the counts table needs a sample_id, taxon_id and counts column. The taxa table of a default tidytacos object has the domain, phylum, class, order, family, genus and species columns. Species here has a special place as it is often ignored by some functions unless specified, due to the low resolution of short read 16S rRNA gene sequencing data.

To read in existing data from a folder called ‘my_data’ you would run:

my_path <- system.file("extdata", "tidytacos", "leaf", package = "tidytacos")
# my_path <- "local/path/my_data"
taco <- read_tidytacos(my_path)

Where you replace my_path with an actual path to your data. The path used here uses data in our package.

To write your data to a folder called “my_data_filtered” you can run:

taco %>% write_tidytacos("my_data_filtered")

However, our example dataset is available in the tidytacos package and doesn’t need to be imported or converted. It is called “urt” and we start by inspecting the samples table:

glimpse(urt$samples)
#> Rows: 217
#> Columns: 9
#> $ run         <chr> "20161207_ilke_urt100", "20161207_ilke_urt100", "20161207_…
#> $ condition   <chr> "CON", "CON", "CON", "CON", "CON", "CON", "CON", "CON", "C…
#> $ participant <chr> "CON100", "CON100", "CON10", "CON10", "CON11", "CON11", "C…
#> $ location    <chr> "NF", "N", "NF", "NF", "NF", "N", "NF", "NF", "N", "NF", "…
#> $ method      <chr> "S", "S", "A", "S", "S", "S", "S", "S", "S", "S", "S", "S"…
#> $ plate       <dbl> 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ passes_qc   <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE…
#> $ sample      <chr> "CON100-NF-S", "CON100-N-S", "CON10-NF-A", "CON10-NF-S", "…
#> $ sample_id   <chr> "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10…

We then have a quick look at the total number of samples, ASVs, and reads in the tidytacos object:

tacosum(urt)
#> n_samples    n_taxa   n_reads 
#>       217      1957   3873478

Making a stacked barplot of a subset of samples

We can very easily create a plot to explore a subset of our samples (e.g., only nose samples taken with the swab method) in the following way:

urt %>%
  filter_samples(location == "N", method == "S") %>%
  tacoplot_stack()

The filter_samples function does what it says: filtering samples. It will also delete taxa from the taxa table that have zero total reads in the remaining samples. The tacoplot_stack function returns a nice stacked bar plot visualization of the most abundant taxa in our samples.

Subsetting dataset

Our next question for this dataset is to what extent nose and nasopharynx are linked from a microbiological point of view. To get an idea we can first visualize this:

urt_s <- urt %>% filter_samples(method == "S")
tacoplot_stack(urt_s)+
  geom_point(aes(y=-0.02,color=location))

tacoplot_stack(urt_s, x = location) +
  facet_wrap(~ participant, nrow = 10)
#> Warning in tacoplot_stack(urt_s, x = location): Sample labels not unique,
#> samples are aggregated.

First, we make sure to include only swab samples. We can then visualize the data in stacked bar plots. The tacoplot functions consist of ggplot function calls and therefore geoms and other ggplot features can be added.

Alpha diversity

To explore alpha diversity, let’s create a rarefied version of the dataset:

urt_rar <- urt %>%
  add_total_count() %>%
  filter_samples(total_count >= 2000) %>%
  rarefy(2000) %>%
  add_alphas()

We can visualize alpha diversities of nose versus the nasopharynx:

The add_total_count function will add total sample read numbers to the sample table.

The rarefy function will randomly subsample all samples n times. It only works if the read count of each sample equals or exceeds n. For determining ASV richness, we chose to rarefy first, but this may depend on your data.

The add_alpha function can be used to add several metrics of alpha diversity to the sample table or in this cas we use the add_alphas to add all the currently supported metrics.

Next we will plot the resulting alpha diversities and group per location of sampling:

urt_rar %>%
  tacoplot_alphas(location)
#> Warning: Removed 4 rows containing non-finite outside the scale range
#> (`stat_ydensity()`).
#> Warning: Removed 4 rows containing missing values or values outside the scale range
#> (`geom_point()`).

The tacoplot_alphas function will use any alpha metric that has been added by add_alpha(s) which it can find in the sample table.

Principal coordinates analysis

We would like to address the differences between nose and nasopharynx. We’re also more interested in genera than ASVs. A PCA might offer insight:

urt_genus <- urt %>%
  filter_samples(method == "S") %>%
  aggregate_taxa(rank = "genus")

tacoplot_ord_ly(urt_genus,location, samplenames = sample, dim = 3)
#> Warning in remove_empty_samples(.): Removed 2 empty samples.

The aggregate_taxa function merges all rows of the taxa table together at a specified taxonomic level, in this case the genus level. As for all tidytacos functions, all other tables in the tidytacos object are adjusted accordingly.

The tacoplot_ord_ly function will determine relative abundances of taxa in the samples, and will then use Bray-Curtis dissimilarities to ordinate samples in a 2-dimensional space based on their taxonomic composition. The plotly “_ly” addition makes the plot interactive, which is really nice for exploratory work. This also works for other plot functions.

Relation between community composition and variables

A logical next question is to what extent the niche (nose versus nasopharynx) determines the community composition variability. Let’s not forget that everyone has their unique microbiome and include the variable “participant” in the model.

perform_adonis(urt_genus, c("participant", "location"), by="margin")
#> Permutation test for adonis under reduced model
#> Marginal effects of terms
#> Permutation: free
#> Number of permutations: 999
#> 
#> adonis2(formula = as.formula(paste("counts_matrix", formula_RHS, sep = " ~ ")), data = metadata, permutations = permutations, by = "margin")
#>              Df SumOfSqs      R2       F Pr(>F)    
#> participant  97   31.076 0.64174  1.8848  0.001 ***
#> location      1    3.066 0.06331 18.0372  0.001 ***
#> Residual     81   13.768 0.28432                   
#> Total       179   48.424 1.00000                   
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

The perform_adonis function will perform a PERMutational ANOVA to determine the effect of sample variables on the Bray-Curtis dissimilarities of the communities. The result shows that participant is a really important contributor to community composition (R squared = 0.65). Furthermore, there are consistent significant differences between the communities of nose and nasopharynx (R squared = 0.06).

Differential abundance analysis

Next, we would like to know which of the 20 most abundant genera, are significantly more abundant in the nasopharynx compared to the nose.

urt_genus <- urt_genus %>% add_codifab(location, max_taxa = 40)
urt_genus$taxon_pairs <- filter(urt_genus$taxon_pairs, wilcox_p < 0.05)
tacoplot_codifab(urt_genus, NF_vs_N)

The add_codifab function will add a table called taxon_pairs to the tidytacos object, with for each pair of a taxon and a reference taxon, the differential abundance of the taxon between the two conditions (with respect to the reference taxon).

The codifab_plot function returns a plot to visualize differential abundance of taxa between conditions, compared to all other taxa as references. We can observe that Haemophilus is most likely to be typical for the Nasopharynx, whereas Anaerococcus is most likely to be typical for the nose.

Of note, there are many differential abundance analysis methods out there and none of them are perfect. Interpret your results with care.