|
| 1 | +#!/usr/bin/env Rscript |
| 2 | + |
| 3 | +# compare_sequences.r |
| 4 | + |
| 5 | +# Get params and files from the command line |
| 6 | +args <- commandArgs(trailingOnly=TRUE) |
| 7 | +alignmentFILE <- args[1] # expected columns: "query+target+ql+tl+qilo+qihi+tilo+tihi+gaps+mism+qstrand", see https://torognes.github.io/vsearch/misc/vsearch-userfields.7.html |
| 8 | +obsabundFILE <- args[2] # observed abundance* |
| 9 | +expabundFILE <- args[3] # expected abundance* |
| 10 | +prefix <- args[4] # prefix string for output files |
| 11 | +similarity_threshold <- as.numeric(args[5]) # similarity threshold for alignmentFILE |
| 12 | +query_or_target <- args[6] # use mismatches to query or to target? |
| 13 | +# tab-separated file with header: first column with sequences name, following one or many columns (=samples) with numeric values (=abundance), only presence (>0)/absence are used here |
| 14 | + |
| 15 | +# Input |
| 16 | +if (file.exists(expabundFILE)) { |
| 17 | + print(paste("Compare",obsabundFILE,"to",expabundFILE)) |
| 18 | +} else { |
| 19 | + print(paste("Compare",obsabundFILE,"to all sequences")) |
| 20 | +} |
| 21 | + |
| 22 | +# PREPARE |
| 23 | + |
| 24 | +# Read sequence alignment table |
| 25 | +alignment = read.table( alignmentFILE, header = FALSE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE) |
| 26 | +colnames(alignment) <- c("query","target","ql","tl","qilo","qihi","tilo","tihi","gaps","mism","qstrand") |
| 27 | + |
| 28 | +# Read pipeline's ASV abundance table (either from QIIME2 [skipping first line] or from previous steps) |
| 29 | +if (grepl("^# Constructed from biom file", readLines(obsabundFILE, n = 1))) { |
| 30 | + observed = read.table( obsabundFILE, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE, comment.char = "", skip=1 ) |
| 31 | +} else { |
| 32 | + observed = read.table( obsabundFILE, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE, comment.char = "" ) |
| 33 | +} |
| 34 | +colnames(observed)[1] <- "ID" |
| 35 | + |
| 36 | +# select available samples |
| 37 | +observed_samples <- colnames(observed)[2:ncol(observed)] |
| 38 | +print(paste( "Observed samples:", paste(observed_samples,collapse=","))) |
| 39 | +SAMPLES <- observed_samples |
| 40 | + |
| 41 | +# Read expected abundance table if it exists |
| 42 | +if (file.exists(expabundFILE)) { |
| 43 | + exp = read.table( expabundFILE, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, strip.white = TRUE) |
| 44 | + colnames(exp)[1] <- "ID" |
| 45 | + # extract samples to analyse |
| 46 | + exp_samples <- colnames(exp)[2:ncol(exp)] |
| 47 | + print(paste( "Expected samples:", paste( exp_samples ,collapse=","))) |
| 48 | + SAMPLES <- exp_samples[exp_samples %in% observed_samples] |
| 49 | + print(paste( "Investigate samples:", paste( SAMPLES ,collapse=","))) |
| 50 | + # warn if samples are missing due to incomplete overlap |
| 51 | + if ( !all(observed_samples %in% SAMPLES) ) { |
| 52 | + print(paste("WARN - sample(s) not expected (file:",expabundFILE,") and omitted:",paste(observed_samples[!observed_samples %in% SAMPLES], collapse="," ))) |
| 53 | + } |
| 54 | + if ( !all(exp_samples %in% SAMPLES) ) { |
| 55 | + print(paste("WARN - sample(s) not observed and omitted:",paste(exp_samples[!exp_samples %in% SAMPLES], collapse="," ))) |
| 56 | + } |
| 57 | + # warn about missing sequences |
| 58 | + alignment_target <- alignment$target[alignment$target != "*"] |
| 59 | + if( !all(alignment_target %in% exp$ID) ) { |
| 60 | + print(paste("WARN - Some expected sequences in alignment results are not in",expabundFILE,". The following are missing:",paste( unique(alignment_target[!alignment_target %in% exp$ID]),collapse=","))) |
| 61 | + } |
| 62 | + if( !all(exp$ID %in% alignment$target) ) { |
| 63 | + print(paste("WARN - Some expected sequences of",expabundFILE,"are not in the alignment results. The following are missing:",paste( unique(exp$ID[!exp$ID %in% alignment$target]),collapse=","))) |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +# check if there are any samples to analyse |
| 68 | +if (length(SAMPLES) == 0) { |
| 69 | + stop("ERROR - Found no samples to investigate") |
| 70 | +} |
| 71 | + |
| 72 | +# Investigate alignment, required columns: query,target,ql,tl,qilo,qihi,tilo,tihi,gaps,mism |
| 73 | +alignment$qterminalgaps <- alignment$ql - (abs(alignment$qihi-alignment$qilo)+1) |
| 74 | +alignment$qmism <- alignment$gaps + alignment$qterminalgaps + alignment$mism |
| 75 | +alignment$tterminalgaps <- alignment$tl - (abs(alignment$tihi-alignment$tilo)+1) |
| 76 | +alignment$tmism <- alignment$gaps + alignment$tterminalgaps + alignment$mism |
| 77 | +if( query_or_target=="query" ) { |
| 78 | + alignment$mismatch_final <- alignment$qmism |
| 79 | +} else if( query_or_target=="target" ) { |
| 80 | + alignment$mismatch_final <- alignment$tmism |
| 81 | +} else if( query_or_target=="alignment" ) { |
| 82 | + alignment$mismatch_final <- alignment$gaps + alignment$mism |
| 83 | +} else { |
| 84 | + stop( paste("ERROR -",query_or_target,"is not valid (valid: query,target,alignment)") ) |
| 85 | +} |
| 86 | + |
| 87 | +# order for reproducibility |
| 88 | +alignment <- alignment[order(alignment$query, alignment$target),] |
| 89 | + |
| 90 | +# write file |
| 91 | +outfile <- paste0(prefix,"nucleotide-differences.tsv") |
| 92 | +print(paste("write",outfile)) |
| 93 | +write.table(alignment, file = outfile, row.names = FALSE, col.names = TRUE, quote = FALSE, na = '', sep="\t") |
| 94 | + |
| 95 | +# ANALYSE |
| 96 | + |
| 97 | +# select matches above threshold |
| 98 | +matches_above_threshold <- alignment[alignment$target !="*",] |
| 99 | + |
| 100 | +for (sample in SAMPLES) { |
| 101 | + if( file.exists(expabundFILE) ) { |
| 102 | + # filter expected sequences in that sample (matches_above_threshold$target) with expected abundances (abundance > 0) of exp$ID |
| 103 | + keep_cols <- c("ID",sample) |
| 104 | + print(paste("Found",nrow(exp),"expected sequences overall in",expabundFILE)) |
| 105 | + s_exp <- subset(exp, select = keep_cols) |
| 106 | + s_exp = s_exp[s_exp[,2] > 0,] |
| 107 | + print(paste("Found",nrow(s_exp),"expected sequences in sample", sample,"in",expabundFILE)) |
| 108 | + s_matches <- matches_above_threshold[matches_above_threshold$target %in% s_exp$ID,] |
| 109 | + } else { |
| 110 | + s_matches <- matches_above_threshold |
| 111 | + } |
| 112 | + |
| 113 | + # sample will be skipped if s_matches has no data |
| 114 | + if( nrow(s_matches)==0 ) { |
| 115 | + print(paste("WARN - Skipping sample",sample,"because found no matches to expected sequences")) |
| 116 | + next |
| 117 | + } else { |
| 118 | + print(paste("Found",length(unique(s_matches$query)),"to be expected sequences of",length(unique(matches_above_threshold$query)),"in sample",sample)) |
| 119 | + print(paste("Found",length(unique(s_matches$target)),"matches of",length(unique(matches_above_threshold$target)),"total in sample",sample)) |
| 120 | + } |
| 121 | + |
| 122 | + # select best match (sort by mismatches and retain only first unique entry) |
| 123 | + s_matches <- s_matches[order(s_matches$mismatch_final, as.numeric(s_matches$mismatch_final)), ] |
| 124 | + s_matches <- s_matches[!duplicated(s_matches$query), ] |
| 125 | + print(paste("Found",length(unique(s_matches$target)),"best matches of",length(unique(matches_above_threshold$target)),"total in sample",sample)) |
| 126 | + |
| 127 | + # filter for ASVs in that sample |
| 128 | + keep_cols <- c("ID",sample) |
| 129 | + s_observed <- subset(observed, select = keep_cols) |
| 130 | + # keep only observed entries in that sample (abundance > 0) |
| 131 | + s_observed = s_observed[s_observed[,2] > 0,] |
| 132 | + print(paste("Found",nrow(s_observed),"of",nrow(observed),"observed sequences in sample",sample)) |
| 133 | + |
| 134 | + # filter alignment result by observed |
| 135 | + s_matches <- s_matches[s_matches$query %in% s_observed$ID,] |
| 136 | + print(paste("Found",nrow(s_matches),"observed sequences with match in sample",sample)) |
| 137 | + s_below_threshold <- s_observed[!s_observed$ID %in% s_matches$query,] |
| 138 | + print(paste("Found",nrow(s_below_threshold),"observed sequences without match in sample",sample)) |
| 139 | + |
| 140 | + # make barplot |
| 141 | + df <- as.data.frame( table(s_matches$mismatch_final) ) |
| 142 | + df <- rbind(df, data.frame(Var1 = paste0(">=",(1-similarity_threshold)*100,"%"), Freq = length(unique(s_below_threshold$ID)))) |
| 143 | + colnames(df) <- c("Mismatches to expected sequences","Number of sequences") |
| 144 | + |
| 145 | + outfile <- paste0(prefix,sample,"_nucleotide-distance_barplot.svg") |
| 146 | + print(paste("write",outfile)) |
| 147 | + svg(outfile, height = 4, width = 5) |
| 148 | + barplot(df[,2], |
| 149 | + names.arg=df[,1], |
| 150 | + main=paste("Sample",sample,"with",length(unique(s_observed$ID)),"observed sequences\n(",length(unique(s_matches$query)),"sequences above",similarity_threshold*100,"% similarity)"), |
| 151 | + xlab="Mismatches to expected sequences", |
| 152 | + ylab="Number of sequences") |
| 153 | + invisible(dev.off()) |
| 154 | + |
| 155 | + outfile <- paste0(prefix,sample,"_nucleotide-distance_barplot.png") |
| 156 | + print(paste("write",outfile)) |
| 157 | + png(outfile, height = 400, width = 500) |
| 158 | + barplot(df[,2], |
| 159 | + names.arg=df[,1], |
| 160 | + main=paste("Sample",sample,"with",length(unique(s_observed$ID)),"observed sequences\n(",length(unique(s_matches$query)),"sequences above",similarity_threshold*100,"% similarity)"), |
| 161 | + xlab="Mismatches to expected sequences", |
| 162 | + ylab="Number of sequences") |
| 163 | + invisible(dev.off()) |
| 164 | + |
| 165 | + # write file |
| 166 | + outfile <- paste0(prefix,sample,"_nucleotide-differences_per-sample.tsv") |
| 167 | + print(paste("write",outfile)) |
| 168 | + write.table(df, file = outfile, row.names = FALSE, col.names = TRUE, quote = FALSE, na = '', sep="\t") |
| 169 | +} |
0 commit comments