#!/usr/bin/env python """Generate cluster quality data.""" # Adapted form script by Kristoffer Sahlin for # isONclust: https://github.com/ksahlin/isONclust import argparse from collections import defaultdict import math from pathlib import Path import matplotlib from matplotlib import pyplot as plt from matplotlib.backends.backend_pdf import PdfPages import pandas as pd import pysam from sklearn.metrics.cluster import adjusted_rand_score, completeness_score,\ homogeneity_score, v_measure_score matplotlib.use('Agg') def parse_inferred_clusters_tsv(tsv_file, args): """parse_inferred_clusters_tsv.""" infile = open(tsv_file, "r") infile.readline() clusters = {} for line in infile: cluster_id, _, read_acc = line.strip().split("\t") if args.simulated: read_acc = "_".join([item for item in read_acc.split("_")[:-1]]) elif args.ont: read_acc = read_acc.split(" ")[0] elif args.modified_ont: read_acc = read_acc else: read_acc = read_acc.split("_strand")[0] clusters[read_acc] = int(cluster_id) return clusters def parse_true_clusters(ref_file): """parse_true_clusters.""" classes = defaultdict(dict) ref_id_to_chrom = {} alignment_counter = defaultdict(int) prev_chrom = -1 curr_class_id = -1 prev_class_stop = -1 prev_read_id = "" unique_reads = set() unclassified = 0 for read in ref_file.fetch(until_eof=True): unique_reads.add(read.query_name) if read.is_unmapped: unclassified += 1 continue # deal with supplementary alignments!! if read.is_secondary or read.is_supplementary: continue # print(read.query_name, read.flag) assert prev_read_id != read.query_name chrom = read.reference_name if chrom != prev_chrom: curr_class_id += 1 classes[read.query_name] = curr_class_id prev_chrom = chrom prev_class_stop = read.reference_end else: read_ref_start = read.reference_start if read_ref_start > prev_class_stop: curr_class_id += 1 classes[read.query_name] = curr_class_id prev_class_stop = read.reference_end else: classes[read.query_name] = curr_class_id prev_class_stop = max(read.reference_end, prev_class_stop) prev_read_id = read.query_name # classes[read.query_name] = int(read.reference_id) # chrom ref_id_to_chrom[int(read.reference_id)] = chrom alignment_counter[int(read.reference_id)] += 1 # if chrom not in class_ranges: # class_ranges[chrom] = {} # print(chrom, read_ref_start, read_ref_end) # for start, stop in class_ranges[chrom]: # if start <= read_ref_start and read_ref_end <= stop: # # entirly within # elif # classes[read.query_name] = #read.reference_name.split("|")[0] # #"|".join(read.reference_name.split("|")[:2]) return classes, len(unique_reads), unclassified def parse_true_clusters_simulated(ref_file): """parse_true_clusters_simulated.""" classes = defaultdict(dict) for read in ref_file.fetch(until_eof=True): # by gene id classes[read.query_name] = read.reference_name.split("|")[0] # by transcript id # classes[read.query_name] = read.reference_name.split("|")[1] return classes def compute_V_measure(clusters, classes): """compute_V_measure.""" class_list, cluster_list = [], [] # not_found_id = 1000000 clustered_but_unaligned = 0 for read in clusters: if read in classes: class_list.append(classes[read]) cluster_list.append(clusters[read]) else: # print("Read was clustered but unaligned:", read) clustered_but_unaligned += 1 # added the unprocessed reads to the measure not_clustered = set(classes.keys()) - set(clusters.keys()) highest_cluster_id = max(clusters.values()) highest_cluster_id += 1 for read in not_clustered: class_list.append(classes[read]) cluster_list.append(highest_cluster_id) highest_cluster_id += 1 v_score = v_measure_score(class_list, cluster_list) compl_score = completeness_score(class_list, cluster_list) homog_score = homogeneity_score(class_list, cluster_list) ari = adjusted_rand_score(class_list, cluster_list) print("Not included in clustering but aligned:", len(not_clustered)) print( "V:", v_score, "Completeness:", compl_score, "Homogeneity:", homog_score) print( "Nr reads clustered but unaligned " "(i.e., no class and excluded from V-measure): ", clustered_but_unaligned) return v_score, compl_score, homog_score, clustered_but_unaligned, ari def compute_V_measure_non_singleton_classes(clusters, classes): """V measure for non-singleton classes.""" max_cluster_id = max(clusters.values()) new_id = max_cluster_id + 1 classes_dict = {} for read_acc, cl_id in classes.items(): if cl_id not in classes_dict: classes_dict[cl_id] = [read_acc] else: classes_dict[cl_id].append(read_acc) nontrivial_classes_reads = [] for cl_id in classes_dict: if len(classes_dict[cl_id]) < 5: continue else: for read in classes_dict[cl_id]: nontrivial_classes_reads.append(read) class_list, cluster_list = [], [] for read in nontrivial_classes_reads: if read in clusters: class_list.append(classes[read]) cluster_list.append(clusters[read]) else: class_list.append(classes[read]) cluster_list.append(new_id) new_id += 1 v_score = v_measure_score(class_list, cluster_list) compl_score = completeness_score(class_list, cluster_list) homog_score = homogeneity_score(class_list, cluster_list) nr_filtered_classes = len( [1 for cl_id in classes_dict if len(classes_dict[cl_id]) >= 5]) print( "NONTRIVIAL CLASSES: V:", v_score, "Completeness:", compl_score, "Homogeneity:", homog_score) print("NUMBER OF CLASSES (FILTERED):", len( [1 for cl_id in classes_dict if len(classes_dict[cl_id]) >= 5])) return v_score, compl_score, homog_score, nr_filtered_classes def compute_V_measure_non_singletons(clusters, classes): """V measure for non-singletons.""" cluster_dict = {} for read_acc, cl_id in clusters.items(): if cl_id not in cluster_dict: cluster_dict[cl_id] = [read_acc] else: cluster_dict[cl_id].append(read_acc) nontrivial_clustered_reads = [] for cl_id in cluster_dict: if len(cluster_dict[cl_id]) <= 1: continue else: for read in cluster_dict[cl_id]: nontrivial_clustered_reads.append(read) class_list, cluster_list = [], [] # not_found_id = 1000000 clustered_but_unaligned = 0 for read in nontrivial_clustered_reads: if read in classes: class_list.append(classes[read]) cluster_list.append(clusters[read]) else: # print("Read was clustered but unaligned:", read) clustered_but_unaligned += 1 v_score = v_measure_score(class_list, cluster_list) compl_score = completeness_score(class_list, cluster_list) homog_score = homogeneity_score(class_list, cluster_list) print( "NONTRIVIAL CLUSTERS: V:", v_score, "Completeness:", compl_score, "Homogeneity:", homog_score) print( "NONTRIVIAL CLUSTERS: Nr reads clustered but unaligned " "(i.e., no class and excluded from V-veasure): ", clustered_but_unaligned) return v_score, compl_score, homog_score, clustered_but_unaligned def percentile(N, percent, key=lambda x: x): """ Find the percentile of a list of values. @parameter N - is a list of values. Note N MUST BE already sorted. @parameter percent - a float value from 0.0 to 1.0. @parameter key - optional key function to compute value from each element of N. @return - the percentile of the values """ if not N: return None k = (len(N) - 1) * percent f = math.floor(k) c = math.ceil(k) if f == c: return key(N[int(k)]) d0 = key(N[int(f)]) * (c - k) d1 = key(N[int(c)]) * (k - f) return d0 + d1 # end of http://code.activestate.com/recipes/511478/ }}} def get_cluster_information(clusters, classes): """Get cluster info.""" # class distribution class_dict = {} for read_acc, class_id in classes.items(): if class_id not in class_dict: class_dict[class_id] = [read_acc] else: class_dict[class_id].append(read_acc) total_nr_classes = len(class_dict) class_distribution = sorted([len(cl) for cl in class_dict.values()]) singleton_classes = set( [acc_list[0] for cl_id, acc_list in class_dict.items() if len(acc_list) == 1]) min_class_size = min(class_distribution) max_class_size = max(class_distribution) mean_class_size = sum(class_distribution) / float(len(class_distribution)) median_class_size = class_distribution[int(len(class_distribution) / 2)] \ if len(class_distribution) % 2 == 1 else ( class_distribution[int(len(class_distribution) / 2)] + class_distribution[int(len(class_distribution) / 2) - 1]) / 2.0 upper_75_class_size = percentile(class_distribution, 0.75) median_class_size = percentile(class_distribution, 0.5) tot_size = sum(class_distribution) e_class_size = sum( [c_s**2 for c_s in class_distribution]) / float(tot_size) tot_iterated_size = 0 for c_s in class_distribution[::-1]: tot_iterated_size += c_s if tot_iterated_size >= tot_size / 2.0: n50_class_size = c_s break # cluster distribution cluster_dict = {} for read_acc, cl_id in clusters.items(): if cl_id not in cluster_dict: cluster_dict[cl_id] = [read_acc] else: cluster_dict[cl_id].append(read_acc) cluster_distribution = sorted([len(cl) for cl in cluster_dict.values()]) # in case unclustered reads are missing from output (as for isoseq3) omitted_from_output_singletons = set(classes.keys()) - set(clusters.keys()) cluster_distribution = [ 1 for i in range( len(omitted_from_output_singletons))] + cluster_distribution total_nr_clusters = len(cluster_distribution) singleton_clusters = set( [acc_list[0] for cl_id, acc_list in cluster_dict.items() if len(acc_list) == 1]) min_cluster_size = min(cluster_distribution) max_cluster_size = max(cluster_distribution) mean_cluster_size = sum(cluster_distribution) / \ float(len(cluster_distribution)) upper_75_cluster_size = percentile(cluster_distribution, 0.75) median_cluster_size = percentile(cluster_distribution, 0.5) tot_size = sum(cluster_distribution) e_cluster_size = sum( [c_s**2 for c_s in cluster_distribution]) / float(tot_size) tot_iterated_size = 0 for c_s in cluster_distribution[::-1]: tot_iterated_size += c_s if tot_iterated_size >= tot_size / 2.0: n50_cluster_size = c_s break unaligned_but_nontrivially_clustered = set( clusters.keys()) - singleton_clusters - set(classes.keys()) # not_considered = set([read for read in classes if read not in clusters ]) not_clustered_classes = defaultdict(int) clustered_classes = defaultdict(int) reads_not_clustered = defaultdict(list) for read in clusters: if read in classes: class_id = classes[read] else: class_id = "unaligned" if read in singleton_clusters: not_clustered_classes[class_id] += 1 reads_not_clustered[class_id].append(read) else: clustered_classes[class_id] += 1 print("UNCLUSTERED:", "Tot classes:", len(not_clustered_classes)) print("CLUSTERED:", "Tot classes:", len(clustered_classes)) print("MIXED:", "Tot classes containing both:", len( set(clustered_classes.keys()) & set(not_clustered_classes.keys()))) print("Total number of classes (unique gene ID):", total_nr_classes) return (total_nr_classes - len(singleton_classes), len(singleton_classes), min_class_size, max_class_size, mean_class_size, median_class_size, total_nr_clusters, len(singleton_clusters) + len(omitted_from_output_singletons), min_cluster_size, max_cluster_size, mean_cluster_size, median_cluster_size, len(unaligned_but_nontrivially_clustered), upper_75_class_size, upper_75_cluster_size, e_class_size, n50_class_size, e_cluster_size, n50_cluster_size) def main(args): """Entry point.""" clusters = parse_inferred_clusters_tsv(args.clusters, args) if not clusters: outfile = open(args.outfile, "w") outfile.write("No clusters created\n") outfile.close() return if args.simulated: ref_file = pysam.AlignmentFile(args.classes, "r", check_sq=False) classes = parse_true_clusters_simulated(ref_file) # by simulation we know classes of all reads, they are therefore the # same number. tot_nr_reads = len(classes) else: ref_file = pysam.AlignmentFile(args.classes, "rb", check_sq=False) classes, tot_nr_reads, unclassified = parse_true_clusters(ref_file) v_score, compl_score, homog_score, clustered_but_unaligned, ari = \ compute_V_measure(clusters, classes) nr_non_singleton_classes, singleton_classes, min_class_size, \ max_class_size, mean_class_size, median_class_size, total_nr_clusters,\ singleton_clusters, min_cluster_size, max_cluster_size, \ mean_cluster_size, median_cluster_size, \ unaligned_but_nontrivially_clustered, \ upper_75_class_size, upper_75_cluster_size, e_class_size, \ n50_class_size, e_cluster_size, n50_cluster_size = \ get_cluster_information(clusters, classes) outfile = open(args.outfile, "w") outfile.write("CLASSES\n") # reads, unaligned, classes, singleton, min,max, mean,median outfile.write( "{0},{1},{2},{3},{4},{5},{6},{7}\n".format( "tot_nr_reads", "unclassified", "nr_non_singleton_classes", "singleton_classes", "upper_75_class_size", "median_class_size", "e_class_size", "n50_class_size")) outfile.write( "{0},{1},{2},{3},{4},{5},{6},{7}\n".format( tot_nr_reads, unclassified, nr_non_singleton_classes, singleton_classes, upper_75_class_size, median_class_size, e_class_size, n50_class_size)) # Reads_nontrivially_clustered_(%), Singletons_(%), # Reads_Nontrivially_clustered_but_unaligned, V, c,h ,V_nt, c_nt,h_nt, # non_singleton_clusters, min, max, median, mean Reads_nontrivially_clustered_percent = round( 100 * (float(tot_nr_reads - singleton_clusters) / tot_nr_reads), 1) # round(1.0 - Reads_nontrivially_clustered_percent, 2) Reads_Nontrivially_clustered_but_unaligned = \ unaligned_but_nontrivially_clustered V, c, h = round(v_score, 3), round(compl_score, 3), round(homog_score, 3) non_singleton_clusters = total_nr_clusters - singleton_clusters print("NONTRIVIAL CLUSTERS: ", (total_nr_clusters - singleton_clusters)) outfile.write("CLUSTERS\n") outfile.write( "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11}\n".format( "V", "c", "h", "ARI", "Reads_nontrivially_clustered_percent", "Reads_Nontrivially_clustered_but_unaligned", "non_singleton_clusters", "singleton_clusters", "upper_75_cluster_size", "median", "e_cluster_size", "n50_cluster_size")) outfile.write( "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11}\n".format( V, c, h, ari, Reads_nontrivially_clustered_percent, Reads_Nontrivially_clustered_but_unaligned, non_singleton_clusters, singleton_clusters, upper_75_cluster_size, median_cluster_size, e_cluster_size, n50_cluster_size)) outfile.close() if args.ctsv is not None: cfh = open(args.ctsv, "w") cfh.write("Read\tCluster\n") for r, c in classes.items(): cfh.write("{}\t{}\n".format(r, c)) cfh.flush() cfh.close() dfc = pd.DataFrame( { 'Statistic': [ 'V-measure', 'ARI', 'Completeness', 'Homogeneity'], 'Value': [ V, ari, c, h]}).set_index('Statistic') dfn = pd.DataFrame( {'Statistic': ['NonSingleton', 'Singletons'], 'Value': [non_singleton_clusters, singleton_clusters]}).set_index('Statistic') dfs = pd.DataFrame( { 'Statistic': [ 'Upper75ClsSize', 'Upper75ClassSize', 'MedianClsSize', 'MedianClassSize'], 'Value': [ upper_75_cluster_size, upper_75_class_size, median_cluster_size, median_class_size]}).set_index('Statistic') dfs2 = pd.DataFrame({'Statistic': ['N50ClsSize', 'N50ClassSize'], 'Value': [ n50_cluster_size, n50_class_size]}).set_index('Statistic') rdo = Path(args.raw_data_out) dfc.to_csv(rdo / 'v_ari_com_hom.csv') dfn.to_csv(rdo / 'sing_nonsing.csv') dfs.to_csv(rdo / 'class_sizes1.csv') dfs2.to_csv(rdo / 'class_sizes2.csv') pages = PdfPages(args.report) yd = 7 * 2.5 ax = dfc.plot(kind='bar', fontsize=7, rot=0) for p in ax.patches: ax.annotate( "{:.2f}".format( p.get_height()), (p.get_x() + p.get_width() / 2.0, p.get_height()), ha='center') pages.savefig() plt.clf() ax = dfn.plot(kind='bar', fontsize=7, rot=0) for p in ax.patches: ax.annotate( "{:.2f}".format( p.get_height()), (p.get_x() + p.get_width() / 2.0, p.get_height()), ha='center') pages.savefig() plt.clf() ax = dfs.plot(kind='bar', fontsize=7, rot=0) for p in ax.patches: ax.annotate( "{:.2f}".format( p.get_height()), (p.get_x() + p.get_width() / 2.0, p.get_height() + yd), ha='center') pages.savefig() plt.clf() ax = dfs2.plot(kind='bar', fontsize=7, rot=0) for p in ax.patches: ax.annotate( "{:.2f}".format( p.get_height()), (p.get_x() + p.get_width() / 2.0, p.get_height() + yd), ha='center') pages.savefig() plt.clf() pages.close() if __name__ == '__main__': parser = argparse.ArgumentParser( description="Align predicted transcripts to transcripts in ensembl " "reference data base.") parser.add_argument( '--clusters', type=str, help='Inferred clusters (tsv file)') parser.add_argument( '--classes', type=str, help='A sorted and indexed bam file.') parser.add_argument( '--ctsv', default=None, type=str, help='Write true classes in this TSV file.') parser.add_argument( '--simulated', action="store_true", help='Simulated data, we can simply read correct classes ' 'from the ref field.') parser.add_argument( '--ont', action="store_true", help='ONT data, parsing accessions differently.') parser.add_argument( '--modified_ont', action="store_true", help='ONT data preprocessed accessions, parsing ' 'accessions differently.') parser.add_argument('--outfile', type=str, help='Output file with results') parser.add_argument( '--report', type=str, help='Output PDF file with report') parser.add_argument('--sizes', type=str, help='Cluster sizes') parser.add_argument( '--raw_data_out', type=str, help='dir to save raw data for plotting') args = parser.parse_args() print("------------------------------------------------------------") main(args) print("------------------------------------------------------------")