Skip to content

Commit ac333e0

Browse files
authored
Merge pull request #38 from hexavier/master
Incorporate crosstalks analysis into mim-tRNAseq
2 parents 236466e + a2fa767 commit ac333e0

4 files changed

Lines changed: 151 additions & 11 deletions

File tree

mimseq/crosstalks.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Created on Wed Oct 13 11:01:53 2021
4+
5+
@author: Xavier Hernandez-Alias
6+
"""
7+
import pandas as pd
8+
import numpy as np
9+
from os import listdir
10+
from shutil import rmtree
11+
from os.path import isdir,isfile,join
12+
from scipy.stats import fisher_exact
13+
from statsmodels.stats.multitest import multipletests
14+
import multiprocessing
15+
from functools import partial
16+
17+
# Define function for parallelization
18+
def analyze_1sample(s,dirpath,thres):
19+
outdf = pd.DataFrame(columns=["sample","ref","var1","var2","pval","odds_ratio","values"])
20+
n=0
21+
ref_files = [f for f in listdir(join(dirpath, s)) if isfile(join(dirpath, s, f))]
22+
for f in ref_files:
23+
# Load table
24+
ref = f[:-7].replace(".","/")
25+
ref = "-".join(ref.split("-")[:-1]) if not "chr" in ref and not "/" in ref else ref
26+
readsdf = pd.read_csv(join(dirpath, s, f),sep="\t",compression="gzip",index_col="READ",dtype="category")
27+
pairs = []
28+
for v1 in readsdf.columns:
29+
for v2 in readsdf.columns:
30+
if v1!=v2 and set([v1,v2]) not in pairs:
31+
pairs.append(set([v1,v2]))
32+
reads1 = readsdf[v1].dropna()
33+
reads2 = readsdf[v2].dropna()
34+
# Do test only if at least % positions are modified
35+
if reads1.shape[0]>0 and reads2.shape[0]>0:
36+
if sum(reads1!="0")/reads1.shape[0]>thres and sum(reads2!="0")/reads2.shape[0]>thres:
37+
# Keep only reads that contain both v1 and v2
38+
tempdf = readsdf[[v1,v2]].dropna(how="any")
39+
# Build contingency table, var1 in rows and var 2 in columns
40+
counts = (tempdf!="0").value_counts()
41+
if counts.shape[0]==4:
42+
cont_tab = np.array([[counts.loc[True,True], counts.loc[True,False]],
43+
[counts.loc[False,True], counts.loc[False,False]]])
44+
#p = chi2_contingency(cont_tab)[1]
45+
oddsr, p = fisher_exact(cont_tab)
46+
outdf.loc[n] = [s,ref,v1,v2,p,oddsr,counts]
47+
n += 1
48+
# Remove temporary files
49+
rmtree(join(dirpath, s))
50+
return outdf
51+
52+
def crosstalks_wrapper(dirpath, thres, threads):
53+
# Multiprocessed function
54+
pool = multiprocessing.Pool(threads)
55+
frozen_fun = partial(analyze_1sample, dirpath=dirpath, thres=thres)
56+
samples = [f for f in listdir(dirpath) if isdir(join(dirpath, f))]
57+
dfs = pool.map(func=frozen_fun, iterable=samples, chunksize=1)
58+
pool.close()
59+
pool.join()
60+
# Concatenate tables
61+
outdf = pd.concat(dfs, ignore_index=True)
62+
# Correct multiple comparisons
63+
outdf["pval_corrected"] = multipletests(outdf["pval"].values,method="fdr_bh")[1]
64+
# Include canonical positions
65+
posinfo = pd.read_csv("/".join(dirpath.split("/")[:-1])+"/mods/mismatchTable.csv", sep="\t",
66+
index_col=["isodecoder","pos"],usecols=["isodecoder","pos","canon_pos"],
67+
dtype="category")
68+
posinfo = posinfo[~posinfo.index.duplicated(keep='first')]
69+
outdf["canon_var1"] = ["Charged" if s[1]=="Charged" else posinfo.loc[(s[0],s[1]),"canon_pos"] if s[0] in posinfo.index.get_level_values(0) else "NA" for s in outdf[["ref","var1"]].to_numpy()]
70+
outdf["canon_var2"] = ["Charged" if s[1]=="Charged" else posinfo.loc[(s[0],s[1]),"canon_pos"] if s[0] in posinfo.index.get_level_values(0) else "NA" for s in outdf[["ref","var2"]].to_numpy()]
71+
# Save table
72+
outdf.to_csv(dirpath+"/crosstalks.tsv",sep="\t",index=False)
73+

mimseq/mimseq.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .tRNAmap import mainAlign
1919
from .getCoverage import getCoverage, plotCoverage
2020
from .mmQuant import generateModsTable, plotCCA
21+
from .crosstalks import crosstalks_wrapper
2122
from .ssAlign import structureParser, modContext
2223
from .splitClusters import splitIsodecoder, unsplitClustersCov, getIsodecoderSizes, getDeconvSizes, writeDeconvTranscripts, writeIsodecoderTranscripts, writeSplitInfo, writeIsodecoderInfo
2324
import sys, os, subprocess, logging, datetime, copy
@@ -49,7 +50,7 @@ def restrictedFloat2(x):
4950
raise argparse.ArgumentTypeError('{} not a real number'.format(x))
5051

5152
def mimseq(trnas, trnaout, name, species, out, cluster, cluster_id, cov_diff, posttrans, control_cond, threads, max_multi, snp_tolerance, \
52-
keep_temp, cca, double_cca, min_cov, mismatches, remap, remap_mismatches, misinc_thresh, mito_trnas, plastid_trnas, pretrnas, local_mod, p_adj, sample_data):
53+
keep_temp, cca, double_cca, min_cov, mismatches, remap, remap_mismatches, misinc_thresh, mito_trnas, plastid_trnas, pretrnas, local_mod, p_adj, crosstalks, sample_data):
5354

5455
# Main wrapper
5556
# Integrity check for output folder argument...
@@ -129,7 +130,7 @@ def mimseq(trnas, trnaout, name, species, out, cluster, cluster_id, cov_diff, po
129130

130131
# if remap and snp_tolerance are enabled, skip further analyses, find new mods, and redo alignment and coverage
131132
if remap and (snp_tolerance or not mismatches == 0.0):
132-
new_mods, new_Inosines, filtered_cov, filter_warning, unsplitCluster_lookup,readRef_unsplit_newNames = generateModsTable(coverageData, out, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, mod_lists, Inosine_lists, tRNA_dict, Inosine_clusters, unique_isodecoderMMs_new, splitBool_new, isodecoder_sizes, unsplitCluster_lookup, cluster)
133+
new_mods, new_Inosines, filtered_cov, filter_warning, unsplitCluster_lookup,readRef_unsplit_newNames = generateModsTable(coverageData, out, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, mod_lists, Inosine_lists, tRNA_dict, Inosine_clusters, unique_isodecoderMMs_new, splitBool_new, isodecoder_sizes, unsplitCluster_lookup, cluster, crosstalks)
133134
Inosine_clusters, snp_tolerance, newtRNA_dict, new_mod_lists, new_inosine_lists = newModsParser(out, name, new_mods, new_Inosines, mod_lists, Inosine_lists, tRNA_dict, cluster, remap, snp_tolerance)
134135
map_round = 2
135136
genome_index_path, genome_index_name, snp_index_path, snp_index_name = generateGSNAPIndices(species, name, out, map_round, snp_tolerance, cluster)
@@ -152,9 +153,15 @@ def mimseq(trnas, trnaout, name, species, out, cluster, cluster_id, cov_diff, po
152153
filtered_cov = list()
153154
if snp_tolerance or not mismatches == 0.0:
154155
if 'newtRNA_dict' in locals():
155-
new_mods, new_Inosines, filtered_cov, filter_warning, unsplitCluster_lookup,readRef_unsplit_newNames = generateModsTable(coverageData, out, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, new_mod_lists, Inosine_lists, newtRNA_dict, Inosine_clusters, unique_isodecoderMMs_new, splitBool_new, isodecoder_sizes, unsplitCluster_lookup, cluster)
156+
new_mods, new_Inosines, filtered_cov, filter_warning, unsplitCluster_lookup,readRef_unsplit_newNames = generateModsTable(coverageData, out, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, new_mod_lists, Inosine_lists, newtRNA_dict, Inosine_clusters, unique_isodecoderMMs_new, splitBool_new, isodecoder_sizes, unsplitCluster_lookup, cluster, crosstalks)
156157
else:
157-
new_mods, new_Inosines, filtered_cov, filter_warning, unsplitCluster_lookup, readRef_unsplit_newNames = generateModsTable(coverageData, out, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, mod_lists, Inosine_lists, tRNA_dict, Inosine_clusters, unique_isodecoderMMs_new, splitBool_new, isodecoder_sizes, unsplitCluster_lookup, cluster)
158+
new_mods, new_Inosines, filtered_cov, filter_warning, unsplitCluster_lookup, readRef_unsplit_newNames = generateModsTable(coverageData, out, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, mod_lists, Inosine_lists, tRNA_dict, Inosine_clusters, unique_isodecoderMMs_new, splitBool_new, isodecoder_sizes, unsplitCluster_lookup, cluster, crosstalks)
159+
160+
if crosstalks:
161+
# Crosstalks analysis
162+
log.info("Analyzing crosstalks between pairs of modifications and modification-charging...")
163+
crosstalks_wrapper(out + "single_read_data", misinc_thresh, threads)
164+
158165
else:
159166
log.info("*** Misincorporation analysis not possible; either --snp-tolerance must be enabled, or --max-mismatches must not be 0! ***\n")
160167

@@ -267,6 +274,8 @@ def main():
267274
help = "Adjusted p-value threshold for DESeq2 pairwise condition differential epxression dot plots. \
268275
tRNAs with DESeq2 adjusted p-values equal to or below this value will be displayed as green or orange triangles for up- or down-regulated tRNAs, respectively. \
269276
Default p-adj <= 0.05")
277+
options.add_argument('--crosstalks', required = False, dest = 'crosstalks', action = "store_true",\
278+
help = "Enable analysis of crosstalks between pairs of modifications and modification-charging. Full details of this method in: https://doi.org/10.1093/nar/gkac1185")
270279

271280
align = parser.add_argument_group("GSNAP alignment options")
272281
align.add_argument('--max-mismatches', metavar = 'allowed mismatches', required = False, dest = 'mismatches', type = float, \
@@ -408,7 +417,7 @@ def main():
408417
mimseq(args.trnas, args.trnaout, args.name, args.species, args.out, args.cluster, args.cluster_id, args.cov_diff, \
409418
args.posttrans, args.control_cond, args.threads, args.max_multi, args.snp_tolerance, \
410419
args.keep_temp, args.cca, args.double_cca, args.min_cov, args.mismatches, args.remap, args.remap_mismatches, \
411-
args.misinc_thresh, args.mito, args.plastid, args.pretrnas, args.local_mod, args.p_adj, args.sampledata)
420+
args.misinc_thresh, args.mito, args.plastid, args.pretrnas, args.local_mod, args.p_adj, args.crosstalks, args.sampledata)
412421

413422
if __name__ == '__main__':
414423
main()

mimseq/mmQuant.py

100755100644
Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import absolute_import
99
import os, logging
1010
import re
11+
import gzip
1112
import pysam
1213
from itertools import groupby, combinations as comb
1314
from operator import itemgetter, le
@@ -112,9 +113,9 @@ def unknownMods(inputs, knownTable, cluster_dict, modTable, misinc_thresh, cov_t
112113

113114
return(new_mods_cluster, new_inosines_cluster)
114115

115-
def bamMods_mp(out_dir, min_cov, info, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, tRNA_struct, tRNA_ungap2canon,remap, misinc_thresh, knownTable, tRNA_dict, unique_isodecoderMMs, splitBool, isodecoder_sizes, threads, inputs):
116+
def bamMods_mp(out_dir, min_cov, info, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, tRNA_struct, tRNA_ungap2canon,remap, misinc_thresh, knownTable, tRNA_dict, unique_isodecoderMMs, splitBool, isodecoder_sizes, crosstalks, threads, inputs):
116117
# modification counting and table generation, and CCA analysis
117-
118+
118119
# output structures
119120
modTable = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
120121
stopTable = defaultdict(lambda: defaultdict(int))
@@ -130,6 +131,21 @@ def bamMods_mp(out_dir, min_cov, info, mismatch_dict, insert_dict, del_dict, clu
130131
cca_dict = defaultdict(lambda: defaultdict(int))
131132
dinuc_dict = defaultdict(int)
132133

134+
# Open files for single read analysis
135+
if crosstalks and not remap:
136+
sample_name = inputs.split("/")[-1].split(".")[0]
137+
if not os.path.exists(out_dir + "single_read_data/" + sample_name):
138+
os.mkdir(out_dir + "single_read_data/" + sample_name)
139+
srfiles = dict()
140+
for r in isodecoder_sizes.keys():
141+
shortname = r.split("/")[0] if "/" in r else "-".join(r.split("-")[:-1]) if not "chr" in r else r
142+
srfiles[shortname] = gzip.open(out_dir + "single_read_data/" + sample_name +"/"+ r.replace("/",".") +".tsv.gz", "wt")
143+
if cca:
144+
cols = ["READ"]+[str(s) for s in tRNA_struct.loc[shortname].index]+["Charged"]
145+
else:
146+
cols = ["READ"]+[str(s) for s in tRNA_struct.loc[shortname].index]
147+
srfiles[shortname].write("\t".join(cols)+"\n")
148+
133149
# process mods by looping through alignments in bam file
134150
bam_file = pysam.AlignmentFile(inputs, "rb")
135151
log.info('Analysing {}...'.format(inputs))
@@ -241,6 +257,41 @@ def bamMods_mp(out_dir, min_cov, info, mismatch_dict, insert_dict, del_dict, clu
241257
elif ref_pos <= ref_length - 3:
242258
dinuc = "Absent"
243259
cca_dict[reference][dinuc] += 1
260+
261+
############################
262+
# Single-read mods and CCA #
263+
############################
264+
265+
if crosstalks and not remap:
266+
# Modifications
267+
shortname = "-".join(reference.split("-")[:-1]) if not "chr" in reference else reference
268+
seqtemp = []
269+
for n in tRNA_struct.loc[shortname].index:
270+
if (n<(offset+1)) or (n>(aln_end)):
271+
seqtemp.append("NA")
272+
elif n not in temp.keys():
273+
seqtemp.append("0")
274+
else:
275+
b = temp[n]
276+
if b!="N":
277+
seqtemp.append(b)
278+
else:
279+
seqtemp.append("NA")
280+
if cca:
281+
# Record charging status
282+
if aln_end==ref_length:
283+
chrg = "1"
284+
elif aln_end==(ref_length-1):
285+
chrg = "0"
286+
else:
287+
chrg = "NA"
288+
add = [read.query_name]+seqtemp+[chrg]
289+
else:
290+
add = [read.query_name]+seqtemp
291+
292+
# Add line
293+
srfiles[shortname].write("\t".join(add)+"\n")
294+
244295

245296
# filter readRef_match_dict to those clusters with more than 10% false reads (i.e. do not match parent) or those with no True reads (usually low count clusters)
246297
readRef_match_dict = {k:v for k, v in readRef_match_dict.items() if ((v[True]) and (v[False]/v[True] >= 0.1)) or (not v[True])}
@@ -413,6 +464,10 @@ def bamMods_mp(out_dir, min_cov, info, mismatch_dict, insert_dict, del_dict, clu
413464
for dinuc, count in data.items():
414465
if dinuc.upper() in ["CA", "CC", "C", "ABSENT"]:
415466
CCAvsCC_counts.write(cluster + "\t" + dinuc + "\t" + inputs + "\t" + condition + "\t" + str(count) + "\n")
467+
if crosstalks:
468+
# Close single-read analysis files
469+
for r in srfiles.keys():
470+
srfiles[r].close()
416471

417472
log.info('Analysis complete for {}...'.format(inputs))
418473

@@ -577,7 +632,7 @@ def addNA(tRNA_struct, data_type, cluster_dict, name, table):
577632

578633
return(table)
579634

580-
def generateModsTable(sampleGroups, out_dir, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, knownTable, Inosine_lists, tRNA_dict, Inosine_clusters, unique_isodecoderMMs, splitBool, isodecoder_sizes, unsplitCluster_lookup, clustering):
635+
def generateModsTable(sampleGroups, out_dir, name, threads, min_cov, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, remap, misinc_thresh, knownTable, Inosine_lists, tRNA_dict, Inosine_clusters, unique_isodecoderMMs, splitBool, isodecoder_sizes, unsplitCluster_lookup, clustering, crosstalks):
581636
# Wrapper function to call countMods_mp with multiprocessing
582637

583638
if cca:
@@ -589,6 +644,7 @@ def generateModsTable(sampleGroups, out_dir, name, threads, min_cov, mismatch_di
589644
try:
590645
os.mkdir(out_dir + "CCAanalysis")
591646
os.mkdir(out_dir + "mods")
647+
if crosstalks: os.mkdir(out_dir + "single_read_data")
592648
except FileExistsError:
593649
log.warning("Rewriting over old mods and CCA files...")
594650

@@ -600,6 +656,7 @@ def generateModsTable(sampleGroups, out_dir, name, threads, min_cov, mismatch_di
600656

601657
try:
602658
os.mkdir(out_dir + "mods")
659+
if crosstalks: os.mkdir(out_dir + "single_read_data")
603660
except FileExistsError:
604661
log.warning("Rewriting over old mods files...")
605662

@@ -634,7 +691,7 @@ def generateModsTable(sampleGroups, out_dir, name, threads, min_cov, mismatch_di
634691
pool = MyPool(multi)
635692
# to avoid assigning too many threads, divide available threads by number of processes
636693
threadsForMP = int(threads/multi)
637-
func = partial(bamMods_mp, out_dir, min_cov, baminfo, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, tRNA_struct_df, tRNA_ungap2canon, remap, misinc_thresh, knownTable, tRNA_dict, unique_isodecoderMMs, splitBool, isodecoder_sizes, threadsForMP)
694+
func = partial(bamMods_mp, out_dir, min_cov, baminfo, mismatch_dict, insert_dict, del_dict, cluster_dict, cca, tRNA_struct_df, tRNA_ungap2canon, remap, misinc_thresh, knownTable, tRNA_dict, unique_isodecoderMMs, splitBool, isodecoder_sizes, crosstalks, threadsForMP)
638695
new_mods, new_Inosines, readRef_match_dict = zip(*pool.map(func, bamlist))
639696
pool.close()
640697
pool.join()

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def package_files(directory):
4747
"pysam",
4848
"seaborn",
4949
"matplotlib",
50-
"pybedtools"],
50+
"pybedtools",
51+
"statsmodels"],
5152
classifiers=[
5253
"Development Status :: 4 - Beta",
5354
"Environment :: Console",
@@ -57,4 +58,4 @@ def package_files(directory):
5758
"Programming Language :: Python :: 3",
5859
"Topic :: Scientific/Engineering :: Bio-Informatics"
5960
],
60-
zip_safe=False)
61+
zip_safe=False)

0 commit comments

Comments
 (0)