3 OTU Clustering

Reproducible workflow for … In this workflow, ….

Hit the Hide Code button to hide the R code (shown by default).

Synopsis

In order to run the workflow, you either need to first run the DADA2 Workflow for 2018 High Temp samples and then the Data Preparation workflow or begin with the output files from the Data Preparation and PIME workflows. See the Data Availability page for complete details.

In this workflow we cluster ASVs into operational taxonomic units (OTUs) using the otu function from the kmer package. Briefly, the workflow consists of XXX steps.

  1. We begin with phyloseq objects, then generate and save fasta files for each object of interest. While we’re at we also save ASV, taxonomy, and sample metadata tables for each object.
  2. Cluster OTUs using otu. There are several parameters we can set to control how OTUs are picked including the threshold (value between 0 and 1 giving the OTU identity cutoff) and the method (central, centroid, farthest). The output will be a table that maps ASVs to OTUs.
  3. Modify the results to make a lookup table so we can select the ASVs that are representative of each OTU.
  4. Make a new OTU table that adds the values of any redundant ASVs. For example, if ASV1 and ASV2 are now collectively OTU10, we need OTU10 in the table to be the sum of the two ASVs.
  5. Make OTU taxonomy tables.
  6. Create phyloseq objects.

Here we perform OTU clustering for the 16S rRNA FULL data set and the merged set but the code will work for any number of phyloseq objects.

# LOAD ONLY REMOVE WHEN WORKFLOW FINISHED
remove(list = ls())
load("page_build/trepo/otu_wf.rdata")

1. Generate FASTA File

ps_list <- c("ssu_ps_work_merge", "ssu_ps_work")
for (i in ps_list) {
  ## Get ASV data
  tmp_get <- get(i)
  tmp_asv <- data.frame(t(otu_table(tmp_get)))
  tmp_asv$total <- rowSums(tmp_asv)
  tmp_asv <- tmp_asv[order(tmp_asv$total, decreasing = TRUE),]
  tmp_asv$total <- NULL
  tmp_asv <- tmp_asv %>% tibble::rownames_to_column("ASV_ID")
  tmp_asv <- tmp_asv %>% tidyr::separate(ASV_ID, c(NA, "tmp_value"),
                                         sep = "ASV", remove = FALSE)
  tmp_asv$tmp_value <- as.numeric(as.character(tmp_asv$tmp_value))
  tmp_asv <- tmp_asv[order(tmp_asv$tmp_value, decreasing = FALSE), ]
  tmp_asv$tmp_value <- NULL

  ## Get TAX data
  tmp_tax <- data.frame(tax_table(tmp_get))
  tmp_tax$ASV_ID <- NULL
  tmp_tax <- tmp_tax %>% tibble::rownames_to_column("ASV_ID")
  tmp_tax <- tmp_tax[base::match(tmp_asv$ASV_ID, tmp_tax$ASV_ID), ]
  ## Fasta file
  tmp_fasta_df <- tmp_tax[, c(1,8)] %>% tibble::remove_rownames()
  tmp_fasta <- dataframe2fas(tmp_fasta_df)

  ## NAMING
  tmp_asv_name <- purrr::map_chr(i, ~ paste0(., "_asv"))
  assign(tmp_asv_name, tmp_asv)
  tmp_tax_name <- purrr::map_chr(i, ~ paste0(., "_tax"))
  assign(tmp_tax_name, tmp_tax)
  tmp_fasta_name <- purrr::map_chr(i, ~ paste0(., ".fasta"))
  assign(tmp_fasta_name, tmp_fasta)
  tmp_path <- file.path("files/trepo/otu/tables/")

  write.fasta(tmp_fasta, paste(tmp_path, tmp_fasta_name, sep = ""))
  write.table(tmp_asv, paste(tmp_path, tmp_asv_name, ".txt", sep = ""),
            quote = FALSE, sep = "\t", row.names = FALSE)
  write.table(tmp_tax, paste(tmp_path, tmp_tax_name, ".txt", sep = ""),
            quote = FALSE, sep = "\t", row.names = FALSE)
  rm(list = ls(pattern = "tmp_"))
  rm(list = ls(pattern = ".fasta"))
}

2. Cluster OTUs

Code for clustering OTUs is based on the kmer-vignette.Important paramets are threshold (value between 0 and 1 giving the OTU identity cutoff) and method (central, centroid, farthest)

# THIS GIVES WEIRD RESULTS WHEN RUN IN LOOP
for (i in ps_list) {
  tmp_fasta_name <- purrr::map_chr(i, ~paste0(., ".fasta"))
  tmp_path <- file.path("files/trepo/otu/tables/")
  tmp_asvs <- read.dna(paste(tmp_path, tmp_fasta_name, sep = ""),
                       format = "fasta")
  # CANT use if different lengths
  #tmp_dna <- tmp_dna[, apply(tmp_dna, 2, function(v) !any(v == 0xf0))]
  tmp_otus <- kmer::otu(tmp_asvs, k = 5, threshold = 0.97,
                        method = "centroid", nstart = 20)
  tmp_df <- data.frame(tmp_otus)
  tmp_otu_name <- purrr::map_chr(i, ~paste0(., "_cluster_results"))
  assign(tmp_otu_name, tmp_df)
  rm(list = ls(pattern = "tmp_"))
}
ssu_ps_work_cluster_results
ssu_ps_work_merge_cluster_results

3. Create Lookup Table

for (i in ps_list) {
  tmp_df <- get(purrr::map_chr(i, ~paste0(., "_cluster_results")))
  ## Make lookup table from results
  tmp_df <- tmp_df %>% tibble::rownames_to_column("ASV_ID")
  tmp_df[[2]] <- paste0("OTU", tmp_df[[2]])
  tmp_df <- tmp_df %>% dplyr::rename("OTU_ID" = 2)
  tmp_df$temp_rep <- tmp_df$ASV_ID
  tmp_df$ASV_ID <- tmp_df$ASV_ID %>% stringr::str_replace("\\*", "")
  tmp_df$temp_rep <- tmp_df$temp_rep %>% stringr::str_replace("\\*", "_rep")

  tmp_df <- tmp_df %>% tidyr::separate(temp_rep, c(NA, "is_asv_rep"), sep = "_", fill = "left")
  tmp_df[[3]][tmp_df[[3]] != "rep"] <- "FALSE"
  tmp_df[[3]][tmp_df[[3]] == "rep"] <- "TRUE"

  tmp_path <- file.path("files/trepo/otu/tables/")
  tmp_lookup_name <- purrr::map_chr(i, ~ paste0(., "_asv_to_otu_map"))
  assign(tmp_lookup_name, tmp_df)
  write.table(tmp_df, paste(tmp_path, tmp_lookup_name, ".txt", sep = ""),
            quote = FALSE, sep = "\t", row.names = FALSE)
  tmp_l_asv <- length(base::unique(tmp_df[[1]]))
  tmp_l_otu <- length(base::unique(tmp_df[[2]]))
  print(c(tmp_l_asv, tmp_l_otu))
  print(tmp_l_otu - tmp_l_asv)
  rm(list = ls(pattern = "tmp_"))
}
objects()

4. ASV to OTU Table

#https://riptutorial.com/data-table/example/13084/using--sd-and--sdcols

for (i in ps_list) {
  tmp_asv <- get(purrr::map_chr(i, ~ paste0(., "_asv")))
  tmp_asv_to_otu <- get(purrr::map_chr(i, ~ paste0(., "_asv_to_otu_map")))
  tmp_asv_otu <- dplyr::left_join(tmp_asv_to_otu, tmp_asv)
  tmp_asv_otu <- arrange(tmp_asv_otu, OTU_ID, desc(is_asv_rep))

  tmp_asv_otu <- data.table(tmp_asv_otu)
  tmp_cols <- which(sapply(tmp_asv_otu, is.numeric))
  tmp_otu <- tmp_asv_otu[order(OTU_ID), lapply(.SD, sum),
                             by = .(OTU_ID), .SDcols = tmp_cols]
  tmp_otu <- tmp_otu %>% tidyr::separate(OTU_ID, c(NA, "tmp_value"),
                                         sep = "OTU", remove = FALSE)
  tmp_otu$tmp_value <- as.numeric(as.character(tmp_otu$tmp_value))
  tmp_otu <- tmp_otu[order(tmp_otu$tmp_value, decreasing = FALSE), ]
  tmp_otu$tmp_value <- NULL

  tmp_otu <- tmp_otu %>% tibble::column_to_rownames("OTU_ID")
  tmp_otu_t <- transpose(tmp_otu)
  colnames(tmp_otu_t) <- rownames(tmp_otu)
  rownames(tmp_otu_t) <- colnames(tmp_otu)
  print(identical(colnames(tmp_otu), rownames(tmp_otu_t)))
  print(identical(colnames(tmp_otu_t), rownames(tmp_otu)))
  tmp_otu_t <- as.matrix(tmp_otu_t)

  tmp_otu_name <- purrr::map_chr(i, ~ paste0(., "_otu_tab"))
  assign(tmp_otu_name, tmp_otu_t)
  rm(list = ls(pattern = "tmp_"))
}

5. Make Taxonomy Files

rm(list = ls(pattern = "tmp_"))
rm(list = ls(pattern = "_otu_tax"))
for (i in ps_list) {
  tmp_lookup <- get(purrr::map_chr(i, ~ paste0(., "_asv_to_otu_map")))
  tmp_lookup <- tmp_lookup %>% dplyr::filter(is_asv_rep == "TRUE")
  tmp_tax <- get(purrr::map_chr(i, ~ paste0(., "_tax")))
  tmp_tax_merge <- dplyr::left_join(tmp_lookup, tmp_tax)
  tmp_tax_merge[, c(1,3)] <- list(NULL)
  tmp_tax_merge <- tmp_tax_merge %>% tidyr::separate(OTU_ID, c(NA, "tmp_value"),
                                         sep = "OTU", remove = FALSE)
  tmp_tax_merge$tmp_value <- as.numeric(as.character(tmp_tax_merge$tmp_value))
  tmp_tax_merge <- tmp_tax_merge[order(tmp_tax_merge$tmp_value, decreasing = FALSE), ]
  tmp_tax_merge$tmp_value <- NULL

  tmp_tax_merge <- tmp_tax_merge %>% tibble::remove_rownames()
  tmp_tax_merge <- tmp_tax_merge %>% tibble::column_to_rownames("OTU_ID")
  tmp_tax_merge <- as.matrix(tmp_tax_merge)

  tmp_tax_name <- purrr::map_chr(i, ~ paste0(., "_otu_tax"))
  assign(tmp_tax_name, tmp_tax_merge)
  rm(list = ls(pattern = "tmp_"))
}
objects1 <- objects()

6. Create Phyloseq Objects

Now we can create phyloseq objects for the new OTU data. This is a little tricky because in the original ps objects, the ASVs are named in order of total abundance. However, the new OTU designations are not named in this fashion.

for (i in ps_list) {
  tmp_otu <- get(purrr::map_chr(i, ~ paste0(., "_otu_tab")))
  tmp_tax <- get(purrr::map_chr(i, ~ paste0(., "_otu_tax")))
  tmp_samp <- data.frame(sample_data(get(i)))
  tmp_samp <- tmp_samp %>% dplyr::select(1:6)
# Reorder by OTU abundance
  tmp_otu <- data.frame(tmp_otu)
  tmp_otu <- tmp_otu  %>% select(order(-colSums(tmp_otu)))
  tmp_tax <- data.frame(tmp_tax)
  tmp_tax <- tmp_tax %>% tibble::rownames_to_column("TEMP") 
  tmp_tax$NAME <- tmp_tax$TEMP
  tmp_tax <- tmp_tax %>% tibble::column_to_rownames("TEMP") 
  tmp_tax <- tmp_tax[match(colnames(tmp_otu), tmp_tax$NAME),]
  print(identical(colnames(tmp_otu), row.names(tmp_tax)))
# Save original name
  tmp_org <- data.frame(colnames(tmp_otu))
  tmp_org <- tmp_org %>% dplyr::rename("OTU_ID" = 1)
  tmp_org_names <- purrr::map_chr(i, ~ paste0(., "_org_names"))
  assign(tmp_org_names, tmp_org)
# Rename to sequential  
  colnames(tmp_otu) <- c(paste0("OTU", 1:ncol(tmp_otu)))
  row.names(tmp_tax) <- c(paste0("OTU", 1:nrow(tmp_tax)))
  tmp_tax$NAME <- NULL
  print(identical(colnames(tmp_otu), row.names(tmp_tax)))
# MAke PS object & save  
  tmp_tax <- as.matrix(tmp_tax)
  tmp_otu <- as.matrix(tmp_otu)

  tmp_ps <- phyloseq(otu_table(tmp_otu, taxa_are_rows = FALSE),
                   sample_data(tmp_samp), tax_table(tmp_tax))
  # Adding tree reorganizes OTU order
  #tmp_tree <- rtree(ntaxa(tmp_ps), rooted = TRUE,
  #                         tip.label = taxa_names(tmp_ps))
  #tmp_ps <- merge_phyloseq(tmp_ps, sample_data, tmp_tree)
  tmp_ps_name <- purrr::map_chr(i, ~ paste0(., "_otu"))
  assign(tmp_ps_name, tmp_ps)
  print(identical(readcount(tmp_ps), readcount(get(i))))
# SAVE stuff  
  tmp_path <- file.path("files/trepo/otu/rdata/")
  saveRDS(tmp_ps, paste(tmp_path, tmp_ps_name, ".rds", sep = ""))
  tmp_path2 <- file.path("files/trepo/otu/tables/")
  write.table(tmp_org, paste(tmp_path2, tmp_org_names, ".txt", sep = ""), 
              quote = FALSE, sep = "\t", row.names = FALSE)
  rm(list = ls(pattern = "tmp_"))
}
objects2 <- objects()

OK, now we need to make sure that all of this fiddling with OTU names didn’t mess anything up. So we need to compare the following to make sure they are all identical:

  1. The original ASV taxa table.
  2. The original OTU taxa table.
  3. The modified OTU taxa table with the new OTU names ordered by OTU abundance.

Specifically, we will compare the lineage and the DNA sequence from all three data frames. If they are equal, we are good to go. First the full data set.

tmp_rn <- data.frame(row.names(t(otu_table(ssu_ps_work_otu))))  
tmp_bind <- cbind(ssu_ps_work_org_names, tmp_rn)    
tmp_bind <- tmp_bind %>% dplyr::rename("NEW_ID" = 2)    
temp_map_true <- ssu_ps_work_asv_to_otu_map[ssu_ps_work_asv_to_otu_map$is_asv_rep == 'TRUE',]   
tmp_join <- dplyr::left_join(temp_map_true, tmp_bind, keep = FALSE) 
ssu_ps_work_asv_to_otu_to_new_otu_map <- tmp_join   
rm(list = ls(pattern = "tmp_")) 
temp_map <- ssu_ps_work_asv_to_otu_to_new_otu_map   
temp_map[,3] <- NULL    
## ORIGINAL ASV 
temp_org_tax <- data.frame(tax_table(ssu_ps_work))  
temp_org_tax <- temp_org_tax[temp_org_tax$ASV_ID %in% temp_map$ASV_ID,]     
## ORIGINAL OTU 
temp_org_otu_tax <- data.frame(ssu_ps_work_otu_tax) 
temp_org_otu_tax <- temp_org_otu_tax %>% tibble::add_column(row.names(temp_org_otu_tax)) %>%    
                   dplyr::rename(., "OTU_ID" = 8)   
#RENAMED OTU    
temp_rn_otu_tax <- data.frame(tax_table(ssu_ps_work_otu))   
temp_rn_otu_tax <- temp_rn_otu_tax %>% tibble::add_column(row.names(temp_rn_otu_tax)) %>%   
                   dplyr::rename(., "OTU_ID" = 8)   
ssu_results <- data.frame() 
for (i in row.names(temp_map)) {    
      tmp_x <- temp_org_tax[temp_org_tax$ASV_ID %in% temp_map[i,1],]    
      tmp_y <- temp_org_otu_tax[temp_org_otu_tax$OTU_ID %in% temp_map[i,2],]    
      tmp_z <- temp_rn_otu_tax[temp_rn_otu_tax$OTU_ID %in% temp_map[i,3],]  
      check_true <- all_equal(tmp_x[, 1:7], tmp_z[, 1:7], tmp_y[, 1:7]) 
      ssu_results <- rbind(ssu_results, check_true) 
      rm(list = ls(pattern = "tmp_"))   
}   
rm(list = ls(pattern = "temp_"))    
[1] TRUE.
<0 rows> (or 0-length row.names)

And then the merged data.

tmp_rn <- data.frame(row.names(t(otu_table(ssu_ps_work_merge_otu))))    
tmp_bind <- cbind(ssu_ps_work_merge_org_names, tmp_rn)  
tmp_bind <- tmp_bind %>% dplyr::rename("NEW_ID" = 2)    
temp_map_true <- ssu_ps_work_merge_asv_to_otu_map[ssu_ps_work_merge_asv_to_otu_map$is_asv_rep == 'TRUE',]   
tmp_join <- dplyr::left_join(temp_map_true, tmp_bind, keep = FALSE) 
ssu_ps_work_merge_asv_to_otu_to_new_otu_map <- tmp_join 
rm(list = ls(pattern = "tmp_")) 
temp_map <- ssu_ps_work_merge_asv_to_otu_to_new_otu_map 
temp_map[,3] <- NULL    
## ORIGINAL ASV 
temp_org_tax <- data.frame(tax_table(ssu_ps_work_merge))    
temp_org_tax <- temp_org_tax[temp_org_tax$ASV_ID %in% temp_map$ASV_ID,]     
## ORIGINAL OTU 
temp_org_otu_tax <- data.frame(ssu_ps_work_merge_otu_tax)   
temp_org_otu_tax <- temp_org_otu_tax %>%    
                    tibble::add_column(row.names(temp_org_otu_tax)) %>% 
                    dplyr::rename(., "OTU_ID" = 8)  
#RENAMED OTU    
temp_rn_otu_tax <- data.frame(tax_table(ssu_ps_work_merge_otu)) 
temp_rn_otu_tax <- temp_rn_otu_tax %>%  
                   tibble::add_column(row.names(temp_rn_otu_tax)) %>%   
                   dplyr::rename(., "OTU_ID" = 8)   
its_results <- data.frame() 
for (i in row.names(temp_map)) {    
      tmp_x <- temp_org_tax[temp_org_tax$ASV_ID %in% temp_map[i,1],]    
      tmp_y <- temp_org_otu_tax[temp_org_otu_tax$OTU_ID %in% temp_map[i,2],]    
      tmp_z <- temp_rn_otu_tax[temp_rn_otu_tax$OTU_ID %in% temp_map[i,3],]  
      check_true <- all_equal(tmp_x[, 1:7], tmp_z[, 1:7], tmp_y[, 1:7]) 
      its_results <- rbind(its_results, check_true) 
      rm(list = ls(pattern = "tmp_"))   
}   
rm(list = ls(pattern = "temp_"))    
[1] TRUE.
<0 rows> (or 0-length row.names)

The last step is to add an OTU_ID column to the new ps objects so they are the same as their ASV counterparts.

tax_table(ssu_ps_work_otu) <- cbind(tax_table(ssu_ps_work_otu), 
                           rownames(tax_table(ssu_ps_work_otu)))    
colnames(tax_table(ssu_ps_work_otu)) <- c("Kingdom", "Phylum", "Class",     
                                          "Order", "Family", "Genus",   
                                          "OTU_SEQ", "OTU_ID")  
tax_table(ssu_ps_work_merge_otu) <- cbind(tax_table(ssu_ps_work_merge_otu), 
                           rownames(tax_table(ssu_ps_work_merge_otu)))  
colnames(tax_table(ssu_ps_work_merge_otu)) <- c("Kingdom", "Phylum", "Class",   
                                          "Order", "Family", "Genus",   
                                          "OTU_SEQ", "OTU_ID")  

7. Create Ampvis2 Objects

for (i in ps_list){
    tmp_ps <- get(purrr::map_chr(i, ~ paste0(., "_otu")))
    tmp_otu <- data.frame(t(otu_table(tmp_ps)))
    tmp_otu[] <- lapply(tmp_otu, as.numeric)
    tmp_otu <- as.matrix(tmp_otu)
    tmp_tax <- as.matrix(data.frame(tax_table(tmp_ps)))
    tmp_samples <- data.frame(sample_data(tmp_ps))
    tmp_amp <- merge_phyloseq(otu_table(tmp_otu, taxa_are_rows = TRUE),
                          tax_table(tmp_tax),
                          sample_data(tmp_samples))
    tmp_amp_name <- purrr::map_chr(i, ~ paste0(., "_ps_otu_amp"))
    assign(tmp_amp_name, tmp_amp)
    rm(list = ls(pattern = "tmp_"))
}
for (i in ps_list){
    tmp_ps <- get(purrr::map_chr(i, ~ paste0(., "_ps_otu_amp")))
    tmp_samp <- data.frame(sample_data(tmp_ps))
    tmp_asv  <- data.frame(otu_table(tmp_ps))
    tmp_asv <- tmp_asv %>% tibble::rownames_to_column("OTU")
    tmp_tax  <- data.frame(tax_table(tmp_ps))
    tmp_tax <- tmp_tax %>% tibble::rownames_to_column("OTU")
    tmp_tax$OTU_SEQ <- NULL
    #tmp_tax$OTU_ID <- tmp_tax$OTU
    colnames(tmp_tax)[colnames(tmp_tax) == "OTU_ID"] <- "Species"
    tmp_asv_tax <- left_join(tmp_asv, tmp_tax, by = "OTU")
    tmp_amp <- amp_load(tmp_asv_tax, metadata = tmp_samp, tree = tmp_ps@phy_tree)
    tmp_amp_name <- purrr::map_chr(i, ~ paste0(., "_otu_amp"))
    assign(tmp_amp_name, tmp_amp)
    tmp_path <- file.path("files/trepo/otu/rdata/")
    saveRDS(tmp_amp, paste(tmp_path, tmp_amp_name, ".rds", sep = ""))
    rm(list = ls(pattern = "tmp_"))
}
rm(list = ls(pattern = "_ps_otu_amp"))

Summary

The ssu_ps_work_merge ASV phyloseq object has 6457054 reads and 47725 ASVs. 
The ssu_ps_work_merge_otu OTU phyloseq object has 6457054 reads and 21910 OTUs. 
The ssu_ps_work ASV phyloseq object has 6457054 reads and 47725 ASVs. 
The ssu_ps_work_otu OTU phyloseq object has 6457054 reads and 21906 OTUs. 
save.image("page_build/trepo/otu_wf.rdata")

Source Code

The source code for this page can be accessed on GitHub by clicking this link.

References

Corrections

If you see mistakes or want to suggest changes, please create an issue on the source repository.

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://github.com/tropical-repo/web/, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".